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

完整案例代码:RAG 检索、重排与评测 Demo

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

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

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

用户最后看到的回答错了,问题究竟发生在哪一层?

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

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

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

先看一个回答错误的案例

知识库中有两个片段:

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

用户问:

text 复制代码
定制商品进入生产后可以退款吗?

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

text 复制代码
可以,因为仍在付款后 30 天内。

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

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

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

检索失败,而不是生成失败。

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

js 复制代码
function generateFromContext(contextFragmentIds) {
  if (contextFragmentIds.includes("片段B")) {
    return {
      text: "不可以,定制商品进入生产后不支持退款。[片段B]",
      answerGrounded: true
    };
  }

  return {
    text: "可以,因为付款后 30 天内可申请退款。[片段A]",
    answerGrounded: false
  };
}

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

js 复制代码
const baselineRun = runPipeline({
  strategy: "仅关键词匹配",
  retrievedFragmentIds: ["片段A"]
});

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

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

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

最终答案变好,不代表模型能力变强了,也可能只是 Runtime 给了模型更准确的上下文。

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

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

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

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

js 复制代码
{
  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 不能相信这个字段。

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

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

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

更强的隔离还可以使用:

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

不能采用的方式是:

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

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

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

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

例如用户问:

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

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

text 复制代码
片段 E:ERR_AUTH_403 表示当前令牌缺少 invoices:read 权限。

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

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

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

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

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

js 复制代码
function mergeUniqueResults(...resultGroups) {
  return [
    ...new Map(
      resultGroups.flat().map((fragment) => [fragment.id, fragment])
    ).values()
  ];
}

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

js 复制代码
function rerankCandidates(candidates, scores, finalK) {
  return [...candidates]
    .sort((left, right) => scores[right.id] - scores[left.id])
    .slice(0, finalK);
}

Demo 使用固定分数:

text 复制代码
片段 E:0.99
片段 F:0.96
片段 G:0.18

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

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

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

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

这个检索策略已经改好了。

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

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

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

运行结果是:

text 复制代码
改进前:1 / 3
改进后:3 / 3

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

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

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

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

改进后的检索策略在当前三个固定案例上优于基线,尚未证明生产环境中的稳定性。

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

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

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

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

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

后来重新梳理才发现:

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

它们解决的是不同问题。

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

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

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

索引更新为什么需要版本

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

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

更可靠的流程是:

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

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

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

最后

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

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

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

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

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

相关推荐
美狐美颜SDK开放平台1 小时前
直播app开发如何实现主播级美颜效果?美颜sdk开发方案详解
人工智能·音视频·美颜sdk·视频美颜sdk·美颜api
东方小月2 小时前
从零开发一个 Coding Agent(四):使用状态机校验大模型事件流
前端·人工智能·后端
大龄码农有梦想2 小时前
使用大模型服务如何保证数据安全?企业 AI 安全、私有化部署与治理实践
人工智能·私有化部署·数据安全·信创·ai agent·智能体·智能体开发平台
冬奇Lab2 小时前
每日一个开源项目(第170篇):CodeWiki - ACL 2026 论文级代码库自动文档生成,递归多 Agent 架构
人工智能·开源·资讯
lxw18449125142 小时前
Claude-Code企业级培训教程
人工智能
冬奇Lab2 小时前
代码库知识库系列(01):技术全景——为什么代码理解比文档检索难十倍
人工智能
MartinYeung52 小时前
[论文学习]迈向自主医疗人工智能智能体:MIRA系统深度分析
人工智能·学习
土星云SaturnCloud2 小时前
边缘侧大模型部署的新利器——国科环宇GK 300I大模型一体机深度评测与架构解析
服务器·人工智能·ai·边缘计算
_Jimmy_3 小时前
Agent 溯源精度提升方案
人工智能·python·langchain