36 次控制变量实验揭示:计数口径偏差、推理 token 挤压、小样本幻觉,三个坑叠在一起导致 93% 的输出超标。
你让 LLM “写 2500 个字”。它不会数。
不是 prompt 没写对,不是参数没调好。自回归架构每一步只预测下一个 token,没有内部计数器。模型对长度的“感知”来自训练数据的分布模式,不是精确计算。
这个结论我花了两周、36 次控制变量的 API 调用才真正接受。
一、93% 的输出超标
在一个中文长文本批量生成管线中,每次调用要求模型输出约 2500 个中文汉字(带结构化大纲和上下文衔接)。系统跑了一段时间后,审计了 1200+ 个输出:
| 目标字数 | 样本数 | 超标率 | 平均超出 |
|---|---|---|---|
| 2500 | 331 | 93% | +1685 字 |
| 2900 | 526 | 31% | +420 字 |
| 3000 | 356 | 61% | +690 字 |
分布严重右偏——几乎总是写多,很少写少。这不是随机误差,而是系统性偏差。
二、你和模型在数两种不同的“字”
系统统计字数用的是 len(re.sub(r'\s', '', text))——去空白后数所有字符。prompt 写的是“请写 2500 个字”。
模型理解的“字”是汉字。系统数的“字符”包含标点、数字、字母。
抽样 90 个输出的统计:
| 字符类型 | 占比 |
|---|---|
| CJK 汉字 | 84.5% |
| 标点符号 | 12.1% |
| 数字+字母+其他 | 3.4% |
模型写了 2500 个汉字,系统计为 2950。模型写了 2600 个汉字,系统计为 3068,判超标。18% 的系统性偏差——不是模型写多了,是量错了。
改成只数 CJK 统一表意文字后,target=2900 的超标率从 31% 直接归零。
def count_cjk_chars(text): return sum(1 for c in text if '\u4e00' <= c <= '\u9fff' or '\u3400' <= c <= '\u4dbf' or '\U00020000' <= c <= '\U0002a6df')一个函数的改动,比后面所有 prompt engineering 加起来都管用。
这种 bug 最阴险的地方在于:两边都“看起来对”。prompt 说“字”,开发者觉得“字符就是字”,各自合理,合在一起就是 18% 的幽灵偏差。在多模块系统里,同一概念的定义在不同组件间产生微妙漂移,是很经典的集成 bug。
三、砍 Token 预算反而更长
模型支持 thinking 模式,max_completion_tokens 初始设 10000。直觉上砍到 5000 应该能压短输出。
结果完全相反:
| max_tokens | thinking | 推理 tokens | 输出 tokens | 实际 CJK 汉字 |
|---|---|---|---|---|
| 10000 | on | 1204 | 2077 | 3041 |
| 6000 | on | 689 | 2678 | 3840 |
| 5000 | on | 502 | 2568 | 3765 |
| 10000 | off | 0 | 2988 | 4313 |
| 5000 | off | 0 | 2032 | 3005 |
max_tokens 从 10000 砍到 5000,thinking 开着,汉字数从 3041 涨到 3765。
原因:推理 token 和输出 token 共享预算池。预算紧缩时模型先砍推理(1204→502),而推理恰恰是模型规划全文结构、感知“该在哪收束”的能力。推理被压缩后,模型来不及想“该收了”就一路写下去。
max_tokens: 10000 → 6000 → 5000推理 tokens: 1204 → 689 → 502 (↓)输出 tokens: 2077 → 2678 → 2568 (↑)实际汉字数: 3041 → 3840 → 3765 (↑)thinking=off 的那组,砍 max_tokens 确实有效(4313→3005),因为没有推理开销,物理上限直接生效。
结论:在有 reasoning 能力的模型上,token 预算是非单调的控制变量。存在一个“推理充分”的阈值,低于它约束力反而变差。
四、两个样本的幻觉
修完计数口径后,试了三种 prompt 变体:
- baseline:“目标 2500,范围 2000~3000”
- strict:加“超出将被丢弃并触发重写,严重浪费算力”
- countdown:把 2500 拆成四段预算,每段指定字数
先跑两个样本,三种变体全部 100% 合规。差点直接 commit。
多跑两个样本:
| 配置 | Round 1 (n=2) | Round 2 (n=4) |
|---|---|---|
| strict + 10k | 100% | 25% |
| countdown + 10k | 100% | 25% |
| baseline + 10k | 100% | 0% |
36 次调用汇总(target=2500 时的 CJK 输出分布):
最小值: 1671 最大值: 4247平均值: 3087 标准差: 520合规率(≤3000): 33%标准差 520——在这种方差下,2 个样本的“100%”纯粹是统计噪声。
还有一个更隐蔽的坑:该模型在 thinking 模式下静默忽略 temperature 参数,强制使用 1.0。API 不报错不警告。我以为在测 temp=0.6 和 temp=0.8 的差异,实际两组都跑在 1.0 上,所有关于 temperature 的“结论”全部作废。
做 LLM 实验的铁律:先验证你改的参数确实生效了。API 的 silent failure 比报错更危险。
五、0% 成功率的“LLM 压缩”
系统里还有一道“保险”:超标时调 LLM “压缩到 3000 字以内”。
压缩尝试: 174 次成功次数: 0 次成功率: 0%一个不能精确计数的模型,让它“删减到精确字数”,不可能收敛。
六、根本原因排序
| 根因 | 贡献度 |
|---|---|
| 计数口径不一致(全字符 vs CJK 汉字) | ~18% |
| 模型自然输出节奏不可控 | ~60% |
| LLM 压缩管道失效 | ~20% |
| 后处理逃逸路径(润色/审查重写后未重新校验) | ~2% |
七、落地方案
1. 对齐计数口径
代码数什么,prompt 就说什么。数 CJK 汉字就写“汉字数量”,别写“字数”。这一步 ROI 最高。
2. 把 target 设在模型舒适区
该模型自然产出 3000~3500 CJK 汉字。target 设 2500 相当于要求它在自然产出的 70% 处刹车,它做不到。设 2900,合规范围覆盖自然区间,合规率到 95%。
| target | 模型自然产出 | 合规范围 | 预期合规率 |
|---|---|---|---|
| 2500 | 3000-3700 | 2000-3000 | ~25% |
| 2900 | 3000-3500 | 2400-3400 | ~95% |
| 3000 | 3000-3500 | 2500-3500 | ~85% |
3. prompt 加收束锚点
“写到 50% 进入高潮,65% 开始收束”。不能保证绝对有效,但可减少极端偏差。
4. 超标重试带反馈,不截断
截断会在任意位置切断逻辑,质量损失不可接受。重试时把具体数字反馈给模型——“上次 3500,上限 3000,请短一些”。比盲重试有效。
5. 记录 token 明细
推理 token、输出 token、finish_reason、CJK 字数。为模型升级后的回归检测提供 baseline。
八、回头看
这个问题的本质是用概率系统执行确定性约束。
LLM 应用里有一个常见的隐含假设——“模型能精确遵循指令”。在分类、抽取这些输出空间小的任务上基本成立。但在长文本生成中,输出空间是指数级的,要求精确控制长度相当于在高维空间中画一条窄带,模型的生成过程没有这个约束机制。
从控制论角度看,大多数 LLM 管线是开环系统——生成一次就完事。加入重试 + 反馈是把开环变成闭环:生成 → 计数 → 判断 → 反馈 → 再生成。精度不够,用迭代补。
关于实验方法:LLM 输出的标准差远大于传统软件。对标准差 500+ 的分布,需要几十个样本才能区分 10% 级别的差异。大部分人跑两三个 case 就下结论——不是不懂统计,是 API 太贵。但省下的 API 费用会以线上 bug 的形式加倍还回来。
以上数据均基于 mimo-v2.5-pro。具体数值(超标率、推理 token 分配、自然产出区间等)不代表其他模型也是如此。核心结论——计数口径对齐、推理预算非单调性、迭代收敛优于截断——在方法论层面通用,但阈值需在目标模型上重新跑。