做 RAG 项目时,我见过一个很常见的现象:

系统刚搭出来的时候,demo 看起来挺不错。上传几份文档,问几个问题,模型都能答上来。

但一旦进入真实业务场景,问题就开始变多:

  • 明明文档里有答案,模型却说不知道;

  • 回答内容看起来很流畅,但事实是错的;

  • 同一个问题,换个问法就答不上来;

  • 测试环境还行,上线后用户一问就翻车;

  • 换了更大的模型,效果也没有明显提升。

很多团队第一反应是:

是不是大模型不够强?

我的经验是,RAG 效果差,第一反应不应该是换模型,而应该先看检索链路。

大模型只是最后负责“组织语言”的那一环。
如果前面给它的上下文是错的、脏的、不完整的,它再强也只能基于错误材料生成一个看似合理的答案。

RAG 的核心不是简单的:

向量数据库 + LLM

而是:

数据治理 + 检索策略 + Rerank + Prompt 约束 + 评测体系

下面这 10 个问题,是我在做企业知识库问答、内部制度问答、文档助手时反复遇到的坑。


1. 文档解析质量差:垃圾进来,垃圾出去

现象

用户问的问题明明在文档里有答案,但 RAG 系统就是答不出来。

把召回的 chunk 打出来一看,里面全是乱码、断行、页眉页脚、目录、无意义编号。

比如原始 PDF 里是:

员工市内交通费按实际发生金额报销,需提供有效票据。

解析后变成:

员工 市 内 交 通 费 按 实 际 发 生 金 额
报 销 需 提 供 有 效 票 据
第 12 页 / 共 38 页

这种内容进入向量数据库后,Embedding 的语义质量会变差,检索自然也不稳定。

根因

很多 RAG 项目一开始只关注模型和向量库,却忽略了文档解析。

真实企业文档很复杂:

  • PDF 有扫描版和文字版;

  • Word 里有表格、标题、批注;

  • Excel 里有多 sheet;

  • PPT 里有图片和文本框;

  • 文档里有页眉、页脚、水印、目录;

  • 制度类文档经常有版本修订记录。

如果解析阶段没有处理好,后面 Milvus、FAISS、LangChain、Rerank 再怎么调,也是在脏数据上做优化。

排查方法

排查 RAG 效果时,我建议第一步不要看模型回答,而是看三样东西:

1. 原始文档内容
2. 解析后的纯文本
3. 最终进入向量库的 chunk

如果这三者之间已经失真,问题就不在大模型。

可以随机抽取一批文档,人工检查解析结果:

# 方法:抽样检查解析后的文本质量
# 思想:不要只看最终回答,要先确认进入检索链路的数据是否干净
# 循环终止条件:检查完 sample_docs 中所有抽样文档后停止
for doc in sample_docs:
    text = parse_document(doc)
    print("文件名:", doc.name)
    print(text[:1000])
    print("-" * 80)

解决方案

我的建议是:

  • PDF、Word、Excel、Markdown 分别使用不同解析策略;

  • 去掉页眉、页脚、目录、版权声明;

  • 表格不要直接拍平成一堆文本;

  • 对扫描版 PDF 做 OCR,并检查识别质量;

  • 保留文档标题、章节、页码、版本号等 metadata;

  • 入库前做清洗,不要把所有内容原样塞进向量库。

简短例子

有一次做制度问答,用户问:

试用期员工能不能申请年假?

系统一直答错。

后来发现文档解析时,把表格中的“适用对象”和“规则说明”拆散了。

原文表格里,“试用期员工”对应的是“不享受年假”。
但解析后两个单元格分到了不同 chunk 里。

模型只召回了“年假申请规则”,没有召回“试用期员工限制”,自然就答错了。


2. chunk 切分不合理:答案被切碎了

现象

RAG 系统能召回相关文档,但回答不完整。

有时只答了一半,有时漏掉关键条件。

比如用户问:

员工出差住宿标准是多少?

召回 chunk 里只有:

一线城市住宿标准为每日 500 元。

但真正完整规则是:

一线城市每日 500 元,二线城市每日 400 元,其他城市每日 300 元。
部门总监及以上级别可上浮 20%。

答案被切碎后,模型只能基于局部内容回答。

根因

