Document与Node

要把一篇方法学 PDF 变成可引用答案,不能把整本塞进提示词。LlamaIndex 用 Document 装一份来源全文,用 Node(通常是 TextNode)装切出来的块。切分后 元数据(metadata)会从 Document 拷到每个 Node;引用能回到 doc_id / 页码,靠的就是这层对象,不是靠模型「记得文件名」。

段末注释DocumentTextNode 的子类,表示一份来源容器;Node 是索引与召回的原子块。二者都走 get_content(),不是只用 .text

Document 是整本、Node 是卡片;SOURCE 回溯,LLM/Embedding 看到的 metadata 可以不同(科普示意)


1. 一句话定位

维度 内容
角色 知识层的数据单元:来源容器 vs 可索引块
输入 → 输出 原始字符串 / 文件内容 → Document →(经 NodeParser)list[TextNode]
典型调用入口 Document(text=..., metadata=...)parser.get_nodes_from_documents()node.get_content()
与 LangChain / LangGraph 近邻是 LangChain Documentpage_content);LangGraph 不管切块,节点里拿到的应是已切好的 Node

出现背景:早期教程把「一段 str」直接 embedding。私有库一上规模,引用、增量更新、父子块都需要稳定 ID 与关系边,于是 Document / Node 成为一等公民。Document 在实现上就是带「整份来源」语义的 TextNode


2. 前置依赖与环境

1
pip install -U llama-index-core
  • Python 3.10+
  • 版本锚点:llama-index-core 0.14.x(与概述相同)
  • 本篇不调 LLM / Embedding;同步 API 即可。后续入库才需要 Ollama。

3. 实现逻辑

一次「手写 Document → 切成 Node」的步骤:

1
2
3
4
5
6
7
1. 构造 Document(text, metadata, doc_id)
2. 可选:excluded_llm_metadata_keys / excluded_embed_metadata_keys
3. SentenceSplitter.get_nodes_from_documents([doc])
4. 每个 TextNode:text=块正文,metadata 继承 Document
5. relationships[SOURCE] 指向原 Document.id_
6. 相邻块挂 PREVIOUS / NEXT
7. 以后 embedding / LLM 读的是 get_content(MetadataMode.EMBED|LLM),不是裸 .text

字段级变形(方法学片段):

1
2
3
4
5
6
7
8
9
10
11
12
Document(
doc_id="paper_001",
text="qPCR 以小鼠肝脏 GAPDH 为内参……",
metadata={"species": "小鼠", "file_name": "gapdh.md"}
)
↓ get_nodes_from_documents
TextNode(
node_id="<uuid 或自定义>",
text="qPCR 以小鼠肝脏 GAPDH 为内参……",
metadata={"species": "小鼠", "file_name": "gapdh.md"},
relationships={SOURCE: RelatedNodeInfo(node_id="paper_001"), ...}
)

4. 原理说明

主轴是:同一份 text + metadata,在不同 MetadataMode 下拼出不同字符串,再被切块继承。

1
2
3
4
5
6
7
8
9
10
1. 写入 Document.text 与 Document.metadata(扁平 dict:str/int/float)
2. doc_id / id_ / node_id 是同一套 ID 的不同别名;增量刷新按 doc_id 对齐
3. get_content(MetadataMode.NONE) → 只有 text
4. get_content(EMBED):用 metadata_template 把未排除的键拼进 text_template
5. get_content(LLM):同上,但排除 excluded_llm_metadata_keys
6. NodeParser 按句/token 切 text,拷贝 metadata 到每个 TextNode
7. 每个子 Node 写 relationships[SOURCE] = 父 Document 的 RelatedNodeInfo
8. 若 include_prev_next_rel=True,再写 PREVIOUS / NEXT
少了第 4 步 → 向量里看不到 species 等过滤信号
少了第 7 步 → 召回后无法从 Node 回到原文件

对象挂在步骤上:

Document(数据类,llama_index.core.Document)出现在步骤 1。功能:一份来源的容器。最小字段:text: str 非空才有检索意义;metadata: dict 默认 {}

get_content(metadata_mode)(方法)出现在步骤 3~5。功能:按模式拼「metadata 字符串 + 正文」。默认模板:metadata_template="{key}: {value}"text_template="{metadata_str}\n\n{content}"

TextNode(数据类,llama_index.core.schema.TextNode)出现在步骤 6。功能:可索引块。关键字段:textid_(属性 node_id)、metadatarelationshipsstart_char_idx / end_char_idx

