RAG 为什么效果不稳定:从切片、召回到重排,拆解知识库问答最常见的 8 个问题


RAG 为什么效果不稳定:从切片、召回到重排,拆解知识库问答最常见的 8 个问题

做过一轮 RAG 的团队,基本都会碰到同一个阶段性困惑:

  • Demo 看起来不错,正式一用就开始飘;
  • 同样一类问题,这次答得很好,下次却漏重点;
  • 文档明明在库里,系统却像没看到;
  • 明明已经接了向量检索,回答还是不够稳;
  • 用户反馈不是“完全不能用”,而是“有时候很好,有时候不敢信”。

这类问题最麻烦的地方在于:
它不是彻底坏掉,而是间歇性不稳定。

而一套“偶尔很好、偶尔翻车”的系统,往往比一套一直普通的系统更难调。因为你很难第一时间判断,问题到底出在:

  • 文档切片;
  • Embedding 模型;
  • 召回策略;
  • 重排逻辑;
  • Prompt 设计;
  • 上下文拼接;
  • 还是知识源本身。

所以这篇文章不讲空泛概念,也不做趋势评论,只解决一个非常具体的问题:

RAG 为什么效果不稳定?

我会从工程视角,把知识库问答里最常见的 8 类问题拆开说,并给出对应的排查顺序、可落地的配置思路和一个最小可用的调试框架。
如果你正在做企业知识库、文档问答、客服知识助手,或者已经接了向量库但效果总是不稳,这篇文章应该能帮你少走很多弯路。


先说结论:RAG 的“不稳定”,大多数不是模型问题,而是链路问题

很多人一看到效果波动,第一反应是:

  • 模型不够强;
  • 上下文不够长;
  • Embedding 模型不够新;
  • 换个更贵的模型可能就好了。

这些当然可能有影响,但在大多数实际项目里,RAG 不稳定的根因通常更靠前,主要集中在这几层:

  1. 知识源本身不干净
  2. 切片策略不合理
  3. 召回质量不稳定
  4. 重排缺失或重排不准
  5. 上下文拼接方式有问题
  6. Prompt 让模型“发挥过头”
  7. 评估方式太主观
  8. 系统根本没按问题类型分流

也就是说,RAG 不是一个“接上向量库就会稳定”的系统。
它本质上是一条多环节链路,只要上游某一环处理不好,下游模型就会“认真地基于错误上下文回答”。

这也是为什么很多团队会产生错觉:
模型看起来很聪明,但系统整体并不可靠。


先建立一个最小排查框架:别一上来就换模型

在深入 8 个问题之前,先给一个很实用的建议:

如果你的 RAG 表现不稳,不要第一步就换模型
先把问题拆成这四步看:

用户问题
  ↓
召回了什么内容
  ↓
最终送进模型的上下文是什么
  ↓
模型是怎么基于这些上下文回答的

你真正要看的,不是“最终答案不对”,而是:

  • 没召回到正确文档?
  • 召回到了,但排序不对?
  • 排序对了,但切片有缺口?
  • 上下文给对了,但 Prompt 让模型过度总结?

建议先给自己的 RAG 链路补一层最基本的可观察日志。

例如,至少记录:

{
  "query": "跨月报销是否需要额外审批?",
  "retrieved_chunks": [
    {
      "doc_id": "expense_policy_v3",
      "chunk_id": "expense_policy_v3#12",
      "score": 0.83,
      "text_preview": "报销提交时间一般应在费用发生后的当月内完成……"
    },
    {
      "doc_id": "expense_policy_v2",
      "chunk_id": "expense_policy_v2#09",
      "score": 0.79,
      "text_preview": "超过当月未报销的情况需说明原因……"
    }
  ],
  "reranked_chunks": [
    "expense_policy_v2#09",
    "expense_policy_v3#12"
  ],
  "final_context_ids": [
    "expense_policy_v2#09",
    "expense_policy_v3#12"
  ],
  "answer": "跨月报销需要说明原因,是否额外审批需以制度为准……"
}

只要你没有这类日志,后面很多优化基本都只能靠猜。


问题一:知识源本身混乱,RAG 只是把混乱放大了

这是最常见,也最容易被低估的问题。

很多团队一上来就开始做:

  • 文档切片;
  • 向量化;
  • 检索;
  • Prompt 优化;

但真正的问题可能根本不在技术层,而在知识层。

常见表现

  • 同一问题有多个版本答案;
  • 系统有时引用旧制度,有时引用新制度;
  • 召回结果看起来都“相关”,但互相冲突;
  • 模型会试图把冲突资料综合成一个模糊回答。

典型根因

你的知识源里可能同时存在:

  • 正式文档;
  • 讨论稿;
  • 临时通知;
  • 历史版本;
  • 非权威总结;
  • 复制粘贴的二次整理稿。