很多人直接用固定长度切分,比如每 500 字切一段,overlap 50。

这对普通文本还凑合,但对制度文档、技术文档、合同文档并不够。

问题在于:

chunk 不是越平均越好,而是要尽量保持语义完整。

如果一个规则跨越多个段落,或者标题和正文被切开,检索效果就会下降。

排查方法

重点看用户问题对应的答案是否完整存在于单个 chunk 中。

可以打印召回结果:

# 方法:打印 TopK 召回内容
# 思想:通过观察 chunk 是否包含完整答案,判断切分策略是否破坏语义
# 循环终止条件:遍历完当前问题召回的所有 chunks 后停止
for i, chunk in enumerate(retrieved_chunks):
    print(f"Top {i + 1}")
    print(chunk.text)
    print(chunk.metadata)

如果 Top1、Top2、Top3 都只包含碎片信息,就要重新设计 chunk 策略。

解决方案

比较稳妥的做法是:

  • 按标题层级切分,而不是纯长度切分;

  • 制度文档按章节切;

  • FAQ 文档按问答对切;

  • 表格按行或业务实体切;

  • chunk 中保留父标题;

  • 对长段落设置合理 overlap;

  • metadata 里保留文档名、章节名、页码、版本号。

简短例子

切分前:

第三章 差旅报销
3.1 交通费标准
3.2 住宿费标准
3.3 餐补标准

如果只按固定长度切,可能标题在前一个 chunk,具体规则在后一个 chunk。

用户问“差旅住宿费”,向量检索可能召回不到带标题的上下文。

更好的 chunk 应该是:

文档:员工报销制度
章节:第三章 差旅报销 / 3.2 住宿费标准
内容:一线城市每日 500 元,二线城市每日 400 元……

这样 Embedding 的语义更完整,后续检索也更稳定。


3. Embedding 模型选型不匹配:语义空间不适合业务

现象

用户的问题和文档语义明明接近,但向量检索结果很奇怪。

比如用户问:

离职后社保什么时候停?

系统召回的是:

员工离职流程审批说明

但没有召回真正相关的:

社保公积金停缴时间说明

根因

Embedding 模型不是随便选一个就行。

不同模型对中文、英文、代码、法律文本、医学文本、企业制度文本的表现差异很大。

常见问题有:

  • 用英文 Embedding 模型处理中文文档;

  • 用通用模型处理大量专业术语;

  • 文档是中文,用户问题中夹杂英文缩写;

  • 企业内部有大量黑话、简称、系统名;

  • 没有做 query rewrite,用户口语化问题和文档正式表达对不上。

比如用户说:

打车票怎么报?

文档里写的是:

市内交通费报销。

这两个表达语义相近,但如果 Embedding 模型不够好,就可能召回失败。

排查方法

准备一批典型问题,人工标注应该召回的文档。
然后测试不同 Embedding 模型的 TopK 召回率。

不要只看单条结果,要看整体指标:

Recall@1
Recall@3
Recall@5
Recall@10

如果正确 chunk 经常排在 Top10 之外,说明 Embedding 或检索策略需要调整。

解决方案

可以从这几个方向优化:

  • 选择更适合中文的 Embedding 模型;

  • 对业务术语做同义词扩展;

  • 对用户问题做 query rewrite;

  • 对文档标题、章节、正文一起做 embedding;

  • 对专业领域数据做微调;

  • 不要只依赖向量检索,引入 BM25 混合检索。

简短例子

用户问:

电脑坏了找谁修?

文档写的是:

IT 资产故障报修流程

单纯向量检索可能不稳定。

如果做 query rewrite,可以把用户问题改写为:

IT 设备故障维修、电脑报修、资产故障处理流程

召回质量会明显提升。


4. 只用向量检索,没有混合检索:关键词被忽略

现象

涉及编号、姓名、系统名、产品型号、制度条款时,向量检索经常不准。

比如用户问:

OA-2024-017 这个流程单是什么?

向量检索可能召回一堆“流程审批说明”,但找不到包含 OA-2024-017 的具体文档。

根因

向量检索擅长语义相似,但不一定擅长精确匹配。

企业知识库里有很多内容不能只靠语义:

  • 合同编号;

  • 工单编号;

  • 员工编号;

  • 系统名称;

  • 产品型号;

  • 法规条款;

  • 函数名和错误码。

