把种子钉死:LLM 复现问题的最小修
复现是工程里最便宜的质量信号:同一份输入、同一段代码、同一天跑两遍,结果一致,你才能判断这次改动是让系统变好了,还是运气好了。

把种子钉死:LLM 复现问题的最小修
复现是工程里最便宜的质量信号:同一份输入、同一段代码、同一天跑两遍,结果一致,你才能判断这次改动是让系统变好了,还是运气好了。
LLM 系统里,"跑两遍结果不一样"太常见了,常见到很多人误以为这是模型的正常属性。其实多数不一致是可修的。看不见的变量通常只有四个:model、seed/decoding、超时和网络、数据顺序。把它们钉死,绝大多数"偶发"就消失了。
先钉死 model
同一个模型名(比如 `gpt-4o`、`qwen-plus`)背后可能指向不同版本。供应商升级权重后 API 行为会变,而你的评测脚本毫无察觉。上个月复现率 100% 的回归用例,换个星期就开始偶发——90% 的情况不是你的代码坏了,是 `qwen-plus` 悄悄升到 `2026-06-17` 版了。
修法很简单:调用时检查响应元数据里的 `model` 字段,把它落库到每次调用的日志里;更重要的,把精确版本号写进评测配置文件,而不是靠"现在线上配的是什么"。CI 里可以直接断言:`assert resp.model == EXPECTED_VERSION`,对不上就报 fail。这样评测结果和模型版本永远一一对应,三个月后翻报告才知道当时测的是哪一版。
再钉死 seed 和 decoding
temperature=0 并不等于确定性。采样器、内核实现的浮点路径都可能引入抖动,同一句话两次跑出不同标点、不同标点之后的措辞都是"合法"的。绝大多数商用 API 不暴露 seed,这时正确做法是把 temperature 设 0(或最低档)、top_p 设 1,并在文档里声明"结果可能仍有 ±1 个 token 的漂移"。评测断言不能写在精确字符串上——要么比对到句子级(embedding 相似度 ≥0.92),要么只检查关键字段(JSON 里的 status 字段、工具调用名)。
自托管时(vLLM、TGI),seed 参数可以真正钉死输出。这一步对 batch 评测和回归测试是刚需:同一个 prompt 集合,两次跑的 token 流必须可以 diff。vLLM 的 `seed` 参数对贪心解码(temperature=0)有效,对采样解码则取决于 kernel 对随机数流的支持,先在 dev box 上跑两遍 diff 确认。
给超时和网络兜底
两次跑的差异里,一部分不是模型变了,而是请求失败后被静默降级:第一次超时、重试走了另一个 endpoint,两个 endpoint 的模型版本可能差一个 minor;第一次 hang 了 40 秒,第二次正常 2 秒,于是"耗时"这个指标看起来模型退化了,其实只测到了一次网络抖动。
最小修:45 秒 hard timeout(在线是 30 秒),重试上限 1 次,且只重试 HTTP 5xx 和网络断开。429 是限流,重试会堆积,应该直接记 fail 并报警。失败就记 fail,不要悄悄重试——静默重试是最大的复现杀手,因为它把环境问题伪装成了模型行为。
最后钉数据顺序
评测脚本最常见的一行事故:`for item in dict.items()` 或者遍历 set。Python 3.7+ 的 dict 保序,set 永远不保序,list 保序但来源如果是 `sorted(key=str)` 之外的任意顺序,两次跑就可能不同。更隐蔽的:如果你的评测数据来自 `requests` 返回的 JSON,对象解析顺序由 JSON 库决定,大部分库保序,但不是全部。
最小修:评测数据一次性 load 成 list,加显式 `assert len(set(id(x) for x in items)) == len(items)` 防止重复,然后始终按同一个 index 遍历。`--shuffle` 的 seed 也要钉死,不然每次跑分布都不同,方差大到你分不清是模型问题还是抽样问题。
一个最小模板
import json, random
def run_eval(items, model_name, seed):
random.seed(seed)
out = []
for i, item in enumerate(items): # list, 顺序固定
resp = call_llm(model_name, item, temperature=0, top_p=1)
out.append({
"idx": i,
"input": item,
"response_model": resp.model, # 精确版本, 不是 model_name
"tokens": resp.usage.completion_tokens,
"latency_ms": resp.latency_ms,
})
return out
把 `response_model` 和 `seed` 打进每份评测报告里。三个月后重跑同一组 prompt,这两行就是你判断"有没有偷偷升级"的唯一证据。
最后
模型在升级,供应商在迭代,数据在漂移。你能控制的只有这四行配置:生效的 model 版本、采样参数、超时策略、数据顺序。把它们全部钉死,复现就从许愿变成流程,"偶发问题"就从玄学变成调查记录。
留言区
欢迎分享你的想法!
加载留言中…