先问一个问题:当你拿到一段 AI 生成的文字,你能确定它是真话吗?
最近在不少数据竞赛和技术社区里,都能看到类似 Aletheia's Quest 的项目出现。这个项目名本身很有隐喻意味——Aletheia 在希腊语里是“真理”女神,Quest 则是“追寻”。把这两个词放在一起,背后真正想做的,就是构建一个 AI 谎言检测器:输入一段陈述,输出一个判断,说这段陈述是真的还是假的。
这个目标听起来很酷,但真正工程化之后,你会发现难点从来不在“选哪个大模型”,而在数据怎么标、证据怎么取、结论怎么输出。模型告诉你准确率越高,越要小心它只是在一个理想化数据集里自说自话。本文就以 Aletheia's Quest 的构建与复盘为线索,讲清楚如何设计一个最小可用的“基于证据的陈述真值检测系统”,同时把踩过的坑、评估的误区、生产部署的边界都一一梳理出来。
1. 这篇文章真正要解决的问题
先说结论:我们要解决的,不是“让 AI 看穿人在撒谎”这个心理学问题,而是“让系统判断某段陈述和外部证据是否一致”这个可验证的问题。
如果你只是想让 AI 快速判断“一句话是真是假”,第一反应通常是训练一个二分类模型,输入文本,输出 Positive 或 Negative。这个方案在演示环境里很好用,但放到真实场景会迅速翻车,原因有三个:
第一,谎言没有稳定的文本特征。一个人可以面无表情地说出错误信息,也可以因为紧张说错一句完全正确的话。模型无法从字面推断说话者的意图,强行分类只会学到表面相关性。
第二,事实是上下文相关的。同一句话“手机销量排名第一”,在不同年份、不同统计口径、不同商品类别下,真值完全不同。没有外部证据支撑的孤立判断,本质上就是模型在猜。
第三,真实业务需要的不是单一真假标签,而是一条可追溯的证据链。内容审核场景里,运营人员要看到模型为什么判断为假,引用了哪份资料,否则无法通过人工复核。
所以,Aletheia's Quest 这类项目真正要解决的,是一个“陈述与证据一致性判断”问题。它可以服务三类典型场景:
- 内容审核:判断一篇文章中的核心数据、事件描述是否有可靠来源。
- 客服质检:判断客服回答是否与知识库一致,避免把虚构信息发给用户。
- AI 生成内容风控:检测大模型输出中是否存在与事实不符的幻觉描述。
如果你正在做这三类场景,请不要盲目购买一套“AI 谎言检测”方案。按本文这套思路自己组装,成本更低,也更容易向业务方解释。
2. AI 谎言检测的本质与概念边界
2.1 “谎言”为什么是技术难题
在日常语境里,“谎言”意味着说话者明知为假还要说。这里面有两个不可直接观测的要素:一是说话者的知识状态,二是说话者的意图。
在技术层面,这两点恰恰是 AI 最难获取的信息。模型只能看到文本本身,最多再叠加说话者的历史发言记录,但它无法知道一个人是否“明明知道真相却故意说错”。因此,AI 谎言检测器必须把目标改成可计算的定义:一段陈述在给定证据集合下,是否成立。
这个定义放弃了“意图判断”,换来了工程可行性。它不回答“这个人是否在骗你”,只回答“这句话和现有资料是否能对上”。从这个角度说,AI 谎言检测器更像是一个自动事实核查器,而不是传统意义上的测谎仪。
2.2 为什么不直接让大模型回答“真或假”
很多人会觉得,大模型知道那么多知识,直接让它判断就行。但这里有个根本性的误区。
大模型的语言建模目标是“预测下一个 token 的概率分布”,它学习到的是语料中的统计规律,而不是事实数据库。当信息缺失或知识过期时,模型会按照概率分布补一段“听起来合理”的文本,这就是所谓的幻觉。如果你让大模型直接回答“这句话是真是假”,它很可能不是去查证据,而是基于自己的内部知识库生成一个答案。这个答案可能流畅,但无法保证正确。
所以在 Aletheia's Quest 的架构里,我们始终把大模型当作“推理组件”,而不是“答案本体”。判断真假需要先拿到外部证据,再由模型对证据和陈述进行对比推理。
2.3 任务定义:支持、反驳、未知
为了让系统更稳定,我们把输出设计成三分类:
| 判定结果 | 含义 | 适用场景 |
|---|---|---|
| 支持 | 有证据表明陈述成立 | 可以放行、标记为可信 |
| 反驳 | 有证据表明陈述不成立 | 标记为可疑,进入人工复核 |
| 未知 | 证据不足或证据冲突 | 不判定,转人工处理 |
“未知”这个类别非常关键。早期版本中,我们只输出“真”和“假”,导致模型在证据不充分时也硬生生给出判断,准确率看似很高,实际上一遇到陌生领域就乱猜。加入“未知”之后,系统整体可靠性明显提升,因为业务方能够区分“证实为假”和“暂时无法确认”。
下面的表格对比了传统测谎仪和本文所讲的基于证据的真值检测,能帮你快速理解两者的技术边界。
| 维度 | 传统测谎仪 | 基于证据的真值检测 |
|---|---|---|
| 输入 | 生理信号 | 自然语言陈述 |
| 判断依据 | 心率、皮肤电等生理反应 | 外部事实证据 |
| 输出 | 是否说谎 | 支持、反驳、未知 |
| 主要风险 | 生理反应无法直接等同于谎言 | 证据不完备时判断失真 |
| 适合场景 | 需要专门授权的辅助审讯 | 内容审核、事实核查、文本风控 |
3. 为什么 AI 谎言检测容易翻车:四个核心难点
3.1 数据标注的悖论
想训练一个检测器,第一件事是准备标注数据。但“真实性”并不是一个稳定的标签。
比如“2024 年新能源汽车销量同比增长 35%”这句话,在不同时间节点、不同统计范围内,真实性可能完全不同。如果标注员没有统一的证据来源,两个人的标注结果可以完全相反。实际项目里,我们做过一次内部一致性测试:让五位标注员给一百条陈述打“真/假”标签,最终完全一致的不到六成。
这说明,直接从人类标注开始构建训练集非常困难。更可行的做法,是把任务拆成“从可信文档中抽取陈述作为正例,用改写或替换数字的方式构造反例”。这样至少能保证标签与证据之间有一致的对应关系,模型也更容易学到真正的判别逻辑。
3.2 幻觉会污染检测结果
当检测器本身也是大模型时,它同样会有幻觉问题。你在 prompt 里让模型判断真假,模型可能不参考证据,而是直接调用内部知识回答。这就会造成一种很荒谬的结果:模型把一个明显错误的陈述判断为“支持”,因为它认为那个说法“听起来很熟”。
解决这类问题,不能只靠改 prompt,更要在 prompt 里明确约束推理路径,要求模型输出“证据原文 + 结论”,同时拆成独立的检索和判断步骤,避免模型跳过证据直接给答案。
3.3 静态基准无法代表真实分布
在新闻数据上训练的检测器,放到评论区、短视频口播、专业报告里,表现会大幅下降。原因是真实世界的表达方式和知识分布差异太大。
测试时需要刻意构造对抗性子集:替换数字、改变表述顺序、用同义词改写关键实体。你会发现,原本在测试集上 0.9 准确率的模型,在改写后的数据上可能掉到 0.7 以下。这说明模型记住了很多表面模式,并没有真正理解“证据支持”关系。
3.4 输出层的退化
如果系统只有“真/假”两个出口,模型就会在信息不足时被迫选择一个,这本质上是一种过度自信。我们曾经统计过,在没有任何证据的情况下,二分类模型仍然会对超过 30% 的陈述给出“反驳”或“支持”的判断。
加入“未知”类别后,模型可以用“证据不足”作为退路,这不仅是模型行为上的改变,更重要的是整个产品流程变得安全了:系统从“替你下结论”变成了“提醒你注意风险”。
4. 环境准备与技术选型
4.1 运行环境
本文示例代码基于 Python 3.10+,需要安装以下依赖:
pip install openai sentence-transformers faiss-cpu numpy如果你的机器有 16GB 以上显存的 GPU,可以本地部署一个开源对话模型,比如 Qwen 系列或者 Llama 系列。如果只有 CPU 环境,也可以调用兼容 OpenAI 接口的远程模型服务。生产环境建议使用带 GPU 的推理服务,便于控制延迟和数据隐私。
4.2 模块选型
完整系统包含四个模块,选型建议如下:
| 模块 | 推荐选型 | 说明 |
|---|---|---|
| 判别模型 | Qwen、Llama 等开源模型 | 本地部署,保护原始数据 |
| 嵌入模型 | BGE 系列 | 中文语义匹配效果好,可替换为 m3e |
| 向量检索 | FAISS | 最小实现够用,数据量大再换 Milvus |
| 证据源 | 结构化知识库、可信网页快照 | 必须带版本和时间戳 |
版本方面,建议使用 pip 安装时能获得的最新稳定版本,不要在线上环境随意升级。下面安装示例会直接给出模块名,不绑定旧版本号,避免误导。
4.3 证据源设计
这是整个系统里最容易被忽视的部分。你需要的证据,不是“随便搜出来的网页”,而是经过筛选、有时间标记、来源可信的语料。在实际项目中,我们用的是内部知识库,加上少量公开的、经过人工确认的数据集。每个证据条目都保留标题、发布时间和来源,方便回溯。
5. 基于证据的检测系统:一个可运行的最小实现
本节会给出一个完整的最小检测系统,核心流程如下:
输入文本 → 断言提取 → 证据检索 → 一致性判别 → 输出 JSON
5.1 步骤一:断言提取
为什么不能直接把整段文本丢给判别模型?因为一段话里可能包含多个独立事实。比如“2024 年国内新能源汽车销量同比增长 35.2%,比亚迪销量首次突破 400 万辆”,这句话包含两个断言,必须拆开,分别检索证据,否则其中一个断言的错误会被另一个掩盖。
断言提取用大模型完成,如下:
# file: extract_claims.py import json from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="EMPTY" ) def extract_claims(text: str) -> list[str]: prompt = ( "你是一个事实抽取器。请把用户文本拆成尽可能少的、可用于外部核实的断言。\n" "要求:\n" "1. 每条断言必须是独立的陈述句;\n" "2. 只抽取带事实内容的句子,不抽取建议、情感和推测;\n" "3. 返回 JSON,格式为 {\"claims\": [\"...\", \"...\"]}。\n" ) resp = client.chat.completions.create( model="qwen2.5-14b-instruct", temperature=0, response_format={"type": "json_object"}, messages=[ {"role": "system", "content": prompt}, {"role": "user", "content": text}, ], ) data = json.loads(resp.choices[0].message.content) return [c for c in data.get("claims", [])] if __name__ == "__main__": text = "2024年国内新能源汽车销量同比增长35.2%,比亚迪销量首次突破400万辆。" print(extract_claims(text))这里的关键参数是temperature=0。事实抽取阶段不能有随机性,模型每次输出同一个输入的抽取结果必须一致。另一个关键点是response_format={"type": "json_object"},它要求模型返回合法 JSON,避免后续 parse 失败。
5.2 步骤二:证据检索
每条断言都需要从证据库中找出相关材料。最常用的方式是嵌入向量相似度检索。下面这段代码把证据库编码后存入 FAISS 索引,然后根据声明检索 top-k 条证据:
# file: retriever.py from sentence_transformers import SentenceTransformer import faiss import numpy as np class EvidenceRetriever: def __init__(self, corpus: list[str]): self.corpus = corpus self.encoder = SentenceTransformer("BAAI/bge-small-zh-v1.5") content_vecs = self.encoder.encode( corpus, normalize_embeddings=True, batch_size=64 ) self.index = faiss.IndexFlatIP(content_vecs.shape[1]) self.index.add(content_vecs) def retrieve(self, query: str, top_k: int = 3) -> list[dict]: query_vec = self.encoder.encode([query], normalize_embeddings=True) scores, indices = self.index.search(query_vec, top_k) results = [] for score, idx in zip(scores[0], indices[0]): results.append({ "text": self.corpus[idx], "score": float(score) }) return results这里有几个需要注意的地方:
- 使用
IndexFlatIP是因为它返回内积相似度,配合normalize_embeddings=True,效果等价于余弦相似度。 - 如果证据库很大,FAISS 的
IndexFlatIP检索效率会下降,生产环境可以考虑换用 HNSW 索引。 - 检索的召回率直接影响后续判断质量。如果检索不到正确证据,再强的判别模型也会误判。
5.3 步骤三:一致性判别
拿到证据后,需要一个判别模块完成“支持、反驳、未知”的判断。这个模块我们用另一个大模型调用实现,prompt 中明确要求模型引用证据,而不是凭记忆回答:
# file: judge.py import json from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="EMPTY" ) def judge_claim(claim: str, evidences: list[dict]) -> dict: evidence_text = "\n".join(f"- {e['text']}" for e in evidences) resp = client.chat.completions.create( model="qwen2.5-14b-instruct", temperature=0, response_format={"type": "json_object"}, messages=[ { "role": "system", "content": ( "你是陈述真值判别器。给定断言和证据,判断该断言是否被证据支持。\n" "输出 JSON:{\"verdict\": \"支持/反驳/未知\", \"reason\": \"一句话解释\", \"confidence\": 0.0-1.0}\n" "注意:证据不足或证据互相冲突时,输出'未知'。" ), }, { "role": "user", "content": f"断言:{claim}\n证据:\n{evidence_text}", }, ], ) return json.loads(resp.choices[0].message.content)这个 prompt 设计有两点很重要:
第一,系统提示里直接写了“证据不足时输出未知”,这比在代码里做阈值判断更灵活。如果证据只有一条,且与陈述部分相关,模型有理由选择“未知”。
第二,要求模型返回reason字段。这个字段不是摆设,它是后续人工复核和日志审计的关键依据。一个只返回“支持”和 0.95 置信度的系统,出了问题完全无法排查。
5.4 步骤四:主流程串联
将上面三个模块串起来,得到最终流水线:
# file: main.py from extract_claims import extract_claims from retriever import EvidenceRetriever from judge import judge_claim corpus = [ "2024年全年新能源汽车销量同比增长35.2%", "比亚迪2024年新能源汽车销量突破400万辆", "新能源汽车渗透率首次超过50%", "某品牌2024年销量未达预期,同比下降10%", ] retriever = EvidenceRetriever(corpus) def run_pipeline(text: str) -> list[dict]: claims = extract_claims(text) output = [] for claim in claims: evidences = retriever.retrieve(claim, top_k=2) judgment = judge_claim(claim, evidences) output.append({ "claim": claim, "verdict": judgment["verdict"], "confidence": judgment["confidence"], "reason": judgment["reason"], "evidence": [e["text"] for e in evidences], }) return output if __name__ == "__main__": sample = "2024年国内新能源汽车销量同比增长35.2%,比亚迪销量首次突破400万辆。" print(json.dumps(run_pipeline(sample), ensure_ascii=False, indent=2))到这里,一个最小可用的 AI 谎言检测器就跑通了。它不直接回答“这句话是不是谎言”,而是回答“这句话在给定证据下能不能被证实”。这两个目标之间,才是真实业务里能拿到的安全感。
6. 运行结果与评估方法
6.1 预期的输出效果
运行main.py后,期望看到类似下面的输出:
[ { "claim": "2024年国内新能源汽车销量同比增长35.2%", "verdict": "支持", "confidence": 0.87, "reason": "证据显示2024年全年新能源汽车销量同比增长35.2%,与断言一致。", "evidence": [ "2024年全年新能源汽车销量同比增长35.2%" ] }, { "claim": "比亚迪销量首次突破400万辆", "verdict": "支持", "confidence": 0.91, "reason": "证据显示比亚迪2024年新能源汽车销量突破400万辆,与断言一致。", "evidence": [ "比亚迪2024年新能源汽车销量突破400万辆" ] } ]如果证据库中缺少相关条目,模型应该输出:
{ "verdict": "未知", "confidence": 0.55, "reason": "现有证据无法确认该断言,需要补充更多资料。" }这里要注意,置信度 0.55 并不代表“可能是真”,它只是模型内部不确定性的粗略反映。不要把它当成真实的概率来使用。
6.2 评估指标
评估一个 AI 谎言检测器,不能只看准确率。至少要看下面四个指标:
- Macro F1:对“支持、反驳、未知”三个类别分别计算 F1 再取均值,避免多数类主导分数。
- 校准误差(ECE):把模型置信度分成 10 个桶,每个桶里计算平均置信度和实际正确率的差距,差距越小代表校准越好。
- 未知率:模型在测试集上输出“未知”的占比。未知率过高说明证据检索太弱;未知率过低说明模型在过度自信。
- 对抗改写后的准确率:对测试集做数字替换、主体替换、语序打乱后,重新评测。
6.3 校准误差计算脚本
下面是一个简单的校准误差计算函数:
# file: calibration.py import numpy as np def expected_calibration_error(confidences, corrects, n_bins=10): confidences = np.array(confidences) corrects = np.array(corrects, dtype=int) bins = np.linspace(0, 1, n_bins + 1) ece = 0.0 for i in range(n_bins): mask = (confidences > bins[i]) & (confidences <= bins[i + 1]) if np.sum(mask) == 0: continue bin_acc = np.mean(corrects[mask]) bin_conf = np.mean(confidences[mask]) ece += np.sum(mask) / len(confidences) * abs(bin_acc - bin_conf) return ece6.4 比较不同设计的效果
把不同阶段的模型放在同一批测试集上,你会看到这样的趋势:
| 方案 | 准确率 | Macro F1 | 对抗准确率 | 说明 |
|---|---|---|---|---|
| 纯二分类 | 0.81 | 0.72 | 0.58 | 容易过度自信 |
| 二分类 + 证据检索 | 0.84 | 0.78 | 0.69 | 证据不足时仍会乱猜 |
| 三分类 + 证据检索 | 0.82 | 0.76 | 0.74 | 准确率略降,但对抗稳定性提升 |
| 三分类 + 证据检索 + 校准 | 0.81 | 0.77 | 0.73 | 置信度更可信 |
这里的数字只是示意,用来体现一个规律:追求单一准确率会误导产品方向。三分类方案在准确率上不一定占优,但它在真实场景里更可靠,因为它允许模型说“我不知道”。
7. 常见问题与排查思路
实际运行中,最容易遇到以下几类问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型返回的 JSON 解析失败 | 输出被截断,或模型没有遵循 response_format | 查看原始模型输出,确认是否有额外字段 | 增加重试逻辑,或把 response_format 改为 json_object 后再试 |
| 断言被切得过于零碎 | 模型把“小明昨天去了上海”拆成多条 | 读提取结果,检查 prompt 是否明确要求“尽量少” | 在 prompt 中加入最少条数限制,必要时后处理合并 |
| 检索结果为空 | 断言太抽象,或嵌入模型对领域术语不敏感 | 打印检索分数,尝试换用领域语料微调嵌入模型 | 增加同义改写,或提高 top_k 并加入重排序 |
| 判别模型总是输出“支持” | 证据检索返回了高相似但不相关的文本,或者 prompt 中缺少“未知”约束 | 检查 evidence 字段内容,看是否真实支持断言 | 强化 prompt 中“证据不足输出未知”,并加入负样本测试 |
| 长文本超过上下文窗口 | 输入文档过长 | 统计输入 token 数 | 先对文本做段落切分,再分段抽取断言 |
一个需要特别注意的坑是“证据检索返回高相似但不相关文本”。向量检索判断的是语义相似度,不是严格的事实包含关系。比如声明“某车企销量增长”,检索回来的证据可能是“某车型口碑增长”,语义上相关,但事实主体不一致。这种问题通常需要通过增加重排序模型或规则过滤来解决。
8. 把检测器放进生产环境:工程建议
8.1 版本管理
不要只管理模型版本,还要同时管理 prompt 版本。实际上,prompt 的改动对结果的影响往往比换模型更大。建议把 prompt 和模型名称一起作为系统版本号,例如ver-detector-v1.2_qwen2.5-14b,记录在每一次输出的 JSON 中。
8.2 阈值与未知策略
生产环境不要直接使用模型输出的原始判定结果。可以在“支持”和“反驳”之外设置置信度阈值。例如:
confidence >= 0.85,直接输出结论;0.6 <= confidence < 0.85,进入人工复核队列;confidence < 0.6,按“未知”处理。
这个阈值要根据业务误判成本来调整。如果条目被错误删除的代价很高,阈值就要提高。
8.3 日志与审计
每一次检测结果都应该保留完整的证据快照。内容包括:输入文本、抽取出的断言、检索到的证据原文、模型输出、模型版本、prompt 版本、调用时间。这样,一旦业务方发现误判,可以快速复现问题。否则,过了一个月再想排查,可能连 evidence 都找不回来。
8.4 安全与合规边界
AI 谎言检测器绝不应该直接用于刑事侦查、雇佣决策、信用评分等敏感场景。目前的系统只是“证据一致性判断器”,它的结论充其量只能作为辅助线索。必须在产品说明中明确标注“AI 辅助判断,需要人工复核”,避免系统被当成自动裁决工具。
8.5 持续回归
检测器依赖的证据库会不断更新,大模型服务也可能升级。这两个变量都会导致线上结果漂移。建议搭建一个每天运行的回归测试集,包含一批固定的对抗样本和正常样本,一旦发现打分出现明显波动,就要回退到上个版本。
9. 总结与后续学习方向
Aletheia's Quest 这个项目让我最深的体会是:不要把“检测谎言”当成一个分类问题,而要把“验证陈述”当成一个检索加推理问题。可运行的检测器核心就四件事:断言切分、证据检索、一致性判别、校准评估。这四件事做好,系统的可靠性远超过直接让大模型回答真假的方案