这些场景下,BM25、关键词检索、倒排索引往往比纯向量更可靠。

排查方法

看失败问题里有没有明显关键词。

如果用户问题中有编号、专有名词、代码、条款号,但召回结果没有包含这些词,说明纯向量检索不够。

可以记录每次检索的结果:

query = "OA-2024-017 这个流程单是什么?"

vector_topk:
1. OA 流程审批说明
2. 费用审批流程
3. 合同流程规范

bm25_topk:
1. OA-2024-017 差旅费用审批记录

这时答案已经很明显了。

解决方案

推荐使用混合检索:

向量检索:负责语义相似
BM25:负责关键词精确匹配
Rerank:负责最终排序

一个常见流程是:

用户问题
   ↓
query rewrite
   ↓
向量检索 TopK
   +
BM25 检索 TopK
   ↓
结果合并去重
   ↓
Rerank
   ↓
送入大模型

简短例子

在知识库问答里,用户问:

ERR_5027 是什么错误?

纯向量检索召回了很多“系统异常处理方案”。

但 BM25 能直接命中包含 ERR_5027 的排障文档。

这种问题,关键词比语义更重要。


5. TopK 设置不合理:召回太少或噪声太多

现象

TopK 太小的时候,正确答案没被召回。

TopK 太大的时候,上下文里混入大量无关内容,模型被干扰。

有些项目为了“尽量不漏”,直接设置 TopK=20。
结果模型拿到一堆相似但不相关的内容,回答反而更容易幻觉。

根因

TopK 不是固定参数,它和很多因素有关:

  • chunk 大小;

  • 文档数量;

  • Embedding 模型质量;

  • 问题复杂度;

  • 是否有 Rerank;

  • 模型上下文窗口大小;

  • Prompt 中是否要求引用来源。

如果没有 Rerank,TopK 过大通常会带来噪声。
如果 chunk 切得很细,TopK 太小又容易漏信息。

排查方法

对同一批评测问题,测试不同 TopK 的效果:

TopK = 3
TopK = 5
TopK = 10
TopK = 20

观察三个指标:

1. 正确 chunk 是否被召回
2. 正确 chunk 排名是否靠前
3. 最终回答是否准确

不要只看召回数量,要看噪声比例。

解决方案

我的经验是:

  • 没有 Rerank 时,TopK 不要盲目太大;

  • 有 Rerank 时,可以先召回多一些,再重排;

  • 简单事实类问题 TopK 可以小一点;

  • 复杂综合类问题 TopK 可以大一点;

  • 根据问题类型动态设置 TopK。

例如:

# 方法:根据问题类型动态设置 TopK
# 思想:简单问题减少噪声,复杂问题增加召回覆盖
# 循环终止条件:这里没有循环,按问题类型直接返回 TopK
def choose_top_k(query_type: str) -> int:
    if query_type == "fact":
        return 5
    if query_type == "summary":
        return 12
    if query_type == "comparison":
        return 15
    return 8

简短例子

用户问:

公司餐补标准是多少?

这种事实类问题,TopK=5 通常够了。

如果 TopK=20,可能把出差补贴、加班餐费、团建报销都混进来,模型就容易答乱。


6. 没有 Rerank:召回到了,但排序不对

现象

正确答案其实在召回结果里,但排在第 7、第 8。

最后送给大模型的上下文只截取了前几个 chunk,导致回答错误。

这是 RAG 里非常常见的问题:

不是没召回,而是排得太靠后。

根因

向量相似度不等于最终相关性。

Embedding 检索通常是粗召回,它能找一批可能相关的内容,但排序不一定精准。

尤其是用户问题比较长、包含多个约束时,向量检索容易只匹配其中一部分。

比如用户问:

上海员工出差到北京,住宿费超过标准但有总监审批,可以报销吗?

向量检索可能分别召回:

  • 上海出差规则;

  • 北京住宿标准;

  • 总监审批流程;

  • 超标报销说明。

但真正最相关的 chunk 应该是“超标准报销审批规则”。

排查方法

不要只看 Top1。

要看 Top10 里有没有正确答案,以及正确答案排在第几。

如果正确答案经常出现在 Top10 但不在 Top3,说明需要 Rerank。

