模型张口就来还言之凿凿:把『幻觉检测』做成一条会拦上线的事实一致性断言

简介: 本文提出用“事实一致性(groundedness)断言”自动化拦截大模型幻觉:不看回答是否“像样”,而逐句校验每个事实性陈述能否溯源至给定上下文。编造即红,零容忍上线,让幻觉无处藏身。

模型张口就来还言之凿凿:把『幻觉检测』做成一条会拦上线的事实一致性断言

一个知识问答应用,用户问某产品的退换货政策。模型答得漂亮:语气笃定、条理清晰,还煞有介事地列了「7 天无理由退换、15 天质量问题包换、运费由商家承担」。用户看完直接照着操作了。

问题是——知识库里根本没有这条政策。那段「7 天无理由、15 天包换」是模型编的,编得比真的还真。而功能测试对此毫无办法:它去测,回答格式合法、字段齐全、语气正常,每一条断言都过。因为回答「看起来完全正确」。

这就是幻觉最难测的地方。幻觉不是「答得差」,是「答得对-looking 但没有依据」——它专门骗过所有只看「像不像样」的测试。

本篇要解决的,是把幻觉检测从「人肉看回答靠不靠谱」,变成一条会拦上线的可回归断言:模型说的每一个事实性句子,都必须能溯源到给定的上下文;溯源不到,就是幻觉,判红。

一、幻觉测不出来,是因为你测的是「像不像样」

先给结论:靠人肉或靠「格式对不对」的测试,天然抓不到幻觉。因为幻觉的输出在「形式」上是完美的——它语法通顺、结构完整、语气自信,唯一的问题是「内容在给定资料里找不到出处」。你测形式,它就过关;你要测内容有没有依据,就得换一套完全不同的思路。

这套思路你其实很熟。测一个报表接口时,你不会只看「数字格式对不对、能不能返回 200」,你还会追问「这个数字能不能追到源数据」。如果报表里冒出一个源库里根本不存在的销售额,那就是数据造假——哪怕它格式再漂亮。

事实一致性(groundedness)断言,就是把这条「引用完整性」校验搬到大模型输出上:模型说的每个事实性句子,都要能指回上下文里的哪一段。指不回去,就是「编造的数字」。下面这张表,把两种做法摊开对照:

维度 靠人肉看回答像不像样 事实一致性(groundedness)断言
能否发现言之凿凿的编造 越自信越像真的,人眼越难察觉 只看有无出处,编造句无支撑即暴露
可回归性 每次改 prompt 都要重新人肉看 用例固化,每次改动自动重跑
可归因到哪句 只能笼统说「感觉不对」 逐句判定,精确指出哪句无支撑
上线门禁 靠主观判断,无法当门禁 无支撑句数超阈值即 exit 1,硬门禁

看清楚了:人肉看「像不像样」,恰恰是幻觉最擅长骗过的东西;而 groundedness 断言根本不看像不像,只看「有没有出处」——这一换视角,言之凿凿的编造就藏不住了。

更何况人肉抽查在规模上也扛不住。知识库问答一天可能跑成千上万条,你不可能条条去看,只能抽样;而抽样偏偏最容易漏掉低频长尾问题里的编造——那些恰恰是知识库里资料最稀薄、模型最爱脑补的地方。更麻烦的是「自信偏差」:模型越是编得笃定、排版越是清晰,人越倾向于相信它,抽查时反而更容易挥手放行。所以把人肉换成断言,省的不只是人力,是把「靠一个人一时的主观判断」换成「靠一条每次都跑、不受语气和排版影响的规则」——规则不会因为回答写得好漂亮就心软。

二、逐句判定:抽出事实句,检查上下文里有没有支撑

要做这条断言,第一步是把模型输出拆成一个个「事实性句子」,再逐句去给定上下文里找支撑。支撑判定可以从便宜到昂贵分层做:最简单的是「关键实体/关键词覆盖」——句子里的核心实体,是否在 context 里出现过;更稳的是调一个判定函数(规则或小模型)判「这句话能不能由 context 推出」。

