如何处理LLM应用中的错误和故障
什么是由大语言模型驱动的应用?
它和其他软件应用一样。不过有一个主要区别:除了常规处理,应用还会将数据发送给大语言模型(large language model,LLM),再利用模型的响应完成任务。例如,客服聊天机器人可能依靠LLM回答用户的问题。文档处理系统可以利用LLM从上传的文档中提取姓名、日期和发票金额。编程助手可能让LLM编写一段代码,再将它整合进程序的逻辑流程。
乍看之下,我们可能会觉得,构建这样的应用相当简单:
- 用户发送请求。
- 应用向LLM发送提示词(prompt)。
- LLM返回答案。
- 应用显示答案,或将这个答案用于其他处理。
然而,其中任何一步都有可能失败。网络可能不可用。LLM提供商可能因各种原因拒绝用户请求。请求可能成功发送,但模型返回的JSON却无效。LLM可能误解指令,并因幻觉(hallucination)编造虚假信息。它也可能过很久才响应。
韧性(resiliency)和错误处理(error handling)技术可以帮助应用做好准备,尽可能妥善地应对这些情况。本文将讨论以下内容:
- 错误处理和韧性是什么意思?
- 为什么LLM应用的错误处理需要特殊对待?
- 请求在LLM应用中如何流转?
- 错误和故障的主要类型
- 如何分类故障以采取恰当的措施?
- 如何正确重试请求?
- 如何管理超时和截止时间?
- 如何在LLM应用中处理业务规则?
- 如何管理回退和优雅降级?
- 熔断器在这种架构中如何工作?
- 限流、队列和并发控制
- 如何利用幂等性保障LLM工具调用的安全?
- 如何流式传输LLM的响应?

图中文字译注:韧性涵盖整个应用;主流程为用户 → LLM应用 → LLM → 工具 → 有用的回复。错误处理框内为“LLM调用失败:超时、429、输出不良;重试一次”。回退路线包括备用模型、缓存答案和人工帮助。
错误处理和韧性的含义
在深入细节之前,我们先弄清楚错误处理和韧性究竟是什么意思。
可以把错误处理看作程序的一部分,负责在出错时决定采取什么措施。例如,考虑一个调用LLM API的应用。如果请求成功,程序就正常处理响应。但如果请求失败,程序可能需要从多种方案中作出选择,例如重试请求、显示解释性消息、使用备用模型,或者记录故障以供进一步调查。
这些办法都比让整个应用崩溃好得多。但我们也不能用同一种方式处理所有问题。真正用于生产环境的应用应当能够区分不同类型的故障,以便采取正确措施。因此,良好的错误处理需要弄清楚哪里出了问题,以及为什么出问题。
相比之下,韧性指的是应用在系统某些部分发生故障时,仍能继续完成工作的能力。具备韧性的应用不必完美运行。出现故障没有关系,但故障应当以受控的方式发生。这也称为优雅降级(graceful degradation)。
例如,考虑一个利用LLM为旅行者制定个性化行程的旅行助手。如果它所依赖的主LLM不可用,应用可以利用较小的备用模型生成行程。如果备用模型也不可用,应用可以显示此前为相同目的地创建的旅行指南。当然,这些答案不再个性化,但用户至少不会只看到一条毫无用处的错误消息。
错误处理针对的是单次故障,而韧性更关心发生故障时整个系统的行为。
为什么LLM应用的错误处理需要特殊对待?
传统软件应用按照明确的规则运行。如果我们编写一个将5和7相加的函数,它总会返回12。只要函数成功返回,就认为结果有效。
使用LLM时,我们就没有这种便利了。这是因为LLM的输出具有概率性。换句话说,同一个提示词每次都可能产生不同答案。此外,向LLM发出的API请求成功,并不保证收到的响应正确,甚至不保证它能用。响应完全可能是一通胡言乱语。这意味着,从状态码来看,LLM API调用可能成功了,但从逻辑上看,它可能失败了。响应可能存在多种问题:
- 模型可能忽略了部分指令。
- 应用期待JSON,它却可能返回纯文本。
- LLM可能产生幻觉,编造没有真实数据依据的产品、政策或信息。
- 模型可能因为达到词元(token)上限而返回不完整的响应。
- LLM可能因误解上下文而拒绝一个无害的请求。
- LLM可能选错工具,或者使用无效参数调用工具。
- 最后,LLM可能返回违反某项重要业务规则的答案。
这里的故障大致可以分为两类:
- 技术故障(technical failures):系统连操作都无法完成时出现的故障,包括网络错误、超时、服务不可用、凭证无效等。
- 语义故障(semantic failures):即使操作在技术上成功,这类故障仍会出现。结果可能错误、不安全、无关,甚至无法使用。
常规错误处理侧重于应对技术故障。然而,LLM应用需要同时处理技术故障和语义故障。
请求在LLM应用中如何流转?
LLM应用包含的远不止一个LLM。许多不同组件共同组成完整的应用。请看下图:

图中文字译注:组件依次为用户问题、应用、知识搜索/检索增强生成(RAG)、LLM、工具和响应。步骤1:提出问题;步骤2:校验输入、限制和权限;步骤3:查找上下文并取得相关数据;步骤4:构建提示词并调用模型;步骤5:按需调用工具并取得工具结果;步骤6:返回生成结果;步骤7:校验后再响应。
例如,考虑一个根据用户偏好查询商品的购物聊天机器人。它首先搜索商品数据库,将结果交给LLM。随后,它让模型为特定用户挑选合适的商品,再调用库存API查询这些商品是否有货。
这个应用的每个组件边界都可能发生故障。用户可能提供无效信息。商品搜索可能没有结果。LLM请求可能因外部原因超时。模型可能返回未知的商品标识符。库存服务可能宕机。最后,即使成功生成了响应,向用户流式传输时也可能中断。
要让应用真正具备韧性,就不能把LLM视为一个孤立组件。我们需要考虑请求从头到尾的完整路径,找出潜在问题。
错误和故障的主要类型
现在,我们来看看此类应用可能出现的主要错误和故障。
无效的用户输入
有些故障甚至会在调用LLM之前出现。例如,用户可能提交空消息,上传不被接受的文件格式,提供过大的文档,或者输入无效日期。
在这些情况下,应用应当能够校验明确的要求。要判断必填的电子邮件地址是否缺失,或上传文件是否超过大小限制,并不需要调用LLM。尽早拒绝无效请求,可以节省时间和处理成本。由于规则固定,我们还可以向用户提供清楚得多的错误消息。
网络和连接故障
应用通过互联网与LLM提供商通信。然而,连接可能中断,DNS查询可能失败,响应可能丢失。
这类故障是暂时的。我们可以重试请求,但应用不能无限重试。还需要考虑重复操作是否安全,以及是否存在意料之外的副作用。
超时
在这种情况下,LLM的响应时间可能超出应用能够等待的时长。如果没有超时限制,请求可能一直卡住,同时持续消耗资源。
超时值规定了应用可以等待多久。例如,交互式聊天应用的超时可能是20秒,但离线文档分析任务可能允许等待几分钟。超时还应取决于用户体验。例如,在屏幕前等待的用户,应当比通宵运行的后台任务有更短的截止时间。
速率限制
LLM提供商会限制特定账户在一段时间内可以使用的请求数或词元数。如果应用超过这个限制,提供商可能返回速率限制错误,以HTTP状态码429表示。
收到速率限制响应,并不表示服务坏了,而是说明我们的应用此时发出的请求超过了提供商可以处理的数量。为应对这种情况,应用可能需要延迟请求、降低并发、将任务放入队列,或者切换到可能仍有可用容量的其他模型。
提供商和服务器故障
在某些情况下,LLM提供商可能因内部临时问题而宕机。这些故障通常用HTTP 5xx状态码表示。
此时,有限次重试可能是个好办法,因为故障可能很快消失。然而,如果故障持续,重复重试只会增加本已吃力的服务的负载。“是合理的,因为故障可能很快消失。”1应用应当停止重试,并在可能的情况下切换到其他执行路径。
身份验证和权限故障
如果应用提供的API密钥已过期或无效,LLM提供商也可能返回401或403响应。
此时,重试几乎没有意义。应用应当记录故障、通知有关人员,并向用户返回合适的消息。必须注意,不能向用户暴露API密钥或其他内部安全细节。
上下文长度故障
每个语言模型都限制了单次请求可以处理的文本量。提示词、对话历史、检索到的文档、工具定义和预期答案,都属于上下文窗口(context window)的一部分,并占用其中的空间。
如果应用向LLM发送过多信息,LLM可能拒绝请求,或者没有足够的空间生成答案。为韧性而设计的应用,通常会在发送请求前统计词元。它还可以删除较早的对话消息、总结先前内容、减少检索到的文档数量,或者将大型任务拆成较小的部分,以保持在限制之内。
格式错误或意料之外的输出
假设我们的应用期待下面这种格式的结果。
{
“customer_name”: “Adam Smith”,
“priority”: “high”
}
译者注:代码按原作保留,其中的弯引号不符合JSON语法,不能直接作为有效JSON解析。
然而,模型可能这样回答:“客户的名字似乎是Adam Smith,这件事看起来很紧急。”
人类可能看得懂这样的答案,但期待JSON响应的应用无法处理它。为解决这个问题,如果模型提供商支持结构化输出(structured output)功能,应用就应当使用它。应用还应当在使用响应前,根据模式(schema)对其进行校验。
错误或编造的信息
最危险的LLM故障,是那些看似可信、却完全错误的答案。这是模型幻觉造成的。
例如,客服助手可能编造并不存在的退款政策。研究工具可能生成不存在的来源。库存助手可能推荐商品目录中根本没有的商品。
捕获异常无法检测到这些问题,因为技术上根本没有发生异常。要处理这类问题,应用需要额外检查、可信的数据源,或者某种形式的人工介入,才能继续往下执行。
工具故障和部分完成
如今,LLM还被用作智能体(agent),可以调用API、搜索数据库、发送消息或执行交易。这带来了一个重大问题:即使整个工作流失败,LLM采取的某个具体动作仍可能已经成功。
例如,假设LLM助手调用支付服务。支付成功了,但应用收到确认之前,网络连接断了。如果应用盲目重试支付,就可能向客户扣款两次。
正因如此,基于工具的系统需要幂等性(idempotency)、状态跟踪和恢复机制。
如何分类故障以采取恰当的措施
那么,我们该如何应对LLM应用中这些不同类型的错误?
第一步是分类。主要有以下类别:
- 暂时性错误(transient error) 是临时的,通常重新尝试操作后就会消失。例如,网络问题、速率限制和服务器故障。
- 永久性错误(permanent error) 会一直存在,直到请求或系统发生变化。这类错误包括凭证无效、文件类型不受支持、权限问题和格式错误的请求。
- 语义错误(semantic error) 指响应在技术上正确,却不满足应用要求的情况。例如,无效JSON、不受支持的工具参数、幻觉等。
一旦能够对错误进行分类,我们就可以恰当地处理。对于暂时性错误,应当短暂延迟后重试,或者采用回退(fallback)。对于永久性错误,需要向能够纠正问题的有关人员报告。对于语义错误,则需要校验、修复,或请求人工审核。
当然,并非每种错误都能完全归入单一类别。例如,上下文长度错误对于当前提示词来说是永久性的,但缩短提示词就能解决。
如何正确重试请求?
我们来看看应对故障的一些技术。首先要看的是重试请求。
重试是提高应用韧性最简单的方法之一。如果操作因为短暂的网络问题而失败,应用等待一会儿再试,往往就能正常运行。