而系统没有给它们做优先级区分。

该怎么处理

先不要着急调检索,先做知识治理:

docs/
├── official/
│   ├── expense_policy_v3.md
│   └── travel_policy_v2.md
├── archived/
│   ├── expense_policy_v2.md
│   └── old_notice_2024.md
├── draft/
│   └── expense_policy_discussion.md
└── faq/
    └── reimbursement_faq.md

给文档打上最基本的元数据:

{
  "doc_id": "expense_policy_v3",
  "title": "费用报销制度 V3",
  "status": "official",
  "version": "3.0",
  "updated_at": "2026-03-01",
  "priority": 100
}

然后在召回或重排阶段把 statuspriority 带进去。
不然你再好的 Embedding,也只能更高效地检索混乱。


问题二:切片策略不合理,导致“找到了,但没找到完整意思”

很多 RAG 效果不稳定,根本问题不是没召回,而是切片切坏了。

常见表现

  • 系统明明召回了相关内容,但回答漏条件;
  • 结论有了,限制条件没了;
  • 示例被召回了,规则正文没被召回;
  • 一段看起来相关,但实际上关键信息在前后片段里。

为什么会这样

因为切片不是纯技术参数,它决定了“知识以什么粒度被理解”。

比如一条规则可能是这样写的:

### 跨月报销规则

员工应在费用发生当月提交报销。
如遇出差、节假日或系统故障等特殊情况,可跨月提交。
跨月提交需补充说明,超过两个月的情形需部门负责人审批。

如果你按固定长度粗暴切片,可能变成:

chunk A:
员工应在费用发生当月提交报销。如遇出差、节假日或系统故障等特殊情况,可跨月提交。

chunk B:
跨月提交需补充说明,超过两个月的情形需部门负责人审批。

于是用户问“跨月报销是否需要审批”,模型如果只拿到 chunk A,就容易答偏。

更合理的做法

优先做语义结构切片,而不是只按字数切。

比如:

  • 按标题切;
  • 按段落切;
  • 按规则块切;
  • 再做适度 overlap。

示例伪代码:

def split_by_sections(doc):
    sections = parse_markdown_sections(doc)
    chunks = []
    for section in sections:
        if len(section.text) < 800:
            chunks.append(section.text)
        else:
            chunks.extend(sliding_window(section.text, size=600, overlap=120))
    return chunks

一个经验原则

  • 规则类文档:按“条款块”切
  • FAQ 类文档:按“问题-回答对”切
  • 技术文档:按“功能说明 / 步骤 / 参数块”切
  • 太长的段落再做二次滑窗

不要指望一个统一切片规则适合所有文档类型。


问题三:只做向量召回,不做混合检索,导致某些问题天然召不准

纯向量检索在很多场景里好用,但不是所有场景都稳。

常见表现

  • 包含精确术语、版本号、接口名的问题召回不准;
  • 包含短关键词的问题表现很差;
  • 语义很像,但业务上不对的内容被召回来;
  • 用户明明问的是特定字段,系统却给了相似背景说明。

为什么会这样

向量检索擅长“语义相似”,不擅长所有“精确匹配”需求。

比如这些问题:

  • “报销单字段 expense_type 有哪些枚举值?”
  • “接口 /v2/auth/token/v3/auth/token 的区别是什么?”
  • “错误码 E4032 是什么原因?”

这些问题里,关键词本身很关键。
只靠向量检索,容易把“语义类似但关键字不对”的内容召回来。

更稳的做法:混合检索

把关键词检索和向量检索结合起来。

一个简单思路:

def hybrid_retrieve(query):
    bm25_results = bm25.search(query, top_k=10)
    vector_results = vectordb.search(query, top_k=10)
    return reciprocal_rank_fusion(bm25_results, vector_results)

RRF 示例:

def reciprocal_rank_fusion(*ranked_lists, k=60):
    scores = {}
    for ranked in ranked_lists:
        for rank, item in enumerate(ranked):
            doc_id = item["id"]
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

什么时候一定要考虑混合检索

如果你的知识库里经常出现:

  • 版本号
  • API 路径
  • 错误码
  • 枚举字段
  • 产品型号
  • 术语缩写

那纯向量检索很容易不稳定,混合检索通常更靠谱。


问题四:召回对了,但没重排,导致“有用内容被埋了”

很多团队做 RAG 到召回这一步就结束了。
但实际上,召回不是终点,重排才是把结果变稳的关键一层。

常见表现

  • 正确文档明明在 top10,但没进最终上下文;
  • 相似但不关键的内容排在前面;
  • 真正回答问题的片段被背景介绍挤掉了;
  • 最终拼给模型的上下文里,最重要的内容反而不突出。

