AI 测试开发面试高频题:"怎么检测和防控大模型幻觉?"
幻觉不是偶发 bug,而是生成模型的概率行为。测试的职责不是"消灭幻觉",而是把它压进阈值、让它可观测、可告警、可回滚。这篇用一次"编造退款政策"的舆情事故,讲透三道防线。
一、真实事故:知识库没有的政策,机器人"确认"了
某运营商客服机器人上线三个月,一位用户问:"30 天内可以无理由退话费吗?"机器人回答:"可以。您可以在 APP 内发起 30 天无理由退话费申请,1-3 个工作日到账。"语气笃定,流程完整。用户截图去营业厅要求兑现,帖子在社交平台传开,客服部一天接了四百个同类咨询。
排查结论:知识库里根本没有这条政策。检索环节召回了两篇弱相关文档——一篇活动文案里出现过"30 天",一篇资费说明里出现过"话费"——模型把两个碎片缝合成了一条不存在的政策,还补全了"1-3 个工作日到账"这种细节。这就是最危险的幻觉类型:不是答错,而是把不存在的事实说得像真的一样。
复盘时测试团队被问了三句话:上线前测出来了吗?上线后多久发现的?发现前还有多少用户收到了编造答案?三个问题,一个都答不上来——因为幻觉既没有进评测集,也没有进监控。
二、核心代码:把"胡说"变成一个可计算的指标
第 1 步:faithfulness 评测——拆分断言,逐条核对
幻觉检测的核心动作是:把回答拆成独立的事实断言,逐条问"这个断言能从检索到的原文推出来吗",算支持率:
import json
from openai import OpenAI
client = OpenAI()
def llm_json(prompt):
r = client.chat.completions.create(
model="gpt-4o", temperature=0,
messages=[{
"role": "user", "content": prompt}])
return json.loads(r.choices[0].message.content)
def faithfulness(answer, contexts):
"""事实忠实度:回答中能被原文支持的断言占比"""
claims = llm_json(
f"把下面的回答拆成独立的事实断言,返回 JSON:{
{\"claims\":[...]}}\n{answer}"
)["claims"]
if not claims:
return 1.0
ok = 0
for c in claims:
v = llm_json(
f"断言「{c}」能否从以下原文推导出来?只返回 "
f"JSON:{
{\"supported\": true/false}}\n原文:{contexts}")
ok += bool(v["supported"])
return ok / len(claims)
事故里那条回答跑这个函数,faithfulness 是 0.25——四个断言三个无出处。如果有门禁,它根本出不了测试环境。
第 2 步:两类硬用例——无中生有 + 缝合怪
def test_unanswerable_must_refuse():
"""知识库里没有的问题,必须拒答或忠实度达标,不许编"""
r = rag.query("30 天内可以无理由退话费吗") # 知识库无此政策
refused = ("无法" in r.answer) or ("没有" in r.answer) or ("以官方" in r.answer)
assert refused or faithfulness(r.answer, r.contexts) >= 0.9, \
f"模型编造了不存在的政策:{r.answer[:80]}"
def test_no_stitching():
"""两个无关片段不许被缝合成因果关系"""
r = rag.query("充值返现活动的话费能退吗")
assert faithfulness(r.answer, r.contexts) >= 0.9
第一类盯"无中生有",第二类盯"缝合怪"——检索回来的每句话都真实,但拼在一起就是假的,这是 RAG 幻觉里最隐蔽的一种。
第 3 步:线上监控——抽样评测 + 敏感词全量
SENSITIVE = ("退", "赔", "政策", "优惠", "承诺", "保证")
def online_monitor():
scores = []
for log in today_logs.sample(rate=0.05): # 日常抽样 5%
if not any(w in log.answer for w in SENSITIVE):
continue
s = faithfulness(log.answer, log.contexts) # 敏感词命中:必评
scores.append(s)
if s < 0.8:
push_human_review(log) # 单条进人工复核队列
if sum(scores) / len(scores) < 0.93:
page_oncall("幻觉率超阈值,检查最近的知识库/提示词变更")
事故之后团队才明白:幻觉是流量问题。单条幻觉率 2% 听起来很小,日均十万次问答就是两千次胡说。所以监控不是"测完上线就结束",而是和错误率一样的日常指标。
三、沉淀成方法:幻觉三道防线
| 防线 | 测什么 | 什么时候跑 |
|---|---|---|
| 离线评测集 | faithfulness + 无答案拒答率 + 缝合用例 | 每次提示词 / 知识库 / 模型变更 |
| 发布门禁 | 拒答率 100%、faithfulness ≥ 0.9 | 发布前,不达标不放行 |
| 线上监控 | 抽样评测 + 敏感词全量 + 均值告警 + 人工复核队列 | 每天 |
三条工程纪律:一是幻觉用比率管理,不用零容忍——追求 0 幻觉会把拒答率搞崩,产品没法用,要管的是"编造率低于阈值且趋势可控";二是拒答是产品能力不是失败——训练集里必须有"该说不知道"的用例,模型敢说"我查不到,建议人工确认"才是合格;三是裁判要校准——每月用人工标注子集对齐 faithfulness 裁判的准确率,裁判本身也是被测对象。
四、面试追问,你答得上来吗
- faithfulness 和 answer_relevancy 有什么区别?——答:faithfulness 查"回答是否忠于检索到的原文",盯编造;answer_relevancy 查"回答是否切中用户问题",盯答非所问。事故里那条回答 relevancy 很高(问退款答退款,非常切题)但 faithfulness 极低——切题的胡说八道才是最危险的,两个指标缺一不可。
- 怎么让模型"敢说不知道"?——答:三管齐下:提示词明确"原文没有就拒答";检索分数低于阈值时直接走拒答分支,不让模型自由发挥;评测集里放足无答案用例进门禁。拒答率要和忠实度一起看,防止模型学会"万事都说不知道"的摆烂。
- 线上监控抽样比例怎么定?——答:按风险分层:敏感词(钱、承诺、政策)全量评,普通问答抽样 5%~10%;阈值从历史均值 ± 标准差定,连续两个窗口越界才告警,避免单条波动刷屏。监控的目标不是抓单条,是抓"分布漂移"——幻觉率突然抬升,往往意味着某次知识库或提示词变更出了问题。
下一篇预告:《用户一句话把客服机器人"带偏"——提示词注入与 AI 安全测试》