图中文字译注:首次尝试失败后,判断错误类型。永久性错误,如输入不良、身份验证或权限问题,应立即停止。暂时性错误则等待约1秒后进行第2次尝试,再等待约2秒后进行第3次尝试;图示在3次尝试后停止。退避加抖动:增加延迟并加入少量随机变化,避免数千个客户端在同一时刻重试。重试次数用尽后,使用回退方案或向用户解释,不要无限循环。
主要需要防范的是无限重试循环。它会增加成本,让服务中断变得更严重。更安全的重试方法会限制尝试次数。每次失败后,下一次尝试前的等待时间都应增加。这种技术称为指数退避(exponential backoff)。一种简单做法是:第一次重试前等待1秒,第二次等待2秒,第三次等待4秒,以此类推。

图中文字译注:Time为时间,Delay为延迟;示意曲线上的延迟随重试推进而增加。
应用还经常在每次延迟中加入少量随机时间。这称为抖动(jitter)。没有抖动时,数千个失败的请求可能在同一时间重试。这可能引发新一轮流量峰值,抵消重试的好处。

图中文字译注:0秒时,全部100个客户端访问过载的服务,失败;1秒时,全部100个客户端同时重试,再次失败;2秒时,全部100个客户端同时重试,仍然失败;3秒时,惊群(thundering herd)仍在继续。
重试应当用于超时、暂时性网络故障、429响应和某些特定的5xx响应。对于凭证无效、请求被禁止、输入不良,或其他每次都会产生相同结果的问题,不应重试。
如何管理回退和优雅降级?
在应用中,回退路径是一条替代路径。当通常优先采用的路径因故障失败时,就使用它。
应用可以按以下方式采用回退:
- 尝试使用主模型,它通常质量较高。
- 如果主模型不可用,切换到较小的备用模型。
- 如果没有模型可用,返回预先定义的回退响应或缓存信息。
- 最后,如果所有选项都无法安全地处理请求,就交给人工进一步处理。
请看下图:

