2355 字
约 7 分钟
16
S20-A14 · RAG 的拒答:什么时候该说"不知道"

一个内部知识库问答上线三个月,用户最不满的不是"答不出来",而是"答得很像但其实错了"。典型例子是"差旅住宿标准是多少",检索到了两份不同年份的制度文件,模型把两份的金额合并成一句话——听起来完整,实际两边都不对。

RAG 系统最缺的不是召回能力,而是拒答能力:知道什么情况下不该给答案。
"倾向于尽力回答"不是模型的缺陷而是训练目标的结果:它被优化成"给出有用的回答",而在信息不足时,"有用的回答"与"诚实的回答"是两条路。系统要做的是把这个选择权拿回来。

而拒答必须由系统设计决定,因为模型本身倾向于"尽力回答"。


1. 三种该拒答的情形

情形 判据
检索为空或极弱 最高相关分数低于阈值
检索结果互相矛盾 多个来源给出不同结论
问题超出知识范围 没有任何片段覆盖问题主体

第三种最容易被漏掉:检索总会返回一些片段(因为近似搜索永远有"最近邻"),于是系统永远"有材料",而材料与问题无关。

第三种还有一个变体:问题主体在资料里出现过,但问的是资料没写的属性。例如资料写了"住宿标准 600 元",而用户问"是否含早餐"——主体覆盖了,属性没有。这类问题靠分数检测不出来,只能靠"答案中的每个断言都要有出处"。

所以"检索到东西"不等于"有依据",判断必须落在分数与覆盖度上,而不是"有没有结果"。

2. 拒答需要在三个位置实现

位置 手段
检索后 分数阈值 + 覆盖度检查
生成前 提示中明确"资料不足则拒答"
生成后 答案与来源一致性校验

只有第二项时,表现是"模型有时拒、有时编"——因为它对"资料是否足够"的判断不稳定。第一项与第三项是硬约束,它们不依赖模型的自觉。

生成后校验有一个便宜的实现:抽取答案里的数字、专有名词、日期,逐个在来源片段里查找。找不到就降级为"未确认"或直接拒答。它能拦住绝大多数"看起来对"的编造。

// 生成后校验:断言级对齐
def verify(answer: str, chunks: list[str]) -> tuple[bool, list[str]]:
    src = " ".join(chunks)
    missing = []
    for token in extract_facts(answer):        # 数字/日期/专名
        if token not in src:
            missing.append(token)
    return (not missing, missing)
// 不通过 → 改写为"以下内容未在资料中找到依据:...",或转人工

第三项最容易被省:很多系统直接把模型的答案返回,而不检查"答案里的每个数字能否在来源中找到"。

// 检索后闸门:不满足就不进入生成
def gate(chunks, query) -> bool:
    if not chunks: return False
    top = max(c.score for c in chunks)
    if top < 0.62: return False                       # 分数过低
    covered = any(term_in(c.text, query) for c in chunks[:3])
    if not covered: return False                      # 主体未被覆盖
    numeric = extract_numbers(query)
    if numeric and not any(all_numbers_present(c.text, numeric) for c in chunks):
        return False                                  # 问题里的数字未出现在资料中
    return True

3. 阈值怎么定:从评测集来

步骤 做法
1 收集真实问题 从日志取,不要自己造
2 标注"该答/该拒" 人工判定
3 看分数分布 两组的分界在哪
4 选阈值并测两类错误 误拒 vs 误答

第四项是关键取舍:误拒(该答却拒)让用户不满,误答(该拒却答)让用户受损。业务上通常更怕后者,所以阈值偏向"宁可多拒"。

评测集还有一个"必须包含"的部分:曾经答错的问题。它们是最能反映边界的那批样本,而团队往往只收集"正常问题",于是评测结果系统性偏乐观。

第三项必须真做:没有分数分布,阈值就是拍脑袋,而拍出来的阈值往往在 0.7 附近恰好卡住一半的正常问题。

