同一句话、同一个模型、同一台机器,连问 15 次,我拿到了 15 个不一样的答案------而且把 temperature 设成 0 也一样,seed 也救不回来。
为了把这件事说清楚,我在本机跑了 254 次真实调用(4 轮脚本、20 项断言),只得出两条能用的规律:
- 有唯一正确答案的题,它 15 次一字不差 :问「9.11 和 9.9 哪个大」,15 次全回
9.9。 - 开放式问题,它永远不给你一模一样的两句 :推荐一个周末去处,
temperature=0时 15 次 15 个答案,平均字符相似度只有 0.42。
代码题也属于第一类:让它写数组去重,30 次里只有 3 种写法,函数体一个字没变。
所以「把 temperature 调成 0,输出就固定了」这个流传很广的说法,在这台机器上是不成立的。下面把数据、脚本和踩坑原始报错都摊开。
一、为什么要做这个实测
写代码的人早晚都要碰这两个场景:
- 缓存和去重。想把 AI 的回答缓存起来省钱,就得回答「同样的输入,输出真的会一样吗」。如果会变,缓存 key 就不能只算 prompt。
- 评测和测试。拿 AI 做抽取、分类、打标,第一次跑出 92% 准确率,第二天同一批数据跑出 89%,到底是模型变了、还是它本来就在抖?
没有「抖动幅度」这个基线,这两种问题都没法判断。
网上常见的建议只有一句「把 temperature 调成 0」。我在本机网关(OpenAI 兼容接口,模型 deepseek-v4.1-flash)上试了一遍,发现这句话只对了一半------它降低的是「胡说的概率」,不是「输出的方差」。真正决定稳不稳的,是题目本身有没有唯一答案。
二、怎么测的
四个脚本、四次调用批次,全部是同一句话重复 N 次、只改一个参数:
| 轮次 | 脚本 | 做了什么 | 调用数 |
|---|---|---|---|
| 第 1 轮 | bench/sampling_bench.py |
8 种参数组合(T=0 / 0.7 / 1.5 / top_p / seed / 代码题)× 15 次 | 120 |
| 第 2 轮 | bench/sampling_bench2.py |
JSON 结构题 15 次、事实题 15 次、4 种「关思考」写法 × 6 次、换模型 2 组 × 10 次 | 74 |
| 第 3 轮 | bench/sampling_bench3.py |
思考开 / 关 × 10 次,比一致性、耗时、token | 30 |
| 第 4 轮 | bench/count_details.py |
把代码题和 JSON 题的每一条原文留下来数分布 | 30 |
判定「算不算同一个答案」我用了两把尺子,都写在脚本里:
- 唯一回答数:15 条回答里有几种不同的字符串(严格比较,差一个空格也算不同);
- 平均相似度 :两两之间用
difflib.SequenceMatcher算字符相似度取平均。1.0 表示一模一样,0.4 上下基本就是「同一个意思,换了措辞」。
用到的 prompt 只有四句,全文可复现:
text
创作题:用一句话推荐一个适合周末放松的地方,20 字以内,不要重复我的提问。
代码题:用一个 JS 函数把数组去重,只给代码,不要解释。
事实题:9.11 和 9.9 哪个大?只回答一个数字,不要解释。
结构题:只输出一行 JSON,不要任何解释和代码块:{"city": 一个中国城市名, "weather": "晴" 或 "雨", "temp": 整数摄氏温度}
三、实测一:temperature 从 0 拉到 1.5,15 次里 14~15 种答案
第一轮全部用创作题,每种参数跑 15 次:
| 参数 | 15 次里有几种回答 | 平均相似度 | 平均耗时 |
|---|---|---|---|
temperature=0(第 1 遍) |
15 | 0.420 | 4,535 ms |
temperature=0(第 2 遍,独立重跑) |
15 | 0.357 | 4,644 ms |
temperature=0.7 |
15 | 0.386 | 4,719 ms |
temperature=1.5 |
14 | 0.436 | 4,549 ms |
temperature=1.0 + top_p=0.1 |
15 | 0.337 | 4,381 ms |
temperature=1.0 + top_p=1.0 |
15 | 0.362 | 5,108 ms |
temperature=1.0 + seed=42 |
14 | 0.459 | 5,191 ms |
几个反直觉的点:
temperature=0跑两遍,两遍都是 15/15 全不同。这不是采样噪声没压住,是压根没生效。- 温度越高 ≠ 越乱 。
T=1.5的平均相似度(0.436)反而比T=0(0.420)高一点,T=0.7更低。原因在第四节:这道题的答案空间本来就小,「莫干山 / 杭州 / 大理 / 温泉」来回就这几个,唯一回答数在 15 条这个样本量下早就饱和到顶了,温度的高低被天花板盖住了。 top_p=0.1把可选范围砍到最小,唯一回答数还是 15。它确实让措辞更集中(相似度从 0.362 降到 0.337 是同一批题里的横向差),但没法把输出锁成一模一样。seed=42在网关上没起作用:15 次里仍出现 14 种回答。接口不报错、参数被正常接收,就是不保证复现。
顺便记一个真实的抖动指标:同一句创作题,completion_tokens 的分布是 177 ~ 622,相差 3.5 倍。它先「想」多长,本身就随机。
四、实测二:换成有唯一答案的题,15 次一字不差
同一个模型、同为 temperature=0,只把题目换掉:
| 题目 | 15 次里有几种回答 | 平均相似度 | 说明 |
|---|---|---|---|
| 创作题(推荐周末去处) | 15 | 0.420 | 每句都不一样 |
| 代码题(数组去重) | 3 | 0.969 | 补跑一轮 15 次里 14 次一字不差 |
| 事实题(9.11 vs 9.9) | 1 | 1.000 | 15 次全回 9.9 |
代码题那 3 种写法的差异,全在代码块围栏和换行上,函数体一个字没变:
js
// 14/15 次长这样
function unique(arr) {
return [...new Set(arr)];
}
// 另外出现过 1 次:外面套了 ```javascript 围栏
// 也出现过:function unique(arr) { return [...new Set(arr)]; }
这就是那条能用的规律:输出分布是「尖峰」还是「平坦」,决定它稳不稳。
有标准答案的题,正确路径的概率远高于其他路径,采样怎么抽都落回同一处。开放式问题的答案空间平坦,T=0 只是「每步都挑当前概率最高的 token」,但每步的候选分差很小,毫厘之差就会让句子走上不同的分支------15 次里出现 15 种回答并不奇怪,它本来就该这样。
这条规律有直接可用的推论:做抽取、分类、打标这类「有唯一答案」的任务,你根本不需要 seed,也不需要在意温度;反过来,写文案、起标题、编故事,别指望两次跑出一样的结果,要一致就只能缓存。
五、实测三:结构化输出------格式稳如模板,取值几乎不变
把要求改成「只输出一行 JSON」,temperature=0 跑 15 次:
- 字段完整率 15/15 ,一次都没漏
city/weather/temp; - 去重后 4 条文本,但 13/15 次取值完全相同(北京、晴、25℃),另两次是「北京/晴/23」和「杭州/晴/24」;
- 差异主要来自空格 :
{"city": "北京", ...}和{"city":"北京",...}都出现过。
结论很实用:要求模型输出结构化数据,比要求它输出自然语言稳定得多 。所以工程上如果必须比对两次输出,别比字符串,比解析后的字段。这也是我在文末建议的「结果比对」方案。
六、实测四:关掉思考快 2.9 倍,但一致性只从 10/10 到 6/10
deepseek-v4.1-flash 会先输出一段 reasoning_content(思考)再给答案,usage 里能看到 completion_tokens_details.reasoning_tokens。
我试了 4 种「关思考」的客户端写法,结果差别很大:
| 写法 | 接口接受 | 思考真的关掉了吗 | 平均思考字符 |
|---|---|---|---|
"reasoning_effort": "none" |
✅ 6/6 | ❌ 没关 | 375 ~ 631 |
"enable_thinking": false |
✅ 6/6 | ❌ 没关 | 396 ~ 807 |
"thinking": {"type": "disabled"} |
✅ 6/6 | ✅ 关掉了 | 0 |
"chat_template_kwargs": {"thinking": false} |
✅ 6/6 | ❌ 没关 | 339 ~ 673 |
四种写法网关全都接受,一个 400 都不报 ------前三种只是被静默忽略。真正生效的是 Anthropic 风格的 thinking.type = disabled。把思考关掉之后(各跑 10 次):
| 设置 | 10 次里有几种回答 | 平均相似度 | 平均耗时 | completion_tokens |
|---|---|---|---|---|
思考开,T=0 |
10 | 0.372 | 5,174 ms | 430 |
思考关,T=0 |
6 | 0.578 | 1,794 ms | 13 |
思考关,T=0.7 |
10 | 0.493 | 1,907 ms | 14 |
代价收益很清楚:关掉思考,耗时降到 1/2.9、输出 token 从 430 降到 13(省的是钱),措辞一致性从 0.372 提到 0.578。
但10 次里还是有 6 种结果 。所以「关思考」能省钱提速、能减少抖动,依然不能当作可复现的保证。
顺手还换了个模型对照:glm-5.3-flash 在 temperature=0 下也是 10/10 全不同。抖动不是某个模型的怪癖,而是这类服务的常态(网关侧并发批处理、不同 GPU 上的浮点累加顺序,都会带来微小差异)。
七、踩坑记录:三条,附原始报错
坑 1:max_tokens 给小了,会被思考吃光,content 直接是空字符串。 我第一版脚本给 max_tokens=200,结果返回:
text
{'ok': False, 'finish_reason': 'length',
'usage': {'prompt_tokens': 49, 'completion_tokens': 200,
'completion_tokens_details': {'reasoning_tokens': 200}},
'raw_error': 'empty content (attempt 1, finish_reason=length, completion_tokens=200)'}
思考用了 200 个 token,正式回答一个都没轮上。给推理模型留 token 时,要把思考算进去,脚本里改成 1400 才稳定拿到内容;content 为空也必须重试,不能当「模型拒答」处理。
坑 2:判定「答案算不算同一个」,用字符串比较会低估稳定性。 前面 JSON 那 15 条,文本去重是 4 条,取值去重只有 2 组------差的是空格。如果拿文本全等当指标,你会得出「格式很不稳」的错误结论。
坑 3:断言要故意反着写。 我这 20 项断言里有 6 项是 FAIL 的,全都是我一开始按常识写的假设,例如:
text
[FAIL] T=0 单次跑内 15 条回答完全一致 → 全部唯一数为 1 (n_unique=15)
[FAIL] 带 seed 的 15 条回答完全一致 (n_unique=14)
[FAIL] 关掉思考后能拿到完全一致的输出 ([])
把假设写成断言、让它自己红给你看,比事后凭印象写「实测发现」靠谱得多------这 6 条 FAIL 就是这篇文章的核心结论。
八、本地复现:3 条命令
脚本都在文章目录下,Python 3.11 + requests 就够(不需要额外依赖):
bash
# 1) 配置网关(OpenAI 兼容 chat/completions,字段:endpoint / api_key / model)
# 本机放在 secret/vision-config.json,脚本从这里读
# 2) 跑三轮主实验(每轮 10~15 次重复,建议 4 并发,约 3 分钟)
python bench/sampling_bench.py # 8 组参数 × 15 次 + 9 项断言
python bench/sampling_bench2.py # JSON / 事实题 / 关思考 / 换模型 + 6 项断言
python bench/sampling_bench3.py # 思考开 vs 关 + 5 项断言
# 3) 数分布明细(把每一轮原文留下来,方便自己验算)
python bench/count_details.py
每轮跑完都会写 bench/result*.json,含每一条原始回答、耗时、usage ,数字可以自己复算。20 项断言合计 14 通过 6 FAIL,FAIL 的那 6 条就是上面第三节到第六节的结论。
样本量的边界也说清楚:每格只有 10~15 次重复、单机单网关、一个时间段内跑完。它足以证否「T=0 就能复现」这种绝对说法,但不适合用来估算精确的多样性概率------要多轮、跨天再跑才够。
九、使用体会:把「不确定」当成工程问题,而不是模型问题
这四轮跑下来,我的做法变了三处:
- 不再用
temperature=0当「可复现」的开关。想省钱就老老实实做缓存(key 用 prompt + 模型 + 参数一起算),想要一致输出就把答案空间收敛成结构化字段,而不是反复调温度。 - 按题型分配力气 。抽取、分类、事实问答这类有唯一答案的任务,直接跑、直接比;文案、命名、创意类任务,接受「每次不一样」,改成一次多要几个候选再挑,比要求它稳定划算得多。
- 评测要报区间,别报单点。同一批数据跑两遍就有差异,那评测报告里除了准确率,还应该带上「重复三次的波动范围」。这次我量到的抖动幅度(开放题 15/15、代码题 3/15)就是最省事的参考线。
留一个没做完的下一步:把同一套脚本挂到定时任务里每天固定时间跑一轮,看同一句 prompt 的抖动幅度是否稳定。
如果思考 token 的分布也随时间漂移,那「用 AI 做批量评测」就得把日期当成变量记进报告里。