为什么我的 LLM 每次输出都不一样
同一个问题问了五遍,拿到五个不同的答案——其中一次开口便否认其余四次。我们锁定 seed 后,回复逐字节复现;可就算 temp=0 且 seed 固定,只把批大小从 512 改到 8192,仍有一个 token 翻了车。逐次方差不是玄学:它是采样、舍入与批次形状共同写下的账单,可测、可复现、可防。
图 1 —— 五次运行,五个答案,一次自相矛盾。第 2 次(暗色)就是收据:同一文件、同一台机器、同一个提示,开头却针锋相对。
为什么我的 LLM 每次给的答案都不一样?
模型从来不“持有”一个答案。在每个 token 槽位上,它握着一份概率列表,temperature 给列表重塑形状,一个 token 赢下抽取。赢家被接上,下一场拍卖换一份新列表开锣——因为它要续写的文本已经变了。一处早期分叉会复利式放大:第 2 次说的是“not”,而第 1、3、4、5 次说的是“Python”,就从这一个词起,五条回复活在了不同的宇宙里。五次运行是五次独立抽样,不是同一个答案印了五遍。
怎样让 LLM 的回复一字不差地复现?
把骰子点数报出来。我们用 seed: 42 在 temperature 0.7 下——也就是那个“不确定”的档位——把同一问题原样重跑了三遍,拿到 3/3 逐字节相同的回复(生成速度在 6.18 → 6.74 → 7.63 tok/s 之间晃动;文字却纹丝没动)。骰子在被掷出的过程中就被钉死了。
curl http://localhost:8080/v1/chat/completions -d '{
"model": "qwen3:4b",
"messages": [{"role": "user", "content": "Explain GGUF quantization"}],
"temperature": 0.7,
"seed": 42
}'
验证:同一提示 + 同一 seed → 逐字节相同的回复,实测 3/3(temp 0.7,不止贪心)· det-results-a.json
同一个旋钮在你已经在用的每个地方都有:ollama 的 /api/generate 里有 options.seed,OpenAI 兼容的 chat 通道里有 seed,llama.cpp 命令行上有 --seed。ollama 的 runner 就是穿了件外套的 llama-server,所以引擎盖下的收据是同一张——seed 到达的是同一个采样器。
为什么 temperature 0 仍然不确定?
因为贪心解码收走了骰子,却收不走舍入误差。网络内部每个 token 分数都是成千上万次加法,而浮点加法不满足结合律:同一长列数字,从左到右加一遍,再分块加一遍——数字相同,末位却可能不同。当头两名候选咬得足够近时,舍入误差就是那个决定。而求和顺序,取决于这一批一次干多少活。
于是我们把一切都钉死——temp=0、seed=42、同一台服务器、同一个提示——然后只改一个开关:微批宽度:
| 提问 | -ub | temp | seed | 输出开头 | 对比 -ub 512 |
|---|---|---|---|---|---|
| 1–2 | 512 | 0 | 42 | “ Python. It is used in many…” | 基线,2/2 相同 |
| 1 | 8192 | 0 | 42 | “ Python. It’s used in many…” | 翻了 —— 一个 token |
| 2–3 | 8192 | 0 | 42 | “ Python. It is used in many…” | 弹回基线,2/2 相同 |
被动过手脚的骰子,当场抓获:每个旋钮都写着确定性,批次形状还是挪动了一个 token——而且只发生在开机后的第一次提问上。
把第三行读两遍。翻车只出现了一次,在冷服务器的第一次提问上;随后同样设置下的两次提问都回到了基线文本。热身后的服务器状态——提示缓存、槽位布局——和求和顺序一起,成了“确定性”回复所依赖的东西。我们抓住了翻车;我们不会假装抓住了元凶。骰子被动了手脚。出千的那只手还藏着。
逐次方差的代价有多大?
账单,乘以五。我们让它“用正好三个词打招呼”,问了五遍,seed 随机,预算 256 token。模型 五次里零次答上——每一轮都把整个预算花在思考上,然后被上限掐断,而思考本身也在晃:同一个请求,884 · 954 · 986 · 915 · 885 个字符。
图 2 —— 波动本身就是重点:一个固定请求,字符数相差 102。这是十月 LFM3 文章里的思考账单,连吃五遍。
我们要一句三个词的问候,收到的却是 884 到 986 个字符的自我拷问,外加零句问候。模型不是对答案犹豫——它是对答案之前的一切都犹豫,而无论答案到不到,你都为此买单。任何“要精确答案”的请求都是一笔预算:掐住思考(think: false,或模型自带的开关),否则上限先把你吃掉。
修复清单
| 症状 | 正在发生什么 | 解法 |
|---|---|---|
| 每次运行答案都不同 | 没有 seed;temp > 0 每次抽取都重塑列表 | 钉住 seed;回复逐字节复现 |
| temp=0 时答案仍不同 | 批次形状 / 热身状态改变了求和顺序 | 相同的 -ub/-b、相同的引擎构建,测量前先热身 |
| 答案迟迟不来 | 思考吃光了 token 预算(此处 0/5) | 限制或关闭思考;读 completion_tokens,别凭感觉 |
run 1: “ Python. Python is widely used in data science, web…”
run 2: “not Python, Java, or C++. It’s actually a language that’s
not even a progr…”
run 3: “ Python. But there are other languages that are…”
run 4: “ Python. But why? Python is a high-level, interpreted…”
run 5: “ Python. Python has a lot of applications in web…”
四次说是 Python。一次开口就在质疑前提。同一份权重,同一台机器,同一个字符串进。五个里有一个看错了“自己是确定性机器”这件事;另外四个只是运气好。
最后实测 2026-11-11
Qwen3-4B Q4_K_M · 2,497,280,256 B · sha256 7485fe6f…df5 · llama-server 0.9.0-dev (b12110) · 一台全新 Arch 虚拟机,4 线程 —— CPU 实测,不做 VRAM 声明