welclaiAI·TREND·DIGEST
教程

优雅地处理错误与超时

模型调用会失败、会卡住、会被限流。一份关于重试、超时、降级和故障安全行为的实用指南,让你的 AI 功能保持可靠。

tutorials2026-04-21 12:49 KST·主编·7 分钟

演示假设一切正常运转。产品则假设事情会出错,并依然保持可用。当你通过网络调用语言模型时,你就继承了一个远程服务的全部失效方式——超时、限流、瞬时错误、缓慢的响应——再加上几个模型特有的,比如输出不符合你所需要的形态。脆弱的功能和可靠的功能之间的差别,几乎全在于它如何处理那些不顺利的路径。这份指南将带你走过你应当预先规划的各种失败,以及如何优雅地处理每一种。

认清你要规划应对的失败

模型调用会以几种可辨认的方式失败,而要处理好,首先得把它们区分开来。瞬时错误是暂时的小毛病——一次短暂的网络抖动,或一个只要重试就能成功的服务端错误。限流是服务商在告诉你慢一点,因为你发请求发得太快了。超时发生在响应耗时超过你愿意等待的时长时,而这对模型来说很常见,因为生成时间随输出长度而变化。无效输入错误意味着请求本身就有问题,原封不动地重试只会同样失败。还有内容层面的失败:调用成功了,但输出是错的、是空的,或者不符合你要求的格式。

这些分类之所以重要,是因为每一种的正确应对都不同。重试一个瞬时错误是对的;重试一个格式错误的请求则毫无意义。对限流退避有帮助;猛敲只会让情况更糟。在写处理代码之前,先把你面对的错误归类,因为分类决定了药方。

重试瞬时错误,礼貌地退避

对于瞬时错误和限流,重试就是答案——但你怎么重试很重要。立即且反复地重试会让情况更糟,限流尤其如此,一波即时重试只会让你一直被限。标准而得体的做法是带抖动的指数退避:第一次重试前等一小段时间,之后每次大致把等待时间翻倍,再加上一个小的随机偏移,这样许多客户端就不会齐刷刷地同步重试。

delay = base
for attempt in range(max_attempts):
    try:
        return call_model(request)
    except Transient or RateLimited:
        wait(delay + random_jitter())
        delay = delay * 2
raise GiveUp

给尝试次数和最大延迟都设个上限,这样一个失败的调用才不会永远重试下去。而且只重试那些重试能修复的错误——把重试包裹在瞬时错误和限流失败外面,但让格式错误的请求快速失败,因为重试它们只会在一个不可能成功的请求上浪费时间和金钱。

永远设置超时

一个没有超时的模型调用是个陷阱。生成时间是可变的,偶尔某个请求会卡得比平常久得多。没有超时,单个缓慢的调用就能占住一个请求处理器、耗尽连接池,并级联成一场看起来像彻底崩溃的故障——尽管实际上只有少数几个调用在捣乱。永远在每个模型调用上设置一个明确的超时,并按周围上下文实际能等待多久来选定它。

刻意地挑选超时值。一个有人在等的交互式功能,需要比一个能耐心等待的后台批处理任务更紧的限制。当一个调用超过超时,就把它当作瞬时失败来处理:取消它,然后要么重试,要么降级。要点在于,是来决定要等多久,而不是让一个无界的调用替你决定。流式传输在这里也有帮助——如果你以流式接收响应,首个词元到达的时间就给了你一个调用尚在存活的早期信号,你还可以对那第一块数据施加一个单独的、更紧的超时。

为重试耗尽时准备一个降级方案

重试为你买来了对抗临时麻烦的韧性,但有时麻烦并不是临时的——某个服务商持续宕机,或者每次尝试都超时。对于这些情况,要预先决定当模型干脆不可用时,你的功能该做什么。错误的答案是一个未经处理的异常冒泡上来,变成一块坏掉的屏幕。

降级方案根据任务有几种风味。你可以降级到一个更可能可用的、更小或备选的模型。你可以提供一个缓存或默认的响应(如果有合适的)。你可以优雅地退到一条非 AI 的路径——一条简单的规则、一个手动选项、一句"稍后再试"的提示,并保留用户的输入,这样什么都不会丢失。正确的降级方案取决于功能,但每一个 AI 功能都应该有一个。上线前要回答的问题是:当模型完全无法响应时,用户看到的是什么?对于任何重要的东西,"一个错误页面"都不是可接受的答案。

校验输出,而不只是调用

一次成功的调用不等于一个成功的结果。模型可能返回空的、跑题的,或者——最常令人头疼的——不符合你代码所期望结构的文本。如果你要的是 JSON,却假定响应有效就去解析它,那么一个格式错误的输出会让你的代码崩溃,和网络错误一样确凿无疑。把模型的输出当作不可信的输入,在依赖它之前先校验。

调用之后,检查输出是否满足你的要求:它能解析、它有必需的字段、它在预期的范围内。当校验失败时,你有几个选择。你可以重试调用,有时附上一条澄清的指令,指出哪里错了。对于小问题,你可以尝试一次容错修复。或者你可以降级。你应该做的,是把未经校验的模型输出直接传进假定它格式良好的代码。模型是一个概率性的组件;防御性校验就是你让一个概率性组件变得可以放心构建于其上的方式。

让失败可见、可观测

你修不了你从没看见的失败。记录错误时要带上足够的上下文来理解它们——失败的类型、触发它的输入、重试了多少次、降级是否触发。盯着超时、限流和校验失败的发生率,能在用户抱怨之前就告诉你某些东西正在恶化。超时的突然飙升可能意味着服务商出了问题;持续不断的校验失败可能意味着你的提示词漂移了,或者输入变了。

向用户诚实而温和地呈现错误。一句清楚、平和的提示,说明出了点问题、他们的输入是安全的,胜过一段晦涩的堆栈跟踪或一个无声的空响应。在内部,要确保失败在你的监控里是响亮的,即便它对用户是安静的。这种组合——对用户优雅,对你可见——正是让你能够长期保持一个功能可靠、而不是通过抱怨才发现其薄弱之处的关键。

总结

可靠是建立在那些不顺利的路径之上的。把你面对的失败归类——瞬时、限流、超时、格式错误,还是坏输出——因为每一种都需要不同的应对。用带抖动的指数退避去重试那些可重试的,对其余的快速失败,并在每个调用上设一个明确的超时,这样一个缓慢的请求就无法卡住你的系统。预先决定当模型不可用时会发生什么,并为每个功能准备一个真实的降级方案。把输出当作不可信输入来校验,再去依赖它,并让失败在你的监控里保持可见。把这些处理好,即便在模型不配合的那些日子里,你的功能也依然有用。

#reliability#errors#timeouts#resilience