解决方案

加入 Rerank 模型,对粗召回结果重新排序。

典型流程:

vector_topk = 30
bm25_topk = 30
merge
rerank_topn = 5
send to LLM

Rerank 比 Embedding 更关注 query 和 document 的细粒度匹配,适合做最终排序。

简短例子

用户问:

异地加班打车能不能报销?

向量检索 Top1 是“加班管理规定”,Top2 是“差旅交通费规则”,Top6 才是“加班打车报销规则”。

加入 Rerank 后,“加班打车报销规则”排到 Top1,最终回答就正常了。


7. Prompt 没有约束引用来源:模型开始自由发挥

现象

检索结果里没有明确答案,但模型还是给出了一个看起来很合理的回答。

这就是典型幻觉。

比如上下文里只写了:

报销需符合公司财务制度。

模型却回答:

员工每月最多可报销 300 元交通费。

这个数字可能来自模型预训练知识,也可能是它根据常见规则“编”的。

根因

很多 Prompt 只写了:

请根据以下内容回答用户问题。

这太弱了。

模型并不会天然知道“没有依据就不要答”。
如果上下文不完整,它可能会根据常识补全。

RAG 场景下,Prompt 的重点不是让模型更会说,而是让它更守规矩。

排查方法

看模型回答里的每个关键信息,是否能在 retrieved chunks 中找到依据。

特别是数字、日期、金额、比例、条件。

如果回答里出现了上下文没有的规则,就说明 Prompt 约束不够,或者上下文污染严重。

解决方案

Prompt 至少要约束三件事:

1. 只能基于提供的上下文回答
2. 找不到依据时明确说不知道
3. 关键结论必须引用来源

示例:

你是企业知识库问答助手。
请严格基于【参考资料】回答问题。

要求:
1. 不要使用参考资料以外的信息。
2. 如果参考资料中没有答案,请回答“根据当前资料无法确认”。
3. 涉及金额、日期、比例、制度条款时,必须说明依据来自哪段资料。
4. 不要自行补充公司制度。

简短例子

用户问:

实习生可以报销打车费吗?

如果资料里没有实习生相关说明,模型应该回答:

根据当前资料无法确认实习生是否适用该报销规则。

而不是猜一个“可以”或“不可以”。


8. 上下文塞太多:模型抓不住重点

现象

为了防止漏召回,有些系统会把很多 chunk 都塞进 Prompt。

结果模型回答变得不稳定,有时引用了不相关资料,有时把多个制度混在一起。

根因

上下文窗口大,不代表可以无脑塞内容。

LLM 对上下文的利用不是完全均匀的。
如果前面放了大量噪声,或者多个 chunk 互相冲突,模型很容易抓错重点。

尤其是企业文档中经常存在:

  • 新旧制度同时存在;

  • 总部规则和地区规则同时存在;

  • 正式员工和实习生规则混在一起;

  • 差旅报销和日常报销规则相似;

  • 文档标题很像,但适用范围不同。

排查方法

检查最终送入 LLM 的 Prompt,而不是只看检索结果。

很多问题不是检索阶段错了,而是组装 Prompt 时把太多东西混在了一起。

重点看:

1. 是否有互相冲突的 chunk
2. 是否有旧版本制度
3. 是否有适用范围不同的内容
4. 正确 chunk 是否被噪声淹没

解决方案

可以做几件事:

  • Rerank 后只保留最相关的 3 到 5 段;

  • 按文档版本过滤旧制度;

  • 按适用范围过滤地区、部门、人群;

  • 对 chunk 做摘要压缩;

  • 把来源、时间、适用范围放进 metadata;

  • Prompt 中要求优先使用最新版本和最精确规则。

简短例子

用户问:

北京员工出差住宿标准是多少?

如果上下文同时包含:

2022 版:北京住宿 450 元
2024 版:北京住宿 500 元
华东区标准:上海住宿 480 元

模型可能会答错。

正确做法是在检索或 Rerank 后优先保留:

2024 版 + 北京 + 住宿标准

对应的 chunk。


9. 缺少评测集:只靠主观感觉调系统

现象

开发同学觉得效果不错,业务同学觉得不行。

今天调 TopK,明天换 Embedding,后天改 Prompt,但没人能说清楚到底有没有变好。

