Day 02 / 共 20 天 · 第 1 周 RAG 全景与数据摄入

核心数据结构:Document 与 Node

昨天画了 RAG 六步流水线。今天学"在这条流水线上流动的东西"——数据。就像学做菜先认食材:LlamaIndex 的"食材"只有两种——Document(整份文档)和 Node(切出来的小块)。认清它们,后面每一步(切、嵌、检、答)都是在加工它们。

📍 你在整条链的位置
Day1 全景 Day2 数据结构(地基) 读→切→嵌→存 检索→合成 Agent
L01

为什么先学数据结构

🤔 为什么不直接讲"读文档",要先讲数据结构? 因为流水线每一步的"输入"和"输出"都是同一种东西。不先认识它,后面会一直懵:"Reader 产出什么?切块切的是什么?嵌入嵌的是什么?"
💡 一句话本质 LlamaIndex 里流动的一切都是 Node(Document 是一种特殊的大 Node)。整条 RAG 流水线 = 不断地"加工 Node":Reader 产出 Node、切块把大 Node 切成小 Node、嵌入给 Node 填向量、检索找出相关 Node、合成把 Node 喂给 LLM。认识 Node = 拿到贯穿全课的"通用货币"。
L02

Document 与 Node 的关系

💡 一句话 Document = 一份完整的原始文档(一个 PDF、一个网页);Node = 从 Document 切出来的一小块(一段话)。
Document(1 份 PDF) 《财务制度》 切块(Day4) Node 1:报销上限 3000 元…id, embedding, metadata, 关系 Node 2:出差标准…id, embedding, metadata, 关系 Node 3:审批流程…id, embedding, metadata, 关系 NEXT/PREV 记住彼此顺序(L6) 💡 源码里 Document 其实继承自 Node(schema.py:1097 class Document(Node))——它就是"一个特殊的大 Node"
一份 Document 被切成多个 Node,每个 Node 带 id/向量/元数据/关系,还记得彼此的前后顺序。

源码继承链(schema.py):BaseComponent:81)→ BaseNode:264)→ TextNode:765)→ Document:1097class Document(Node))。

一句大白话 Document 是"整本书",Node 是"撕下来的一页"。整个 RAG 就是"把书撕成页 → 给每页贴索引卡 → 提问时抽出相关的几页 → 拿这几页去答题"。
L03

先看一个真实 Node 长什么样

抽象说了半天,直接看一个 Node 打印出来是什么(这样后面讲字段你就有画面了):

📝 一个真实 Node(切块后、嵌入后)
TextNode(
  id_       = "a1b2-c3d4",                       # 唯一编号
  text      = "单次报销上限为 3000 元,需附发票…",  # 这块的正文
  embedding = [0.021, -0.15, 0.33, ...],         # 向量(1536 个数字,嵌入后才有)
  metadata  = {"file_name": "财务制度.pdf",       # 元数据:来源
               "page": 3},                        #        第几页
  start_char_idx = 420, end_char_idx = 468,       # 在原文档的字符位置
  relationships  = {SOURCE: 财务制度.pdf,          # 我来自哪份文档
                    PREV: Node0, NEXT: Node2},     # 前一块、后一块
)
读法:下面 L04-L06 就是逐个讲这些字段"为什么存在、干什么用"。现在有了这个画面,字段就不抽象了。
L04

BaseNode 的五个核心字段

schema.py:264-302 定义 BaseNode(用 Pydantic,类型安全、能存盘)。对着 L03 的例子看:

class BaseNode(BaseComponent):
    id_: str = Field(default_factory=lambda: str(uuid.uuid4()))   # ① 身份证号
    embedding: Optional[List[float]] = None                       # ② 向量(Day6 填)
    metadata: Dict[str, Any] = Field(default_factory=dict)        # ③ 元数据(来源等)
    excluded_embed_metadata_keys / excluded_llm_metadata_keys     # ④ 元数据的"两副面孔"开关(L5)
    relationships: Dict[NodeRelationship, ...] = {}               # ⑤ 与其他 Node 的关系(L6)
💡 五字段各管一件事id_:唯一标识(存取/去重用);② embedding:向量(Day6 嵌入后填,检索靠它);③ metadata:附加信息(文件名/页码,溯源用);④ 两个 excluded 开关:控制 metadata 在不同场景显不显示(L5);⑤ relationships:和别的 Node 的联系(L6)。记住这五样,就掌握了 Node 的全貌。
L05

