2.2 文档与节点模型


2.2 文档(Document)与节点(Node)模型

本节摘要:Document 是源文件的完整容器,Node 是检索单元(从 Document 切出的一块文本,携带出身元数据与前后关系)。本节解剖两者的字段结构,演示手工构造与切分生成的两种来路,并用一个"表格被斩断"的实验说明:Node 粒度是检索质量的第一决定因素。

两个容器的字段解剖

Document 的关键字段:text(正文)、metadata(自由字典:文件名、URL、部门、日期)、id_(稳定标识)、metadata_template(元数据注入提示词的格式)。Node(TextNode)在此基础上增加:text(这一块的正文)、metadata(继承自 Document,也可在节点级追加)、relationships(指向来源 Document、前一节点、下一节点的关系字典)、embedding(该节点的向量,建索引时填充)。relationships 最容易被忽视却极有价值——它是"句子窗口检索"(第 4 章)与"上下文相邻扩展"能实现的结构基础。

from llama_index.core import Document from llama_index.core.node_parser import SentenceSplitter from llama_index.core.schema import NodeRelationship, RelatedNodeType # 来路一:手工构造(结构化数据走这条路) doc = Document( text="第三章 报销制度:单笔超过 5000 元需部门总监审批……", metadata={"file_name": "财务制度汇编", "chapter": "3", "dept": "finance"}, ) # 来路二:切分生成(长文档走这条路) splitter = SentenceSplitter(chunk_size=256, chunk_overlap=32) nodes = splitter.get_nodes_from_documents([doc]) n = nodes[0] print("节点正文长度:", len(n.text)) print("继承的元数据:", n.metadata) print("关系键:", list(n.relationships.keys())) # SOURCE / PREVIOUS / NEXT

一个"表格被斩断"的实验

取一份含表格的制度文档,用不同 chunk_size 切分,观察同一问题召回的内容差异:

policy = """差旅报销标准表: | 职级 | 住宿上限/晚 | 餐补/天 | | P5及以下 | 350 | 100 | | P6-P7 | 500 | 150 | | P8及以上 | 800 | 200 | 备注:表中金额均为含税上限,超支部分自理。""" for size in (128, 512): nodes = SentenceSplitter(chunk_size=size).get_nodes_from_documents( [Document(text=policy)]) print(f"chunk_size={size} → {len(nodes)} 个节点") for i, node in enumerate(nodes): print(f" 节点{i}末尾: ...{node.text[-25:]}")

chunk_size=128 时表格大概率被切成两半——"P8 及以上"那行与表头分家,检索"P8 住宿标准"时召回到的片段没有表头,模型只能靠猜列含义。512 时整表在一个节点里,一次召回全部信息。这个微型实验揭示的规律在任何真实语料上都会重演:切分单位要和"回答问题所需的最小完整信息单元"对齐。制度条文按条款切、代码按函数切、对话按轮次切——默认的通用切分只是没有更好信息时的折中。

元数据:被低估的检索杠杆

metadata 不只是"贴标签",它在三个层面参与检索:其一,过滤检索——查询时限定 filters={"dept": "finance"},直接砍掉一半噪音候选;其二,注入提示词——默认模板会把元数据拼进节点文本,模型能看到"这段来自 2024 版制度"从而正确处理新旧版本冲突;其三,出处渲染——回答引用里显示文件名与章节。

# 节点级追加元数据:给每个节点带上它在原文中的位置 for i, node in enumerate(nodes): node.metadata["chunk_index"] = i node.metadata["total_chunks"] = len(nodes) # 过滤检索的用法预览(第4章展开) from llama_index.core.vector_stores import MetadataFilters, ExactMatchFilter filters = MetadataFilters(filters=[ExactMatchFilter(key="dept", value="finance")]) retriever = index.as_retriever(filters=filters)

⚠️ 常见坑:敏感字段放进 metadata 后默认会进入提示词。用 excluded_embed_metadata_keysexcluded_llm_metadata_keys 把不该给模型看的(内部主键、手机号)排除掉,这是第 6 章合规话题在数据层的伏笔。

本节要点回顾

  • 两级容器:Document 承载源文件全貌,Node 是检索的最小单元,字段里最值钱的是 relationships。
  • 粒度对齐原则:切分单位对齐"最小完整信息单元",通用切分只是无结构信息时的折中。
  • 表格实验:128 与 512 两种 chunk_size 的对照,直观展示切分如何决定召回内容的完整性。
  • 元数据三重角色:过滤、注入提示词、出处渲染;敏感字段要显式排除。
  • 两种来路:结构化数据手工造 Document,长文档走切分,来路决定粒度自由度。

常见问题

chunk_overlap 设多少合适? 经验起点是块大小的 10% 到 20%。重叠是为了防止关键信息恰好落在切分边界上被斩断,但重叠过大意味着同样的内容被存多份、召回时互相挤占名额。与其调大重叠,不如先把切分边界对齐到自然结构(段落、条款、函数),边界对了重叠的需求自然小。

节点里要不要保留原文档的层级路径(第几章第几节)? 强烈建议保留,放进 metadata。它有两个回报:检索时可以按章节过滤;回答引用时能显示"某某制度第三章第二节"这种人类友好的出处,而不是一串文件名加分数。层级路径的抽取在 Reader 或 Transformer 阶段做一次,全链受益。

同一篇文档的不同版本(2023 版与 2024 版制度)怎么处理? 版本号进 metadata,摄取时明确标注生效状态。检索时优先新版本(可用 7.2 节的自定义检索器加权),合成时把版本信息注入提示词让模型正确处理"新旧对比"类问题。把新旧版本无差别混在一个索引里,模型会随机引用作废条款——这是知识库问答的经典事故。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U