NodeRelationship(枚举)出现在步骤 7~8。常用:SOURCE(溯源)、PREVIOUS / NEXT(邻块)、PARENT / CHILD(层级切分)。

砍掉 excluded_llm_metadata_keys 时:内部路径、绝对文件名会进生成提示,模型可能把路径当证据。


5. 最小可运行示例

1
pip install -U llama-index-core
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from llama_index.core import Document
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.schema import MetadataMode, NodeRelationship

# 输入:方法学短文 + 要传到引用里的 metadata
doc = Document(
text=(
"定量 PCR 以小鼠肝脏 GAPDH 为内参。反应体系 20 μL,退火 60°C。"
"每个生物学重复 3 只 SPF 级 C57BL/6 小鼠。"
),
doc_id="paper_001",
metadata={"species": "小鼠", "file_name": "gapdh.md", "section": "methods"},
excluded_llm_metadata_keys=["file_name"], # 关键参数:生成侧不看路径
)

print("--- NONE ---")
print(doc.get_content(MetadataMode.NONE))
print("--- LLM ---")
print(doc.get_content(MetadataMode.LLM))
print("--- EMBED ---")
print(doc.get_content(MetadataMode.EMBED))

parser = SentenceSplitter(chunk_size=64, chunk_overlap=16)
nodes = parser.get_nodes_from_documents([doc])

# 输出:切出的 TextNode;metadata 继承;SOURCE 指向 paper_001
print("n_nodes =", len(nodes))
n0 = nodes[0]
print(n0.node_id, n0.metadata)
src = n0.relationships[NodeRelationship.SOURCE]
print("SOURCE =", src.node_id)
# 预期:SOURCE == paper_001;LLM 段没有 file_name,EMBED 段有

预期形态:LLM 输出含 species / section 不含 file_nameEMBED 三者都在;至少一个 Node 的 SOURCE.node_idpaper_001


6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
text str,必填 来源正文,构造器传入 空串可构造但检索无意义 非空 保持原文,不要先摘要再入库
metadata dict,{} 扁平注解,会注入 embed/LLM 文本并传到子 Node 嵌套 dict / 列表多数向量库不收 speciespage、int/str 只要扁平标量
doc_id / id_ str,默认 UUID 来源级稳定 ID,增量刷新对齐键 改 ID 等于新文档,旧向量成孤儿 路径或 pmid:123 filename_as_id 或显式赋值
excluded_llm_metadata_keys list[str],[] 从 LLM 所见文本里拿掉这些键 不填则路径/内部 ID 污染生成 file_namefile_path 引用仍可从 node.metadata
excluded_embed_metadata_keys list[str],[] 从 embedding 文本里拿掉这些键 拿掉过多会丢过滤信号;拿掉过少会让路径主导向量 少排除 默认让物种、章节进向量
metadata_template str,"{key}: {value}" 每个键值如何格式化进文本 改格式会改变 embedding,重建索引才一致 保持默认 改了必须全量重嵌
text_template str,"{metadata_str}\n\n{content}" metadata 串与正文如何拼接 同上,属于嵌入契约 保持默认 与 template 成对冻结

7. 适用 / 不适用

维度 适用 不适用
任务形态 私有文档要引用、要按 metadata 过滤 一次性 prompt 里塞三句话,不必起 Document
集成约束 向量库要求扁平 metadata 需要深层 JSON 当 payload——先拍平再入库
工程阶段 任何要审计 source_nodes 的 RAG 只调 Chat 模型、无私有库

出现环、审批、会话恢复时:仍用这些 Node 当检索结果,控制流交给 LangGraph。


8. 易踩坑

  1. .text 当成模型真正吃到的字符串:embed/LLM 走 get_content(),含 metadata。调试必须按 MetadataMode 打印。
  2. doc_id 每次 UUID:同一文件重建索引无法 upsert,重复块堆积。
  3. metadata 塞嵌套对象:Qdrant / pgvector 一类后端写入失败或被静默丢掉。

小结

  • Document 装来源,TextNode 装块;切分默认继承 metadata 并挂 SOURCE
  • 引用看 metadatarelationships;模型看到的字符串看 get_content(MetadataMode)
  • doc_id 是增量更新的对齐键,不要每次随机。
  • 切分剂量(chunk_size)在 NodeParser 专篇拧;本篇只固定数据契约。

参考链接

-------------本文结束感谢您的阅读-------------