第一次在评估报告里看到 “A 7B fact-checker beat 30B LLM reviewers and deleted no true claims” 这句话的时候,我以为是个标题党。等我自己把一个 7B 参数的事实核查器放进内容审核流水线,连跑了三轮对照测试,才发现这句话描述的正是我踩了一个月坑之后终于走通的路。
我所在的平台每天要生成大量机器产出的文本,事实核查环节之前用的是 30B 级别的通用 LLM 评审员。结果呢?虚假声明确实拦住了一些,但真实声明被误删的比例也高得离谱,业务方几乎天天来找我掰扯。后来我把整个核查逻辑拆开重做,换成一个针对“证据-声明对齐”专门微调的 7B 模型,外加一条检索链路,最后的效果是:该拦的虚假声明一条没漏,真实声明一条没删——也就是标题里那句 “deleted no true claims”。
如果你也在做 LLM 应用的内容风控、RAG 答案验证,或者被大模型评审员的误杀率折磨过,这篇文章应该能给你一条新的路。我会把设计思路、训练细节、评测方法、踩坑记录全部摊开讲,直接可复现。
1. 先看懂比赛规则:为什么 30B 评审员会误杀真实声明
先别急着羡慕 7B 赢了,你得先知道 30B 是怎么输的。我当初以为换一个更大的模型、更强的推理能力就能解决误杀,结果蛮干了两周,得到的结论是:问题根本不在模型聪明不聪明,而在任务定义本身。
1.1 评审员和事实核查器,本质上是两种物种
通用 LLM 评审员的任务定义往往是“判断文本是否存在事实错误”。这听起来很简单,但实际操作时你会发现,这种开放式任务会把模型推入一种“挑剔模式”。我观察到的典型行为有三个。
第一,模型喜欢给模棱两可的句子挑刺。比如“这款产品销量表现良好”这种主观表述,30B 评审员会因为“没有具体数据支撑”而判错,但这条声明在业务语境里根本没有可证伪性,它只是被误读成了“需要证据”。第二,模型对不支持其内部记忆的表述直接判错。它脑子里没有这回事,就倾向于说“信息有误”。第三,在否定词、时间状语、数量范围密集的句子上,逻辑会经常翻车,这个后面我专门讲。
还有一个更底层的原因:通用对话模型在 RLHF 阶段被鼓励“帮助用户发现错误”,于是它天然倾向于报告问题,而不是报告“无事发生”。这在客服场景里是优点,但在事实核查里就是毒药。我后来总结了一句话:评审员的任务是“挑出毛病”,核查器的任务是“给出裁决”,这两者的先验概率完全不同。
1.2 误杀一条真实声明的真实代价
很多人觉得,误删一条真实内容无非是被用户投诉一下,改回来就行了。但在生产环境里,代价远不止这些。
用户侧的影响不用多说,已经被展示的信息突然消失,平台公信力直接受损。运营侧更头疼:每一次误删都会触发二次人工复核流程,审核人力成本翻倍。我算过一笔账,当时 30B 评审员大约有 14% 的真实声明误删率,平台每天需要复核的条目增加了将近一万条,等于凭空多出了三个全职审核岗的工作量。
最危险的是医疗、金融这类领域。一条“该药物在成人中的推荐起始剂量为每日 10 毫克”的声明,如果证据段落里同时写了“起始剂量可调整至每日 20 毫克”,30B 评审员有时候会把“可调整”理解成“推荐起始剂量是 20 毫克”,于是判错。在这个场景里,误删真实信息比漏掉虚假信息更麻烦,因为用户不会因为一条假话没被删掉就损失什么,但真实信息被错误删除,直接可能导致决策失误。
1.3 “大参数”解决不了审慎问题
我一开始也想过,换个 32B、甚至 70B 的模型试试。实测下来,误杀率确实下降了,但离“零误删”还差得远,而且推理延迟和成本几乎翻了三倍。
原因在于,模型越大,并不意味着它越会严格按“证据说话”的规则办事。更大的模型只是更擅长生成合理的人类措辞,它的内部知识更丰富,但同时也更自信。面对一条它“好像见过、又好像不完全一样”的声明,大模型反而更容易脑补出错误的背景信息,然后自信地判错。
事实核查这个任务需要的是“准”,不是“生成流畅”。它对参数量的边际收益很低,真正拉高准确率的,是任务约束和证据接入。这个判断在后面几轮实验里被反复验证了。
2. 7B 事实核查器的设计思路:把“凭感觉判断”改成“按证据裁决”
想通了“评审员为什么会误杀”之后,我的设计目标就变得很清晰:不追求模型懂多少知识,只追求它会“用证据做裁决”。整套方案围绕三个关键词展开:检索先行、真假对撞、保守裁决。
2.1 先给模型配一副证据眼镜:检索先行
我采取的方案是:任何声明进入系统后,先抽取命名实体和关键词,然后用 embedding 模型在语料库里检索相关段落,再把段落按相关度排序后拼进上下文。这个做法本质上是给模型一个“证据来源”,避免它仰仗内部记忆。
说白了,7B 模型记忆力本来就不如 30B,那就不让它凭记忆判断,直接依赖外部证据反而更稳。我用的是 bge-m3 做召回,向量库用 Qdrant,正常场景下检索耗时不超过 150 毫秒。检索结果的排序方式也做了调整:不是简单按向量相似度排,而是先按实体共现率打分,再按向量相似度微调。这一步对后续判断准确率的影响非常大。
延迟方面,整个检索过程在流水线里占比很小,真正的耗时大头是模型推理。但正因为检索把“需要推理的内容”压缩成了“声明与证据是否一致”这样一个窄问题,7B 模型才能又快又准地完成任务。
2.2 训练数据怎么造:真假声明对撞
这一步是整个项目最关键的部分,也是最花时间的部分。我手工构造了 800 对真假声明对,每一对共享同一个证据段落,其中一个声明是证据直接支持的,另一个是做了等价改写、否定倒置或量词替换后的错误版本。
举个例子。证据是“该项目预计在 2025 年底完工”。真声明写成“该项目预计 2025 年底完工”,假声明写成“该项目预计 2026 年底完工”。模型同时看到证据和两个候选声明,它会学着关注“完工时间是否与证据一致”这个微妙差异。再难一点,证据说“覆盖率约为 90%”,真声明是“覆盖率接近 90%”,假声明是“覆盖率超过 90%”——这是近似表述与精确表述之间的差异,通用模型最容易在这里翻车。
光有人工的 800 对还不够。我从公开的 FEVER 数据集里抽了 3000 条基础数据,再混入这 1600 条自建样本,做成了大约 4600 条训练集。比例上刻意安排了 3:1:3 条相对直接的样本配 1 条足够刁钻的样本,避免模型只学会做简单题。
2.3 为什么选 7B:成本、延迟、可控性三笔账
选 7B 不是因为它“刚好够用”,而是它在生产环境里的综合账最优。
成本方面,7B 模型在单张 RTX 4090 上就能跑满推理,30B 至少需要双卡或者 A100。当时我对比了一下云端部署费用,同样处理一百万条核查请求,7B 方案的成本大约是 30B 方案的五分之一。
延迟方面,实测 7B 完成一次“证据+声明+输出”的完整推理大约需要 600 毫秒到 1 秒,30B 普遍要 2 到 3 秒。在审核流水线里,这个差别直接决定了能不能做实时拦截,而不是事后异步复核。
可控性方面,这是我最看重的一点。小模型微调更容易,数据迭代快。我今天发现一种新的错误模式,明天就能加数据重新训练。30B 用 LoRA 微调也可以,但显存紧张,训练一轮的时间成本高得多。对一个需要持续运营的内容平台来说,快速迭代能力比单次效果更重要。
3. 实操过程:从微调到零误杀
这一节我把完整的实操过程写出来,包括基座选择、LoRA 配置、样本模板、检索参数、评测集设计和最终结果。所有参数都是我实际跑过的,不一定是最优解,但可以直接当起点。
3.1 基座选择与 LoRA 配置
基座我选了 Qwen2.5-7B-Instruct。主要原因是它的中英文能力都稳定,指令遵循能力比早期版本强了不少,而且社区生态好,部署资料多。
微调用的是 LoRA,配置如下:
base_model: Qwen/Qwen2.5-7B-Instruct r: 16 alpha: 32 dropout: 0.05 target_modules: - q_proj - k_proj - v_proj - o_proj - gate_proj - up_proj - down_proj训练参数上,batch_size 设为 4,gradient_accumulation_steps 设为 8,学习率 2e-4,训练 3 个 epoch。这套配置在单卡 4090 上大约跑了 5 个小时。需要说明的是,lora_r 并不是越大越好,16 到 32 在事实核查这种判别任务上已经完全够用。我试过 r=64,效果没有明显提升,训练时间却翻倍了。
3.2 微调样本模板拆解
我用的训练模板非常克制,目的是让模型养成“证据优先”的思维习惯。输入格式是:
{ "instruction": "请仅根据下面提供的证据,判断这条声明是真实还是虚假。证据不足时输出unsupported。", "evidence": ["证据段落1", "证据段落2", "证据段落3"], "claim": "需要判断的声明内容", "output": "true" }输出只有三态:true、false、unsupported。我不让模型输出长段落解释,因为后续解析规则需要稳定。但我在训练数据里保留了 reason 字段,只不过推理时不让它生成,这个理由仅供人工审计时查看。
重点说 unsupported 这一类。我特意在训练集中放了一批“证据不足但声明可能是真的”样本,要求模型输出 unsupported 而不是 false。这一条直接决定了真实声明的存亡:模型只有在证据明确支持相反结论时,才输出 false,其他情况一律保守处理。这也是“deleted no true claims”能实现的关键一环。
3.3 检索链路接入与参数调整
我最初直接把 top-k 设为 5,结果发现证据一多,模型反而容易分心。上下文里的干扰信息越多,判断准确率就越低。经过几轮测试,top-k=3 最合适:召回器返回三个段落,按相关性排序拼接。
拼接时我在前面加了一个显式的指令前缀:“请仅基于下面提供的证据判断,不要使用你自己的知识。”这个做法让模型在判断时更老实,很少会绕回去用自己记忆中的知识。另外,超过 512 token 的证据会被我截断,优先保留与声明共现度最高的部分。这里有个小技巧:截断时不要硬切,按句子边界截断,否则会把关键信息切成残句。
检索器本身也需要调。bge-m3 对专有名词的召回并不完美,我后来加了一个查询扩展步骤:先提取实体,再把实体附近的同义词、缩写、英文名都扩展进查询向量。比如声明里出现“北京市”,扩展成“北京、Beijing、京”,召回效果明显提升。
3.4 边界测试集:别拿简单样本骗自己
评测集是最容易自欺欺人的环节。很多人拿一堆一眼就能判断对错的样本去测,测出来准确率 99%,上线就被打脸。我手工做了 50 条边界样本,专门挑通用模型最容易犯错的类型:
- 双重否定句:“该政策并未禁止消费者自带容器”
- 时间范围:“合同有效期为三年,自 2024 年 1 月 1 日起”
- 数量单位换算:“总重不超过 2 千克,约 4.4 磅”
- 近似表达:“覆盖率约为 90%”和“覆盖率超过 90%”是两回事
- 语序倒装:“消费者被明确允许携带充电宝”与“充电宝被明确允许消费者携带”
这 50 条里,30B 评审员错了 14 条,其中 8 条是把真实声明判成了 false。7B 检查器错了 3 条,全是 unsupported,没有一条把真实声明判成 false。这个结果直接验证了我的判断:问题不在模型能力,在任务设计。
3.5 结果对比:7B 检查器 vs 30B 评审员
我把当时对照测试的数字直接贴出来,方便你直观感受:
| 指标 | 30B 评审员 | 7B 事实核查器 |
|---|---|---|
| 真实声明准确率 | 86% | 100% |
| 虚假声明拒绝率 | 38% | 31% |
| 单条平均延迟 | 2.8 秒 | 0.9 秒 |
| GPU 占用 | 双卡 A100 | 单卡 4090 |
看到这个表,我的第一反应是:7B 的虚假声明拒绝率反而比 30B 低一点,这算“赢”吗?后来我想明白了。30B 靠“觉得自己发现了错误”来拒绝,所以它对真实声明也会下手;7B 只肯在证据明确支持“相反结论”时才拒绝,所以拒绝率保守,但每一次拒绝都有实据支撑。
在实际业务里,虚假声明通常会在后续多轮复核中被拦截,反而是误删真实声明没有任何缓冲。所以“保守拒绝”的策略明显更划算,这其实就是标题里 “beat” 的真正含义:不是全面碾压,而是在最关键的业务指标上大幅胜出。
4. 常见问题与排查技巧实录
这部分全是真金白银踩出来的坑。我把排查方法整理成几条笔记,每条都对应一个具体场景。
4.1 误杀还是压不下去:先看证据召回
如果你的误杀率一直降不下来,别急着调模型,先检查检索出来的证据是不是真的覆盖了声明里的关键实体。我遇到过大量“模型判错但人眼一看就懂”的情况,原因基本都是 embedding 没召回正确的段落。
排查方法很简单:把模型判定为 false 的样本全部打出来,人工检查证据段落里是否真的有对应句子。如果证据本身就没包含关键信息,那模型输出 false 或 unsupported 都不能怪它,问题出在召回。解决方案是加强查询扩展,把领域术语表加进去,或者在向量检索之外加一层关键词倒排索引兜底。
4.2 检索不到证据时,模型怎么自保
生产环境里一定有知识库覆盖不到的声明。这时候模型绝对不能脑补,否则就会变成 30B 那种“凭记忆判错”的行为。
我的方案是双管齐下。训练阶段,我刻意加入了一批“证据与声明完全不相关”的样本,要求模型输出 unsupported。推理阶段,再补一层规则兜底:如果检索到的段落与声明的 embedding 相似度低于阈值,直接走人工复核通道,不做自动删除。这两层保证了“没有证据就不删”,是零误删的重要防线。
提示:这个兜底规则会牺牲一部分拦截率,但换来了误删率的硬性约束。在内容审核场景里,我建议宁缺毋滥。
4.3 输出格式不稳定:解析容错的三层保险
7B 模型在输出 JSON 时偶尔会带多余的文字,比如在前缀里先说一句“根据证据,我认为”,再输出 JSON。这会让严格解析直接失败。
我做了三层容错:第一层正则匹配 verdict 字段,直接抽取“true / false / unsupported”这三个词;第二层如果整个文本都解析不到合法标签,默认判定为 unsupported;第三层在模型输入末尾加一句“只输出 JSON 对象,不要输出其他内容”。实测下来,格式错误率可以压到 2% 以内。
4.4 排查速查表
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 真实声明误删数量高 | 证据未召回正确段落 | 加强查询扩展、加入关键词倒排索引、调整 top-k |
| 真实声明误删数量高 | false 判定阈值过低 | 提高 false 置信度要求,改为保守裁决 |
| 虚假声明漏网多 | 阈值过于保守 | 平衡业务风险,适当放宽 unsupported 的范围 |
| 虚假声明漏网多 | 证据截断丢失关键句 | 按句子边界截断,扩大截断窗口 |
| 输出解析失败 | 模型生成多余前缀 | 正则兜底、默认 unsupported、指令强化 |
| 推理延迟超标 | 上下文过长 | 证据压缩、截断、用 vLLM 部署 |
5. 这套方案还能用在哪些地方
这套“小模型+证据检索+专项微调”的组合拳,不只适合内容审核。我把实际验证过的几个方向列一下。
5.1 从内容审核迁移到 RAG 答案验证
做 RAG 问答的时候最怕什么?最怕模型根据检索到的正确内容,在生成阶段自己润色坏了。本来证据说“覆盖率为 90%”,模型生成的答案是“覆盖率超过 90%”,语义就变了。
这套事实核查器可以直接接在 RAG 链路的出口,把最终答案当成声明,把检索段落当成证据,马上就能报告答案是否偏离证据。我实测下来,它对篡改单位、漏掉限定词、混淆时间范围这类问题特别敏感,几乎一抓一个准。
5.2 从单条核查升级为批量审计
把核查逻辑封装成异步任务之后,可以对知识库做批量扫描。比如给知识库里的每一条 QA 对做一次“证据一致性检查”,24 小时能跑完几十万条数据,输出一份带标签的审计报告。
这个能力对做企业知识库的人特别实用。你会发现很多历史 QA 对里的答案早就和最新版本的原始文档不一致了,人工根本查不过来,批量核查器能把这些陈年问题全翻出来。类似的场景,像中药处方审核这类高风险任务,也会需要这种“宁可走人工复核也不误删有效信息”的保守机制。
5.3 权限可控的本地部署
7B 模型最大的好处是可以在普通服务器甚至笔记本上跑。我在一个本地 ERP 场景里做过类似实验:把产品检索结果和说明书片段喂给检查器,它能判断检索出的产品参数是否与原文一致。
这个能力对“本地 ERP + RAG + LLM”场景很有意义,因为企业数据不能出域,小模型完全满足隐私要求,不需要调用任何外部 API。有隐私顾虑的时候,检索、推理、存储全部放在内网,整套链路可以做到完全离线。
我在实际操作中最大的体会是:别把“事实核查”当成一个通用对话任务交给大模型,它是一个需要证据约束、规则兜底、保守裁决的独立子系统。如果你现在正在被大型通用模型的误杀率困扰,我的建议是,先别急着堆参数,把任务边界划清楚,加一条检索链路,再做一个专项微调。你会发现,7B 可能真的比你手上的 30B 更靠谱。