ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

十万个why:为什么做RAG 最怕的,不是检索失败,是明令禁止瞎答,它还硬编答案

十万个why:为什么做RAG 最怕的,不是检索失败,是明令禁止瞎答,它还硬编答案

拒答日志应该作为知识库迭代的一个输入源,高频被拒的问题定期拉出来看,哪些是知识库确实该覆盖但没覆盖的,补进去;哪些是真的不该这个系统来回答的,在产品和交互层面做引导。

做 RAG 的人早晚都会撞上这个问题,而且通常不是在 demo 阶段,是上了生产、用户量上来之后才发现有多头疼。

大部分讲 RAG 的文章都在说怎么召回得更准——调 chunk size、换 embedding 模型、加 reranker。这些当然有用,但都是在解决"怎么把对的内容找出来"。而知识库外提问是另一件事:内容根本就不在你库里,你检索再准也没用,但系统照样会给你返回东西,而且返回的东西往往看起来还挺像那么回事。

这是很烦人的一个事,搜不到还好说,就怕没搜到还给你乱七八糟的东西!

系统基于一堆不相关的内容拼出了一个自信满满、但事实性错误的答案,用户看不出来,以为你的产品很靠谱。

这个问题麻烦在哪

第一个坑:相似度阈值基本不可靠。

刚上手做 RAG 的时候直觉就是设阈值,低于 0.7 就不采用,跑几天就发现不行。

同一个 embedding 模型,不同领域的 query,相似度分数的分布完全不一样。有的场景 0.7 已经是强相关了,有的场景 0.85 的内容跟 query 的关系还是很牵强。没法用一个全局阈值来区分相关和不相关,除非你的知识库内容极其单一、用户提问方式极其稳定,但生产环境基本不可能是这样。

更麻烦的是,有些 query 和文档之间关键词重合度很高、向量距离也很近,但文档回答的其实是另一个问题。比如用户问"怎么取消自动续费?",你知识库里确实有续费相关的文档,但那是讲续费规则的,不是讲取消操作的。

Embedding 模型分不出这种差别,它看到的就是续费这个词在两个地方都出现了,语义空间里距离很近。检索系统兴高采烈地把这篇文档排在 top-3 返回给你,然后 LLM 拿着这篇文档给用户编了一个取消续费的流程

第二个坑:LLM 在有 context 的情况下,天然倾向于用 context 回答,而不是质疑 context。

加一句"如果没有相关信息就说不知道",在 context 完全空白或者明显不相关的时候确实有用。

但问题在于,检索系统永远会返回 top-K,向量数据库不会返回空结果,它只会返回距离最近的那几条。LLM 看到的永远是有内容的状态。

只要 context 里有文字,LLM 的默认行为就是基于这些文字组织答案,而不是先判断这些文字跟 query 到底有没有关系。把判断逻辑塞进 prompt 能缓解,但不能根治,因为 LLM 对这个文档能不能回答这个问题的判断本身也在受 context 影响,它看到文档里有一些看起来沾边的词,就容易说服自己:这大概算是相关吧。

这两个问题叠在一起,线上跑出来的效果就是:

用户提了一个知识库覆盖不到的问题 → 检索照样返回了 top-K 文档 → LLM 基于这些文档拼出了一个答案 → 答案读起来通顺、自信、像模像样 → 但关键信息是错的或者编的。

这种 failure mode 比检索漏召回严重得多,漏召回用户至少知道你没答上来,而这种用户以为你答对了。

在哪一层处理

我的经验是两层,搜前搜后各一道

检索层能做的是粗筛,把明显不靠谱的拦住,单一阈值不靠谱,但多种信号叠加之后能拦住不少。

一个低成本有效的方法是token overlap,query 里的关键词和检索回来的文档之间做词级别的 overlap 检查。向量相似度高但关键词几乎不重叠,说明 embedding 被语义相近但领域不同的内容带偏了。反过来,关键词大量撞上了但你读一遍发现是在讨论不同的事情,可能是术语刚好重合。

两种情况都可以作为置信度降低的信号。

多路召回交叉验证也能提供信号,dense retrieval 和 sparse retrieval(BM25)各自召回 top-K,看两边的重叠度。如果重叠度很低,说明 semantic match 和 lexical match 之间分歧很大,这次检索的结果不该被无条件信任。

但这些只能拦下明显不相关的,那些看起来相关但其实答非所问的检索结果,靠规则信号拦不住,需要下一步。

生成前做一次相关性校验

检索结果拿到之后,别直接塞给 LLM 生成答案,先让它判断:这些文档到底能不能回答用户的问题。

至于需不需要专门训一个分类模型,我的看法是不需要,直接复用 RAG 链路上已经在调的那个 LLM 就够。

不是说训一个 BERT 做 query-document 二分类技术上不可行,推理快、延迟低,理论上适合。但实际维护成本比看起来高得多。

知识库内容一更新、用户问法一变化,分类边界就跟着漂。你今天标了一批数据训出来的模型,过两个月知识库加了几篇新文档,模型对"什么是相关"的判断可能就不对了。持续标注、持续重训,这个工作量放在一个小规模团队里根本不现实。

用 LLM 做这个判断的优势,不需要额外维护一个模型,链路少一个组件,排查问题少一个变量。而且 LLM 对"这段文字讨论的是 A,而用户问的是跟 A 表面相似但实质不同的 B"这种细微差别的分辨能力,目前的判别式模型差了一个量级。

落地上几个点值得注意:

Prompt 里要让 LLM对每一条文档做判断,不要笼统地问"这些文档和问题相关吗"。笼统地问,它倾向于给一个模糊的评价。逐条打 tag,relevant/partially_relevant/irrelevant,再要求给一句话理由,效果会稳定很多。建议用 structured output 约束格式,不约束的话 LLM 很容易跑偏。

相关的定义要在 prompt 里写清楚。不是文档和 query 是否属于同一主题,而是**文档内容是否能直接用来回答用户的问题"**。这个定义直接决定拒答的准确率和召回率之间的平衡。

拒答分层做,不要一刀切。

全都不相关就直接拒答,告诉用户这个系统的能力边界在哪,部分相关就把已有的信息给出来,同时说明哪些部分找不到答案。最坏的情况是把明明有的信息判成不相关然后拒了,用户比收到一个不太准的答案更恼火。

Prompt 层面的最后一道防线

相关性校验也可能漏,prompt 层面的防御指令必须有,但写法比有没有更重要。

如果没有相关信息就说不知道这句话实际效果很差,因为 LLM 看到 context 里有内容的时候,不会真的去质疑这些内容跟 query 的关系。

改成把判断作为一个显式步骤嵌入 prompt:先判断资料是否真的能回答用户的问题,如果不能,不要硬拼凑,直接说明原因。把"判断 → 决定是否回答"作为第一步而不是一个注意事项,行为上会有明显差别。

说在最后

拒答的目的主要是是兜底,如果用户频繁问知识库外的问题,说明两件事至少有一件成立:要么知识库覆盖有严重缺口,要么产品层面没有让用户搞清楚这个系统应该用来干什么。这两个问题都不是调 prompt 能解决的。

拒答日志应该作为知识库迭代的一个输入源,高频被拒的问题定期拉出来看,哪些是知识库确实该覆盖但没覆盖的,补进去;哪些是真的不该这个系统来回答的,在产品和交互层面做引导。

返回列表