Skip to content

第 3 课:从文档到答案的完整工作原理

RAG 课程 · 03 |目标:说清“存什么、召回什么、交给模型什么”|用时:25 分钟

你刚才的问题可以浓缩成一句话:如果我存进去一个关于请假的片段,查询“请假”时,RAG 到底会返回什么?

答案是:RAG 通常返回数据库中的多个 chunk 记录;每条记录的文本就是存入时定义的那一段,而不是自动返回整篇原始文档。返回几条由 top_k、过滤条件、相似度阈值和后续重排共同决定。

先区分四个对象

对象含义例子
原始文档用户真正上传或系统读取的完整资料。《员工考勤与请假制度》
Chunk原始文档切分后用于检索的文本单元。“年假申请需要提前 3 个工作日……”
召回候选向量搜索或关键词搜索返回的候选 chunk 集合。top-k 返回的 10 个片段
最终上下文过滤、重排、去重、扩展后放进 prompt 的片段集合。最终给模型的 3 个片段

“召回一整段”中的“一整段”没有固定大小。它可能是一段话、一个小节,也可能是按固定 token 数切出的文本;关键取决于你如何做 chunking。

一、离线入库:知识如何进入 RAG

这条链路通常只在文档新增或更新时执行:

text
原始文档
  → 解析文本与结构
  → 切分成 chunks
  → 给每个 chunk 添加 metadata
  → 计算 embedding
  → 写入向量数据库与索引

1. 解析

系统先读取 Markdown、PDF、Word、网页等内容,提取正文、标题、页码、表格和链接。解析质量很重要:如果 PDF 读出来的段落顺序已经错了,后续 embedding 再好也无法补救。

2. 切分

假设原文是:

text
# 员工请假制度

## 年假
员工入职满一年后,每年享有 5 天年假。年假申请应至少提前 3 个工作日提交。

## 病假
病假超过 2 天时,需要上传医院证明。病假申请应在返回工作后补充材料。

可以切成:

text
Chunk A:员工入职满一年后,每年享有 5 天年假。
Chunk B:年假申请应至少提前 3 个工作日提交。
Chunk C:病假超过 2 天时,需要上传医院证明。
Chunk D:病假申请应在返回工作后补充材料。

也可以按标题保留上下文:

text
Chunk A:员工请假制度 > 年假
员工入职满一年后,每年享有 5 天年假。年假申请应至少提前 3 个工作日提交。

Chunk B:员工请假制度 > 病假
病假超过 2 天时,需要上传医院证明。病假申请应在返回工作后补充材料。

第二种通常更容易检索和引用,因为 chunk 自身带有章节语义。切得太小会丢失上下文,切得太大则会带入无关内容;这就是后续要评测和优化的 chunking 问题。

3. 添加 metadata

每个 chunk 不应该只有一段字符串,还应该带有可过滤和可引用的信息:

json
{
  "id": "leave-policy-年假-001",
  "text": "员工请假制度 > 年假\n年假申请应至少提前 3 个工作日提交。",
  "metadata": {
    "document_id": "leave-policy",
    "title": "员工请假制度",
    "section": "年假",
    "page": 2,
    "tenant_id": "company-a",
    "parent_id": "leave-policy-年假"
  }
}

document_id 用于追溯来源,sectionpage 用于展示引用,tenant_id 用于权限隔离,parent_id 可以在召回后找回父章节。

4. 向量化与写入

Embedding 模型把每个 chunk 转成向量:

text
Chunk A → [0.12, -0.31, 0.44, ...]
Chunk B → [0.10, -0.28, 0.47, ...]
Chunk C → [0.71,  0.05, -0.12, ...]

向量数据库保存 id + text + embedding + metadata,并为 embedding 建立 FLAT、HNSW、IVF 等索引。数据库保存的是 chunk 记录,不是“理解后的整篇文档”。

二、在线问答:查询如何变成答案

用户输入:

text
请假

在线链路如下:

text
用户问题
  → 查询理解或改写
  → 计算 query embedding
  → 向量检索 / 关键词检索
  → metadata 权限过滤
  → 得到 top-k 候选 chunks
  → reranker 重排
  → 选择最终上下文
  → 组装 prompt
  → LLM 生成答案与引用

