一个内部知识库问答上线三个月,用户最不满的不是"答不出来",而是"答得很像但其实错了"。典型例子是"差旅住宿标准是多少",检索到了两份不同年份的制度文件,模型把两份的金额合并成一句话——听起来完整,实际两边都不对。
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. 自测
- 三种该拒答情形里,哪一种最容易被漏掉?为什么"检索到东西"不等于"有依据"?
- 拒答为什么要在三个位置实现?只靠提示约束会有什么不稳定表现?
- 阈值为什么必须从评测集来?没有分数分布时会怎样?
- "误拒"与"误答"的取舍取决于什么?合规场景为什么取更高阈值?
- 资料自身矛盾时正确行为是什么?为什么需要"生效日期"元数据?
- 判断"是否需要拒答闸门"的那个问题是什么?