LlamaIndex 数据模型 — 那块到处流动的「砖」
这章讲什么: RAG 流水线里从头到尾流动的单位是 Node。先把这块砖看清楚——它由什么组成、几种变体、节点之间怎么连成关系、
hash怎么用 于去重、MetadataMode怎么让「给嵌入看的文本」和「给 LLM 看的文本」不一样。后面三章全靠这套词汇。
1.1 为什么不是「一段字符串」就够了
最朴素的想法:检索就是「存一堆文本片段,查相似的」。但真实 RAG 需要更多:
- 这片段是从哪份文档切出来的?(引用溯源、按文档删除)
- 它的前一段 / 后一段是什么?(窗口扩展、自动合并)
- 哪些元数据该参与嵌入、哪些只给 LLM 看、哪些都不要?
- 内容变了没有?(增量更新时跳过没变的)
所以 LlamaIndex 不用裸字符串,而是 BaseNode:内容 + 元数据 + 关系 + 嵌入 + 哈希,一个对象全带上。
1.2 类型全家福
BaseComponent (可序列化基类)
└─ BaseNode (抽象:id/metadata/relationships/embedding)
├─ Node 多模态新基类(text/image/audio/video Resource)
├─ TextNode 纯文本块(RAG 最常用)
│ ├─ ImageNode 图文块
│ └─ IndexNode 「指向另一个对象」的节点(递归检索用)
└─ Document = Node 的别名级子类(代表「一整份」文档)
NodeWithScore = 包一层 {node, score},检索结果用这个
RelatedNodeInfo = 关系里存的轻量引用 {node_id, type, hash}
| 类型 | 是什么 | 典型场景 |
|---|---|---|
Document | 一整份输入文档 | Reader 读进来的原始单位,灌库前会被切成 TextNode |
TextNode | 一个文本块(chunk) | 检索/嵌入的基本单位 |
ImageNode | 带图的块 | 多模态检索 |
IndexNode | 「占位符」节点,obj/index_id 指向另一个 retriever/query engine/node | 递归检索(见第 03 章) |
NodeWithScore | {node, score} | 检索返回值;score 是相似度/重排分 |
源码锚点:schema.py:264(BaseNode)、schema.py:765(TextNode)、schema.py:948(IndexNode)、schema.py:1035(NodeWithScore)、schema.py:1097(Document)。
1.3 一个 Node 里有什么
BaseNode 的核心字段(schema.py:264-320):
| 字段 | 类型 | 作用 |
|---|---|---|
id_ | str | 唯一 ID,默认 uuid4 |
embedding | list[float] | None | 该 Node 的向量(灌库时填上) |
metadata | dict | 扁平元数据;别名 extra_info |
excluded_embed_metadata_keys | list[str] | 哪些元数据不进嵌入文本 |
excluded_llm_metadata_keys | list[str] | 哪些元数据不给 LLM 看 |
relationships | dict | 指向 SOURCE/PREV/NEXT/PARENT/CHILD 的关系 |
metadata_template / metadata_separator | str | 元数据拼成字符串时的格式 |
1.4 关系图:Node 不是孤岛
NodeRelationship 枚举(schema.py:207-224)定义五种关系:
SOURCE(指向原始 Document)
▲
│
PREV ◀──── [ TextNode ] ────▶ NEXT (同一文档相邻块,链表)
│
PARENT │ CHILD (层级:大块↔小块)
▼
- SOURCE:每个切出来的块都记得「我来自哪份 Document」。切块时一次性算好这个关系再分发给所有块,避免对每个块重复算文档哈希(
node_parser/node_utils.py:39-42有一段注释专门解释这个优化)。 - PREV / NEXT:同文档相邻块,用于「检索到一块后把前后文也带上」(sentence-window、auto-merging)。
- PARENT / CHILD:层级结构(如 hierarchical node parser:大块拆小块)。
关系访问是只读 property,且做了类型校验(单个 vs 列表),例如 source_node 必须是单个,child_nodes 必须是列表(schema.py:390-448)。
1.5 MetadataMode:同一个 Node,给不同读者看不同文本
这是个容易忽略但很关键的设计。get_content(metadata_mode=...) 会按模式拼接「元数据 + 正文」(schema.py:328 抽象,TextNode 在 schema.py:810 实现;元数据筛选在 get_metadata_str,schema.py:337-358):
MetadataMode | 含义 | 谁用 |
|---|---|---|
EMBED | 拼接时剔除 excluded_embed_metadata_keys | 算嵌入时 |
LLM | 拼接时剔除 excluded_llm_metadata_keys | 喂给 LLM 时 |
ALL | 全部元数据 | 计算 hash / 调试 |
NONE | 只要正文 | 不需要元数据时 |
为什么要分开? 举例:文件路径 metadata["file_path"] 你可能想让 LLM 引用它(LLM 可见),但不想让它污染嵌入向量(EMBED 排除);反过来,一段「检索关键词」可能只想进嵌入而不想 LLM 看到。这套机制让你逐键控制。
证据链:
- 灌库算嵌入时用的是
MetadataMode.EMBED(indices/vector_store/base.py:302过滤空内容时即用 EMBED)。 - 合成喂 LLM 时用的是
MetadataMode.LLM(response_synthesizers/base.py:291:n.node.get_content(metadata_mode=MetadataMode.LLM))。
1.6 hash:增量更新的「指纹」
每个 Node 有个 hash(schema.py 各子类的 hash property,如 Node.hash 在 schema.py:741-762):对元数据 + 各资源内容做 sha256。
用途:
# 示意,非源码:增量刷新的核心判断(对应 indices/base.py:refresh_ref_docs)
existing = docstore.get_document_hash(doc.id_)
if existing is None: # 没见过 → 插入
index.insert(doc)
elif existing != doc.hash: # 内容变了 → 删旧插新
index.update_ref_doc(doc)
# else: 一字未改 → 跳过,省下嵌入和 LLM 调用
真实实现见 indices/base.py:429-452(refresh_ref_docs)。灌库流水线的去重也基于同一个 hash(见第 02 章)。
关键细节: as_related_node_info()(schema.py:494-501)会重算整段内容的 hash。切块时如果对每个子块都去算「父文档的 RelatedNodeInfo」会非常慢,所以 build_nodes_from_splits 在循环外算一次、复用给所有块(node_parser/node_utils.py:39-42 的注释明说了这个坑)。
1.7 巧妙之处
- 逐键的元数据可见性(EMBED / LLM 分离)让「检索召回质量」和「给 LLM 的提示」可以独立调,而不必复制两份文本。
schema.py:294-301、schema.py:337-358。 - 关系内嵌轻量引用(
RelatedNodeInfo只存node_id+hash,不存整段内容,schema.py:249-257),让 Node 之间能连成图却不爆序列化体积。 - 多模态向
Node收敛:新Node用text_resource/image_resource/...四个MediaResource槽位统一表达文本/图/音/视频,get_content_blocks()再转成 LLM 的 content blocks(schema.py:685-731)。
1.8 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 抽象节点 | schema.py | BaseNode |
| 关系枚举 | schema.py | NodeRelationship, RelatedNodeInfo |
| 元数据模式 | schema.py | MetadataMode, BaseNode.get_metadata_str |
| 文本块 | schema.py | TextNode, TextNode.get_content |
| 检索结果 | schema.py | NodeWithScore |
| 文档 | schema.py | Document |
| 关系构建优化 | node_parser/node_utils.py | build_nodes_from_splits |