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 被切成多个 Node,每个 Node 带 id/向量/元数据/关系,还记得彼此的前后顺序。
源码继承链(schema.py):BaseComponent(:81)→ BaseNode(:264)→ TextNode(:765)→ Document(:1097,class 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 在不同场景"露出不同字段"
MetadataMode(schema.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)
NodeRelationship:SOURCE(我来自哪份文档,溯源)、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:1035 的 NodeWithScore = 一个 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:怎么遍历目录、按文件类型选解析器、并行加载。