这类项目很容易陷入“玄学调参”。

根因

很多 RAG 项目没有评测集。

测试方式就是随便问几个问题,看回答顺不顺眼。

但 RAG 系统至少要评估几件事:

1. 检索有没有召回正确资料
2. 正确资料排名是否靠前
3. 回答是否忠实于上下文
4. 是否出现幻觉
5. 响应延迟是否可接受
6. 单次调用成本是否可控

只看最终答案,很难定位问题。

因为错误可能来自文档解析、chunk、Embedding、TopK、Rerank、Prompt,也可能来自模型生成。

排查方法

建立一个小型评测集,哪怕一开始只有 50 条,也比没有强。

每条评测数据至少包含:

{
  "question": "员工出差住宿标准是多少?",
  "gold_doc": "员工差旅报销制度 2024 版",
  "gold_answer": "一线城市 500 元/天,二线城市 400 元/天,其他城市 300 元/天。",
  "tags": ["报销", "差旅", "住宿"]
}

然后分别评估检索和生成。

解决方案

推荐拆成两层评测。

第一层:检索评测:

Recall@K
MRR
正确 chunk 排名

第二层:回答评测:

答案是否正确
是否有依据
是否幻觉
是否遗漏条件

可以结合人工评测和自动评测。

一些 RAGAS 类指标也可以参考,但不要完全迷信自动分数。
业务规则类问答最终还是要有人抽查。

简短例子

一次优化前,系统对 100 个问题的 Recall@5 是 68%。

加入混合检索和 Rerank 后,Recall@5 提升到 86%。

这时再看最终回答准确率,才有意义。

否则模型答错了,你根本不知道是没召回,还是生成阶段没用好上下文。


10. 权限、时效性、脏数据没处理:demo 好看,上线翻车

现象

demo 阶段效果很好,上线后问题暴露:

  • 普通员工问到了管理层制度;

  • 用户查到旧版流程;

  • 已废弃文档仍然被召回;

  • 不同部门制度混在一起;

  • 测试文档、临时文档、草稿文档进入知识库;

  • 用户问 A 部门规则,系统回答 B 部门规则。

根因

企业级 RAG 和个人知识库不一样。

企业知识库问答一定要处理:

权限
版本
时效
适用范围
数据来源
文档状态

很多项目只把文档全量向量化,然后统一放进向量数据库。
这在 demo 里没问题,但在生产环境风险很大。

排查方法

看召回 chunk 的 metadata 是否完整。

至少要有:

doc_id
doc_name
department
version
effective_date
expire_date
owner
permission_scope
status
source_url

如果 metadata 缺失,后续就很难过滤。

解决方案

企业级 RAG 里,检索不能只靠相似度。
还要加业务过滤条件。

例如:

# 方法:在向量检索前加入权限和状态过滤
# 思想:先保证用户只能检索到有权限、有效、正式发布的文档,再谈语义相关性
# 循环终止条件:这里没有循环,构造一次过滤条件后交给向量数据库执行
filter_expr = {
    "status": "published",
    "department": user.department,
    "permission_scope": {"$in": user.roles},
    "effective_date": {"$lte": today},
    "expire_date": {"$gte": today}
}

Milvus、Elasticsearch、一些向量数据库都支持类似 metadata filter。

如果只做向量相似度,不做权限和版本过滤,迟早会出问题。

简短例子

用户问:

销售部的客户招待费标准是多少?

如果系统召回了“研发部团建费用标准”,语义上也许相似,但业务上完全错误。

这种错误靠换大模型解决不了,只能靠 metadata 和过滤策略解决。


一个完整排查案例:用户问公司报销标准,RAG 回答错误

下面用一个真实项目里很常见的场景串一下排查过程。

问题现象

用户问:

我下周去上海出差,住宿费最多能报多少?

系统回答:

公司出差住宿费标准为每天 400 元。

业务同学反馈:

上海属于一线城市,2024 版制度里应该是每天 500 元。

这时不要急着换模型,也不要马上改 Prompt。
先把链路拆开看。


第一步:查看用户问题

用户问题里有几个关键信息:

上海
出差
住宿费
最多报多少

这说明它不是普通报销问题,而是差旅住宿标准问题,并且城市是上海。


第二步:查看召回 chunk

