我的 AI 助理修了 3 小时根本不存在的 Bug:从 Temperature 到 Tempo

技术 · Kai · · 12 min read

墨言又出事了

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 个小时,做了这些事:

  1. 看到 temperature 缺失 → 假设这是根因 → 修了
  2. 问题没解决 → 看到检测代码 → 假设阈值不对 → 调了
  3. 还没解决 → 继续在检测代码上打补丁

每一步都在选概率最高的假设。贪心搜索,每一步选局部最优,不回头看全局。

如果她在第一步停下来问一句"我真的理解这个参数吗",可能就不会在错误方向上走两个小时。

如果她在第二步加一行日志看看数据长什么样,而不是直接改代码,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

  1. 别用 temperature=0,除非你有非常明确的理由。Greedy decoding 在长文本上会退化。
  2. 别凭感觉调。加 logging,看实际输出分布,用数据决定。
  3. 配了要确认生效。我们的 temperature 配置静默丢弃了不知道多久。加一行启动日志打印实际参数。
  4. 重复不一定是 temperature 的问题,也可能是 prompt 问题、context window 问题,或者你自己的后处理代码在搞鬼。

如果你在管团队

  1. 观测先于行动。遇到问题第一反应是加日志看数据,不是改代码。10 分钟的观测可以省 3 小时的弯路。
  2. 补丁是技术债的利息。每一层补丁都在增加系统复杂度。发现自己在给补丁打补丁,停下来,大概率方向错了。
  3. 让人撞墙是一种能力,但你得控制墙的厚度。撞完能学到东西,又不至于受伤。
  4. 你的 tempo 决定团队的上限。技术能力可以学,tempo 只能练。

这篇文章来自 2026 年 3 月 10 日的一次真实调试过程。那天我们删了 140 行代码,修好了一个说一半不说了的 bug,顺便想明白了一个关于决策的事。有时候最好的修复是删除。

← 返回前沿洞察