# groundedness.py —— 事实一致性(groundedness)逐句判定
import re

def split_factual_sentences(answer: str) -> list[str]:
    """把回答拆成事实性句子(本文示例:按句末标点切分,过滤纯客套)。"""
    polite = [" hope this helps", "希望有帮助", "如有疑问"]
    raw = re.split(r"[。;\n]", answer or "")
    return [s.strip() for s in raw
            if s.strip() and not any(p in s for p in polite)]

def extract_entities(sentence: str) -> set:
    """抽出句子里的关键实体:数字、天数、金额、专有名词(本文示例规则)。"""
    return set(re.findall(r"\d+\s*[天日元%]|\d+", sentence))

def is_supported(sentence: str, context: str, min_cov: float = 0.6) -> bool:
    """判定一句话是否被 context 支撑:核心实体在上下文中的覆盖率 >= 阈值。"""
    ents = extract_entities(sentence)
    if not ents:                       # 无实体的一般性表述,本文示例视为不涉事实
        return True
    hit = sum(1 for e in ents if e.replace(" ", "") in context.replace(" ", ""))
    return hit / len(ents) >= min_cov

def check_groundedness(answer: str, context: str) -> dict:
    """逐句判定,返回无支撑句明细。"""
    details = []
    for s in split_factual_sentences(answer):
        ok = is_supported(s, context)
        details.append({
   "sentence": s, "supported": ok,
                        "entities": sorted(extract_entities(s))})
    unsupported = [d for d in details if not d["supported"]]
    return {
   "details": details, "unsupported": unsupported,
            "unsupported_count": len(unsupported)}

为什么这么写。第一,split_factual_sentences 先把客套话过滤掉——「希望有帮助」这类句子不承载事实,不该参与判定,否则会把无害的寒暄也误判成幻觉。第二,is_supported 用「核心实体在上下文里的覆盖率」当判据,是一条便宜、确定、可解释的规则:句子里说「7 天」「15 天」,就去 context 里找这两个数字在不在,找不到就是无源之水。阈值 min_cov = 0.6 是本文示例,意思是「一句话里至少六成的核心实体能在上下文找到」,太严会误杀合理的同义改写,太松会放过编造,得按你的场景调。第三,extract_entities 返回空集时我判为 True(视为不涉具体事实),这是个刻意的取舍——纯定性表述难以用实体覆盖判定,本文示例先放行,真实场景里这类句子应该交给更强的判定函数(小模型或 NLI)去兜。踩过的坑是,早期直接拿整段回答去 context 里做子串匹配,结果模型只要换个说法(「7 天」写成「一周」)就判成无支撑、假红一片;改成「抽实体 + 算覆盖率」之后,才把「换措辞但事实一致」和「真编造」区分开。

三、把 groundedness 写成会拦上线的 pytest 断言

有了逐句判定,下一步是把它接进 pytest:断言「无支撑事实句数量 == 0」,超阈值即红,并把 context、输出、逐句判定结果落盘成一份可复核的证据——出了争议,能拿出「哪句、缺哪个实体、上下文里确实没有」的铁证。

# test_groundedness.py —— 事实一致性上线门禁(pytest)
import json, pytest
from groundedness import check_groundedness

# 每行一条样本:{"context": 给定知识库片段, "answer": 模型回答}
CASES = [
    {
   
        "context": "本产品支持 7 天无理由退货,退货运费由买家承担。质量问题 15 天内可换货。",
        "answer": "本产品支持 7 天无理由退货;质量问题 15 天内可换货。",
        "expect_unsupported": 0,     # 全部有据,应通过
    },
    {
   
        "context": "本产品支持 7 天无理由退货,退货运费由买家承担。",
        "answer": "本产品支持 7 天无理由退货,15 天质量问题包换,运费由商家承担。",
        "expect_unsupported": 1,     # 『15 天包换』上下文没有 = 幻觉
    },
]

