RAG 工程化:从文档到可评估答案
这篇文章解决什么问题
很多人学习 RAG 时只关注"怎么接向量库"——上传文档、切分文本、写入向量库、检索 TopK、拼接上下文、调用模型。这个流程能跑通 Demo,但上线后会发现效果不稳定、答案不可信、问题无法定位。
真正做项目时,RAG 的效果取决于完整链路:
文档解析 → 文本清洗 → Chunk 切分 → Embedding → 向量入库 → Query 改写 → 检索 → Rerank → 上下文构建 → 答案生成 → 引用溯源 → 质量评测 → 失败样本迭代任何一环出问题,最终答案质量都会受影响。文档解析失败会导致知识缺失,Chunk 粒度不合适会导致召回不准,Embedding 版本混乱会导致结果不可复现,没有引用溯源就无法判断答案依据,没有评测集就只能凭感觉判断效果。
核心观点:RAG 的难点不是把文档塞进向量库,而是建立一条可评估、可追踪、可迭代的知识增强链路。
RAG 为什么不能只理解为"向量库 + Prompt"
向量库只解决相似内容召回,不保证答案正确。用户问"订单 12345 的状态",向量检索可能召回一堆关于"订单状态"的文档片段,但不一定包含 12345 这个具体订单的信息。
Prompt 只能组织上下文,不解决文档解析和召回质量。如果检索结果本身就不对,Prompt 再精巧也救不回来。
没有引用溯源,就很难判断答案依据。答案看起来正确,但你不知道它是基于知识库生成的,还是模型根据自身知识猜出来的。如果是后者,知识库更新后答案可能悄悄变错。
没有评测集,就只能凭感觉判断效果。"感觉还行"不是工程标准。改了 Chunk 策略、换了 Embedding 模型、调了 top_k,如果没有指标对比,就不知道改动是变好了还是变差了。
没有失败样本库,就无法持续迭代。每次上线后发现的坏案例应该被收集、分析、归类,变成下一轮优化的输入。不收集失败样本,优化就是盲目的。
RAG 工程链路总览
| 阶段 | 作用 | 常见问题 |
|---|---|---|
| 文档解析 | 把 PDF、Word、HTML、表格等格式转成可处理文本 | 解析失败静默丢弃、表格结构丢失、扫描件 OCR 质量差 |
| 文本清洗 | 去除页眉页脚、导航栏、广告、重复空白 | 过度清洗删除标题和编号、清洗规则不统一 |
| Chunk 切分 | 把文档切成可检索的最小单元 | Chunk 太大引入噪声、太小丢上下文、切断语义 |
| Embedding | 把 Chunk 文本转成向量表示 | 模型版本不记录、不同版本混用、不支持增量更新 |
| 向量入库 | 写入向量库和元数据库 | 不支持幂等、metadata 缺失、权限信息没写入 |
| Query Rewrite | 改写用户问题以提升召回 | 过度改写改变原意、不改写导致口语化问题召回差 |
| Hybrid Search | 结合向量检索和关键词检索 | 只用向量检索导致编号/术语召回差、融合权重不合理 |
| Rerank | 对候选结果重新排序 | 不做 Rerank 导致噪声进上下文、Rerank 模型选择不当 |
| Context Build | 把候选 Chunk 组织成模型输入 | 总长度超限、来源标识丢失、矛盾内容混入 |
| Answer Generation | 基于上下文生成答案 | 上下文不足时强行编造、不引用来源、格式不一致 |
| Citation | 把答案依据连接回原始文档 | 引用缺失、引用指向错误 Chunk、引用格式不统一 |
| Evaluation | 系统化评估 RAG 效果 | 没有评测集、只靠人工感觉、不版本化评测结果 |
| Feedback Loop | 收集失败样本驱动迭代 | 失败样本不收集、不分析、不回到测试集 |
文档解析:RAG 质量的第一道关
文档解析是知识进入系统的第一个环节。如果解析阶段就丢失了信息,后面的 Chunk、Embedding、检索再怎么优化也补不回来。
不同格式的处理重点不同:
- PDF:多栏排版、页眉页脚、表格、扫描件、分页断句。解析时要保留页码和段落位置。扫描件需要 OCR,OCR 结果可能有错别字,应记录置信度。
- Word:标题层级、列表、表格、嵌入对象。解析时保留标题层级和段落结构。
- Markdown:结构相对清晰,标题、列表、代码块、表格容易识别。代码块要保持完整,不要切成多个片段。
- HTML:去除导航、广告、脚注,只保留正文。保留
source_uri用于引用溯源。 - 表格:不能简单按行拼接。表头、单位、列含义对理解关键。建议转成结构化文本或同时保存原始结构和文本摘要。
错误示例:直接用 split("\n\n") 切分 PDF 文本,不保留页码、标题和表格结构。结果是 Chunk 没有来源信息,引用溯源无法工作,表格数据被拆散成无意义的片段。
更合理的做法:使用结构化解析工具提取 PDF 的标题、段落、表格和页码。解析结果携带 document_id、page、section 等 metadata。解析失败的文档记录错误原因,进入重试队列,而不是静默丢弃。
Chunk 策略:不是越大越好,也不是越小越好
Chunk 质量直接影响召回质量。Chunk 太大容易引入噪声,模型在长文本中找不到关键信息;Chunk 太小容易丢上下文,模型看到片段但不知道前后文在说什么。
| Chunk 策略 | 优点 | 风险 | 适用场景 |
|---|---|---|---|
| 固定长度切分 | 实现简单,Chunk 大小稳定 | 容易切断语义 | 文本结构弱、快速原型 |
| 按标题切分 | 保留文档结构,语义完整 | Chunk 大小可能不均匀 | Markdown、技术文档、手册 |
| 按语义切分 | 更符合自然语义边界 | 成本较高,实现复杂 | 高质量知识库、长文档问答 |
| 滑动窗口 | 保留相邻上下文 | 增加重复内容和存储成本 | 需要上下文连续性的文档 |
| Parent-Child Chunk | 兼顾小粒度召回和大上下文生成 | 数据结构更复杂 | 长文档、章节型知识库 |
| Chunk Overlap | 降低边界信息丢失 | Overlap 过大导致重复召回 | 段落边界不稳定的文本 |
Parent-Child Chunk 的思路值得单独说明:用小 Chunk 做召回(提高精度),用父级段落或章节做上下文补充(保留背景)。检索时命中小 Chunk,但送入模型的是包含该 Chunk 的更大段落。
Chunk 切分时必须记录 document_id、chunk_id、章节、页码、顺序号和 token 数。这些信息是引用溯源和问题排查的基础。
Embedding 版本管理
RAG 项目必须记录以下信息:
| 字段 | 作用 |
|---|---|
embedding_model | 记录使用的 Embedding 模型名称 |
embedding_version | 记录模型版本,支持升级后重建 |
chunk_hash | 判断 Chunk 内容是否变化,支持去重和增量更新 |
document_id | 关联原始文档 |
chunk_id | 唯一标识 Chunk |
vector_id | 向量库中的存储标识 |
created_at | 记录生成时间,支持过期清理 |
为什么需要这些信息:
- 模型升级后需要重建向量。不同 Embedding 模型的向量空间不兼容,不能简单混用。
- 不同版本混用会影响召回稳定性。一次查询可能在旧模型向量和新模型向量之间比较,分数没有可比性,召回结果变得不可解释。
chunk_hash支持去重和增量更新。文档重新上传但 Chunk 文本不变时,可以避免重复生成 Embedding。- version 信息有利于评测对比和问题排查。对比不同版本的召回结果,定位问题是模型退化还是数据变化。
检索策略:从单一向量检索到 Hybrid Search
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 向量检索 | 根据语义相似度召回 Chunk | 用户问题和文档表达不同但语义相近 |
| 关键词检索 | 根据 BM25 等方式召回 | 编号、术语、错误码、精确短语 |
| Hybrid Search | 融合向量检索和关键词检索 | 企业知识库、技术文档、工单系统 |
| Metadata Filter | 按租户、权限、类型、时间过滤 | 多用户、多知识库、权限敏感场景 |
| Query Rewrite | 改写用户问题以提升召回 | 问题口语化、缺少上下文、术语不统一 |
| Multi-query | 生成多个查询表达再融合结果 | 问题复杂或表达可能有多种角度 |
| Rerank | 对候选结果重新排序 | 初始召回有噪声但候选中包含正确答案 |
向量检索适合语义匹配,但对编号、型号、错误码不一定稳定。用户问"ERR-4012 怎么解决",向量检索可能召回一堆"错误码"相关的内容,但不包含 ERR-4012 的具体信息。
关键词检索适合精确匹配,但对同义表达不够灵活。用户问"怎么退款",关键词检索可能找不到标题为"退货流程"的文档。
Hybrid Search 结合两者,在 RAG 工程中很常见。真实业务问题往往既有语义表达,也包含专有名词和结构化条件。
Query Rewrite 解决用户问题表达不完整的问题。"订单出问题了"太模糊,改写后可以变成"订单状态查询"或"订单异常处理"。
Rerank 是检索的第二道关。先用宽松策略召回较多候选(如 Top20),再用更精细的模型或规则选出 Top5 进入上下文。
引用溯源:答案可信度的证据
答案应尽量返回以下信息:
| 字段 | 作用 |
|---|---|
document_id | 标识来源文档 |
chunk_id | 标识具体 Chunk |
source_uri | 指向原始文档位置 |
page | 页码,方便定位 |
section | 章节标题,方便理解上下文 |
score | 检索分数,辅助判断相关性 |
引用不是装饰,而是答案可信度的证据。用户可以根据引用判断答案是否来自正确资料,评测系统可以根据引用判断检索是否命中目标文档。
没有引用的答案很难评测和排错。即使答案看起来正确,也无法判断它是基于知识库生成,还是模型根据自身知识猜出来的。如果是后者,知识库更新后答案可能悄悄变错。
工程上建议把引用作为响应结构的一部分,而不是让模型在自然语言中自由生成引用。结构化引用可以被程序验证,自由生成的引用可能指向不存在的来源。
RAG 评测指标
| 指标 | 关注点 |
|---|---|
| Recall@K | 正确文档或 Chunk 是否出现在前 K 个检索结果中 |
| MRR | 第一个正确结果排在多靠前 |
| Context Precision | 进入上下文的内容中有多少是相关的 |
| Context Recall | 答案所需信息是否被上下文覆盖 |
| Faithfulness | 答案是否忠实于给定上下文 |
| Answer Relevance | 答案是否真正回答了用户问题 |
| Citation Accuracy | 引用是否指向支持答案的正确来源 |
| Human Review | 人工从业务正确性和可用性角度复核 |
Recall@K 适合评估召回阶段。如果正确文档没有进入候选集,后面的 Rerank 和生成再强也很难补救。
Faithfulness 关注答案是否基于上下文。它可以帮助发现模型在上下文不足时编造内容的问题——上下文里没有的信息,答案不应该包含。
Citation Accuracy 对企业 RAG 很关键。答案正确但引用错误,会降低可信度,也会影响问题排查。
Human Review 是最后的兜底。自动评测能覆盖大部分场景,但边界案例、业务正确性、用户体验还需要人工判断。
失败样本库:RAG 迭代的核心资产
失败样本库应该记录以下类型:
| 失败类型 | 典型表现 | 迭代方向 |
|---|---|---|
| 检索不到 | 正确文档不在候选集中 | 检查 Chunk、Embedding、检索策略 |
| 检索到了但排序靠后 | 正确文档在 Top20 但不在 Top5 | 增加 Rerank、调整融合权重 |
| 检索结果无关 | 召回的 Chunk 和问题不相关 | 检查 Query Rewrite、Metadata Filter |
| 答案幻觉 | 答案包含上下文没有的信息 | 调整 Prompt、检查 Faithfulness |
| 引用错误 | 引用指向不相关的 Chunk | 检查引用映射逻辑 |
| 文档解析失败 | 文档没有进入知识库 | 检查解析器、记录错误原因 |
| 权限过滤错误 | 用户看到无权限的内容 | 检查 Metadata Filter |
| 用户反馈差 | 用户标记答案无用 | 综合分析,进入人工审查 |
失败样本不是简单收集错误答案,而是要记录:问题、期望答案、实际答案、检索结果、引用、模型版本、检索参数、用户反馈。
失败样本驱动迭代的典型路径:
- 正确文档从未被召回 → 检查解析、Chunk、Embedding、检索策略
- 正确文档被召回但答案错误 → 检查上下文构建、生成 Prompt
- 答案正确但引用错误 → 检查引用映射逻辑
- 大量用户反馈差 → 综合分析,可能需要调整整体架构
一个最小 RAG 工程伪代码
def rag_query(question: str, user_context: dict):
# 1. Query 改写
rewritten_query = rewrite_query(question)
# 2. Hybrid Search + 权限过滤
candidates = hybrid_search(
query=rewritten_query,
filters={
"tenant_id": user_context["tenant_id"],
"permission_level": user_context["permission_level"],
},
top_k=20,
)
# 3. Rerank
reranked = rerank(question, candidates)
# 4. 上下文构建
context = build_context(reranked[:5])
# 5. 答案生成
answer = generate_answer(question, context)
# 6. 引用收集
citations = collect_citations(reranked[:5])
# 7. Trace 记录
record_trace(question, candidates, reranked, answer, citations)
return {
"answer": answer,
"citations": citations,
}这个伪代码表达的是生产 RAG 的基本结构:先扩大候选召回,再精排,再构建上下文,最后生成答案和引用。每一步都应该有独立的日志和错误处理。
真实系统还需要补充:Trace 记录(用于问题排查)、失败样本收集(用于迭代)、评测指标计算(用于版本对比)、缓存(用于性能优化)。
对个人项目的启发
RAG 工程化的能力可以迁移到多个方向的项目中。
通用迁移思路:不管做什么类型的项目,只要涉及"让模型基于外部知识回答问题",就需要考虑文档解析、Chunk 策略、Embedding 版本管理、引用溯源和评测闭环。这些能力不绑定具体业务,是 RAG 工程的通用骨架。
项目 A RAG 工单系统可以迁移:
- 文档解析:工单系统的产品文档、历史工单、知识库都有自己的格式,解析时要保留标题、页码、表格结构,失败不能静默丢弃。
- Chunk 策略:工单文档的粒度和通用文档不同,需要根据工单场景调优 Chunk 大小和切分方式。
- Embedding 版本:记录每次入库使用的模型和版本,模型升级后能重建向量,也能对比不同版本的召回效果。
- 引用溯源:答案必须返回来源工单 ID、文档 ID、页码,让用户能验证答案依据。
- 用户反馈:用户标记"答案无用"的案例自动进入失败样本库。
- 失败样本库:收集检索不到、答案幻觉、引用错误等失败类型,定期分析后更新测试集和优化策略。
- RAG 评测:构建覆盖常见场景和边界情况的评测集,每次调整 Chunk、Embedding 或 Rerank 后运行回归测试。
项目 B 多 Agent Copilot 可以迁移:
- Agent 检索外部知识时记录引用:多 Agent 系统中,每个 Agent 调用 RAG 工具时,返回结果必须携带引用信息,方便追踪知识来源。
- Agent 使用知识库时记录 run_id:每次 RAG 查询关联到 Agent 的 run_id,出了问题可以通过 run_id 追溯完整的执行链路。
- 多 Agent 输出结果要能追踪依据:最终聚合结果需要能还原到每个 Agent 的检索来源和引用,不能只看最终答案。
面试表达
我不会把 RAG 简化成"向量库 + Prompt"。向量库只解决相似内容召回,不保证答案正确;Prompt 只能组织上下文,不解决文档解析和召回质量。真正做项目时,RAG 的效果取决于完整链路——从文档解析、Chunk 切分、Embedding、检索、Rerank、上下文构建、答案生成到引用溯源和质量评测,每一环都可能影响最终效果。
我会把 RAG 拆成解析、切块、向量化、检索、重排、上下文构建、生成、引用和评测几个环节。如果 RAG 效果不好,我会先定位是解析问题、召回问题、排序问题、上下文构建问题还是生成问题。比如正确文档没有被召回,就检查 Chunk、Embedding 和检索策略;正确文档被召回但答案错误,就检查上下文构建和生成约束。
生产级 RAG 必须有评测集和失败样本库。评测集覆盖常见场景和边界情况,每次修改后运行回归测试验证没有退化。失败样本库收集线上发现的坏案例,分析失败模式后驱动迭代——调 Chunk、调检索参数、加 Rerank、改 Query Rewrite。引用溯源是答案可信度的基础,没有引用的答案无法评测也无法排错。
后续 TODO
- 补充 RAG 评测样例,包括测试用例的标注方法和评测脚本。
- 补充 Chunk 策略对比实验,验证不同切分方式对召回质量的影响。
- 补充 Hybrid Search 实现示例,包括 RRF 融合排序的代码。
- 补充项目 A 的 RAG 链路图,展示从文档入库到查询回答的完整流程。
相关链接
- RAG 工程化笔记 — 更详细的工程化知识点
- 向量数据库工程化 — 向量库选型和索引设计
- Evaluation Pipeline — 评测流水线设计
- Agent Trace 执行轨迹 — Trace 记录方案
- 从 RAG 到生产级 Agent Harness 的工程化学习路线 — 完整学习路线