1. 查询向量化

系统用和文档相同或兼容的 embedding 模型,把“请假”转换成 query vector。它不是在数据库里直接搜索字符串“请假”,而是在向量空间里寻找语义相近的 chunk。

2. 召回候选

假设配置 top_k=4,向量数据库可能返回:

排名Chunk相似度说明
1年假申请流程0.91与“请假”高度相关
2病假证明要求0.88与“请假”高度相关
3婚假申请规定0.84也属于请假制度
4考勤异常处理0.62可能相关但噪声较大

这时召回的是 4 个候选 chunk,而不是整篇《员工请假制度》。如果数据库里只存了一条“完整请假制度”记录,那么返回的就会是那一整条记录;系统不会自动替你重新切分或恢复原文。

注意:top_k=4 表示最多返回 4 个,不一定最终使用 4 个。还可能受到 metadata filter、相似度阈值和去重规则影响。

3. 过滤与重排

向量相似不等于业务上一定相关。系统可能先过滤:

text
tenant_id = 当前用户所属租户
document_status = published

然后使用 reranker 对候选文本和完整 query 做更精细的相关性判断。典型策略是先召回 20 个候选,再重排后只保留 3 个。

4. 上下文组装

最终交给 LLM 的可能是:

text
[来源:员工请假制度 / 年假]
年假申请应至少提前 3 个工作日提交。

[来源:员工请假制度 / 病假]
病假超过 2 天时,需要上传医院证明。

这一步可以做去重、按文档顺序排列、限制总 token 数,也可以根据 parent_id 把命中的小 chunk 扩展为父章节的一部分。扩展上下文是应用层的策略,不是向量数据库的默认行为。

5. 生成与引用

应用把用户问题和最终上下文放进 prompt,要求模型只依据证据回答:

text
问题:请假

参考资料:
1. 年假申请应至少提前 3 个工作日提交。
2. 病假超过 2 天时,需要上传医院证明。

请只根据参考资料回答,并标注对应来源。

LLM 根据上下文生成答案,但它仍可能误解、遗漏或编造。因此 RAG 不是“检索后就一定正确”,还需要引用约束和离线评测。

三种常见的“召回范围”设计

方案 A:直接召回 chunk

最简单的方案。数据库返回哪个 chunk,prompt 就使用哪个 chunk。适合内容本身已经完整、结构简单的知识库。

方案 B:小 chunk 检索,大 chunk 生成

用较小的 chunk 提高命中精度,命中后根据 parent_id 取回更大的父章节。适合“关键答案很短,但理解答案需要上下文”的文档。

方案 C:chunk 检索,文档级聚合

先找到多个 chunk,再按 document_id 聚合,选择相关性最高的文档或章节。适合需要跨多个片段综合回答的场景,但必须控制上下文噪声。

如何定位“答案不好”的原因

现象可能问题优先检查
明明有答案却没有召回切分、embedding、索引或过滤错误Recall@k、chunk 文本和 metadata
召回了,但前几条不相关query 太宽、top-k 不合适、向量排序不准候选分数、reranker、混合召回
片段相关,但缺少上下文chunk 太小或没有父章节扩展parent_id、上下文组装策略
上下文相关,答案仍然错误prompt、模型、引用约束或问题歧义最终 prompt 和模型输出
答案很长且夹杂无关内容chunk 太大、top-k 太大或没有去重上下文 token 数和重复率

本课练习

请设计一个“公司请假制度”知识库:

  1. 写出至少 3 个 chunk,并给每个 chunk 设计 document_idsectionparent_id
  2. 对查询“请假”预测会召回哪些 chunk。
  3. 对查询“年假需要提前几天申请”预测召回结果会如何变化。
  4. 说明什么时候应该直接使用 chunk,什么时候应该根据 parent_id 扩展上下文。

把你的设计发给我。能正确区分这四个对象后,下一步就进入 embedding、chunking 和数据建模的代码实践。

主读材料

Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks。阅读时重点关注“外部检索记忆”和“生成模型如何使用检索结果”这两个概念。

返回课程

有任何不清楚的地方,直接向老师提问。