第 3 课:从文档到答案的完整工作原理
RAG 课程 · 03 |目标:说清“存什么、召回什么、交给模型什么”|用时:25 分钟
你刚才的问题可以浓缩成一句话:如果我存进去一个关于请假的片段,查询“请假”时,RAG 到底会返回什么?
答案是:RAG 通常返回数据库中的多个 chunk 记录;每条记录的文本就是存入时定义的那一段,而不是自动返回整篇原始文档。返回几条由 top_k、过滤条件、相似度阈值和后续重排共同决定。
先区分四个对象
| 对象 | 含义 | 例子 |
|---|---|---|
| 原始文档 | 用户真正上传或系统读取的完整资料。 | 《员工考勤与请假制度》 |
| Chunk | 原始文档切分后用于检索的文本单元。 | “年假申请需要提前 3 个工作日……” |
| 召回候选 | 向量搜索或关键词搜索返回的候选 chunk 集合。 | top-k 返回的 10 个片段 |
| 最终上下文 | 过滤、重排、去重、扩展后放进 prompt 的片段集合。 | 最终给模型的 3 个片段 |
“召回一整段”中的“一整段”没有固定大小。它可能是一段话、一个小节,也可能是按固定 token 数切出的文本;关键取决于你如何做 chunking。
一、离线入库:知识如何进入 RAG
这条链路通常只在文档新增或更新时执行:
原始文档
→ 解析文本与结构
→ 切分成 chunks
→ 给每个 chunk 添加 metadata
→ 计算 embedding
→ 写入向量数据库与索引1. 解析
系统先读取 Markdown、PDF、Word、网页等内容,提取正文、标题、页码、表格和链接。解析质量很重要:如果 PDF 读出来的段落顺序已经错了,后续 embedding 再好也无法补救。
2. 切分
假设原文是:
# 员工请假制度
## 年假
员工入职满一年后,每年享有 5 天年假。年假申请应至少提前 3 个工作日提交。
## 病假
病假超过 2 天时,需要上传医院证明。病假申请应在返回工作后补充材料。可以切成:
Chunk A:员工入职满一年后,每年享有 5 天年假。
Chunk B:年假申请应至少提前 3 个工作日提交。
Chunk C:病假超过 2 天时,需要上传医院证明。
Chunk D:病假申请应在返回工作后补充材料。也可以按标题保留上下文:
Chunk A:员工请假制度 > 年假
员工入职满一年后,每年享有 5 天年假。年假申请应至少提前 3 个工作日提交。
Chunk B:员工请假制度 > 病假
病假超过 2 天时,需要上传医院证明。病假申请应在返回工作后补充材料。第二种通常更容易检索和引用,因为 chunk 自身带有章节语义。切得太小会丢失上下文,切得太大则会带入无关内容;这就是后续要评测和优化的 chunking 问题。
3. 添加 metadata
每个 chunk 不应该只有一段字符串,还应该带有可过滤和可引用的信息:
{
"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 用于追溯来源,section 和 page 用于展示引用,tenant_id 用于权限隔离,parent_id 可以在召回后找回父章节。
4. 向量化与写入
Embedding 模型把每个 chunk 转成向量:
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 记录,不是“理解后的整篇文档”。
二、在线问答:查询如何变成答案
用户输入:
请假在线链路如下:
用户问题
→ 查询理解或改写
→ 计算 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. 过滤与重排
向量相似不等于业务上一定相关。系统可能先过滤:
tenant_id = 当前用户所属租户
document_status = published然后使用 reranker 对候选文本和完整 query 做更精细的相关性判断。典型策略是先召回 20 个候选,再重排后只保留 3 个。
4. 上下文组装
最终交给 LLM 的可能是:
[来源:员工请假制度 / 年假]
年假申请应至少提前 3 个工作日提交。
[来源:员工请假制度 / 病假]
病假超过 2 天时,需要上传医院证明。这一步可以做去重、按文档顺序排列、限制总 token 数,也可以根据 parent_id 把命中的小 chunk 扩展为父章节的一部分。扩展上下文是应用层的策略,不是向量数据库的默认行为。
5. 生成与引用
应用把用户问题和最终上下文放进 prompt,要求模型只依据证据回答:
问题:请假
参考资料:
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 数和重复率 |
本课练习
请设计一个“公司请假制度”知识库:
- 写出至少 3 个 chunk,并给每个 chunk 设计
document_id、section和parent_id。 - 对查询“请假”预测会召回哪些 chunk。
- 对查询“年假需要提前几天申请”预测召回结果会如何变化。
- 说明什么时候应该直接使用 chunk,什么时候应该根据
parent_id扩展上下文。
把你的设计发给我。能正确区分这四个对象后,下一步就进入 embedding、chunking 和数据建模的代码实践。
主读材料
Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks。阅读时重点关注“外部检索记忆”和“生成模型如何使用检索结果”这两个概念。
返回课程
有任何不清楚的地方,直接向老师提问。