详情

首页手游攻略 RAG 回答错了,问题到底出在召回、重排,还是生成?

RAG 回答错了,问题到底出在召回、重排,还是生成?

佚名 2026-07-31 09:51:57

最近几天,我一直在学习 RAG。

我已经知道,文档需要先清洗、切片,再通过 Embedding 模型向量化并写入向量数据库。用户提问后,系统检索相关片段,把片段和问题一起交给 LLM,才形成完整的 RAG。

但当我真正开始分析错误案例时,又遇到了一个更具体的问题:

以前我容易直接归因于“LLM 回答不准确”。今天跟着一个可运行 Demo 逐步分析后,我发现最终回答之前至少经过了几道不同的关口:

用户问题→ 硬条件过滤→ 关键词检索 / 向量检索→ 合并候选→ 重排→ 截取最终 Top-K→ LLM 生成回答→ 可选的回答核验→ UI 展示

只有把这些阶段拆开,才能知道一次改动究竟改善了什么。

先看一个回答错误的案例

知识库中有两个片段:

片段 A:标准商品付款后 30 天内可申请退款。片段 B:定制商品一旦进入生产,不支持退款。

用户问:

定制商品进入生产后可以退款吗?

如果检索系统返回片段 A,LLM 很可能回答:

可以,因为仍在付款后 30 天内。

这个回答虽然引用了真实规则,却把“标准商品”的规则错误地用到了“定制商品”上。

问题并不是 LLM 没有读懂片段 A,而是正确的片段 B 根本没有进入它的上下文。

因此,这个案例首先应该定位为:

Demo 中用一个固定生成器模拟这个过程:

functiongenerateFromContext(contextFragmentIds) {if (contextFragmentIds.includes("片段B")) {return {text: "不可以,定制商品进入生产后不支持退款。[片段B]",answerGrounded: true};}return {text: "可以,因为付款后 30 天内可申请退款。[片段A]",answerGrounded: false};}

这里故意不改变生成逻辑,只改变传给它的检索片段:

const baselineRun = runPipeline({strategy: "仅关键词匹配",retrievedFragmentIds: ["片段A"]});const improvedRun = runPipeline({strategy: "商品类型过滤后再做关键词匹配",retrievedFragmentIds: ["片段B"]});

改进前,目标片段没有被召回;改进后,同一个生成器拿到片段 B,才生成有依据的答案。

这让我第一次很具体地看到:

“元数据过滤”其实就是先按硬条件筛数据

学习过程中,我看到“元数据过滤”这个词时,一度觉得自己没有学过。

换成具体场景后,我马上发现这个知识其实早就理解了。

假设每个片段除了正文,还带有这些字段:

{tenantId: "company-a",docType: "policy",language: "zh-CN",indexVersion: "v2"}

当前用户属于 company-a,本次只查询 policy 类型文档,那么 Runtime 应该先排除其他公司和其他类型的片段,再进行关键词或向量相似度排序。

即使 company-b 的某个片段相似度是 0.99,也不应该进入候选池。

所以,元数据过滤用白话说就是:

它不只是为了提升相关性,还可能参与权限、版本和业务范围控制。

权限隔离不是让 LLM 自觉保密

如果登录用户属于 company-a,但他手动把前端请求中的 tenantId 改成 company-b,Runtime 不能相信这个字段。

安全的数据范围应该来自服务端已经核验的登录身份:

服务端确认当前用户属于 company-a→ Runtime 强制加入 company-a 的查询条件→ company-b 的数据不进入候选池

“权限隔离”是安全目标,服务端元数据过滤只是它的一种实现方式。

更强的隔离还可以使用:

  1. 每个租户独立索引;
  2. 每个租户独立数据库;
  3. 数据库行级权限;
  4. 服务端统一的授权策略。

不能采用的方式是:

先查询所有公司的数据→ 全部交给 LLM→ 在 Prompt 中要求它不要泄露

Prompt 是软约束,不能代替服务端权限边界。

混合检索先扩大候选,重排再决定最终 Top-K

单独使用关键词或向量检索,各有适合的场景。

例如用户问:

ERR_AUTH_403 是什么原因,应该怎样恢复权限?

关键词检索擅长命中准确的错误码:

片段 E:ERR_AUTH_403 表示当前令牌缺少 invoices:read 权限。

向量检索更容易命中自然语言含义接近的恢复说明:

片段 F:访问被拒绝时,请检查角色权限,并在授权后重新获取令牌。

同时,它也可能召回一个只有少量语义关联的干扰片段:

片段 G:发票模板支持调整页眉颜色和公司 Logo。

混合检索先把多路候选合并:

functionmergeUniqueResults(...resultGroups) {return [...newMap(resultGroups.flat().map((fragment) => [fragment.id, fragment])).values()];}

候选更多,不等于全部都应该交给 LLM。下一步还需要重排:

functionrerankCandidates(candidates, scores, finalK) {return [...candidates].sort((left, right) => scores[right.id] - scores[left.id]).slice(0, finalK);}

Demo 使用固定分数:

片段 E:0.99片段 F:0.96片段 G:0.18

按分数从高到低排序并取 Top-2,最后留下 E 和 F,淘汰 G。

这里的固定分数只是为了展示控制流,并不代表真实重排模型的计算方式。真实系统可能使用向量相似度、关键词分数、专用 Reranker,或者多种信号组合。

只看一个成功案例,不能证明改进可靠

修好“定制商品退款”案例后,很容易产生一种错觉:

但一次成功只能证明一个案例。

因此,我又在 Demo 中加入了三个固定问题:

案例期望片段改进前改进后
定制商品退款BA,失败B,通过
标准商品退款AA,通过A,通过
权限错误恢复E、F只有 E,失败E、F,通过

运行结果是:

改进前:1 / 3改进后:3 / 3

这个结果至少提供了两条证据:

  1. 新策略修复了两个已知失败案例;
  2. 原本正确的标准商品案例没有在这次评测中退化。

但我不能据此声称“线上不会再出问题”。

三个案例太少,也不代表真实用户流量。更严谨的结论只能是:

这也是 Eval 很重要的一点:它不仅衡量结果,还限制我们能够下多大的结论。

生成前的召回,和生成后的回答核验不是一件事

我今天还把两个阶段混在了一起。

我原本把下面三种方式都理解为“召回方案”:

  1. 只用 Prompt 约束模型;
  2. Runtime 按句子或片段检查;
  3. 等完整回答生成后,统一质检再输出。

后来重新梳理才发现:

  1. 召回通常发生在 LLM 生成之前,决定哪些知识进入上下文;
  2. 回答核验发生在生成期间或生成之后,检查模型已经生成的结论是否有依据。

它们解决的是不同问题。

方案优点代价与边界
Prompt 约束后直接流式输出实现简单,首字快不能保证每个结论都有依据
按结论块核验后再发送可以拦截当前块的无依据内容跨块语义复杂,增加延迟
完整缓存回答,全部核验后再输出最容易做整体核验真实首字延迟最高,无法即时展示原始模型流

“完整核验后再输出”也不能直接叫作最准确。

它只是给 Runtime 更多机会在内容暴露给 UI 前发现问题。最终是否更可靠,还取决于核验规则、引用结构以及核验器本身是否可信。

索引更新为什么需要版本

如果修改了文档清洗或切片规则,直接覆盖线上索引会带来风险:

  1. 构建过程中数据可能不完整;
  2. 新旧切片可能混在一起;
  3. 新规则没有经过评测;
  4. 出现问题时难以快速回退。

更可靠的流程是:

v1 继续在线服务→ 独立构建 v2→ 用固定问题验证 v2→ 将 activeVersion 从 v1 切换到 v2→ 观察稳定→ 清理 v1

这就是我今天重新建立连接的另一个术语:增量更新。

我知道版本构建和切换,却没有记住这个专业名称。问题不在知识缺失,而在术语没有和实践连起来。

最后

今天最重要的收获,不是又记住了几个 RAG 术语,而是学会把一次错误回答拆回完整链路:

硬条件是否正确→ 目标片段是否被召回→ 正确片段是否进入最终 Top-K→ LLM 是否依据片段回答→ 回答是否经过必要核验→ UI 最终展示了什么

只有知道问题发生在哪一层,改动才有意义。

过滤和权限控制负责排除不应该出现的数据;混合检索扩大候选;重排决定最终上下文;Eval 比较改进前后;回答核验则处理生成内容是否有依据。

我现在更愿意把 RAG 优化理解为一组有取舍的工程决策,而不是寻找一个永远正确的“最高标准答案”。

相关资讯
点击查看更多
游戏推荐
推荐专题
热门阅读
推荐下载