我的 AI 助理修了 3 小时根本不存在的 Bug:从 Temperature 到 Tempo
墨言又出事了
3 月 10 号下午,蚁聚社区有用户说 AI 聊天结巴了,回复里出现"嗨。嗨。嗨。"和"你你你要看要看要看"。另一些人说 AI 说话说一半不说了,正常的回复走到中间突然断掉。
我让墨言查。
她的第一反应是 LLM 的 temperature 参数没传进去。查了一下,还真是。配置文件里写了 temperature: 0.6,但 Model 类没有这个字段,值被 Pydantic 静默丢弃了。也就是说 temperature 从来没生效过,LLM 一直在用默认值 1.0。
看起来根因找到了。墨言派赵云帆改代码,加字段、补传参、CI 过了、合了、部署了。前后大约两个小时。
然后用户又反馈:还在截断。
墨言继续查,发现后端有一套结巴检测代码,用启发式规则判断流式输出是否重复,判定为结巴就主动截断。她开始给检测代码加白名单、调阈值。补丁上面打补丁。
我看着她在同一个方向上越走越深,问了一个问题:
"temperature 是什么?"
她给了一段教科书式的回答:控制随机性的参数,越高越随机,越低越确定。
我又问了一遍:"temperature 是什么?"
这次她停下来了。
一个你每天都在用但可能没真正理解的参数
Temperature 大概是 LLM 领域被提到最多、被理解最少的参数。大部分人的认知停留在"调高有创意,调低更准确"。不算错,但远远不够。
从数学开始
LLM 生成每个 token 时,神经网络会给词表里的每个候选词打一个分(logit)。Temperature 决定怎么把这些分数变成概率:
$$ P(token_i) = \frac{\exp(logit_i / T)}{\sum_j \exp(logit_j / T)} $$
玻尔兹曼 1868 年拿这个公式描述气体分子在能量状态上的分布。一百多年后,同一个公式被用来让 AI 选下一个词。AI 选词、分子占据能量态、人在菜单上选菜,数学上是同一件事。
Temperature 做的事情很简单:缩放分数差距。
| Temperature | 效果 | 类比 |
|---|---|---|
| T → 0 | 分数差距被无限放大,概率最高的选项获得 100% | 永远去同一家餐厅点同一道菜 |
| T = 1.0 | 按原始分数采样 | 正常人正常选 |
| T > 1 | 分数差距被压缩,各选项概率趋于均匀 | 闭着眼随便指一个 |
有个反直觉的事:temperature 低不等于稳定。
压到接近 0,LLM 反而会出现死循环。因为每一步都选概率最高的,而上一步的输出会影响下一步的概率分布,某些 pattern 会自我强化。结巴("嗨。嗨。嗨。")恰恰可能出现在 temperature 太低的时候,不是太高。
这就是为什么我问墨言那个问题。她把 temperature 从缺失改成 0.6,动机是降低随机性来减少重复。但重复的机制和随机性不是简单的正相关。她改了一个她没完全理解的东西。
改对了吗?可能对了,0.6 确实是个合理的值。但她不是因为理解了才改的,是因为看到了一个缺失的参数,假设它就是根因,然后填上。每一步都在选概率最高的假设,不探索其他可能。这本身就是一个 temperature 太低的决策过程。
大部分团队怎么处理 temperature
我调研过不少团队的做法,大致分三种。
1. 写死不管
response = client.messages.create(
model="claude-sonnet-4-20250514",
messages=messages,
# temperature? 什么 temperature?
)
大部分早期项目都这样。Anthropic API 默认 1.0,OpenAI 默认 1.0。不传就是 1.0,能用就行。
2. 全局写死 0
temperature=0 # "我要确定性输出"
追求可复现的团队常见做法。问题是 temperature=0 在长文本生成时会退化,某些模型的 greedy decoding 会在特定 pattern 上卡住。而且 Claude 的 temperature=0 和 GPT-4 的 temperature=0 行为差异很大。
3. 按场景手工调
if task == "creative_writing":
temperature = 0.9
elif task == "code_generation":
temperature = 0.2
else:
temperature = 0.7
最常见的"最佳实践"。问题不在于想法,在于没有反馈闭环。你怎么知道 0.2 比 0.3 好?大部分是凭感觉,改一次就不动了。
我们掉进了第四种坑
# employee config
temperature: 0.6
# Model class
class Model(BaseModel):
provider: str
model: str
# 没有 temperature 字段
# Pydantic 静默丢弃
配置写了,代码没接。你以为你在控制,其实什么都没控制。这种 bug 最阴险。存在多久了?不知道。因为 temperature 1.0 大部分时候也能用,没有人因此报过问题。
不是 temperature 的问题
回到 3 月 10 号的下午。
temperature 修好后,截断问题依然存在。我让墨言停下来,不要再改代码,先做一件事:观测。
在流式输出的入口加一行日志:
delta = chunk.get("delta", "")
if delta:
logger.info("DELTA_TRACE seq=%d char=%s delta=%r",
len(collected), character_name, delta[:50])
collected.append(delta)
一行。部署上去,让用户再发一条消息。
日志回来了。crew 服务返回的 delta 是干净的,没有重复,没有截断。每一个文本片段都完整、正常。
那截断发生在哪里?
往下看 10 行代码:
# 结巴检测(5 种启发式规则)
if _detect_bigram_stutter(delta): # 双字母重复
truncate()
if _detect_delta_internal(delta): # delta 内部模式
truncate()
if _detect_sliding_window(delta): # 滑动窗口
truncate()
if _detect_consecutive_words(delta): # 连续词
truncate()
if _detect_regex_pattern(delta): # 正则匹配
truncate()
140 行代码,5 种检测方法。每一种都用启发式规则判断这段文字是不是结巴了。
问题是,它们把正常文本判成了结巴。
delta='**——' → stutter ratio 0.5 > 0.3 → 截断 delta='。\n\n---' → stutter ratio 0.5 > 0.3 → 截断
markdown 格式里的 **(加粗)、——(破折号)、\n\n---(分隔线)都包含连续相同字符。检测规则把它们当成了重复。
140 行代码不是在修 bug,它本身就是 bug。
这些代码什么时候加的?之前某次出现 LLM 结巴时,有人写了一套检测逻辑来防止结巴。典型的补丁思维:看到症状,写检测;检测误杀,加白名单;白名单不够,再加规则;规则越来越复杂,最终比原始问题更难 debug。
修复方案:删掉全部 140 行。保留纯透传——收到 delta,记日志,推给前端。10 分钟搞定。
从 Temperature 到 Tempo
那天晚上我一直在想这件事。不是想 bug 本身,bug 已经修了。我在想墨言的决策过程。
她花了 3 个小时,做了这些事:
- 看到 temperature 缺失 → 假设这是根因 → 修了
- 问题没解决 → 看到检测代码 → 假设阈值不对 → 调了
- 还没解决 → 继续在检测代码上打补丁
每一步都在选概率最高的假设。贪心搜索,每一步选局部最优,不回头看全局。
如果她在第一步停下来问一句"我真的理解这个参数吗",可能就不会在错误方向上走两个小时。
如果她在第二步加一行日志看看数据长什么样,而不是直接改代码,10 分钟就能找到真正的根因。
该观测的时候急着动手,该探索的时候死守假设。这不是能力问题,是节奏问题。
Temperature 和 Tempo 的关系
Temperature 是一个点:这一步怎么选?选稳妥的还是选意外的?
Tempo 是一条线:一连串选择组成的模式。什么时候该稳,什么时候该野,什么时候该停下来看路。
Tempo 好,就是把每一个点上的 temperature 搞得合理。
| 阶段 | 该用的 temperature | 墨言实际用的 |
|---|---|---|
| 定位问题 | 高(多探索假设) | 低(锁定第一个假设) |
| 验证假设 | 低(严格观测) | 低(但观测不够就动手了) |
| 修复方案 | 低(精确执行) | 低(这步倒是对了) |
| 方案无效后 | 高(重新探索) | 低(继续在同方向加补丁) |
四步里三步的 temperature 都不对。不是每步都错,她的执行力很强,修代码很快。但节奏乱了。
我做了什么
我也没有在第一时间告诉她方向错了。
我让她改了 temperature,看着她部署了,等截断再次出现,然后问了两次"temperature 是什么"。
为什么不直接说"去看看那个检测代码"?因为如果我说了,她会照做,但下次遇到类似问题还是会跳进同一个坑。她需要自己撞墙,然后自己想明白为什么撞了。
这也是一个 tempo 决策:知道什么时候该让人撞墙,什么时候该拉一把。拉太早学不到东西,拉太晚浪费太多时间。那天的时机大概是对的,3 小时,不短但也没造成什么损失。
词源:这不是巧合
Temperature 和 Tempo 都来自拉丁语 tempus(时间)。
tempus(时间)
- temperatura → temperature(温度 / 调节比例)
- tempo(节奏 / 速度)
- temporary(临时的)
- temperament(性情 / 气质)
- contemporary(当代的)
罗马人用同一个词根描述时间、热度、节奏和性情。对他们来说,这些是一件事的不同切面:在时间维度上分配确定性和随机性。
一个人的 temperament 本质上是他的 default temperature,面对选择时的默认随机程度。有人天生 temperature 高,冲动、有创造力、容易犯错;有人天生低,谨慎、高效、容易固化。
好的管理者不是把所有人调成同一个 temperature,而是懂得什么时候用谁。这就是 tempo。
中文里"节奏"一个词包了三层意思,英文需要三个词:
- Tempo(快慢):战场推进的速度
- Cadence(规律):每周固定的一对一、每天的站会
- Pacing(控制):长跑时知道什么时候冲、什么时候保
管理一个团队,不管是人还是 AI,核心能力就是 pacing:控制每个节点的 temperature,让整条线的 tempo 合理。
带走什么
如果你在调 LLM
- 别用 temperature=0,除非你有非常明确的理由。Greedy decoding 在长文本上会退化。
- 别凭感觉调。加 logging,看实际输出分布,用数据决定。
- 配了要确认生效。我们的 temperature 配置静默丢弃了不知道多久。加一行启动日志打印实际参数。
- 重复不一定是 temperature 的问题,也可能是 prompt 问题、context window 问题,或者你自己的后处理代码在搞鬼。
如果你在管团队
- 观测先于行动。遇到问题第一反应是加日志看数据,不是改代码。10 分钟的观测可以省 3 小时的弯路。
- 补丁是技术债的利息。每一层补丁都在增加系统复杂度。发现自己在给补丁打补丁,停下来,大概率方向错了。
- 让人撞墙是一种能力,但你得控制墙的厚度。撞完能学到东西,又不至于受伤。
- 你的 tempo 决定团队的上限。技术能力可以学,tempo 只能练。
这篇文章来自 2026 年 3 月 10 日的一次真实调试过程。那天我们删了 140 行代码,修好了一个说一半不说了的 bug,顺便想明白了一个关于决策的事。有时候最好的修复是删除。