同一句话问 AI 15 次,答案 15 个:temperature 0 实测也不稳

同一句话、同一个模型、同一台机器,连问 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,输出就固定了」这个流传很广的说法,在这台机器上是不成立的。下面把数据、脚本和踩坑原始报错都摊开。

一、为什么要做这个实测

写代码的人早晚都要碰这两个场景:

  1. 缓存和去重。想把 AI 的回答缓存起来省钱,就得回答「同样的输入,输出真的会一样吗」。如果会变,缓存 key 就不能只算 prompt。
  2. 评测和测试。拿 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 就能复现」这种绝对说法,但不适合用来估算精确的多样性概率------要多轮、跨天再跑才够。

九、使用体会:把「不确定」当成工程问题,而不是模型问题

这四轮跑下来,我的做法变了三处:

  1. 不再用 temperature=0 当「可复现」的开关。想省钱就老老实实做缓存(key 用 prompt + 模型 + 参数一起算),想要一致输出就把答案空间收敛成结构化字段,而不是反复调温度。
  2. 按题型分配力气 。抽取、分类、事实问答这类有唯一答案的任务,直接跑、直接比;文案、命名、创意类任务,接受「每次不一样」,改成一次多要几个候选再挑,比要求它稳定划算得多。
  3. 评测要报区间,别报单点。同一批数据跑两遍就有差异,那评测报告里除了准确率,还应该带上「重复三次的波动范围」。这次我量到的抖动幅度(开放题 15/15、代码题 3/15)就是最省事的参考线。

留一个没做完的下一步:把同一套脚本挂到定时任务里每天固定时间跑一轮,看同一句 prompt 的抖动幅度是否稳定。

如果思考 token 的分布也随时间漂移,那「用 AI 做批量评测」就得把日期当成变量记进报告里。

相关推荐
前端小D19 分钟前
JS核心底层
前端·javascript
SameX33 分钟前
ArkTS 做 5 态数据流 —— 咖啡电量的代谢状态机怎么驱动 UI
前端
Dovis(誓平步青云)34 分钟前
启元 T1 Pro 人机相声搭档:语音-触摸双通道的启元机器人节奏引擎设计
开发语言·前端·javascript·microsoft·交互
爱吃烧烤的技术渣36 分钟前
别再手写 CRUD 了:我把 Element Plus 的工作量压缩成一份列 schema
前端·vue.js
MetWeave39 分钟前
风的三十年:一个机场的风在变小吗:10,958 天的账(《风不是一个数》系列第 6 篇)
前端·数据可视化
青烟小生42 分钟前
前端实时协同编辑与 CRDT 冲突解决实战:从 OT 到 Yjs 底层原理(完整版)
前端
陈随易1 小时前
神级 Skill,让 AI 用 HTML 代替 Markdown 回复
前端·后端·程序员
eidnxndrdkdns1 小时前
PE 自校验的三个坑:恒失败、基址变化、以及被漏在外面的检测器
前端
90后的晨仔1 小时前
解决 Android 构建报错:Unable to delete file .../file-map.txt
android·前端