metadata 的两副面孔(精妙设计)

🤔 同一份 metadata,为什么要分场景显示? 一个 Node 的 metadata 可能有:文件名、页码、创建时间、内部 ID……但——算向量(嵌入)时,带"创建时间"会干扰语义匹配;给大模型看时,带"内部 ID"没意义还占空间。一刀切显示全部,效果就差。
💡 解法:用 excluded 开关 + MetadataMode,让同一 Node 在不同场景"露出不同字段" MetadataModeschema.py:242:ALL/EMBED/LLM/NONE)+ 两个 excluded_*_metadata_keys 配合,实现"同一份 metadata、三副面孔"。
📝 同一个 Node,三种视图 假设 metadata = {file_name, page, 内部ID, 创建时间},设 excluded_embed_metadata_keys=[创建时间]excluded_llm_metadata_keys=[内部ID]
get_content(mode=EMBED) → 文本 + {file_name, page, 内部ID}(嵌入时不带创建时间)
get_content(mode=LLM) → 文本 + {file_name, page, 创建时间}(给 LLM 不带内部ID)
get_content(mode=NONE) → 只有纯文本
为什么这直接影响检索质量 嵌入时带对的 metadata(如"标题")能让向量更贴合语义、检索更准;带错的(如"时间戳")会污染向量。给 LLM 带"文件名"能让它答案标出处、带"内部ID"纯属噪声。这个"分场景控制 metadata"的细节,是 LlamaIndex 检索质量的隐形功臣。
L06

Node 之间的关系:不做孤岛

🤔 切块后,Node 之间还需要联系吗? 需要!检索命中"报销上限 3000 元"这一块,但用户可能还想知道上下文("什么情况下例外?"就在下一块)。如果 Node 是孤立的,就没法"顺藤摸瓜"。
💡 五种关系(schema.py:207-224) NodeRelationshipSOURCE(我来自哪份文档,溯源)、PREVIOUS/NEXT(同文档的前后块,取上下文)、PARENT/CHILD(层级:大块套小块)。
📝 关系怎么被用起来(Day 14 会实现) 检索命中 Node2(报销上限3000),score 很高但内容太短 → 顺着 NEXT 取到 Node3(例外情况)一起给 LLM → 答案更完整。
这就是后面 PrevNextNodePostprocessor/SentenceWindow 的底层依据(Day 4/14)。
读法:关系用 RelatedNodeInfo:249,存 node_id + type + hash)表达;BaseNode 提供 source_node/prev_node/next_node 等属性(:398-445)访问。切块时(Day 4)会自动建好这些关系——今天先知道"关系存在、为什么存在"。
L07

NodeWithScore:带分数的 Node

💡 一句话 schema.py:1035NodeWithScore = 一个 Node + 一个相关度分数。检索器返回的就是它——不只给你 Node,还告诉你"这块和问题有多相关"。
📝 检索结果长这样
[ NodeWithScore(node=Node2, score=0.89),   # 最相关
  NodeWithScore(node=Node7, score=0.81),
  NodeWithScore(node=Node3, score=0.74) ]   # 相关度递减
后处理(Day 14)就是操作这些分数:只留 score>0.7 的按 score 重排
读法:把它当"检索结果的标准信封"——里面装一块内容 + 一个相关度。整个第 3 周(检索/后处理/合成)都在传递它。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 为什么先学数据结构?(Node 是贯穿全课的"通用货币")
  • Document 和 Node 的关系?(整本书 vs 撕下的一页;Document 继承 Node)
  • BaseNode 的五个核心字段各管什么?
  • metadata 为什么要分 EMBED/LLM 视图?怎么影响检索质量?
  • 五种 Node 关系?"顺着 NEXT 取上下文"是怎么回事?

✋ 动手(对照 L03 的例子看源码字段)

cd /Users/bitmart/work/codes/github/llama_index/llama-index-core/llama_index/core
sed -n '264,302p' schema.py     # BaseNode 五字段
sed -n '207,224p' schema.py     # NodeRelationship 五种关系
sed -n '242,248p' schema.py     # MetadataMode 三副面孔
grep -n "def source_node\|def next_node\|def get_content" schema.py | head
明天预告 · Day 03:地基有了,开始走流水线第①步——读数据(Reader)。各种格式(PDF/网页/数据库)怎么统一变成 Document?精读 SimpleDirectoryReader:怎么遍历目录、按文件类型选解析器、并行加载。
← Day 01 Day 03 · Reader →