@pytest.mark.parametrize("case", CASES)
def test_no_unsupported_facts(case, tmp_path):
    r = check_groundedness(case["answer"], case["context"])
    # 证据落盘:context / answer / 逐句判定,出争议时可复核
    (tmp_path / "grounded_evidence.json").write_text(
        json.dumps({
   **case, "result": r}, ensure_ascii=False, indent=2),
        encoding="utf-8")
    assert r["unsupported_count"] == case["expect_unsupported"], \
        f"发现无支撑事实句:{[d['sentence'] for d in r['unsupported']]}"

def test_gate_zero_hallucination_on_golden():
    """门禁口径:golden 集上无支撑事实句必须为 0,否则不允许上线。"""
    total = 0
    for case in CASES:
        total += check_groundedness(case["answer"], case["context"])["unsupported_count"]
    # 演示门禁阈值:真实上线场景通常要求 total == 0(零容忍)
    assert total <= 1, f"golden 集累计无支撑句 {total} 超门禁阈值"

为什么这么写。第一,参数化的两条样本刻意一正一反:第一条句句有据、expect_unsupported == 0,第二条偷偷塞了「15 天包换、运费商家承担」而上下文只写了「买家承担」——正好演示「言之凿凿的编造」怎么被逐句判定抓出来。把正反样本都固化进用例,才能防住有人哪天把判定逻辑放松了而没人发现。第二,每条都把 context / answer / 逐句判定 写进 grounded_evidence.json,这是 groundedness 断言比「人肉看」强的地方——它不只给结论,还给证据链,评审或申诉时能精确指出「哪句、缺哪个实体」。第三,test_gate_zero_hallucination_on_golden 是门禁入口,示例里写 total <= 1 只是为了让演示样本能过;真实上线口径通常是 total == 0 零容忍——一个会误导用户按假政策操作的问答应用,容不下任何一句编造。踩过的坑是,早期只断言「有没有幻觉」不给证据,红了之后开发根本不认「凭什么说这句是编的」,扯皮半天;把逐句判定和缺失实体一起落盘后,证据摆在眼前,改起来也不再靠猜。文中样本、覆盖率、阈值均为本文示例。

四、判定函数自己也会错:别用一个没校准的裁判去抓幻觉

groundedness 断言有个绕不开的元问题:判「有没有支撑」的那个判定函数,本身也可能判错。用实体覆盖率这种规则判定,会把「7 天」和「一周」当成两回事而假红,也可能因为上下文里恰好出现过某个数字、就放过一句真编造而漏检。所以判定函数不是写完就完事——它自己也得被校准、被回归。

做法跟你校准一个自动裁判(LLM-as-judge)是同一套:先手工标一批「这句话到底有没有出处」的金标准样本,再拿判定函数去跑,统计它的假红率和漏检率;等判定函数升级(比如从实体覆盖换成 NLI 模型或更强的判定服务),就重跑这批金标准,确认它没有悄悄变松或变严。只有对判定函数本身的准确率心里有数,你才敢拿它的输出去卡上线门禁——否则你只是把「模型编造」的风险,平移成了「裁判误判」的风险,看着有门禁,实则没底。

这也是为什么前面代码里我把判定逻辑单独抽成 is_supported 一个函数:它越独立、越好替换,你就越能拿金标准集去单独校准它,而不必动整套用例。判定函数是可插拔的一件「量具」,量具本身要先校准过,量出来的结论才作数。文中假红率、漏检率、阈值均为本文示例。

把这条断言接进 CI,幻觉检测就从「上线后人肉抽查、出了事才回溯」变成了「合并前自动跑、编造句直接拦下」。它治不了模型爱编的毛病,但它能保证——编出来的东西,上不了线。

幻觉最狡猾的地方是「答得对-looking」,所以别再看它像不像样,去看它有没有出处——每个事实句都溯源不到上下文,就该被一条断言拦在上线之前。

你们的知识问答/RAG 应用,是怎么防「模型言之凿凿地编政策」的?是靠人肉抽查,还是已经做了 groundedness 这类事实一致性断言?评论区聊聊你踩过的幻觉坑。

相关文章
|
9天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
|
9天前
|
人工智能
千问办公官网入口:阿里AI办公QwenWork产品页和免费网页端链接
千问办公官网含两大入口:一是网页端(qwenwork.cn),即开即用,支持浏览器直接访问;二是阿里云产品页 https://t.aliyun.com/U/JNKJuO 提供免费/付费版详情、功能介绍及使用指南。
|
15天前
|
网络协议 Linux iOS开发
【2026实测】Wireshark下载+安装+汉化+使用教程(图文版,巨详细)
Wireshark 是一款免费开源的网络协议分析工具,可实时捕获、解析并可视化数据包,助你诊断网络故障、分析通信协议(如HTTP、DNS、TCP等)。支持Windows/macOS/Linux,含中文界面,新手入门便捷。(239字)
|
9天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
1903 15
|
8天前
|
IDE 开发工具
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
Qoder国际版上线全新内置大模型Sonus(/ˈsoʊnəs/),全球领先,专精超长任务执行与电脑操作(Computer Use)。配合Qoder桌面端0.2.3版本,可自主完成编程、金融建模、科研及表格制作等复杂工作。现全面支持Qoder全系产品,效率提升3.2倍。
1012 1
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
|
14天前
|
人工智能 运维 BI
阿里云千问办公QwenWork深度解析:基于Qwen3.8,六大核心能力重构企业全自动化工作流与计费选型指南
传统AI办公工具大多停留在对话问答、文档摘要、简单文案生成层面,只能完成单点碎片化任务,无法自主拆解复杂业务流程,很难串联多工具、多文档、外部业务系统完成端到端完整工作交付。很多企业在落地AI办公的时候,需要组合多款不同工具,来回切换界面,手动复制粘贴中间结果,智能化改造落地门槛居高不下。千问办公QwenWork是整合多款智能体产品能力打造的一体化企业办公智能体平台,底层基座依托Qwen3.8大模型,打通桌面端Agent、云端Agent、企业协同Agent三种运行形态,不再局限简单问答,接收业务目标之后自主拆解任务步骤,调用各类工具,处理文档、表格、浏览器自动化、数据查询,直接输出可交付的办公
1669 4
|
10天前
|
缓存 人工智能 自然语言处理
阿里云qwen3.8-flash大模型介绍:模型能力、模型价格、免费额度与最新活动
本文是阿里云百炼平台Qwen3.8-Flash大模型的选型接入指南,作为兼顾性能与响应速度的高性价比多模态模型,它支持百万级上下文窗口、全场景多模态输入与完整智能体能力矩阵,适配编程辅助、智能体协作等核心场景。文中同步梳理了最新下调的阶梯定价、夜间4折等优惠活动,搭配OpenAI兼容流式调用示例,帮助开发者低成本快速落地高并发AI应用。
阿里云qwen3.8-flash大模型介绍:模型能力、模型价格、免费额度与最新活动
|
16天前
|
缓存 数据可视化 开发工具
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
DeepSeek Harness 的更新分两层:本体更新(npx 自动最新、npm update -g、源码 git pull)与插件更新(插件市场点更新、命令行覆盖安装)。本文按「准备 → 更新本体 → 更新插件 → 更新后检查」四步走,覆盖新手常见疑问。
1810 1
DeepSeek Harness 怎么更新?dsh 更新完整指南:更新本体(npx、npm、源码)与更新插件两种方式
|
11天前
|
SQL 人工智能 前端开发
QoderWake 1.0 正式发布:从桌面里的 Agent,到工作现场的数字员工
QoderWake v1.0正式发布:企业级数字员工团队平台。支持“一句话建岗”,预置10类特训岗位;Waker常驻钉钉/飞书群,@即响应、自动协作、跨任务记忆;具备定时/事件/API多触发方式与统一任务看板;已沉淀27.6万条记忆、12.3万项技能,助力组织实现人机协同增效。
819 2
|
8天前
|
缓存 测试技术 API
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)
DeepSeek V4.1 Flash 内测不用申请,base_url 不变、改个模型名就能调,9/10 到期。本文讲清接入、计费限流与多模态注意点。
829 0
DeepSeek V4.1 Flash 内测接入:改个模型名即可调用(附代码)

热门文章

最新文章