跳至主要內容

程序员小富大约 8 分钟

基于大模型的RAG系统里面,怎么解决用户提问不在知识库范围内的问题?

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

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

这个才是真正危险的地方。不是系统说我不知道,而是系统基于一堆不相关的内容拼出了一个自信满满、但事实性错误的答案。用户看不出来,以为你的产品很靠谱。

这个问题麻烦在哪 第一个坑:相似度阈值基本不可靠。

刚上手的时候直觉就是设阈值,低于 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:先判断资料是否真的能回答用户的问题,如果不能,不要硬拼凑,直接说明原因。把”判断 → 决定是否回答”作为第一步而不是一个注意事项,行为上会有明显差别。

拒答之后 这事很多团队上线前不会想,上线后发现拒答率 20%~30%,用户反馈里一半在骂为什么问什么都说不知道。

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

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

技术层面对知识库外提问的处理,做的是兜底,让系统在该说不知道的时候说不知道。但兜底做得再好,如果 30% 的 query 都被拒了,用户该走还是走。本质上这是一个产品问题:你的知识库和你的用户需求之间,到底还有多大 gap。技术能保证你不胡说,但保证不了用户满意。

上次编辑于: