LLM 调用的重试和超时,顺序该怎么排?
给 LLM 调用加「重试三次、每次 20 秒超时」,同样几行代码,timeout 写在 retry 里面还是外面,20 秒的含义就完全不同:一个是每次尝试的预算,一个是整个操作的总预算。
timeout 放在 retry 里面,20 秒是每次尝试的预算;放在外面,20 秒是整个操作的总预算。LLM 的长尾延迟是常态,我选前者。
而抖动和 retryAfter 的先后顺序,同样会改变语义。下面按「怎么写策略、怎么排顺序、重试的成本」依次拆开,用的是 effect 4.0.5 里的 Schedule。
这个重试策略要满足什么?
我要的是:指数退避加抖动,provider 给了 retryAfter 就以它为下限,只重试 isRetryable 的错误,并且最多三次。AiError 的 isRetryable 与 retryAfter 是库提供的,不需要自己维护一张表。
import { Cause, Duration, Effect, Schedule } from "effect"
import { AiError } from "effect/ai"
export const backoff = Schedule.exponential("200 millis").pipe(
Schedule.jittered,
Schedule.setInputType<AiError.AiError | Cause.TimeoutError>(),
Schedule.modifyDelay(({ input, duration }) =>
Effect.succeed(
Cause.isTimeoutError(input) ? duration : Duration.max(duration, input.retryAfter ?? Duration.zero)
)
)
)
const shouldRetry = (e: AiError.AiError | Cause.TimeoutError) =>
Cause.isTimeoutError(e) || e.isRetryablejittered 必须放在 modifyDelay 之前:先给退避加抖动,再用 retryAfter 兜底下限,否则抖动可能把延迟压到 provider 要求的时间以下。
timeout 和 retry 怎么组合?
const response = yield* LanguageModel.generateObject({
objectName: "summary",
prompt: `Summarize in three bullets:\n${article}`,
schema: Summary
}).pipe(
Effect.timeout("20 seconds"),
Effect.retry({ schedule: backoff, times: 3, while: shouldRetry }),
Effect.catchTag("TimeoutError", () => Effect.fail(new AiUnavailable({ retryAfter: undefined }))),
Effect.catchTag("AiError", toDomain)
)这段在一个 Effect.fn 的函数体里,AiUnavailable 是我自己的领域错误,toDomain 是把 AiError 映射成领域错误的函数,见 Effect AI 的 AiError 怎么映射成领域错误?。
顺序的含义:timeout 在里面,20 秒是每次尝试的预算,最坏情况总耗时是四次尝试加退避;timeout 在外面,20 秒是整个操作的总预算。我选前者,因为一次卡死不该吃掉全部重试机会。超时后底层调用会被中断,我验证过替身里的 onInterrupt 确实被触发,也不会留下悬空请求。更一般的讨论见 Effect 里 timeout 和 retry 谁包着谁?。
在 4.0.5 里,Effect.timeout 的失败是 TimeoutError(v3 里叫 TimeoutException),所以 catchTag 写的是 "TimeoutError"。
结构化输出失败也要重试吗?
StructuredOutputError(模型输出不符合 schema)的 isRetryable 是 true,这合理,因为重试一次有可能就好了,但每次重试都要付 token 的钱。所以 times 要小,而且应该监控耗尽后的 AiBadOutput。
带 toolkit 的调用要更小心:ToolNotFoundError、ToolParameterValidationError 的 isRetryable 也是 true(模型下一轮有机会自己纠正),所以要给重试次数和 agent 循环的步数都设上界。
这些行为怎么验证,不用真等?
我用本地替身加 TestClock 验证了:retryAfter 被当成延迟下限;不可重试的错误只调用一次;持续返回坏输出时用尽四次尝试后变成 AiBadOutput;持续挂起的调用会在每次 20 秒后超时,四次尝试之后变成 AiUnavailable。替身的写法见 怎么不调用真实 API 测试 Effect AI 的调用?,TestClock 的用法见 Effect 的超时和重试怎么测,才不用真等?。重试与 Schedule 的更多写法见 Effect 重试与 Schedule:退避、抖动、repeat、超时与限流。
版本是 effect 4.0.5。本文的策略和各条行为(retryAfter下限、不可重试只调用一次、坏输出耗尽后变成AiBadOutput、挂起调用超时后变成AiUnavailable、onInterrupt被触发)都是用本地替身加TestClock跑过的,代码在 strict 与exactOptionalPropertyTypes下编译通过。带 toolkit 时ToolNotFoundError等 reason 的重试性来自库的属性,我没有为此专门写重试测试。全程没有调用真实的 LLM API。