一个"评估"任务,同一个提示跑了两次,一次答案正确、一次错误。团队以为是"模型不稳定",于是把温度调成 0——结果还是偶尔不同。原因是:温度只是采样参数之一,而"可复现"还依赖模型版本、批处理顺序、上下文长度、以及服务端的推理实现。
"可复现"在 AI 系统里是一个需要主动设计的目标,而不是一个开关。它有两种含义:同一输入同一输出(严格),与同一输入同一质量分布(工程可用)。这个混淆有现实的代价:为了让"每次一致",团队会降低温度、削减上下文、限制输出长度,结果系统变得更僵硬,而在"通过率"这个真正的目标上并不更好。
实际系统多数只需要后者,但会把前者当成验收标准,于是陷入"为什么不能 100% 一致"的困惑。
1. 影响输出的五个因素
| 因素 | 可控性 |
|---|---|
| 温度 / top_p | 完全可控 |
| 模型版本(含小版本) | 可控(锁定) |
| 上下文内容与顺序 | 可控 |
| 批处理与并发顺序 | 部分可控 |
| 服务端实现(kernel、并行策略) | 不可控 |
第五项是最容易被忽略的:同一模型在不同硬件、不同批大小下,浮点运算的归约顺序不同,于是 logits 有微小差异。当某个 token 的概率接近时,采样结果就会翻转。
所以"温度=0 就一定确定"这句话不成立——贪心解码仍可能因数值差异而分叉。这类分叉还有个"放大器":一旦某个 token 选得不同,后续上下文全变,于是一个微小的数值差异会演化成完全不同的答案。这就是"两次结果差很远"的机制。
2. 两种"可复现"的定义
| 定义 | 判据 | 用途 |
|---|---|---|
| 严格复现 | 逐 token 相同 | 调试、审计 |
| 统计复现 | 同分布、同通过率 | 线上业务 |
| 语义复现 | 同一结论 | 问答类 |
第三项(语义复现)在问答类系统里最实用:措辞不同但结论一致即可。所以验收时应当比对"结构化结论"(如抽取出的字段),而不是比对文本。
第一项在闭源服务上通常做不到(因为第三、五项不可控),而它在调试时最有价值——所以正确做法是"在能控制的环境里做严格复现"(本地小模型、固定版本),而不是要求线上服务做到。
第二项才是线上验收标准:跑 100 次,通过率稳定在 92%±2%。而"某两次结果不同"不是缺陷。
// 两种验收的写法对比
错误验收:同一提示必须每次输出相同 → 线上永远不达标
正确验收:100 次运行通过率 ≥ 90%,方差 < 3% → 可测、可改进
3. 提高可复现性的六件事
| 做法 | 效果 |
|---|---|
| 固定模型版本(含日期) | 消除版本差异 |
| 温度设为 0 + 固定 seed | 减少采样随机 |
| 固定上下文(顺序与内容) | 消除输入差异 |
| 禁用/固定并行批处理 | 减少归约顺序差异 |
| 记录完整请求参数 | 事后可复算 |
| 缓存结果(按输入哈希) | 工程上"必然一致" |
第六项是最实用的工程解法:在你控制的那一层做缓存第六项还有个"审计"用途:把"输入哈希 + 输出"存档,于是半年后能回答"当时系统给了什么答案"。这在合规场景里往往比"能重现"更重要。
,于是"同一输入"必然得到"同一输出"——即使底层模型不确定。代价是缓存失效与版本管理。
// 结果缓存的键:必须包含所有影响输出的因素
key = sha256(json.dumps({
"model": "x-1.2.3", # 含版本
"prompt_hash": h(prompt),
"temperature": 0, "top_p": 1, "seed": 42,
"max_tokens": 1024,
}, sort_keys=True))
// 漏掉任何一项 → 缓存会返回"错版本的答案"
4. 采样参数的真实作用
| 参数 | 作用 |
|---|---|
| temperature | 缩放 logits(越低越集中) |
| top_p | 累积概率截断 |
| top_k | 只从 k 个候选中采样 |
| seed | 伪随机种子(若服务支持) |
| presence/frequency penalty | 惩罚重复 |
第一项与第二项的交互常被误解:temperature=0.7 且 top_p=0.9 时,top_p 先截断再采样,所以"只调温度"不一定得到预期的多样性变化。
第二项(top_p)有个易错点:top_p=0 在很多实现里等价于"只取最高概率",但在另一些实现里是非法值。所以"想要确定性"应当用 temperature=0 而不是 top_p=0。
第四项在多数托管服务上不保证生效:接口接受 seed 但实现可能忽略。所以"设了 seed 就确定"是不可依赖的假设。
// 两套配置:评估用 vs 生产用
eval:
temperature: 0
top_p: 1.0
seed: 42 # 尽力而为,不作为保证
model: "x-1.2.3" # 锁定版本
prod:
temperature: 0.3 # 保留一点多样性
top_p: 0.95
model: "x-1.2" # 允许补丁更新,但记录实际版本
还有一个"提示"层面的确定性来源:提示本身要版本化。如果提示文本是拼出来的(含时间、用户名、随机示例),那"同一任务"的输入其实不同——于是输出不同是必然的,而团队会误以为模型不稳定。
// 提示版本化的三个要求
1 模板与变量分离:模板有 hash,变量单独记录
2 不引入"无意义的变化":如当前时间、随机 few-shot
3 示例固定:评估与生产用同一批示例
// 判据:同一任务两次请求,模板 hash 与示例是否一致
5. 版本漂移:最容易被忘的一项
| 现象 | 原因 |
|---|---|
| 上周好这周差 | 服务端模型更新 |
| 同一提示两套答案 | 负载均衡到不同后端 |
| 输出风格变了 | 小版本调整 |
第三项在托管服务上很常见:"同一个模型名"背后可能是多个后端第二项(负载均衡到不同后端)在自建集群里同样存在:不同批大小、不同 tensor 并行度都会改变数值结果,而它们在同一个"服务"里混用。
,而它们的实现细节略有差异。
所以工程上必须"记录实际响应的模型版本",而不是只记录你请求的名字。多数 API 会在响应里返回具体版本,把它存进日志与缓存键。
工程上还有一个"折中"值得写清:同一次请求里做"多次采样 + 自一致"(self-consistency)。比如跑 3 次取多数,代价是 3 倍成本,收益是"显著降低单次波动"。
// 自一致的适用边界
适用:有明确正确答案(抽取、判定、计算),单次错误代价高
不适用:开放式生成、创意任务(多次结果本就不同,选题无意义)
成本:N 倍 token 与延迟;收益:错误率大约按 1/√N 下降(经验值)
6. 排错:五种"结果不确定"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 温度 0 仍不同 | 数值差异/后端不同 | 记录版本,改验收标准 |
| 加 seed 仍不同 | 服务忽略 seed | 不依赖 seed |
| 换机器结果不同 | 硬件归约顺序 | 固定部署形态 |
| 昨天对今天错 | 模型更新 | 锁定版本 |
| 同一请求两条不同答案 | 负载均衡后端不同 | 记录后端/版本 |
第一项(温度 0 仍不同)的排查顺序:先看响应里的实际版本是否一致,再看部署形态是否一致,最后才怀疑采样参数。反过来查会浪费很多时间。
第五项对评估影响最大:评估脚本必须把"实际版本"记下来,否则"模型变差"与"换了后端"无法区分。
最后一条与"记录"有关的边界:日志里应当能重放这次调用。也就是:模型版本、完整提示、参数、以及响应中的实际版本,四者齐全。缺任何一项,半年后都无法解释"为什么当时给了这个答案"。
// 可重放的最小记录(脱敏后)
{"req_id":"r-7781","model_req":"x-1.2","model_actual":"x-1.2.3-20261101",
"params":{"temperature":0,"top_p":1.0,"max_tokens":512},
"prompt_hash":"sha256:9f2c...","prompt_vars":{"uid":123},
"output_hash":"sha256:1a3b...","latency_ms":840}
7. 边界:什么情况不必追求可复现
| 场景 | 为什么 | 做法 |
|---|---|---|
| 创意生成(文案、命名) | 多样性是需求 | 不必 |
| 对话 | 用户期待变化 | 不必 |
| 单次分析(有人复核) | 结果被审 | 不必 |
| 规则明确的抽取(有校验) | 错误会被拦 | 不必 |
判断口径一句话:两次不同的结果会不会导致"不同的人做不同的事"。最后一条与"评估"有关的边界:评估集必须固定输入,但允许输出波动。所以"同一提示跑三次取多数"这类做法在评估里是可接受的,而在"审计单次结果"的场景里不可接受。
会(评估、审计、自动化决策),就必须设计可复现;不会(生成文案),多样性是优点。
8. 自测
- 影响输出的五个因素里,哪一项不可控?为什么"温度=0 就一定确定"不成立?
- 三种"可复现"定义分别适合什么用途?为什么线上验收不该用严格复现?
- 结果缓存的键必须包含哪些因素?漏掉版本会造成什么后果?
- temperature 与 top_p 的交互是什么?
seed为什么不可依赖? - 为什么必须记录"实际响应的模型版本"而不是"请求的模型名"?
- 判断"是否需要追求可复现"的那个问题是什么?