图中文字译注:第1层,优先采用主模型;失败后进入第2层,回退到备用模型;再次失败后进入第3层,受限模式,使用缓存或基于规则的答案;若不安全,则进入第4层,安全地转交人工。各路径的目标都是让用户仍能收到安全、有用的响应。
最合适的回退方法取决于任务。例如,用较小的模型总结内部会议,可能可以接受。但用它解读复杂的法律文件,可能就不是一个好选择。同样,缓存响应或许足以应付一般的常见问题场景,却不适合当前账户余额这类问题。
换句话说,回退策略不能违反操作原本的要求。不能把任何可用模型随意塞进流程中。避免依赖单点故障(single point of failure)也很重要。例如,如果主模型和备用模型都通过同一家提供商访问,一次服务中断就可能让两者都无法使用。要实现真正的冗余,两条路径应当彼此分离。
熔断器在这种架构中如何工作?
考虑LLM提供商不可用的情况。每个请求都在超时。此时,继续发送更多请求并不明智。这样只会浪费资源,让用户等来失败结果。
熔断器(circuit breaker)可以暂时停止调用发生故障的服务。它通常有三种状态:
- 在闭合(closed)状态下,可以按正常节奏发送请求。
- 在断开(open)状态下,请求会被立即拒绝或重定向,因为服务被认为不健康。
- 在半开(half-open)状态下,会允许少量测试请求通过,以判断服务是否已经在一定程度上恢复。

图中文字译注:熔断器的三种状态为闭合、断开、半开。下方分别示意正常放行、阻断调用,以及允许部分请求通过进行测试。
如果半开状态下的测试请求成功,熔断器就可以闭合。但如果测试请求失败,就会重新进入断开状态。通过这种方式,熔断器有助于避免反复发生的故障波及应用的其余部分,也让我们能够配置合适的回退行为。
限流、队列和并发控制
LLM应用不能仅仅因为用户能在界面上提交任务,就接受无限量的LLM工作。
例如,如果1万个请求同时到达,立即将它们全部转发给LLM提供商,可能会耗尽提供商限额、数据库连接、内存,或者应用的预算。
利用限流(rate limiting),我们可以控制用户或客户端提交请求的频率。通过并发控制(concurrency control),我们可以限制用户或客户端提交请求的频率。此外,并发控制还会为应用同时处理的请求数量设置上限。队列(queue)则保存多出的工作,直到有可用容量。2
用户等待响应的交互式请求,通常应当比后台任务获得更高优先级。例如,用户等待聊天响应时,不应因为后台正在总结数千份文档而被阻塞。
应用还应当限制词元使用量、文档大小、检索段落数量、工具调用次数,以及工作流的总时长。
结论
本文详细探讨了LLM应用的韧性和错误处理。
核心要点是:LLM应用可能出现有别于传统应用的故障。这源于LLM固有的概率性方式。重要的是对故障类型进行分类,使应用能够采取恰当措施。
尽管错误类型有所不同,我们仍然可以借用传统应用中的许多错误处理技术。例如,重试、回退、熔断器、限流和排队等都有帮助。
此文章由 AI 翻译