// 阈值选择的依据:两组分数分布的重叠区
该答组:中位数 0.81,P10 = 0.68
该拒组:中位数 0.44,P90 = 0.63
→ 阈值取 0.65:误拒约 8%,误答约 6%
// 若取 0.75:误拒升到 20%,误答降到 1%
// 选择取决于业务:合规场景取 0.75,客服场景取 0.65

4. 矛盾来源的处理

情况 做法
新旧制度并存 按生效日期取最新
多部门口径不同 按提问者归属选择
文档本身矛盾 拒答 + 提示存在冲突

第三项最容易被忽略:资料自身冲突时,正确行为是说"存在两种说法",而不是选一个。因为选错的代价由用户承担。

第二项(按提问者归属选择)在集团型组织里很常见:同一个问题,总部与子公司的口径不同,而两边都是"正确"的。此时拒答不是好答案,"按提问者过滤来源"才是。

第一项需要元数据支撑:每份文档要有生效日期与失效日期,否则"取最新"无从判断。

// 文档元数据:让"取最新"可实现
doc:
  id: travel-policy-2024
  effective_from: 2024-01-01
  effective_to: 2025-12-31        # 空表示长期有效
  owner: finance
  scope: [all-staff]
  status: active                   # active | superseded | draft

5. 拒答的表达方式

表达 适用
"资料中没有相关内容" 检索为空/弱
"找到两份冲突的资料,分别是…" 矛盾
"这方面我无法确认,建议咨询 X" 超出范围
带来源的部分回答 部分覆盖

第四项是 RAG 独有的能力:只回答有依据的部分,明确说出"其余部分没有依据"。这比全拒或全答都更有用。

// 部分回答的形态:每句都挂来源,未覆盖的显式说明
问:出差住宿标准与报销流程分别是什么?
答:住宿标准见《差旅制度 2024》第 3 条:一线城市 600 元/晚 [1]。
    报销流程未在检索到的资料中找到,建议咨询财务共享中心。

6. 排错:五种"该拒没拒"

现象 成因 处理方向
答得很完整但错 检索弱但分数不低 加覆盖度检查
把两份制度合并 矛盾未检测 加冲突检测
同一问题有时答有时拒 只靠提示约束 加检索后闸门
答案数字来源找不到 无生成后校验 数字对齐来源
拒答率极低 无阈值 建阈值与评测集

还有一个指标值得长期跟踪:用户在被拒答后的行为。追问、换个说法再问、转人工,这三种行为分别说明"阈值太高"、"提问方式受限"、"拒答表达得不够有用"。

第五项是系统性的:如果拒答率长期低于 5%,通常不是系统强,而是没有闸门。

7. 边界:什么情况不必拒答

场景 为什么 做法
开放闲聊 无事实约束 不必拒
有明确单一来源 冲突概率低 弱化闸门
内部草稿助手(人会复核) 错误可被发现 可放宽
检索质量极高(如代码搜索) 覆盖度好 阈值可低

最后一条与"体验"有关的边界:拒答必须给出下一步。"资料中没有相关内容"是终点,而"建议查阅《差旅制度 2024》第 3 条,或咨询财务共享中心"才是可用的拒答——拒答的价值在于把用户送到正确的地方。

判断口径一句话:答错一次,代价由谁承担。由用户承担(合规、医疗、财务),就必须有闸门;由系统或人工复核兜住,可以放宽。


8. 自测

  1. 三种该拒答情形里,哪一种最容易被漏掉?为什么"检索到东西"不等于"有依据"?
  2. 拒答为什么要在三个位置实现?只靠提示约束会有什么不稳定表现?
  3. 阈值为什么必须从评测集来?没有分数分布时会怎样?
  4. "误拒"与"误答"的取舍取决于什么?合规场景为什么取更高阈值?
  5. 资料自身矛盾时正确行为是什么?为什么需要"生效日期"元数据?
  6. 判断"是否需要拒答闸门"的那个问题是什么?
S20-A14 · RAG 的拒答:什么时候该说"不知道"
https://www.clxhxhhr.top/posts/6469/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。