为什么会这样

初始召回追求的是“别漏掉”,不是“最懂你现在这个问题”。
而生成模型的上下文窗口又有限,你最终只能塞进去一部分内容。

如果不做重排,系统就会经常出现这种情况:

  • 召回结果“看起来还行”
  • 最终答案却“不太对劲”

更稳的做法

引入一层 reranker。

伪代码:

retrieved = hybrid_retrieve(query)
reranked = reranker.rank(query=query, documents=[x["text"] for x in retrieved])
final_chunks = reranked[:5]

重排时要看什么

比起只看向量分数,重排更关注:

  • 这个片段是不是直接回答了问题;
  • 它是不是主规则,不是背景噪声;
  • 它是不是当前版本的有效内容;
  • 它的“回答性”强不强。

实战建议

如果你的 RAG 还没做重排,而你已经在折腾 Prompt、模型和参数,大概率顺序反了。
先把“给模型什么材料”这件事做好,再谈“让模型怎么说”。


问题五:上下文拼接太粗暴,把模型喂“撑”了

这也是非常常见的问题。

很多系统的做法是:
召回 top-k,全部拼进去,交给模型。

结果往往是:

  • 上下文太长,噪声太多;
  • 多个片段互相冲突;
  • 模型抓不住重点;
  • 越想“多给一点保险”,结果越不稳。

常见错误拼接方式

以下是检索到的内容:
[chunk1 全文]
[chunk2 全文]
[chunk3 全文]
[chunk4 全文]
[chunk5 全文]
请回答用户问题。

这种方式的问题是:
系统没有告诉模型“哪些更重要、哪些是补充、哪些可能冲突”。

更合理的做法

把上下文按角色组织,而不是简单堆文本。

例如:

你将根据下面资料回答问题。
优先依据“官方正式资料”。
如果资料之间有冲突,优先采用版本更新更近、优先级更高的内容。
如果资料不足以支持明确结论,请直接说明“资料不足”。

[资料1 - 官方制度 V3 - 优先级100]
...

[资料2 - FAQ 补充说明 - 优先级80]
...

[资料3 - 历史版本,仅供参考 - 优先级20]
...

一个很实用的原则

不要一味追求“多塞一点”。
很多时候:

  • top3 精准上下文 > top10 杂乱上下文

如果你发现系统越来越不稳,先试着减少上下文数量,而不是继续加。


问题六:Prompt 设计在鼓励模型“补全常识”,而不是“忠于资料”

这是很多团队容易忽视的一层。

常见表现

  • 文档没明确说,模型却给了肯定结论;
  • 检索结果不完整,模型开始自己脑补;
  • 系统回答看起来很通顺,但依据不够扎实。

常见错误 Prompt

请根据以下资料和你的知识,尽可能完整地回答用户问题。

问题就出在“和你的知识”“尽可能完整”这类表达上。
它会鼓励模型在资料不足时补全常识。

更稳的 Prompt 写法

你是知识库问答助手。
请只根据提供的资料回答问题,不要使用资料之外的常识补充结论。
如果资料无法支持明确答案,请直接回答“根据当前资料无法确认”。
如果存在条件、例外或审批要求,必须优先写出。
回答后附上引用的资料编号。

推荐的输出格式

结论:
...

依据:
- 资料1
- 资料2

待确认:
...

结构化输出能显著降低“说得很像,但逻辑不清”的问题。


问题七:没有做问题分流,把所有问题都硬塞进同一条 RAG 链路

很多团队把所有知识问题都统一走一套 RAG 管道。
这通常会带来稳定性问题。

为什么

因为不同问题类型,本来就应该有不同处理方式。

比如:

  • 高频标准问答 → FAQ
  • 精确术语查询 → 搜索 / 关键词检索
  • 开放式文档问答 → RAG
  • 状态类问题 → API / 数据库查询

如果不分流,系统就会出现两种典型症状:

  1. 本该用固定答案的题,被模型自由生成了;
  2. 本该做精确查询的题,被语义检索绕弯了。

一个最小路由思路

def route_query(query):
    if is_structured_query(query):
        return "db"
    if is_faq_query(query):
        return "faq"
    if has_strong_keyword_pattern(query):
        return "search"
    return "rag"

哪怕这个路由一开始不够聪明,也比“所有问题都统一扔进 RAG”更稳。


问题八:没有自己的评测集,优化全靠感觉

这是很多 RAG 项目到后期最痛苦的点。

团队常常这样讨论:

  • “我感觉这次好一点了”
  • “这类问题好像稳定了”
  • “但另一些又变差了”

问题在于,没有评测集,你就无法稳定判断优化到底有没有效果。

