跳至主要內容

程序员小富大约 18 分钟

这个问题我太有感触了。 因为我们组上个月刚好在做一个RAG项目的迭代,其中一个核心痛点就是这个——用户问了一个知识库里根本没有的问题,系统不但没有老老实实说”我不知道”,反而一本正经地瞎编了一个答案。

而且编得特别像那么回事,要不是业务方的人恰好懂这个领域,差点就信了。

这个问题在学术上有个名字叫”幻觉”(hallucination),但在RAG的语境下它有一个更具体的子类型——检索失败导致的幻觉。模型在知识库里没找到相关内容,但它不会告诉你”我没找到”,而是凭自己参数里的知识(或者干脆是瞎编的)生成一个看起来合理的回答。

这个问题不解决,RAG系统在严肃场景下基本不可用。你想想,一个法律咨询系统、一个医疗问答系统、一个企业内部的知识助手——如果它在不知道的时候还硬答,造成的损害比没有这个系统还大。

下面我把我们踩坑的过程和目前的解法整理一下。不敢说是最优方案,但至少是实际跑通了的。

一、先搞清楚问题出在哪个环节 很多人一遇到这个问题就去调prompt——加一句”如果你不知道就说不知道”。

有用吗?有一点用。能根治吗?远远不能。

因为问题的根源不在生成端,而在更上游。要理解这一点,得先把RAG的链路拆开看:

用户提问 ↓ ① Query处理(改写、意图识别) ↓ ② 检索(从知识库中召回相关文档片段) ↓ ③ 相关性判断(召回的内容真的跟问题相关吗?) ↓ ④ 生成(基于检索到的内容回答问题) ↓ ⑤ 输出校验(生成的答案是否有依据?) “用户提问不在知识库范围内”这个问题,可能在每一个环节都需要处理,但大部分人只在第④步做了干预(改prompt),前面几步全是裸奔的。

这就好比一个工厂的质检只在出厂的时候做——前面的原料、加工、组装环节全不管,出厂质检的压力能不大吗?

我们后来的方案是在多个环节设置”拦截点”,而不是把所有压力都堆到最后的生成环节。

下面逐个说。

二、第一道防线:意图识别与边界判断 在检索之前,先判断用户的问题是否在系统的服务范围内。

这一步很多人跳过了,但实际上它的ROI非常高——如果能在最上游就把明显越界的问题拦截掉,后面所有环节的压力都会减轻。

具体怎么做?

方案一:基于规则的边界判断

最简单粗暴的办法。如果你的知识库有明确的领域边界(比如是一个”公司内部IT运维知识库”),你可以定义一组规则来过滤明显无关的问题。

比如用户问”今天天气怎么样”“帮我写一首诗”“你觉得人生的意义是什么”——这些明显不是IT运维问题,可以直接拦截,返回一个预设的引导话术。

这个方法简单但有效,能过滤掉一大批”显然越界”的问题。缺点是覆盖不了模糊的边界情况。

方案二:用LLM做意图分类

让LLM在处理用户问题的第一步先做一个判断:这个问题是否属于本系统的服务范围?

prompt大概长这样(简化版):

你是一个意图分类器。请判断以下用户问题是否属于【XX领域】的问题。

判断标准:

  • 如果问题明确与【XX领域】相关,输出 IN_SCOPE
  • 如果问题明确与【XX领域】无关,输出 OUT_OF_SCOPE
  • 如果你不确定,输出 UNCERTAIN

用户问题:{query} OUT_OF_SCOPE的直接拦截,UNCERTAIN的放行但后续环节加强校验,IN_SCOPE的正常走RAG流程。

这个方案的效果比纯规则好很多,但有一个问题:边界本身就是模糊的。

比如你的知识库是关于”公司产品使用指南”的,用户问”你们公司的竞品有哪些?”——这算不算在范围内?跟产品相关,但知识库里大概率没有竞品信息。

这种边界模糊的case,单靠意图分类是处理不好的。所以这只是第一道防线,不是全部。

三、第二道防线:检索质量评估 这一步是整个方案的核心,也是我觉得最多人忽略的环节。

RAG系统在检索的时候,会返回Top-K个最相关的文档片段。但问题在于:“最相关”不等于”真的相关”。

向量检索的机制决定了它永远会返回结果——你搜什么都能搜出几条”最接近”的,即使这些结果跟你的问题其实八竿子打不着。

打个比方:你在一个只有中国历史书籍的图书馆里搜”量子力学”,图书管理员不会告诉你”没有”,而是会递给你几本”看起来最沾边”的书——可能是一本讲古代天文学的,因为里面提到了”物质”和”运动”这些词,跟”量子力学”有一点点语义上的关联。

