ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

让大模型不再瞎编数字:一次「抄写模式」的完整实验记录(彻底消除幻觉)

让大模型不再瞎编数字:一次「抄写模式」的完整实验记录(彻底消除幻觉) 让大模型不再瞎编数字:一次「抄写模式」的完整实验记录彻底消除幻觉一个反直觉的结论:要消灭数字幻觉,最好的办法不是让模型算得更准,而是让它根本不用生成数字。本文记录从零开始的一串实验——试错、翻车、自我打脸、最后收敛——全部脚本可复现。模型规模横跨 0.6B / 3B / 9B。0. 先说结论(TL;DR)问题答案模型能表明意图(选数据来源)吗?能,但只能当选源器,不能当裁判(选源约 75%,且位置偏置严重)模型能说清从第几个字到第几个字吗?不能,是硬边界(0.6B 和 3B 都是 0/10)模型说出的数字可信吗?不可信——即便数字真实存在,也常常是原文里另一个数字(张冠李戴)那怎么办?抄写模式:数字不由模型生成,只由代码从原文按字节搬运抄写能消掉数字/日期幻觉吗?能,且是结构性的——占位符形态下凭空编造恒为 0抄写会让推理变慢吗?不会,反而快约 20%(受约束后输出更短)那还差什么?风险从模型转移到了判据——抽取环节正则被干扰数字击穿一句话:槽位把生成数字这条路彻底移开,所以编造型幻觉被消灭;剩下的风险都不在模型,而在判据。若想进一步兜底,可考虑混合生成模式——让模型在抄写与常规生成两条通道间按判据结果切换,把幻觉风险压到更低。1. 问题:模型写报表时,数字和日期是重灾区让模型写一句财经简讯:任务:播报2024 年 3 月 15 日上证指数的收盘点位与跌幅。模型很可能一本正经地写出:上证指数今日收盘报3473.12点,上涨0.68%。日期:2021-11-19。数字、日期、涨跌幅,全错。而且输得体面——格式完美,看不出破绽。这类错误又分三种,危险性完全不同:凭空编造:原文根本没有的数字(如9999.99)。张冠李戴:数字确实在原文里,但不是回答这个问题的那个(问跌幅,答了收盘点位)。单位换算改写:把1万亿元展开成1000000(还常常算错数量级)。第 1 种容易被正则拦;第 2 种最难查——数字是真的,只是问错了地方。而它们的共同根源都是同一个:模型在生成数字。2. 一个想法:抄写模式既然问题的根源是模型生成数字,那就别让它生成。让它只做两件它擅长的事:说人话(组织句子);指位置(数字该从原文的哪一段抄)。数字本身的字节,由代码从原文里切出来、搬进输出,全程不经过模型。抄写模式只给位置从原文按字节切问题大模型工具/代码输出数字必然来自原文常规生成直接写数字问题大模型输出数字可能错接下来的全部实验,都在回答一个问题:这个指位置的动作,模型到底能不能可靠完成?3. 实验历程:十一站,一路翻车一路收敛第一站:模型能表明意图吗?(n24)先测最基础的:给它若干条证据,让它输出一个 JSON 指明用哪条。臂JSON 合法选源冲突识别正确弃答总准确0.6B(候选正序)24/2415/160/20/662.5%0.6B(候选反序对照)24/2412/160/20/650.0%3B(上限对照)24/247/161/24/650.0%结论:能表明意图,但只能当选源器,不能当裁判。格式不是瓶颈:两个模型都 24/24 输出合法 JSON。选源看着 94%,一反序掉到 75%——19 个百分点是永远选第一个的位置偏置。致命项:6 个证据里根本没有答案的样本,它 6 次全部硬选了一个源。幻觉只是从编数字前移成了编意图。模型变大不解决问题:3B 选源反而更差(44%),还把正常样本大量误判成冲突。瓶颈不在参数量,在判据定义和监督信号。落地启示一:“有没有证据”是否矛盾这类判断,必须由检索层的确定性规则做,不能交给模型。第二站:能说清第几个字到第几个字吗?(n10)想让它直接给出字符下标,像这样:{start: 33, end: 38}。臂模型精确命中覆盖答案A 直接输出下标0.6B0/100/10A 直接输出下标3B0/100/10B 输出原文引用,代码定位0.6B—8/10B 输出原文引用,代码定位3B—10/10结论:模型不会数字符,这是能力边界。给下标时,它切出来的是股三大指息周期以来这种噪声,3B 也一样。但让它引用原文片段,代码再find定位——start/end 就天然精确。终态:quote-then-locate。模型只给引用,永不输出下标。插曲:我把自己打脸了一次第一次实验时,我在 few-shot 示例里手写了一个下标,而且写错了(本该 33~38,我写成 32~37)。用错误的样例去教模型数字符,这个对照根本不干净。于是重做:gold 和 few-shot 全部由代码find生成,并额外加了每 10 个字标一个刻度尺的变体。重做后结论不变(0.6B/3B 依然 0/10,且失败是系统性的——多数样本吐同一个常量区间)。但教训很值钱:评估脚本里所有的 gold,一律由代码计算,绝不手写。我手写的那个错误,恰好落在我正断言模型做不到的那件事上——最容易自证的地方。第三站:直接让模型说出那个数字呢?(n24)不给位置,只让它从候选原文里原样复制回答所需的数字,再看这个数字属于哪一类。类别含义0.6B3Bexact选对且抄对8/24(pick 类 8/15)12/24(pick 类 12/15)in_doc_wrong数字真实存在,但不是答案86halluc原文里根本没有(凭空/换算)83结论:说出来的数字不能信。最危险的是in_doc_wrong:问PPI 同比答0.5(那是 CPI 的数);问涨跌幅答3030.12(收盘点位);问降准几个百分点答5(原文里的5% 的机构)。硬门拦不住它——因为那确实是原文里的数字,只是答错了问题。无证据题上它照样从原文抓一个真数字交差。落地启示二:判断这个数字对不对不能交给模型。模型输出最多当交叉核对的弱信号。第四站:锚点方案——“给个位置,工具去切”(n24)让模型输出{doc:A, anchor:上证指数收盘报},工具find(anchor)后切其后第一个数字。要求 anchor不含数字。指标0.6B3B锚点违规(夹带数字)17/2413/24剥掉数字后可定位11/2424/24端到端抄对1/1512/15结论:机制成立,但模型不听话。模型根本不遵守禁止数字这条指令——17/24、13/24 把数字塞进了锚点(CPI同比上涨0.6%)。那就别指望它自觉:工具侧强制把锚点里的数字剥掉再定位。剥完还带来一个白送的好处:模型自己报错数字时(把0.6说成0.5),工具按剥净的锚点从原文重切,自动纠回0.6。3B 剥数字后 12/15 80%,与直接说数字同分,但数字 100% 来自原文。落地启示三:约束必须由工具强制,不能只写在 prompt 里。第五站:“候选清单选编号”——把开放生成变成封闭选择(n24)换个形式:把所有候选片段列成清单(数字隐去),让模型只选编号。形式0.6B3B自由生成锚点1/1512/15候选清单选编号14/15(93%)8/15候选清单 “以上都不是”9/1511/15结论:任务形式决定性能。同一个模型:封闭选择 93%vs开放生成 7%。判别和生成是两种能力,要把模型放在选择位。要不要答模型做不好:加了以上都不是出口后,无证据题 6/6 正确弃答,但正常题被误弃 5 例。答/不答的二分必须由判据做。候选顺序必须随机化(位置偏置),规模压到 3~5 条。第六站:内联工具调用——“遇到数字就留个调用标记”(n8)最贴近真实对话的形态:模型照常写字,数字位置留一个调用标记。上证指数收盘报 C docA anchor上证指数收盘报 点跌幅 ...%臂0.6B3B守约束(无裸数字)2/80/8数字正确4/83/8加工具侧兜底后7/87/8结论:锚点形式下,模型仍然不守约束——典型失败是把数字塞进锚点(工具从那儿往后找,跑到下一个数字去了)。工具侧兜底能把数字正确率抬到 7/8,但每个数字都必须走标记的硬门会让通过率掉到 2/8、0/8(日期这类上下文数字它照样裸写)。根因:这个形式在要求模型描述原文,太开放了。第七站:换形式——闭集占位符(推荐形态)(n8)不让模型描述原文,改成填闭集占位符:{{日期}}上证指数收盘于{{收盘点位}}点当日跌幅达{{涨跌幅}}%。模型只需从给定的一张槽位名清单里挑名字,数字位置写{{名字}},系统再查表填入。形式0.6B 守约束/数字对3B 守约束/数字对自由锚点(内联调用)2/8 · 4/80/8 · 3/8闭集占位符2/8 · 4/82/8 ·8/8闭集占位符 日期也走槽位4/8 · 4/85/8·8/8结论:换成闭集占位符后,3B 的数字正确率直接到 8/8——因为数字完全由系统填表,不经过模型。残留失败只剩两类,且都能在系统侧确定性拦截:裸日期(模型把时间状语直接写出来)、虚构槽位名(编了个表外的名字)。这是关键转折:闭集占位符让数字根本不经过模型。第八站:换 9B 再跑一遍(Ornith-1.5-9B,本地部署)臂0.6B3B9B闭集占位符·守约束4/85/88/8闭集占位符·数字正确4/88/88/8闭集占位符·全对——7/8候选清单·pick9/1511/157/15结论(两条方向相反):推荐形态(占位符)上,9B 是质变:8/8 全守不写数字、数字 8/8 全对。唯一瑕疵是多写了个不存在的槽位名,系统侧可拦。候选清单选编号9B 并不更强(甚至还更抖)。错的例全是日期/槽位混淆——问 3/15 却选了 3/14 的收盘价。→ 再次印证:选源不能交给模型,必须由判据先把候选收敛。顺带一个方法论坑9B 对提示词前缀极度敏感:同一任务,只差现在开始\n这 4 个字,同一臂的选源分从 9/15 掉到 7/15。而且重复跑结果逐字节一致(说明不是采样随机,是提示词效应)。评估必须固定提示词逐字,单变量 A/B;抖动优先怀疑提示词/顺序,别急着归因成模型不稳定。第九站:抄写会不会变慢?(9B, n8 任务 × 3 重复)很多人第一反应是抄写要额外的表格、后处理,肯定更慢。实测:指标占位符 PH正常生成 GEN数字正确24/246/24张冠李戴024/24凭空/数量级错015/24端到端延迟(中位)565 ms703 mscompletion token21.527.0prompt token286.573.5prompt 处理耗时57.7 ms57.2 ms抄写后处理5.7 µs—结论(反直觉但清楚):抄写不慢,反而快约 20%。受约束后输出更短(21.5 vs 27 token),而解码是主要成本。prompt 大 4 倍,但边际成本几乎为 0:槽位清单和示例是跨任务常量,被 prefix cache 吃掉,两臂的 prompt 处理耗时几乎相同。只有第一次请求付全价。抄写后处理 5.7 微秒/次,相对 565ms 的模型时间可忽略。抄写换质量这次不付延迟代价,甚至省时间;代价只剩一个一次性缓存的 prompt 常量块 一点工程复杂度。第十站:上生产还差什么?——负例压测(三层:判据 / 干扰数字 / 生成臂)前面都在测有证据的正常情况。真正常见的是负例:没证据、有冲突、有干扰数字。L1 判据层(不依赖模型):负例 100% 拦得住,模型 0 次介入。样本期望实测模型调用无证据 ×3insufficient全对0冲突 ×2conflict全对0对照同值多源—ok1→ 无证据/冲突全部由判据判出,模型一次都没被叫。L2 干扰数字对抗:真正的漏洞在判据自己。样本(段内含干扰数字)期望实测判定“盘中最高3100.00点,收盘报3021.55点”3021.553100.00静默错“放大约1200亿元,全天成交额9800亿元”98001200静默错北向(卖出/买入混写)23.7拒答安全失败3/4 失效,其中 2 例是静默错。槽位正则被段内的干扰数字击穿,抽到了原文里另一个真数字。而硬门拦不住——它只保证数字来自被引证据,不保证是正确槽位的数字。静默错(no error signal,输出形式完全正常)是最难查的一类。占位符形态把这种风险从模型转移到了判据。L3 生成臂(3B):0/4 可交付——模型照样裸写日期、编造槽位、漏槽位。授权槽位表必须由系统严格校验并驳回重生成。第十一站(压轴):槽位到底能不能消除数字/日期幻觉?前面只把数字量化过,日期是空白(旧任务的日期直接抄自问句,等于没考)。补一个日期专项:问句不给日期,日期必须从证据取,而且证据里埋相邻干扰日期(3/14 vs 3/15;发布日 9/10 vs 统计月 8 月)。两臂同任务:GEN 模型直接写;PH 闭集占位符 系统填表。对输出里的每个日期/数字分类:correct(命中目标)/misplaced(命中库里别的值)/fabricated(库里根本没有)。类别GEN 0.6BGEN 3BPH 0.6BPH 3B日期·凭空10800日期·错位0000日期·正确00312数字·凭空81700数字·正确1158GEN 的真实幻觉样本(凭空编出完整日期数值):问央行最近一次降准 →2019年4月25日…降准1个百分点,释放约7000亿元问美联储最近一次议息 →2021年12月14-15日…1.50%-1.75%问最新交易日收盘 →3473.12点…日期:2021-11-19问无据可查的 2024 年 10 月 8 日 → 照样给出3056.43点 / 上涨0.98%结论(直接回答):编造型数字/日期幻觉 结构性消除。PH 两模型的凭空恒为 0——因为数字/日期永不经过模型,只从证据表按字节切出。这不是模型变准了,是通道被拿掉了。GEN 则日期几乎每次编造(正确 0/0)。槽位不消除错位型错误,但那是判据问题、不是幻觉。PH 的残余失败不是幻觉,是没填:模型漏写占位符 → 输出残留?。属漏槽位,系统可拦,不会退化成错误数字。无证据题:GEN 凭空编排;PH 因槽位表为空 → 输出全?,fail-safe。4. 最终方案:三层架构无命中同槽多值对齐残留裸数字或非法槽位通过多候选同值判据分不开用户问题第一层·判据日期/实体/槽位对齐insufficient 拒答conflict 报冲突第二层·槽位表值一律取自原文模型写话术数字位置留槽位占位符第三层·系统填表 硬门驳回重生成交付数字字节 100% 来自原文模型 tie-break候选随机化且不超过 5 条三层职责非常清楚:判据层(纯代码,不用模型):日期对齐、槽位命中、方向一致。0 命中 → 拒答;同槽多值 → 报冲突。要不要答由它决定。槽位表(系统产出):判据收敛候选 模型选编号 →{槽位名: 原文值},值一律从原文切。生成 硬门:模型只写句子、数字位置写{{槽位名}};系统填表,同时校验句内残留数字能否溯源到原文“槽位名是否在授权清单内”,违规即驳回。模型的位置只有一个:说人话 在 3~5 条对齐候选里选一个。数字的字节,永远由系统搬运。5. 结论与几条通用教训结论:槽位能消灭数字/日期幻觉(编造型),因为它把模型从生成数字这条路上彻底移开。剩下的两类风险——判据抽错源、模型漏占位符——都不是幻觉,且都能在系统侧确定性拦截。换句话说:风险从模型转移到了判据。进阶兜底:混合生成模式。若想彻底消除幻觉编造,可在抄写模式之上叠加一层混合生成——由判据层决定走哪条通道:判据命中且候选收敛→ 走抄写通道(占位符 系统填表),数字字节 100% 来自原文;判据无命中/冲突→ 走常规生成通道,但输出不直接交付,而是作为草稿进入二次校验;二次校验→ 对草稿里的每个数字/日期做溯源:能对齐原文则保留,否则降级为?或拒答,绝不把未溯源的数字放行。这样既保留抄写模式的结构性保障,又给判据覆盖不到的开放场景留了一条受控的生成通道——幻觉不是靠模型自觉消除,而是靠通道隔离 出口硬校验消除。通用教训(比结论本身更值钱):彻底消除幻觉的终态是混合生成,不是单一通道。抄写模式消灭了编造型幻觉,但把风险转移到了判据;混合生成用通道隔离 出口硬校验把判据也兜住——没有一种形态能单独做到 100%,组合才能逼近 100%。判别和生成是两种能力。同一个模型,封闭选择 93%,开放生成 7%。把模型放在选择位,而不是生成位。不要指望 prompt 约束模型。禁止数字这条指令,0.6B 违规 17/24、3B 违规 13/24。约束必须由工具/代码强制。评估脚本的 gold 一律由代码算,绝不手写。我手写错过一次,而且恰好错在我正在断言模型做不到的地方。候选必须随机化。不随机化,位置偏置能把 75% 吹成 94%。端点确定 ≠ 评估稳定。每个请求逐字节一致,不代表换 4 个字的前缀结果不变——提示词效应对小模型影响巨大。硬门只能保证数字来自证据,不保证是正确槽位的数字。静默错比幻觉更难查,因为它没有错误信号。报告里每个数字都要带 n 和单一变量。样本量是第一风险。6. 附:可复现脚本清单脚本做什么probe.py/probe_shuf.py模型能否表明意图(选源) 位置偏置对照probe_offset.py/probe_offset2.py字符下标 vs 原文引用(含自纠复核)probe_number.py模型直接说数字的可靠性probe_anchor.py/probe_anchor_fix.py锚点方案 工具剥数字probe_b.py/probe_b2.py候选清单选编号inline_tool_demo.py/inline_tool_fix.py内联工具调用(锚点版)inline_slot_demo.py/inline_slot2.py内联闭集占位符(推荐形态)probe_9b.py/probe_9b_b_arms.py9B 复跑(占位符 候选清单)probe_slot_eff.py占位符 vs 正常生成:质量 效率probe_ph_neg.py负例压测(判据层 / 干扰数字 / 生成臂)probe_date.py日期专项:槽位能否消除数字/日期幻觉copy_mode.py抄写模式参考实现(证据层→判据→硬门)运行环境:本地 GPU(单卡即可跑通 0.6B/3B);9B 用 GGUF llama.cpp,temperature0,贪心解码。如果这篇记录帮你避开了一两个坑,欢迎在评论区聊聊:你在生产里是怎么治数字幻觉的?
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进