RAG 里的拒答:什么时候应该说‘我不知道

个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <

文章目录

  • [RAG 里的拒答:什么时候应该说"我不知道"](#RAG 里的拒答:什么时候应该说"我不知道")
    • [一、为什么 RAG 系统不敢说"不知道"](#一、为什么 RAG 系统不敢说"不知道")
      • [1.1 模型的默认倾向是"答"](#1.1 模型的默认倾向是"答")
      • [1.2 "不知道"在训练信号里是弱项](#1.2 "不知道"在训练信号里是弱项)
      • [1.3 业务压力也在推着它答](#1.3 业务压力也在推着它答)
    • 二、什么情况下应该拒答
      • [2.1 明确该拒:知识库根本没有相关信息](#2.1 明确该拒:知识库根本没有相关信息)
      • [2.2 明确该拒:问题超出知识库范围](#2.2 明确该拒:问题超出知识库范围)
      • [2.3 明确该拒:材料相互冲突且无法判断](#2.3 明确该拒:材料相互冲突且无法判断)
      • [2.4 不该拒:材料部分覆盖,可以作答但需标注边界](#2.4 不该拒:材料部分覆盖,可以作答但需标注边界)
      • [2.5 不该拒:常识和通用知识](#2.5 不该拒:常识和通用知识)
    • 三、怎么让模型可靠地拒答
      • [3.1 第一层:检索侧的阈值判断(最可靠)](#3.1 第一层:检索侧的阈值判断(最可靠))
      • [3.2 阈值怎么标定](#3.2 阈值怎么标定)
      • [3.3 第二层:生成侧的提示词约束](#3.3 第二层:生成侧的提示词约束)
      • [3.4 第三层:生成后的自检(可选但有效)](#3.4 第三层:生成后的自检(可选但有效))
      • [3.5 三层的关系](#3.5 三层的关系)
    • 四、拒答话术怎么写
      • [4.1 好的拒答包含三要素](#4.1 好的拒答包含三要素)
      • [4.2 几种场景的话术模板](#4.2 几种场景的话术模板)
      • [4.3 话术要避免的坑](#4.3 话术要避免的坑)
    • 五、拒答率是个需要监控的指标
      • [5.1 拒答率异常高说明什么](#5.1 拒答率异常高说明什么)
      • [5.2 拒答率异常低说明什么](#5.2 拒答率异常低说明什么)
      • [5.3 建议监控的指标](#5.3 建议监控的指标)
      • [5.4 把拒答的查询收集起来](#5.4 把拒答的查询收集起来)
    • 六、常见失败模式
      • [6.1 提示词写了"不要编造",但系统还是编](#6.1 提示词写了"不要编造",但系统还是编)
      • [6.2 阈值设了,但误拒率很高](#6.2 阈值设了,但误拒率很高)
      • [6.3 模型把"部分缺失"答成了"完整答案"](#6.3 模型把"部分缺失"答成了"完整答案")
      • [6.4 拒答话术被模型自由发挥](#6.4 拒答话术被模型自由发挥)
    • 七、一个完整的拒答决策流程
    • 小结

RAG 里的拒答:什么时候应该说"我不知道"

一个 RAG 系统上线后,最容易被用户投诉的不是答错,而是把没有的东西说得像真的一样。用户问了个知识库里没有的问题,系统洋洋洒洒给了一段看上去很专业但完全是编造的答案------这种"自信的幻觉"比直接说不知道危害大得多。

让模型学会拒答,是 RAG 工程里一个被严重低估的环节。这篇讲清楚:什么时候该拒、怎么让模型可靠地拒、拒答话术怎么写、以及为什么这件事光靠提示词做不好。


一、为什么 RAG 系统不敢说"不知道"

1.1 模型的默认倾向是"答"

大模型在预训练和对齐阶段,被强化的行为是提供有帮助的回答。这种倾向非常顽固------即使你给了它一堆不相关的材料,它也更倾向于"基于这些材料尽力编一个",而不是说"材料不够"。

更麻烦的是:给它上下文这个动作本身,就隐含了"这些材料与问题相关"的暗示。模型会默认这些材料里应该有答案,于是努力去找------找不到也要凑。

1.2 "不知道"在训练信号里是弱项

模型很少被明确训练"承认自己不知道"。虽然 RLHF 阶段会奖励诚实,但这部分的信号强度远弱于"给出有帮助的答案"。

结果就是:拒答是个需要额外工程才能获得的能力,不是模型自带的能力。

1.3 业务压力也在推着它答

产品侧往往不喜欢"我不知道"------转化率不好看、用户体验显得差。于是提示词里被加上"尽可能回答用户问题""不要说不知道"这类指令,把最后一点拒答的可能也堵死了。

这是典型的短期指标换长期信任。一个偶尔说不知道但从不胡说的系统,比一个什么都能答但经常胡说的系统有价值得多,尤其在医疗、法律、金融、企业内部知识库这类场景。


二、什么情况下应该拒答

不是所有"不确定"都要拒。区分几类情况:

2.1 明确该拒:知识库根本没有相关信息

检索回来的最高相关性分数低于阈值,或者所有材料里都找不到问题涉及的实体。

用户问:"公司 2025 年的股权激励计划是什么?"

知识库里只有 2023 年的制度文档,没有任何 2025 年的内容。

应该答:"知识库中没有 2025 年股权激励计划的相关文档。目前可查到的是 2023 版制度,需要我介绍吗?"

2.2 明确该拒:问题超出知识库范围

用户问:"明天杭州天气怎么样?"

知识库是企业内部文档。

这类问题的正确做法是路由到别的工具(天气 API),而不是让 RAG 硬答。如果系统没有别的工具,就明确说"我没有天气查询能力"。

2.3 明确该拒:材料相互冲突且无法判断

知识库里同时存在新旧两版政策,检索结果里两版都有,无法确定哪个是现行版本。

这时候应该把冲突呈现给用户,而不是替他选一个:

"知识库中存在两个版本的差旅标准(2023 版和 2025 版),且未标明哪个是现行版本。两版的主要差异是......建议向 HR 确认现行版本。"

注意:这不算严格意义的"拒答",而是降级为信息呈现。这是比硬拒更好的处理方式。

2.4 不该拒:材料部分覆盖,可以作答但需标注边界

用户问:"A 产品和 B 产品的定价分别是多少?"

材料里有 A 的定价,没有 B 的。

不该整体拒答。正确做法是答 A,明确说 B 未找到:

"A 产品的定价是 XXX。知识库中没有 B 产品定价的相关记录,建议你咨询销售。"

部分拒答是个很实用但常被忽略的能力------很多系统要么全答要么全拒,缺少中间态。

2.5 不该拒:常识和通用知识

用户问:"Python 里怎么反转列表?"

即使知识库里没有,也不应该拒------这是模型的通用知识,属于它本来就有的能力。

判据:如果问题属于模型的通用知识范畴(不依赖你的私有知识库),即使检索为空也可以答,但要标注"这是通用知识,非公司文档内容"。


三、怎么让模型可靠地拒答

3.1 第一层:检索侧的阈值判断(最可靠)

不要指望模型自己判断材料够不够。 在检索侧就做硬性判断,这是最可靠的:

python 复制代码
def should_refuse(question, candidates, reranker, threshold=0.35):
    """基于重排分数判断是否应该拒答"""
    if not candidates:
        return True, "no_candidate"

    scores = reranker.score(question, candidates)
    top_score = max(scores)

    if top_score < threshold:
        return True, f"low_relevance:{top_score:.3f}"

    return False, None

为什么用重排分数而不是向量相似度?

  • 向量相似度的绝对值没有可比性。不同 embedding 模型的分数分布完全不同,0.7 在 A 模型里可能很高,在 B 模型里可能很低。
  • 交叉编码器(reranker)的分数语义更接近"相关性",通常经过 sigmoid 归一化到 0~1,跨模型可比性好一些。

不过即便如此,阈值必须自己标定,不能拍脑袋。

3.2 阈值怎么标定

方法:构造一批"知识库里有答案"和"知识库里没答案"的样本,跑重排打分,画分布,找分界线。

python 复制代码
# 1. 正样本:从知识库里挑 100 个问题,人工确认答案在库里
# 2. 负样本:构造 100 个与知识库主题相近但库中无答案的问题
# 3. 跑重排,收集 top1 分数

pos_scores = [...]   # 有答案的 top1 分数
neg_scores = [...]   # 无答案的 top1 分数

# 4. 找一个阈值,使得误拒率(正样本被拒)可接受
for t in np.arange(0.1, 0.9, 0.05):
    false_refuse = sum(s < t for s in pos_scores) / len(pos_scores)
    false_answer = sum(s >= t for s in neg_scores) / len(neg_scores)
    print(f"t={t:.2f}  误拒={false_refuse:.2%}  误答={false_answer:.2%}")

关键决策:误拒和误答,你更怕哪个?

  • 医疗、法务、金融合规:宁可误拒。阈值调高,牺牲回答率换准确性。
  • 客服、导购、内部助手:可以适度误答。阈值调低,多答一点,因为用户可以自己判断。

这两者的取舍必须由业务定,不是技术问题。

3.3 第二层:生成侧的提示词约束

即使检索侧判断"可以答",生成侧也要给拒答留出空间。提示词里显式授权:

python 复制代码
ANSWER_PROMPT = """你是基于知识库回答问题的助手。

严格规则:
1. 只使用【材料】中的信息作答,不得使用你自己的知识补充;
2. 如果材料中完全没有能回答该问题的信息,直接回答:"知识库中没有相关信息,无法回答该问题。"不要试图猜测或补充;
3. 如果材料只能部分回答,就只答能答的部分,并明确说明哪部分缺失;
4. 每个结论必须能对应到材料中的某一段,用 [1][2] 标注来源;
5. 如果材料之间存在冲突,如实呈现冲突,不要自行选择。

【材料】
{context}

【问题】
{question}"""

几个要点:

  • 明确写出拒答时该说什么(给出确切话术),比说"不确定时请说不知道"有效得多。模型对"不要做 X"的指令遵循度低于"做 Y"。
  • 禁止用自身知识补充(第 1 条)。没有这条,模型会拿通用知识填空,你根本看不出来哪句是编的。
  • 允许部分作答(第 3 条),避免全有全无。

3.4 第三层:生成后的自检(可选但有效)

让模型回答完之后,再自己检查一遍:

python 复制代码
VERIFY_PROMPT = """检查下面的回答是否严格基于材料。

回答:{answer}
材料:{context}

请逐句检查回答中的每个事实性陈述,判断:
- 能否在材料中找到直接依据?
- 是否存在材料之外的补充或推断?

输出 JSON:
{{"grounded": true/false, "ungrounded_parts": ["...", "..."]}}"""

如果 grounded=false,要么改成拒答,要么把无依据的部分删掉。

这层的代价是一次额外 LLM 调用,但能显著提升可靠性。适合对准确性要求极高的场景。

3.5 三层的关系

复制代码
检索侧阈值  →  拦住"明显没有材料"的情况(最可靠、最便宜)
     ↓
生成侧提示  →  处理"材料有但答不上"的情况(覆盖中间地带)
     ↓
生成后自检  →  兜住"模型还是编了"的情况(最贵、最后防线)

三层不是都要上。我的建议:第一层必做,第二层必做,第三层按场景选。


四、拒答话术怎么写

拒答不只是一个技术判断,更是用户体验问题。干巴巴一句"知识库中没有相关信息"会让用户觉得系统很笨。

4.1 好的拒答包含三要素

  1. 明确说没有(不含糊、不兜圈子)
  2. 说明检索了什么(让用户知道系统确实查了,不是没查)
  3. 给出下一步建议(替代方案、找谁、去哪查)

4.2 几种场景的话术模板

完全无相关材料:

我在知识库里检索了"XX"相关内容,没有找到可以回答这个问题的文档。

可能的原因:该信息尚未录入知识库,或使用了不同的术语表述。

建议:① 试试换个说法描述你的问题;② 联系 XX 部门确认;③ 如果这是应该录入的内容,可以在 XX 提交补充申请。

部分缺失:

根据知识库,我可以回答你问题的前半部分:

能答的部分

关于"XX",知识库中没有找到相关记录,为避免给你错误信息,这部分我不做回答。建议咨询 XX。

材料冲突:

知识库中存在两条不一致的记录:

  • 文档 A(更新于 2023-05)显示:XXX
  • 文档 B(更新于 2025-11)显示:YYY

两者未标明哪个是现行版本,我无法判断。建议向 XX 确认后使用。

超出能力范围:

这个问题需要查询实时信息(如天气/股价/物流状态),我没有接入相关数据源,无法回答。你可以通过 XX 渠道查询。

4.3 话术要避免的坑

  • ❌ "很抱歉,我无法回答这个问题。" ------ 太敷衍,没有信息量
  • ❌ "根据我的知识,可能是 XXX,但不确定。" ------ 半拒半答,最糟糕,等于给了错误信息又加了免责
  • ❌ 长篇解释为什么答不了 ------ 用户要的是解决方案,不是技术说明
  • ✅ 简洁 + 说明查了什么 + 指路下一步

五、拒答率是个需要监控的指标

很多团队只监控"回答准确率",不监控"拒答率"。这是错的。

5.1 拒答率异常高说明什么

  • 知识库覆盖不足(该有的文档没有)
  • 检索质量有问题(embedding 不合适、切分太碎、有答案但没召回)
  • 阈值设太高

5.2 拒答率异常低说明什么

  • 阈值太松,系统在硬答
  • 提示词约束失效,模型在用自身知识编造
  • 这是更危险的信号

5.3 建议监控的指标

指标 计算方式 健康区间(参考)
拒答率 拒答数 / 总查询数 5%~20%(视场景)
误拒率 有答案却被拒 / 有答案总数 < 5%
幻觉率 人工抽检中编造的比例 < 2%
部分作答率 部分拒答 / 总查询数 10%~30%

误拒率和幻觉率要做人工抽检,不能靠自动化。每周抽 50 条,人工过一遍,这个成本是必须的。

5.4 把拒答的查询收集起来

被拒答的查询是知识库补漏的最佳素材。按频次排序,高频被拒的问题就是知识库最该补充的内容。

python 复制代码
# 记录拒答日志
refusal_log.append({
    "question": q,
    "top_score": top_score,
    "timestamp": now,
    "user_id": uid,
})

# 每周统计高频拒答问题
from collections import Counter
top_refused = Counter(r["question"] for r in refusal_log).most_common(20)

这个动作跑几个月,知识库的覆盖率会有肉眼可见的改善。


六、常见失败模式

6.1 提示词写了"不要编造",但系统还是编

原因:光说"不要"没用,要给替代行为。改成"如果没有相关信息,请直接回复:XXX"(给出确切话术)。

6.2 阈值设了,但误拒率很高

原因:重排模型的分数分布和你预期的不一样。必须用自己的数据标定,不能抄别人的阈值。

另一可能:知识库里有答案,但切分太碎导致单块信息不完整,重排分数上不去。这时候该修的是切分,不是阈值。

6.3 模型把"部分缺失"答成了"完整答案"

原因:提示词里没有明确要求标注缺失部分。加上第 3 条规则(部分作答要求),并且要求输出时显式写"以下部分材料未提供"。

6.4 拒答话术被模型自由发挥

原因:话术没写死。把拒答的标准回复作为固定字符串写进提示词,甚至可以做成后处理------检测到拒答标记时,直接返回预定义的模板,不让模型自由生成。

python 复制代码
REFUSAL_TEMPLATE = """我在知识库里检索了相关内容,没有找到能回答这个问题的信息。

建议:{suggestion}"""

# 后处理:模型输出拒答标记时,用模板替换
if "[[REFUSE]]" in answer:
    return REFUSAL_TEMPLATE.format(suggestion=guess_suggestion(question))

这样能保证话术的一致性和合规性(法务、品牌部门会喜欢这个做法)。


七、一个完整的拒答决策流程

python 复制代码
def rag_with_refusal(question, retriever, reranker, llm,
                     threshold=0.35, max_docs=5):
    # 1. 粗召回
    candidates = retriever.invoke(question)
    if not candidates:
        return refusal_response(question, reason="empty_index")

    # 2. 重排打分
    scored = reranker.rank_with_score(question, candidates)
    top_score = scored[0].score

    # 3. 检索侧阈值判断
    if top_score < threshold:
        return refusal_response(question, reason="low_relevance",
                                score=top_score)

    # 4. 取 top_n 构建上下文(最相关的放末尾)
    docs = [s.doc for s in scored[:max_docs]]
    context = build_context(docs)

    # 5. 生成(提示词内含拒答授权)
    answer = llm.invoke(ANSWER_PROMPT.format(context=context,
                                             question=question)).content

    # 6. 解析拒答标记
    if "[[REFUSE]]" in answer:
        return refusal_response(question, reason="generation_refused")

    # 7. (可选)自检
    if ENABLE_VERIFY:
        check = verify_grounded(answer, context)
        if not check["grounded"]:
            answer = strip_ungrounded(answer, check["ungrounded_parts"])

    # 8. 附带来源
    return answer_with_sources(answer, docs)

这个流程把三层防御串起来了,每一层都可以独立开关,方便按场景调整。


小结

  • 拒答是需要额外工程才能获得的能力,模型默认倾向于硬答,业务压力还在推着它答。
  • 该拒的三类:知识库无相关信息、超出能力范围、材料冲突无法判断。不该拒的两类:可部分作答(应部分答)、属通用知识(可答但需标注)。
  • 三层防御:检索侧阈值(最可靠)→ 生成侧提示词(给确切拒答话术)→ 生成后自检(兜底)。前两层必做。
  • 阈值必须用自己数据标定,并明确"误拒 vs 误答"的业务取舍。
  • 拒答话术要包含三要素:明确说没有、说明查了什么、给出下一步建议。可以用固定模板做后处理保证一致性。
  • 把拒答率纳入监控,被拒的查询是知识库补漏的最佳素材。

一个判断标准:如果你的系统从来不说"不知道",那它不是太聪明,而是太敢编。在生产环境里,后者是要出事的。

相关推荐
狄龙1 小时前
RLDS 数据格式详解:结构、下载与实操(九)
人工智能
狄龙1 小时前
动捕数据完整工作流:从公开动捕 / 单目视频到机器人训练数据(八)
人工智能
江湖中的阿龙1 小时前
AI Agent 调用工具怎么做才稳?参数校验、幂等与审计的 Python 实战
人工智能·python·ai agent·工具调用
陈然信息站1 小时前
食品日化产线铝箔袋检测集成:明治VDS20视觉传感器安装与通讯要点
人工智能
桃桃乌龙_1 小时前
从 ChatGPT 到 Agent Harness:近五年 AI 技术演进全景(MCP / Skills / A2A 详解)
人工智能·agent
狄龙1 小时前
SkillGen 与域随机化解析:Isaac Lab 规划式数据生成进阶(十)
人工智能
QinHsiu1 小时前
从Transformer到原生多模态:一文看懂大模型架构的“变”与“不变”
人工智能
QinHsiu1 小时前
1.7B打赢8B:看了UNITAS的3D原生架构,发现“不经图片Token”才是具身智能的真护城河
人工智能