最低限度也该有一个样本集

例如建一个最小 eval.jsonl

{"query":"跨月报销是否需要审批?","expected_docs":["expense_policy_v3#12"],"expected_keywords":["跨月","审批","两个月"]}
{"query":"错误码 E4032 是什么意思?","expected_docs":["api_error_code#44"],"expected_keywords":["E4032","权限","token"]}
{"query":"/v2/auth/token 和 /v3/auth/token 有什么区别?","expected_docs":["auth_api_v2#3","auth_api_v3#2"],"expected_keywords":["v2","v3","token"]}

评测至少看三层

  1. 召回是否命中
  2. 最终上下文是否包含关键片段
  3. 回答是否覆盖关键条件

一个简单的调试脚本可以先这么写:

def evaluate_case(case):
    retrieved = hybrid_retrieve(case["query"])
    retrieved_ids = [x["id"] for x in retrieved[:10]]

    hit = any(doc in retrieved_ids for doc in case["expected_docs"])
    return {
        "query": case["query"],
        "hit_top10": hit,
        "retrieved_ids": retrieved_ids
    }

没有评测集,所有优化都容易变成“玄学调参”。


一个更稳的 RAG 最小工程结构,可以长什么样

如果你现在要把一个 RAG 系统做得更可维护,我建议最少把结构拆成下面这样:

rag-system/
├── data/
│   ├── raw/
│   ├── cleaned/
│   └── metadata/
├── ingest/
│   ├── parser.py
│   ├── cleaner.py
│   ├── splitter.py
│   └── embed.py
├── retrieval/
│   ├── bm25.py
│   ├── vector.py
│   ├── fusion.py
│   └── reranker.py
├── prompt/
│   └── qa_prompt.txt
├── eval/
│   ├── eval.jsonl
│   └── run_eval.py
└── app/
    └── api.py

你不一定非要照这个目录来,但核心思路要有:

  • 数据清洗独立
  • 切片独立
  • 检索独立
  • 重排独立
  • Prompt 独立
  • 评测独立

只有这样,系统出问题时你才知道该改哪一层。


一个排查顺序建议:别再盲目按模型价格往上堆

如果你今天就要开始排查 RAG 效果不稳定的问题,我建议按这个顺序来:

第一步:先查知识源

  • 有没有旧版本混入
  • 有没有冲突资料
  • 有没有权威优先级

第二步:再查切片

  • 规则有没有被切散
  • 标题和正文有没有分开
  • 文本块是不是太碎或太大

第三步:再查召回

  • 纯向量是否适合你的问题类型
  • 是否需要混合检索
  • top-k 是否太小或太大

第四步:再查重排

  • 正确结果是否被埋在后面
  • 最终送模型的片段是不是最该看的

第五步:再查 Prompt

  • 有没有鼓励模型脑补
  • 有没有要求依据优先
  • 有没有让模型承认“不确定”

第六步:最后再查模型

  • 模型是否能更稳地压上下文
  • 模型是否擅长长文本整合
  • 是否真的有必要升级

这个顺序的核心逻辑很简单:

先修“给模型什么”,再修“模型怎么说”。


结语:RAG 不稳定,不是因为它不行,而是因为它不是一个单点功能

很多人第一次做 RAG,会把它想成一个功能:

  • 接文档
  • 接向量库
  • 接模型
  • 就能问答

但真正做过之后就会发现,RAG 更像一条系统链路。
它不是只有一个“模型质量”变量,而是多个环节叠加后的结果。

所以,当你感觉系统“时好时坏”时,最该做的不是立刻换更贵的模型,而是把整条链路拆开看:

  • 文档干净吗?
  • 切片合理吗?
  • 召回稳吗?
  • 重排做了吗?
  • 上下文拼得好吗?
  • Prompt 有没有鼓励乱发挥?
  • 评测是否可重复?

把这些问题理顺之后,你会发现很多所谓的“不稳定”,其实并不神秘。
它只是系统在用一种不太友好的方式提醒你:

RAG 的难点,从来不只是生成,而是检索链路的工程化。


关键词建议

  • RAG
  • 知识库问答
  • 向量检索
  • 重排模型
  • 检索增强生成
  • AI 知识库

摘要建议

RAG 项目最常见的问题不是完全不能用,而是“时好时坏”。本文从工程视角拆解了 RAG 效果不稳定的 8 个常见原因,覆盖知识源治理、切片策略、混合检索、重排、上下文拼接、Prompt 设计、问题分流和评测方法,并给出可落地的代码片段与排查顺序。对于正在做知识库问答、文档助手或企业 AI 应用的团队,这篇文章更适合作为一次系统性体检,而不是单点调参指南。


文章作者: 左哥
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 左哥 !
  目录