你拿着这几本”古代天文学”的书去回答”量子力学”的问题,能不出错吗?

所以关键在于:检索到结果之后,需要评估这些结果跟用户问题的真实相关度。

怎么评估?我们试过几种方案。

方案一:相似度分数阈值

最直觉的办法。向量检索本身会返回一个相似度分数(cosine similarity或者L2 distance),设一个阈值,低于阈值的认为不相关。

我们一开始就是这么做的。

效果怎么样呢?说实话,不太行。

原因是向量相似度分数的分布因query而异,很难找到一个universal的阈值。 有些query天然跟知识库中的内容语义距离比较远(即使内容确实相关),有些query天然距离比较近(即使内容其实不相关)。

一个固定阈值,要么漏判太多(该拦的没拦住),要么误杀太多(本来能答的也被拦了)。

我们调了很久阈值,最后发现这个方案的天花板很低。

方案二:Reranker重排序 + 分数校验

在向量检索返回Top-K之后,加一个Cross-Encoder做重排序。Cross-Encoder相比向量检索的优势在于,它能做更精细的语义匹配——把query和document拼在一起过一个模型,直接输出一个相关性分数。

这个分数比向量相似度分数靠谱得多。

我们用reranker重排之后,再对最高分做阈值判断。如果重排之后的最高分仍然低于某个阈值,就认为知识库中没有相关内容。

效果比方案一好了一截,但还是有边界case处理不好的问题。

方案三:LLM作为相关性裁判(我们目前在用的)

在检索和重排之后,把Top-K的结果跟用户的原始问题一起喂给LLM,让LLM来判断:这些检索到的内容是否真的能回答用户的问题?

prompt大概是这样的(简化版):

你是一个相关性判断助手。

用户的问题是:{query}

以下是从知识库中检索到的内容片段:

{chunk_1}

{chunk_2}

{chunk_3}

请判断:以上检索到的内容是否包含了能够回答用户问题的信息?

  • SUFFICIENT:检索内容中包含足够的信息来回答问题
  • PARTIAL:检索内容中包含部分相关信息,但不足以完整回答
  • IRRELEVANT:检索内容与用户问题基本无关

请给出你的判断并简要说明理由。 根据返回的判断来决定后续行为:

SUFFICIENT → 正常走RAG生成流程 PARTIAL → 走RAG生成,但在回答中明确标注”以下信息可能不完整,仅供参考” IRRELEVANT → 不走RAG生成,直接返回”抱歉,这个问题超出了我的知识范围” 这个方案效果最好,但代价也最大——多了一次完整的LLM调用,延迟和成本都上去了。

所以这里有一个工程上的trade-off:你对”不该答的不答”这件事有多重视? 如果是闲聊机器人,偶尔编一点也无所谓,用方案一甚至不做都行。 如果是企业内部知识助手,最好用方案二,平衡精度和成本。 如果是法律、医疗、金融等高风险场景,建议用方案三,宁可牺牲一些速度也要保证可靠性。 四、第三道防线:生成环节的约束 前面两道防线做好了,大部分越界问题已经能拦住了。但总有一些漏网之鱼——意图识别觉得在范围内,检索也找到了一些”看起来相关”的内容,但这些内容其实不能直接回答用户的问题。

这时候就需要在生成环节加约束了。

这一步大部分人都会做(就是改prompt),但我发现大部分人做得太粗了。

一句简单的”如果你不知道就说不知道”基本没用——因为模型在拿到一堆检索结果之后,它”觉得”自己知道了。你告诉它”不知道就别答”,它的判断是”我知道啊,你看你给我这些参考资料了”。

更有效的prompt策略是把约束写得非常具体:

请严格基于以下参考内容来回答用户的问题。

重要规则:

  1. 你的回答必须完全基于下方提供的参考内容,不要使用你自己的知识
  2. 如果参考内容中没有直接涉及用户问题的信息,请明确告知用户 "根据我所掌握的资料,暂时无法回答这个问题",不要尝试推测或编造
  3. 如果参考内容只能部分回答用户的问题,请回答能回答的部分, 并明确指出哪些部分你无法确认
  4. 在回答的末尾,标注你引用了哪些参考内容

参考内容: {retrieved_chunks}

用户问题:{query} 几个关键点:

“不要使用你自己的知识”——这句话很重要,它明确告诉模型只能用检索到的内容 给出具体的拒答话术——不要只说”说不知道”,而是给出一个完整的模板,模型更容易遵循 允许部分回答——比”要么全答要么不答”更灵活,也更符合实际需求 要求标注引用来源——这个既方便用户验证,也隐含地约束了模型不能编造 这些prompt技巧能不能百分之百解决问题?不能。但配合前面两道防线一起用,漏网的概率会大大降低。

五、第四道防线:输出校验 如果你的场景对准确性要求极高(比如医疗、法律),可以在生成之后再加一道校验。

具体做法是:用另一个LLM调用(或者同一个模型的另一次调用)来检查生成的回答是否有依据。

以下是一个问答系统的输出。请检查回答中的每一个事实性陈述 是否都能在提供的参考资料中找到依据。

参考资料: {retrieved_chunks}

系统回答: {generated_answer}

请列出回答中所有无法在参考资料中找到依据的陈述(如果有的话)。 如果所有陈述都有依据,请回答"全部有据可查"。 如果校验发现有无依据的陈述,可以选择:

去掉那些陈述,只保留有据可查的部分 整体打回,返回兜底话术 标注哪些部分是有依据的、哪些部分无法确认 这个方案的成本最高(又多了一次LLM调用),但在高风险场景下是值得的。

你可以把它理解成一个”出厂质检”——前面的工序再怎么严格,最后出厂前再检查一遍,总比放一个有缺陷的产品出去强。

六、还有一些实用的工程技巧 上面说的是整体方案框架,下面补充一些具体实操中比较有用的技巧。这些东西比较碎,但都是实际做的时候会用到的。

技巧一:给知识库建一个”边界描述” 很多人搭RAG系统,知识库就是一堆文档往里面灌,灌完就开始用。

但你有没有想过:你自己都没有明确定义过这个知识库的边界是什么,你怎么指望系统帮你判断一个问题是不是在范围内?

我们后来做了一件事——给知识库写一个”边界描述文档”,大概几百字,说清楚:

这个知识库覆盖了哪些主题 不覆盖哪些主题 有哪些容易混淆的边界情况 然后把这个描述塞到意图分类的prompt里,让LLM基于这个描述来判断用户问题是否在范围内。

听起来很朴素对吧?但效果出奇地好。因为你等于给了模型一把”尺子”,让它有了一个明确的参照物来做判断,而不是凭”感觉”。

技巧二:维护一个”高频越界问题库” 系统上线之后,你会发现用户问的越界问题其实是有规律的。不同的知识库场景下,用户最常问的越界问题就那么几类。

比如我们做的一个企业内部知识助手,上线之后发现用户最爱问的越界问题集中在:

问薪资福利(知识库里只有业务知识,没有HR信息) 问公司战略方向(知识库里只有操作指南,没有战略文档) 闲聊(”你是谁”“你能做什么”) 把这些高频越界问题收集起来,针对性地准备好引导话术,做成一个简单的FAQ路由。用户问到这些问题时直接走FAQ匹配,不经过RAG流程。

这个办法没有任何技术含量,但实际效果立竿见影——因为它精准地解决了最高频的问题。

很多时候解决问题最有效的不是什么精妙的技术方案,就是老老实实地看数据、看badcase,然后对症下药。

技巧三:分级回答策略 不要把”能回答”和”不能回答”搞成一个非此即彼的二分类。

实际场景中有大量的灰色地带:知识库里有一些相关信息,但不够完整;或者能回答问题的某个方面,但不能回答全部。

比较好的做法是分成几级:

完全命中:正常回答,标注引用来源

部分命中:回答能回答的部分,明确告知哪些方面无法确认 "关于X的部分,根据现有资料,情况是…… 但关于Y的部分,我目前掌握的资料中没有涉及, 建议您咨询……"

完全未命中:诚实告知,给出替代建议 "这个问题超出了我目前的知识范围。 您可以尝试咨询XX部门,或者换一种方式描述您的问题。" 分级策略的好处是让系统的表现更”像人”——一个靠谱的同事遇到他不太确定的问题,不会要么全知要么一问三不知,他会说”这个我知道一部分,另一部分我不太确定,你可以去问问谁谁谁”。

技巧四:引导用户重新提问 很多时候用户的问题”不在知识库范围内”,不是因为知识库真的没有相关内容,而是用户的提问方式跟知识库里的表述对不上。

一个典型的场景:知识库里有”员工出差报销流程”的详细说明,但用户问的是”我上周去北京的差旅费怎么搞”。”怎么搞”跟”报销流程”之间的语义gap,可能导致检索效果不好。

