有一次用户反馈:回答看起来挺专业,但点开引用,发现对不上。

我一开始不信。检索结果是我看着召回的,回答也是基于这些结果生成的,怎么会对不上?直到我把回答里的 [1]、[2] 一个个对应回原文,才发现问题:回答是对的,引用是错的。

这比“答错”更麻烦。答错,用户会怀疑;引用错,用户会相信。

先别急着换模型

第一反应是模型不够好,想换更大的模型。但我忍住了,先把链路拆开:

  1. 用户问题有没有被正确理解;
  2. 检索有没有召回到真正相关的证据;
  3. 召回的内容有没有被正确压缩;
  4. 生成时有没有把引用和证据绑定;
  5. 引用编号有没有在压缩、合并、重排中错位。

一查,问题在 2 和 5:检索召回了不少“主题相关”的段落,但不是“能支撑结论”的证据;压缩时又把几段拼在一起,引用编号还是按拼接顺序来的,和原文对不上。

检索“相关”不等于“有用”

原来的检索是单路向量召回,看的是语义相似。问题是,很多段落的语义和问题很像,但里面没有可引用的结论。

后来加了几件事:

  • 混合检索:关键词和向量一起用,避免只靠语义漂移;
  • 元数据过滤:时间、类型、来源范围先收窄;
  • 重排:召回一批之后,再用更细的模型排一次;
  • 证据阈值:分数不够的,不进上下文。

不是越多越好。上下文塞得越满,模型越容易“看见什么说什么”。

引用要绑定,不是编号

以前的做法是:把几段材料拼成一个长文本,前面加 [1][2],让模型自己引用。模型很容易把编号当装饰。

后来改成:每段材料带一个稳定的 evidenceId,生成时要求模型引用 evidenceId,后处理再映射成 [1][2]。如果模型引用了不存在的 ID,直接判失败,重试或降级。

再加一层校验:回答里的关键句,和引用段落的原文做重叠/蕴含检查。对不上,就把这句标成“证据不足”,而不是硬答。

评测不能只看“答得像不像”

我们一开始的评测,是找几个人看回答“专不专业”。结果大家都觉得挺专业,但引用错误没人发现。

后来改成三层:

  • 检索层:Recall@K、MRR,看该找的有没有找到;
  • 引用层:引用准确率、证据覆盖率,看引用能不能支撑结论;
  • 回答层:忠实度、完整性,看有没有编。

再准备一小套金标问题,每个问题标好“必须出现的证据”。数量不用多,几十条就能暴露大部分问题。

最后一句

RAG 不是“把文档塞进向量库,然后让模型回答”。它更像一个证据系统:先找到证据,再让模型基于证据说话,最后检查它有没有胡说。

检索决定上限,引用决定可信度,评测决定你能不能持续变好。