推理超时不是玄学:给 LLM 调用设三个数
LLM 调用的超时纠结常常是这样的:20 秒太短,长 prompt 偶发被掐;120 秒太长,上游挂了你的队列先堵死。多数团队最后是拍脑袋定一个值,然后每次故障复盘都要重新吵一遍。

推理超时不是玄学:给 LLM 调用设三个数
LLM 调用的超时纠结常常是这样的:20 秒太短,长 prompt 偶发被掐;120 秒太长,上游挂了你的队列先堵死。多数团队最后是拍脑袋定一个值,然后每次故障复盘都要重新吵一遍。
其实只需要拍板三个数:预算(budget)、hard timeout、重试政策。定了,写进配置,剩下就是执行。
失效模式先分清楚
超时的来源不是一种,混在一起设阈值只会互相打架:
- **模型排队**:上游并发打满,请求进队列干等。特征:延迟整体抬高,成功变慢。
- **长 prompt / 长输出**:prefill 和 decode 本身就慢。特征:延迟和 token 数强相关。
- **上游卡死**:连接建了但永远不返回。特征:个别请求顶格撞超时。
这三种对应的解法完全不同:第一种限流,第二种调预算或换模型,第三种才是超时 + 重试。超时只能救第三种,但对三种都有效——这是它能兜底的原因,也是单独靠它不够的原因。
预算:预算 = p95 × 1.5
别从"平均延迟 3 秒所以设 10 秒"推。看最近 7 天全部调用的 p95 延迟(按 prompt token 分桶更准),乘 1.5 作为正常调用的延迟预算。
例:某接口 p95 = 8.2s(medium prompt 桶),预算 ≈ 12s。偶发超过预算的请求不是"超时故障",是长尾,它们应当在监控里单独计数,而不是混进错误率。预算这个数字以后每次调超时都有锚点,不用重新吵。
Hard timeout:预算 + 缓冲
hard timeout 设预算的 1.5–2 倍,但必须考虑 decode 特性:**输出越长,越不能按固定值掐**。如果接口支持流式(SSE),更好的做法是只检测空闲——首 token 前给一个 prefill timeout(比如 15s),之后每收到一个 chunk 就续命,两个 chunk 间隔超过 30s 判死。这样 2 万 token 的合法长输出不会被误杀,而真卡死的请求照样被切断。
非流式的接口就老老实实一个 hard timeout。经验值:hard timeout ≥ 该接口见过的最大合法输出耗时,否则你会把"慢但成功"和"死了"判成一类。
重试:只重试一次,条件写进代码
重试的坑不在次数在条件:
- **只对 HTTP 5xx 和连接断开重试**,502/503/504 算,client 自己撞超时的算半个(有一次机会)。
- **429 不重试,记 fail 报限流**。对 429 重试只是把队列压力放大,限流不会因此消失。429 应该触发"降速"而不是"再试"。
- **重试过一次仍超时 → 记 fail**,让调用方决定降级。不叠指数退避:在线路径上退避 30 秒比直接失败更伤体验,离线 batch 才值得退避。
另外,重试必须带幂等判断。同一个用户请求如果两次都到模型侧,用户可能得到两个不同答案,或者账单算两次。能带 idempotency key 的带上;不能的,至少日志里用同一个 request_id 串起来,复现时看得清是"一次失败"还是"两次半成功"。
三个数落进配置
llm_call:
budget_ms: 12000 # p95 x 1.5
timeout:
prefill_ms: 15000 # 流式: 首 token 前
chunk_idle_ms: 30000 # 流式: chunk 间隔
hard_ms: 30000 # 非流式: única
retry:
max: 1
on: [http_5xx, connection_error, hard_timeout]
not_on: [http_429, http_4xx]
告警也照这个分:超时率按「预算内长尾」「撞 hard timeout」「重试后仍失败」三层分别计数,每层独立报警。这样复盘时看到的不是一个笼统的"超时 3%",而是"长尾变多 → 上游该扩容"或"撞 hard 变多 → 有东西挂了",指向完全不同的动作。
最后
超时的本质不是"等多久放弃",是把"上游变慢"和"上游死了"这两种信号分开。预算、hard timeout、重试条件,三个数定了,每一次超时故障都能落到上图三类之一,修复动作也就唯一了。能这样切分的超时配置才是完成了,剩下的只是定期用最新 p95 校准预算。
留言区
欢迎分享你的想法!
加载留言中…