RAG(Retrieval-Augmented Generation,检索增强生成)常被用来解决大语言模型知识不足、知识过期、无法访问私有数据的问题。它的基本思路很直接:用户提问后,系统先从知识库里检索相关资料,再把这些资料连同问题一起交给 LLM(Large Language Model,大语言模型)生成答案。
一个最小可运行的 RAG Demo 并不复杂:解析文档、切成 chunk、计算 Embedding、写入向量库、检索、拼 Prompt、调用模型。真正困难的是生产落地,因为每一步都会把误差传给后面的环节。
flowchart LR
A[原始文档] --> B[文档解析与清洗]
B --> C[Chunking 切分]
C --> D[Embedding 向量化]
D --> E[(向量库 / 索引)]
U[用户问题] --> Q[Query 改写]
Q --> R[召回检索]
E --> R
R --> K[Rerank 重排]
K --> P[拼接上下文]
P --> L[LLM 生成答案]
L --> O[返回结果]
RAG 落地最难的地方通常不是某个单点技术,而是整条链路的质量控制。文档解析不干净,检索就会拿到错误材料;检索召回不到关键内容,模型只能凭空猜;没有评估体系,优化就只能靠人工试几条问题。
可以把生产级 RAG 的难点拆成三层:
| 层级 | 主要问题 | 典型后果 |
|---|---|---|
| 文档预处理 | PDF 乱序、表格丢结构、扫描件无法提取、图片信息缺失 | 知识库内容本身就是错的 |
| 检索质量调优 | chunk 不合理、Embedding 不匹配、Query 与文档表达不一致、精确词召回差 | 找不到真正相关的知识 |
| 效果评估 | 缺少标准问题集、指标只看最终回答、无法定位问题环节 | 不知道优化是否有效,也不知道该改哪里 |
文档预处理:知识库质量从源头决定
很多 RAG 系统效果差,并不是模型能力不够,而是知识库里的内容已经被解析坏了。原始文档进入系统前,需要经历格式识别、正文抽取、结构还原、清洗、元数据补充等步骤。任何一步处理粗糙,都会影响后续检索。
常见的文档类型和处理难点如下:
| 文档类型 | 难点 | 处理建议 |
|---|---|---|
| 普通 PDF | 段落顺序可能错乱,页眉页脚混入正文 | 使用版面感知能力更强的解析工具 |
| 表格型 PDF | 行列关系容易丢失 | 用 pdfplumber、camelot 等表格抽取工具 |
| 扫描件 | 没有可直接抽取的文本层 | 先做 OCR(Optical Character Recognition,光学字符识别) |
| 图片型文档 | 关键信息在截图、流程图、票据中 | 结合 OCR 或多模态模型解析 |
| 代码文档 | 代码块被切断后语义失真 | 按函数、类、标题层级切分 |
| 合同 / 财报 / 专利 | 版式复杂,信息密度高 | 高价值文档可使用多模态模型辅助解析 |
PDF 是最容易低估的坑。很多通用 PDF 工具更擅长抽取文本流,但复杂排版并不只是“把文字读出来”这么简单。双栏论文、带合并单元格的报表、嵌套表格、页脚注释,都需要还原阅读顺序和结构关系。
比如一个产品规格表原本长这样:
| 型号 | 内存 | 价格 |
|---|---|---|
| A100 | 8GB | 3999 |
| A200 | 16GB | 4999 |
如果解析后变成:
型号 内存 价格 A100 A200 8GB 16GB 3999 4999
向量化模型并不知道哪个价格对应哪个型号。后续即使召回了这个 chunk,LLM 也可能根据错误上下文生成错误答案。
更可靠的文档预处理流程一般长这样:
flowchart TD
A[上传文档] --> B{识别文档类型}
B -->|文本 PDF / Word / HTML| C[结构化正文抽取]
B -->|扫描件| D[OCR 识别]
B -->|表格密集| E[表格抽取]
B -->|图片信息重要| F[多模态解析]
C --> G[清洗噪声]
D --> G
E --> G
F --> G
G --> H[保留标题 / 表格 / 页码 / 来源等元数据]
H --> I[进入 Chunking]
预处理阶段不只要抽文本,还要尽量保留结构信息。标题层级、表格字段、页码、章节路径、文档来源、更新时间,这些元数据后面都能用于过滤、重排和答案溯源。
一个 chunk 可以设计成这样的结构:
{
"id": "doc-001-page-12-chunk-03",
"content": "退款申请需要在订单完成后的 7 天内提交,超过期限需人工审核。",
"metadata": {
"doc_name": "售后服务手册",
"section": "退款规则 > 申请时限",
"page": 12,
"updated_at": "2026-04-01"
}
}
这样做的好处是,检索时不只依赖正文相似度,还可以按文档类型、业务线、时间范围做过滤;生成答案时也能给出来源,方便用户核对。
检索质量调优:召回不到,模型再强也没用
RAG 的生成质量有一个硬前提:相关材料必须被检索出来。如果检索阶段没有召回关键文档,LLM 只能基于不完整上下文回答,轻则答非所问,重则产生幻觉。
检索质量差通常来自四类问题。
Chunking 切分不合理
Chunking 是把长文档切成较小文本块的过程。切得太大,chunk 内部混入大量无关内容,向量表示会变“平均”,检索时相关度下降;切得太小,上下文不完整,单个 chunk 无法回答问题。
| 切分方式 | 优点 | 风险 | 适合场景 |
|---|---|---|---|
| 固定长度切分 | 实现简单,速度快 | 容易切断语义 | 普通说明文档、低复杂度文本 |
| 按标题层级切分 | 保留章节结构 | 依赖文档结构质量 | 手册、知识库、制度文档 |
| 语义切分 | 更接近自然语义边界 | 实现成本更高 | 问答知识库、长文档 |
| 代码结构切分 | 保留函数、类、模块边界 | 需要语言解析器 | API 文档、代码仓库 |
一个比较稳妥的策略是:优先按结构切分,再用长度约束兜底,同时保留适当 overlap(重叠文本)。例如:
chunk_config = {
"split_by": ["heading", "paragraph"],
"max_tokens": 500,
"overlap_tokens": 80,
"keep_metadata": ["doc_name", "section", "page"]
}
overlap 的作用是避免关键信息刚好落在两个 chunk 边界上。例如“退款条件”在上一段,“申请入口”在下一段,如果完全硬切,两个信息点可能被拆散。适当重叠能提高召回完整性,但 overlap 太大会增加存储量和重复召回。
Query 和文档表达不一致
用户提问往往很口语化,文档写法通常更正式。用户可能问:
为什么这个功能打不开?
知识库里对应的标题却是:
系统异常排查指南:功能入口不可用的处理流程
两者语义相关,但字面表达差异很大。单纯依赖向量相似度时,召回结果可能不稳定。
常见解决方式有两种:
- Query 改写:把用户问题改写成更适合检索的形式。
- 假设性问题增强:为每个 chunk 生成可能被用户问到的问题,并一起写入索引。
Query 改写示例:
用户问题:
这个功能怎么用不了?
改写后:
功能入口不可用的原因是什么?如何排查系统功能无法使用的问题?
假设性问题增强可以把一个知识块扩展成多个检索入口:
{
"content": "当功能入口不可用时,需检查用户权限、系统版本、灰度开关和服务状态。",
"generated_questions": [
"为什么功能入口打不开?",
"功能无法使用应该怎么排查?",
"用户看不到某个功能是什么原因?"
]
}
这样做会增加索引规模,但能缓解用户表达和文档表达之间的差距。
纯向量检索不擅长精确词
Embedding 擅长语义相似,但不一定擅长精确匹配。产品型号、订单号、错误码、接口名、缩写、专有名词,往往更适合关键词检索。
例如用户搜索:
ERR_AUTH_401 怎么处理?
如果知识库里有一段明确包含 ERR_AUTH_401,BM25 这类关键词检索通常比纯向量检索更可靠。BM25 是一种经典文本相关性算法,会根据关键词出现频率、文档长度和词项稀有度计算相关度。
生产系统更常见的做法是混合检索:
flowchart LR
A[用户 Query] --> B[Query 改写]
B --> C[向量检索]
B --> D[BM25 关键词检索]
C --> E[候选结果合并]
D --> E
E --> F[去重]
F --> G[Rerank 重排]
G --> H[Top-K 上下文]
向量检索负责找语义相关内容,BM25 负责抓精确词和专有名词,Rerank 负责在候选结果中重新排序。Rerank 模型通常会同时读取 Query 和候选 chunk,判断二者是否真的相关,精度比单纯向量相似度更高,但延迟和成本也更高。
Embedding 模型不匹配业务语料
Embedding 模型不是越大越一定合适。不同模型在中文、英文、代码、金融、医疗、法律等领域的表现差异明显。选型时至少要关注这些点:
| 维度 | 需要确认的问题 |
|---|---|
| 语言 | 中文、英文、多语言是否都稳定 |
| 领域 | 是否理解业务术语、缩写、行业表达 |
| 上下文长度 | 单个 chunk 的长度是否超过模型支持范围 |
| 向量维度 | 存储成本和检索性能是否可接受 |
| 延迟 | 批量入库和在线检索是否满足要求 |
| 可迁移性 | 更换模型后是否需要全量重建索引 |
Embedding 一旦更换,历史向量通常需要重新计算并重建索引,所以不要只凭几条样例做决定。更稳的方式是准备一批真实问题和标准相关文档,在同一套评估集上比较不同模型的 Hit@K、召回耗时和成本。
效果评估:没有指标就无法稳定优化
RAG 系统最容易陷入的状态是:改了 chunk 大小,感觉好了一点;换了 Embedding,又感觉有些问题变差了;加了 Rerank,单条样例看起来更准,但整体效果不清楚。
解决办法是把评估拆开,不要只看最终回答。RAG 至少要评估两层:检索层和生成层。
flowchart TD
A[评估问题集] --> B[检索层评估]
A --> C[端到端生成评估]
B --> B1[Hit@K]
B --> B2[MRR]
B --> B3[nDCG]
C --> C1[Faithfulness 忠实度]
C --> C2[Answer Relevancy 答案相关性]
C --> C3[Context Recall 上下文召回率]
B1 --> D[定位召回问题]
C1 --> E[定位幻觉问题]
C2 --> F[定位答非所问]
C3 --> G[定位上下文缺失]
检索层评估:相关文档有没有被找回来
检索层不关心 LLM 最终怎么回答,只看相关文档是否出现在 Top-K 结果中。最常用的指标是 Hit@K。
Hit@K 的含义是:对于一批问题,正确文档出现在前 K 条检索结果中的比例。
Hit@K = 命中问题数 / 总问题数
例如有 100 个问题,其中 82 个问题的标准相关文档出现在 Top 5 检索结果里,那么:
Hit@5 = 82 / 100 = 0.82
一个简单的评估代码可以这样写:
def hit_at_k(retriever, eval_set, k=5):
"""
eval_set:
[
{
"question": "退款申请时限是多久?",
"relevant_doc_ids": {"doc-001-page-12-chunk-03"}
}
]
"""
hit_count = 0
for item in eval_set:
results = retriever.search(item["question"], top_k=k)
retrieved_ids = {doc.id for doc in results}
if retrieved_ids & item["relevant_doc_ids"]:
hit_count += 1
return hit_count / len(eval_set)
Hit@K 适合快速判断检索链路有没有问题。如果 Hit@5 很低,就不要急着调 Prompt,因为模型根本没拿到正确材料。
除了 Hit@K,还可以看 MRR(Mean Reciprocal Rank,平均倒数排名)和 nDCG(Normalized Discounted Cumulative Gain,归一化折损累计增益)。Hit@K 只关心有没有命中,MRR 更关心第一个正确结果排在第几位,nDCG 则适合处理多个相关文档且相关度有强弱差异的场景。
生成层评估:答案是否基于上下文正确回答
检索命中并不代表最终答案一定正确。LLM 可能忽略上下文、过度发挥,或者回答了另一个问题。生成层评估需要关注三个核心维度:
| 指标 | 评估内容 | 低分时通常说明什么 |
|---|---|---|
| Faithfulness(忠实度) | 答案是否严格依据检索上下文 | 模型编造、过度推理 |
| Answer Relevancy(答案相关性) | 答案是否正面回应用户问题 | Prompt 设计不好,或问题理解错误 |
| Context Recall(上下文召回率) | 检索上下文是否覆盖回答所需信息 | 检索遗漏关键材料 |
RAGAs 是常用的 RAG 自动评估框架,可以围绕这些指标做批量打分。它不能完全替代人工评审,但能帮助工程团队快速发现趋势:某次改动到底让忠实度提高了,还是让召回率下降了。
一个评估样本通常包含问题、检索上下文、生成答案和参考答案:
{
"question": "退款申请需要在多久内提交?",
"contexts": [
"退款申请需要在订单完成后的 7 天内提交,超过期限需人工审核。"
],
"answer": "退款申请需要在订单完成后的 7 天内提交。",
"ground_truth": "订单完成后 7 天内可以提交退款申请。"
}
当评估结果异常时,可以按下面的方式定位:
| 现象 | 更可能的问题环节 | 排查方向 |
|---|---|---|
| Hit@5 低 | 检索层 | chunk、Embedding、Query 改写、混合检索 |
| Hit@5 高,但 Faithfulness 低 | 生成层 | Prompt 约束、引用格式、拒答策略 |
| Hit@5 高,但 Answer Relevancy 低 | 问题理解或 Prompt | Query 改写、意图识别、系统提示词 |
| Context Recall 低 | 召回覆盖不足 | Top-K、Rerank、chunk 粒度、多路召回 |
| 答案引用来源错误 | 元数据或拼接逻辑 | doc id、页码、section、去重逻辑 |
生产落地的调优顺序
RAG 调优不要一上来就换模型。更合理的顺序是从数据源头到生成末端逐层排查:
flowchart TD
A[构造真实评估集] --> B[检查文档解析质量]
B --> C[检查 Chunking 是否破坏语义]
C --> D[评估 Embedding 与向量检索]
D --> E[加入 BM25 混合检索]
E --> F[加入 Rerank]
F --> G[优化 Prompt 与引用约束]
G --> H[端到端评估]
H --> I{指标是否达标}
I -->|否| B
I -->|是| J[上线与持续监控]
一套可执行的检查清单:
| 检查项 | 判断标准 |
|---|---|
| 文档解析 | 随机抽样查看正文、表格、标题层级是否正确 |
| chunk 粒度 | 单个 chunk 是否能表达完整知识点 |
| 元数据 | 是否包含来源、页码、章节、更新时间 |
| 检索召回 | Hit@K 是否达到业务要求 |
| 精确词查询 | 错误码、型号、接口名是否能搜到 |
| 重排效果 | Rerank 后相关内容是否进入更靠前位置 |
| 生成约束 | 答案是否只依据上下文,缺信息时是否拒答 |
| 可观测性 | 是否记录 Query、召回内容、分数、最终答案和用户反馈 |
RAG 的工程难点可以归结为一句话:跑通链路靠组件拼装,生产可用靠质量闭环。文档预处理决定知识库的原料质量,检索调优决定模型能拿到什么材料,效果评估决定每次优化是否真的朝正确方向前进。只有这三层都建立起来,RAG 才能从 Demo 变成稳定可维护的知识系统。