遇到这种情况,与其直接说”不知道”,不如引导用户换一种方式提问:

"关于这个问题我没有找到直接对应的信息。 不过我们的知识库中有关于【出差报销流程】【差旅费标准】等内容。 您想了解的是否与这些主题相关?" 这样做的好处是双重的——一方面帮用户找到了正确的入口,另一方面避免了一次不必要的”不知道”回答。

怎么实现这个功能?一种办法是维护知识库的主题索引——把知识库里的内容按主题归类,当检索结果相关度低的时候,展示相关的主题供用户选择。

七、说一个容易被忽视的根本性问题 上面说的所有技巧,都是在”系统侧”做文章。但有一个问题是需要在更上游解决的:

你的知识库本身的质量和覆盖度怎么样?

我见过不少项目,RAG系统搭得很精致,各种防线都拉了,但知识库本身是一团糟——文档格式混乱、内容过时、重要信息缺失、同一个知识点在不同文档里说法不一致。

这种情况下再怎么优化RAG的各个环节都是事倍功半。

一个我认为很重要但经常被跳过的工作:在搭建RAG系统之前,先做知识库的治理。

具体来说:

梳理知识库的覆盖范围,明确哪些问题应该能回答、哪些不能 清理过时和错误的内容 统一格式和表述方式 识别出知识库的空白区域——哪些用户经常问但知识库里没有的问题 尤其是最后一点。如果某个问题用户反复在问但知识库里确实没有,那最应该做的不是优化”拒答策略”,而是把这个内容补进知识库。

拒答是兜底手段,补全知识库才是正道。

设一个定期的”越界问题review”机制——每隔一段时间看看最近被拒答的问题里有没有高频出现的、本来应该能回答的问题,如果有就补充到知识库里。

这个闭环跑起来之后,知识库的覆盖度会越来越好,需要拒答的情况会越来越少。

八、把整个方案串起来 最后把前面说的所有东西串成一个完整的流程,方便你对照着落地:

用户提问 │ ▼ ┌──────────────────────────┐ │ 第一道防线:意图识别 │ │ 明显越界 → 直接拒答+引导 │ │ 高频越界 → FAQ路由 │ │ 不确定/在范围内 → 放行 │ └──────────┬───────────────┘ │ ▼ ┌──────────────────────────┐ │ Query处理 │ │ 改写、补全、分解 │ └──────────┬───────────────┘ │ ▼ ┌──────────────────────────┐ │ 向量检索 + Reranker重排 │ └──────────┬───────────────┘ │ ▼ ┌──────────────────────────┐ │ 第二道防线:相关性判断 │ │ IRRELEVANT → 拒答+引导 │ │ PARTIAL → 标注后继续 │ │ SUFFICIENT → 正常继续 │ └──────────┬───────────────┘ │ ▼ ┌──────────────────────────┐ │ 第三道防线:受约束的生成 │ │ 严格基于检索内容回答 │ │ 要求标注引用来源 │ │ 分级回答策略 │ └──────────┬───────────────┘ │ ▼ ┌──────────────────────────┐ │ 第四道防线:输出校验 │ │ (高风险场景启用) │ │ 核查事实陈述是否有依据 │ └──────────┬───────────────┘ │ ▼ 输出给用户 │ ▼ ┌──────────────────────────┐ │ 离线闭环 │ │ 收集越界问题 → 分析高频 │ │ → 补充知识库 / 优化边界 │ └──────────────────────────┘ 实际落地的时候不一定每道防线都要上。根据你的场景选择——

闲聊场景:第一道 + 第三道就够了 企业知识助手:四道都建议上,第四道可以抽样做 高风险场景(医疗、法律、金融):四道全上,第四道必须做

最后多说几句 写这篇的过程中回头翻了一下我们项目当时的文档和讨论记录,发现一个有意思的事情:我们最初设计方案的时候,花了大量时间在讨论”怎么让模型回答得更好”,几乎没有讨论”怎么让模型在该闭嘴的时候闭嘴”。

但上线之后收到的投诉里,超过一半是关于”答了不该答的”,只有不到两成是关于”答得不够好”。

后来我们mentor说了一句话,我觉得可以作为这篇回答的总结:

“对于一个知识问答系统来说,在不知道的时候说不知道,是比在知道的时候答对更重要的能力。因为答错了用户会失去信任,而一旦信任没了,你后面答对一百次都挽回不了。” 这句话改变了我们整个项目组的优先级排序。

希望对你也有帮助。这篇写得比较长,如果你正好也在做类似的东西,欢迎评论区交流,看到了会回。

上次编辑于: