一、两个核心抽象
Document
- 一段原始文本 + 元数据
- 通常对应"一个文件 / 一条记录 / 一个网页"
- 不直接参与检索,需要先切成 Node
- 类比:数据库的表行
Node
- Document 切成的一个 chunk
- 是索引与检索的最小单位
- 带回指向 Document 的 relationships
- 类比:搜索引擎的倒排项
二、Document 最小构造
from llama_index.core import Document
doc = Document(
text="古法编程是 AI 时代之前的编程技艺传承网站...",
metadata={
"source": "about.md",
"author": "nina",
"created_at": "2026-05-07",
},
doc_id="about-001", # 可选,不传会用 uuid
)
print(doc.text[:30])
print(doc.metadata)
几个要点:
text是核心——要送入 embedding 的就是它,外加可见的 metadata 部分metadata是 dict,键和值必须是简单类型(str/int/float/bool)doc_id建议显式给——增量更新、去重、溯源都靠它Document继承自TextNode,所以下面讲的 Node 能力它也有
三、metadata 是 RAG 的秘密武器
新手常把 metadata 当成"只是记一下来源",但在 LlamaIndex 里 metadata 会参与三件事:
- 参与嵌入:默认 metadata 的可见字段会被拼到文本里一起 embed,这样"查今年的财报"的 query 才能匹配上带
year: 2025的 chunk - 参与检索过滤:
MetadataFilters可以硬过滤——"只在 2025 年的文档里搜" - 进入 LLM prompt:合成答案时,LLM 能看到 chunk 来自哪本手册,才能引用
[Source: chapter3.pdf]
精细控制 metadata 怎么用
doc = Document(
text="2025 Q3 营收 120 亿,同比 +15%...",
metadata={
"filename": "Q3-report.pdf",
"page": 12,
"year": 2025,
"internal_id": "xyz-secret-42",
},
# 不让 LLM 看见 internal_id
excluded_llm_metadata_keys=["internal_id"],
# 不让 embedding 看见 internal_id(也不参与向量)
excluded_embed_metadata_keys=["internal_id"],
# metadata 拼到正文时的分隔符
metadata_seperator="\n",
# metadata 每一项的模板
metadata_template="{key}: {value}",
# 内容模板:{metadata_str}\n\n{content}
text_template="{metadata_str}\n---\n{content}",
)
# 看看它怎么拼给 embedding / LLM
print(doc.get_content(metadata_mode="embed"))
print(doc.get_content(metadata_mode="llm"))
生产经验:metadata 少而精,不是越多越好。每条 metadata 都吃 token,而且太多会稀释语义。推荐 3-5 个关键字段(如
source/date/category/lang),其它用 excluded_embed_metadata_keys 藏起来仅供过滤/展示。
四、Node 的三种常见类型
| 类型 | 承载 | 典型场景 |
|---|---|---|
TextNode | 一段文本 + metadata | 所有文本 chunk(主力) |
ImageNode | 图片数据/路径 | 多模态 RAG,图文混合 |
IndexNode | 指向另一个 Index/QueryEngine 的"桥"节点 | 层级/递归检索、路由 |
TextNode 完整字段
from llama_index.core.schema import TextNode, NodeRelationship, RelatedNodeInfo
node = TextNode(
text="2025 Q3 营收 120 亿...",
id_="node-42",
metadata={"page": 12, "section": "营收概览"},
embedding=None, # 建索引时会填
relationships={
NodeRelationship.SOURCE: RelatedNodeInfo(node_id="doc-001"),
NodeRelationship.PREVIOUS: RelatedNodeInfo(node_id="node-41"),
NodeRelationship.NEXT: RelatedNodeInfo(node_id="node-43"),
},
)
五、relationships:Node 不是孤岛
Node 之间可以通过 relationships 建图。LlamaIndex 预定义的关系类型:
SOURCE
指向 Node 来自的原 Document,最常用——用于"找回原始出处"。
PREVIOUS / NEXT
同一 Document 内的上下文邻居。PrevNextNodePostprocessor 检索后可以把左右邻居拉出来扩充上下文。
PARENT / CHILD
层级关系——比如章节是 parent,段落是 child。用在层级检索和"小块嵌入、大块回答"模式。
关系不是摆设,后面讲 RecursiveRetriever、AutoMergingRetriever 时用到——好的 relationships = 检索命中小块、回答时合并回大块,既准又有上下文。
六、Node 从哪来:切 vs 手构
方式 A:Splitter 自动切
from llama_index.core.node_parser import SentenceSplitter
splitter = SentenceSplitter(chunk_size=512, chunk_overlap=50)
nodes = splitter.get_nodes_from_documents(docs)
print(len(nodes), "个 nodes")
print(nodes[0].text[:80])
print(nodes[0].metadata) # 继承自 Document + 增加 chunk 位置
自动切会自动建 PREVIOUS/NEXT/SOURCE relationships——很贴心。Ch4 详讲 Splitter 选型。
方式 B:手工构造
结构化数据(数据库行、JSON API)不需要切——一行一 Node 可能更合适:
from llama_index.core.schema import TextNode
rows = db.execute("SELECT id, title, body, tag FROM articles")
nodes = [
TextNode(
id_=f"article-{r.id}",
text=r.body,
metadata={"title": r.title, "tag": r.tag, "id": r.id},
)
for r in rows
]
要点:能手构就手构。数据库里每行 200-1000 字,直接一行一 Node;大段自由文本才需要 Splitter。手构的 Node 边界自然,metadata 也完整——比切出来的好用多了。
七、Document 的 hash 与增量
每个 Document 有隐藏的 hash 字段,由 text + metadata 计算得出:
doc1 = Document(text="hello", doc_id="x")
doc2 = Document(text="hello", doc_id="x", metadata={"v": 1})
print(doc1.hash, doc2.hash) # 不同:metadata 变了
增量索引的关键——同 doc_id 不同 hash 才更新,不然跳过。配合 DocstoreStrategy.UPSERTS(Ch4 讲),你能做到"每天跑一次完整目录扫描,但只重嵌入发生变化的文件"。
八、IndexNode:递归检索的钥匙
IndexNode 不直接存文本,它是一个"桥梁"——指向另一个 Index 或 QueryEngine。
from llama_index.core.schema import IndexNode
# 先为"财报 2025"建一个子 QueryEngine
qe_2025 = VectorStoreIndex.from_documents(docs_2025).as_query_engine()
# 给总索引加一个 IndexNode 作为入口
bridge = IndexNode(
text="财报 2025 全年数据。包含 Q1-Q4 营收、利润、现金流。",
index_id="qe_2025",
metadata={"year": 2025},
)
# 当总检索命中 bridge,会"下钻"到 qe_2025 继续查
这个模式就是 LlamaIndex 的"检索路由":上层是描述性文本(每本书的摘要),被命中就深入对应子索引。在 Ch7、Ch10 会深挖。
九、常见 Document/Node 反模式
- 把整份 300 页 PDF 当一个 Document,不切:embedding 平均化后,啥都查不准。
- metadata 里塞大对象或 list:LlamaIndex 要求简单类型,序列化失败或 embedding 噪声大。
- doc_id 不稳定:每次跑重新 uuid,增量更新变全量重建,贵。
- excluded_embed_metadata_keys 没设:内部 ID/时间戳全进 embedding,稀释语义。
- 手构 Node 不带 relationships:Postprocessor 扩上下文失效,回答缺邻居信息。
- TextNode 和 Document 混用不清:Document 是入口、Node 是原子;建索引前都应是 Node。
- metadata 和文本内容冗余:同样的信息写两遍,浪费 token 还增加 embedding 噪声。
- 用 Node 存整张 PDF 的 base64 图:TextNode 的 text 应是文本。图用 ImageNode 或 LlamaParse 解成文字。
十、本章小结
记住:
① Document = 原始一条(含 metadata),Node = 切出来可检索的 chunk——默认 Document 继承自 TextNode。
② metadata 三用途:参与 embed、参与过滤、进 LLM prompt;用
③ relationships 不是摆设:PREVIOUS/NEXT 做邻居扩展,PARENT/CHILD 做层级检索,IndexNode 做路由下钻。
④ 能手构就手构:结构化数据直接一行一 Node,边界干净 metadata 完整,比 splitter 切好一截。
① Document = 原始一条(含 metadata),Node = 切出来可检索的 chunk——默认 Document 继承自 TextNode。
② metadata 三用途:参与 embed、参与过滤、进 LLM prompt;用
excluded_*_metadata_keys 精细控制各自可见。
③ relationships 不是摆设:PREVIOUS/NEXT 做邻居扩展,PARENT/CHILD 做层级检索,IndexNode 做路由下钻。
④ 能手构就手构:结构化数据直接一行一 Node,边界干净 metadata 完整,比 splitter 切好一截。