打印 TopK 结果:

Top1:
《员工报销制度 2022 版》
二线城市住宿费标准为每日 400 元。

Top2:
《差旅申请流程》
员工出差前需在 OA 系统提交差旅申请。

Top3:
《员工报销制度 2024 版》
一线城市住宿费标准为每日 500 元,适用城市包括北京、上海、深圳、广州。

Top4:
《费用审批流程》
超过标准的费用需部门负责人审批。

正确答案其实在 Top3,但模型用了 Top1。


第三步:分析错误原因

这个问题不是模型不会回答,而是检索和排序有问题。

主要有三个原因:

1. 旧版制度排在新版制度前面
2. Top1 只命中了“住宿费标准”,但没有命中“上海”
3. Prompt 没要求优先使用最新版本

第四步:优化检索策略

先做 metadata 过滤:

status = published
effective_date <= today
expire_date >= today

过滤掉 2022 版旧制度。

然后引入混合检索:

向量检索:匹配“出差住宿报销标准”
BM25:强匹配“上海”“住宿费”

这样可以避免只靠语义相似度,把不包含城市条件的 chunk 排到前面。


第五步:加入 Rerank

粗召回阶段可以多取一些:

vector_topk = 20
bm25_topk = 20

合并后交给 Rerank,让模型重新判断哪个 chunk 最相关。

Rerank 后结果变成:

Top1:
《员工报销制度 2024 版》
一线城市住宿费标准为每日 500 元,适用城市包括北京、上海、深圳、广州。

Top2:
《费用审批流程》
超过标准的费用需部门负责人审批。

这时送入 LLM 的上下文就干净多了。


第六步:修改 Prompt

Prompt 加上版本和引用约束:

请严格基于参考资料回答。
如果存在多个版本,优先使用生效日期最新且状态为 published 的资料。
涉及金额、城市、日期时,必须引用依据。
如果资料中没有明确依据,请回答无法确认。

第七步:补充评测用例

这次问题不能只修一条,要沉淀成评测集。

新增几条用例:

1. 上海出差住宿标准是多少?
2. 北京出差住宿标准是多少?
3. 二线城市住宿标准是多少?
4. 超过住宿标准能否报销?
5. 旧版制度和新版制度冲突时用哪个?

之后每次调 Embedding、TopK、Rerank、Prompt,都跑一遍评测集。

这样才知道优化是不是真的有效。


RAG 效果排查清单

最后给一份我自己比较常用的排查清单。
RAG 回答差时,可以按这个顺序查。

1. 先查数据

原始文档是否正确?
文档是否过期?
解析后文本是否干净?
表格、图片、标题是否丢失?
是否混入草稿、旧版、测试文档?

2. 再查 chunk

chunk 是否保持语义完整?
标题和正文有没有被切开?
表格有没有被拆乱?
overlap 是否合理?
metadata 是否完整?

3. 再查检索

正确 chunk 是否进入 TopK?
正确 chunk 排名是否靠前?
是否只用了向量检索?
是否需要 BM25 混合检索?
Embedding 模型是否适合当前业务文本?

4. 再查 Rerank

正确答案是否召回到了但排得靠后?
Rerank 前后排序是否有改善?
Rerank 输入是否包含太多噪声?
最终送给 LLM 的 chunk 是否足够精简?

5. 再查 Prompt

是否要求只能基于上下文回答?
是否要求找不到就说无法确认?
是否要求引用来源?
是否处理多版本冲突?
是否约束金额、日期、比例等关键字段?

6. 最后查评测

有没有固定评测集?
有没有 Recall@K?
有没有统计幻觉率?
有没有线上日志?
有没有按问题类型分析失败原因?

总结

RAG 效果差,不要第一时间甩锅给大模型。

大多数时候,问题出在前面的链路:

文档解析脏
chunk 切分乱
Embedding 不匹配
检索策略单一
TopK 不合理
缺少 Rerank
Prompt 约束弱
上下文噪声多
没有评测集
权限和版本没处理

真正能落地的 RAG 系统,不是把 LangChain、Milvus、FAISS、Embedding、Prompt 拼起来就结束了。

它更像一个搜索系统、数据治理系统和生成系统的组合。

我的经验是:

先保证检索到正确资料,再让模型回答。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