AI Agent错误处理与工程化实践:从Demo到生产的稳定之道
发布时间:2026/10/2 4:18:35 锦皓数字建站

做 Agent 开发的人应该都有这种体会写一个能跑的 Demo 简单但想让它稳定地跑在生产环境里难。尤其是错误处理这块传统后端那套 try-catch 根本不够用。Agent 的每一步都可能出错模型输出不可靠、工具调用失败、上下文窗口溢出、多步流程状态错乱而且错误之间还会叠加放大。我这些年踩过的坑几乎都集中在这些地方。这篇是 Agent 系列的第 9.4 篇专门聊聊错误处理与工程化实践聊聊为什么 Agent 的错误处理不能照搬传统后端以及我在实际项目中沉淀下来的处理机制、观测手段和避坑经验。这篇文章适合谁正在做 AI Agent 开发的工程师、准备把 Agent 从 Demo 推向生产的团队以及面试前想把 Agent 工程化知识补全的同学。我会尽量用实际项目和踩坑经历来讲不会只停留在理论层面。1. Agent 系统的错误全景很多坑是传统程序里没有的1.1 为什么不能照搬传统后端的错误处理模型传统后端服务的错误处理核心就是一个异常模型函数抛出异常框架捕获异常日志记录异常中间件做重试和熔断。这套模型的基础是确定性——你调用的数据库、缓存、消息队列它们要么成功要么失败行为是可预期的。Agent 系统完全不一样。你以为你在写代码其实你在指挥一个毛茸茸的大脑去写代码。模型输出具有内在不确定性同一个 prompt 同一套参数两次的返回可能天差地别。而 Agent 又是多步执行的规划、调用工具、观察结果、再规划每一步都可能出错且错误会沿着流程往下传播。比如工具返回了非预期格式Agent 就开始胡言乱语把错误结果当成事实继续往下推。我在刚开始做 Agent 项目时用的是传统思维给每个工具调用包一层 try-catch出错了就在日志里记一笔。结果发现完全没有用。因为 Agent 的很多错误根本不是异常而是静默的偏差——代码正常执行了但模型判断错了工具选错了参数传错了这种错误 try-catch 根本捕获不到。所以 Agent 的错误处理核心思路要从捕获异常转向构建护栏不是等错了再抓而是通过机制设计让系统在犯错时能够感知、能够纠正、能够兜底。1.2 一张表看清 Agent 全流程的错误类型我把 Agent 运行过程中的错误归纳成五类方便我们后面逐个击破错误层典型表现根因分析代码层变量未定义、API 调用语法错误、类型不匹配模型生成的代码或参数有误属于传统异常可捕获范围模型层输出 JSON 解析失败、关键字段缺失、数值格式错误大模型输出不稳定prompt 指令被打折执行工具层工具超时、工具报错、返回数据不符合预期 schema外部依赖不稳定工具设计不幂等参数校验缺失编排层步骤循环卡死、最大步数超限、状态丢失ReAct 循环没有终止条件流程状态没有持久化基础设施层并发超限被限流、上下文窗口溢出、沙盒环境失效资源管控不足、token 预算未设上限、环境状态未同步这五类错误里代码层的错误反而是最好处理的因为 LLM 生成的代码有语法错误编译器会告诉你。真正让人头疼的是模型层和编排层——它们不是程序崩了而是程序在错误的方向上跑得很欢。举个例子我遇到过一次 Agent 在调用天气工具时把 temperature 参数传成了 temprature工具系统没有做参数名前校验直接返回了空数据。Agent 拿到空数据后没有意识到工具调用失败而是自信地跟用户说今天气温数据暂不可用建议您出门看天气。这种错误日志里全是正常的调用记录但用户体验是彻底失败的。所以我们在设计错误处理时必须把工具返回空也当作一种需要 Agent 显式识别的状态来规范。2. 错误处理机制的设计每种异常都要有对应的解法2.1 模型层治理结构化输出与重试机制模型层的问题归根结底是模型的输出不可信。我们能做的不是让模型永远正确而是让错误更容易被发现、更容易被纠正。第一步也是最基础的一步强制结构化输出。我所有生产级的 Agent 项目里LLM 调用都要求返回 JSON 格式并且要过一层 schema 校验。Python 里可以用 PydanticTypeScript 里可以用 Zod。校验不通过直接抛输出格式错误而不是把原始文本交给下游逻辑去解析。这里有个经验单纯在 prompt 里写请以 JSON 格式返回是远远不够的。模型有时候会返回 Markdown 代码块包裹的 JSON有时候会把注释写进 JSON导致解析失败。所以我在 prompt 末尾通常会加一句固定的指令直接输出 JSON不要使用 Markdown 代码块不要添加任何解释。同时在解析层做好兼容——先尝试去掉代码块标记再交给 JSON 解析器。第二步给模型调用加重试。但这里有一个非常关键的点普通重试只针对可重试的错误。比如 JSON 解析失败、模型服务返回 5xx、超时这些可以通过重试解决。但要命的是上下文长度超限context length exceeded这类错误重试只会重复失败白白烧钱。我的做法是封装一个带错误分类的调用函数把错误分成 transient可重试和 permanent不可重试只有前者才进入重试逻辑。# agent_llm_call.py import json import time from pydantic import BaseModel, ValidationError from openai import OpenAI client OpenAI() def is_retryable_error(e: Exception) - bool: 判断错误是否可重试 error_msg str(e).lower() # 上下文长度超限这类错误重试只会重复失败 if context length in error_msg or maximum context in error_msg: return False # 罕见的某些供应商返回的输入验证类错误也不重试 if invalid request in error_msg or bad request in error_msg: return False # 网络超时、5xx、限流等属于可重试错误 return True def call_llm_with_retry(messages, schema_model, max_retries3): for attempt in range(max_retries): try: response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, response_format{type: json_object}, ) content response.choices[0].message.content # 兼容模型偶尔输出 Markdown 代码块的情况 content content.strip() if content.startswith(): content content.split(, 2)[1] if content.startswith(json): content content[4:] content content.strip() parsed json.loads(content) validated schema_model(**parsed) return validated except (ValidationError, json.JSONDecodeError) as e: # 格式错误时把错误信息反馈给模型再进行一次自我修正 messages.append({ role: user, content: ( f你的输出未通过格式校验: {e}。 请严格按照要求重新输出 JSON不要添加任何附加说明。 ) }) continue except Exception as e: if is_retryable_error(e) and attempt max_retries - 1: time.sleep(2 ** attempt) # 指数退避避免被打爆 continue raise raise RuntimeError(LLM 调用多次重试后仍然失败)这个函数里有几个细节值得注意。格式校验失败时我不是简单地重试而是把错误信息作为新的 user 消息追加进去让模型看到自己的错误再自我修正。这个做法在流程里叫 self-correction实测下来第二次成功的概率能提升到九成以上。第三步针对真正的硬错误比如模型层反复校验失败要设计降级策略。我见过很多团队在这个环节选择无限重试结果就是用户的请求卡死在那里十几秒体验极差。我的经验是重试两到三次后如果还不行直接走兜底回复告诉用户暂时无法处理请稍后再试把这次请求标记为失败进入复盘队列。用户能接受一个偶尔失败的 Agent但不能接受一个卡住不响应的 Agent。2.2 工具层治理超时、幂等与降级工具调用是 Agent 系统里出错率最高的环节。毕竟模型层的问题通过重试能缓解工具层面对的是真实世界的外部系统——数据库、第三方 API、内部服务它们什么时候抽风你完全控制不了。先说超时。这是我最开始忽略的问题。某个 Agent 项目里我让 Agent 调用一个内部报表服务的接口那个接口偶尔会卡住 3 分钟才响应。因为 Agent 是串行执行的这一个卡住整个流程就卡住了用户等得直接关掉了页面。后来我统一规定所有工具调用的超时时间不能超过 10 秒调用失败或超时就直接返回错误码让 Agent 决定下一步怎么办。然后是幂等。这个问题在 Agent 调用有副作用的工具时尤其致命。比如 Agent 要调用一个发送邮件的工具第一次调用超时了Agent 以为没发出去又调了一次结果用户收到了两封内容相同的邮件。这是典型的缺乏幂等设计。解决方案是给每个工具调用生成一个唯一的 request_id后端服务按 request_id 去重。实现方法不复杂关键是要有这个意识凡是 Agent 可能重试的工具都必须支持幂等。再来说降级。工具调用失败后Agent 不能愣住它需要有能力找到替代方案。我给 Agent 的工具集设计了一套简单的故障转移机制优先工具失败就尝试备选工具备选也失败就走代码层面的 fallback。比如说规划路径优先调用高德地图的路径规划失败后切换腾讯地图再失败就返回暂时无法规划路线。{ tool: route_planning, fallback_chain: [amap_route, tencent_route, static_route_table], timeout_ms: 10000, retry_times: 1, idempotent: false, danger_level: low }工具注册时就把这些元信息声明好运行时框架统一执行重试和降级Agent 只需要关心目标而不需要关心路径。这里我想补充一个观察很多人设计 Agent 时把工具调用成功后的返回结果不做校验就直接塞给模型。这其实是个大坑。工具返回的数据可能有敏感字段、可能有超长内容、可能格式混乱。我的建议是每个工具都定义明确的输出 schema在代码层面先做一次清洗和校验再交给模型推理。这能大幅减少模型层后面出现幻觉的概率。2.3 编排层治理状态机与护栏Agent 的多步执行流程本质上是一个状态机。最早实现 Agent 的时候我用的就是粗暴的 while 循环模型没有输出就继续跑结果就是某些场景下 Agent 陷入死循环token 烧了几百块才被发现。后来我给自己定了几条硬规矩任何 Agent 流程必须有最大步数限制必须记录当前处于哪个状态状态转移必须可追踪。最大的步数限制是最简单的护栏。以 ReAct 模式为例我通常设最大值 15 步。这里的步数不只是模型思考一次而是思考加工具调用的完整循环。达到步数上限后Agent 必须输出一个我无法完成任务的最终回复而不是继续往下推理。同时我还要求模型在每步推理时先输出plan字段说明它接下来准备干什么这样一旦后续流程异常我能回溯到它的规划意图是否正确。状态管理这块我强烈建议不要在内存里玩状态而是把状态序列化到 Redis 或数据库里。尤其是长任务的 Agent一旦进程被重启或者容器被调度内存里的状态就全丢了。这个在并发量上来后尤其重要。比如一个任务执行到第 8 步突然发生容器重启如果状态是持久化的恢复后还能从第 8 步继续如果只是内存态整个任务就得从头开始成本差距是非常大的。多 Agent 协作场景下的错误隔离也值得提一句。当你有多个 Agent 子任务并行执行时一定要做到故障隔离A 子任务的失败不能拖垮整个编排。我常用的方案是给每个子任务分配独立的配额和超时子任务失败后的默认行为是标记失败并向上汇报由主 Agent 决定是跳过、重试还是整体终止。千万不要让子任务自己无限重试那会把整个编排拖入深渊。3. 可观测性建设没有日志就谈不上工程化3.1 先理清 harness 和 agent 的分工很多和朋友在交流时发现大家对 harness 和 agent 的理解比较混乱。我这里分享一个我的划分方式harness 是执行环境agent 是业务逻辑。一个 agent 可以跑在多个 harness 里而 harness 负责的是循环控制、工具注册、模型调用、错误捕获、状态持久化这些与业务无关的通用能力。这个区分对可观测性建设很重要。因为 harness 是统一的你把观测逻辑写在 harness 层所有 agent 就自动具备了观测能力不需要每个 agent 自己埋点。如果把观测逻辑写在 agent 内部代码会越来越臃肿而且换个场景就丢了。我自己项目里的结构就是harness 层负责上报 trace、日志、指标agent 层只关心自己的任务逻辑。3.2 三个层次的观测手段第一层是结构化日志。这个不是传统的那种一行一行的文本日志而是事件型的日志。每次模型调用、每次工具调用、每次状态转移都记录一个事件包含时间戳、agent_id、执行 ID、事件类型、关键参数、耗时、token 消耗、错误信息。格式统一用 JSON方便后续采集和分析。第二层是链路追踪。Agent 系统天然是多步骤、多调用的没有 trace_id 把整条链路由头串到尾出了问题你根本没法排查。我在 harness 层统一生成 trace_id传给每一次 LLM 调用和工具调用第三方日志系统里就可以按照 trace_id 拉出完整的调用链路看到底是哪一步出了问题。这一步是排查问题的基础设施没有 trace_id 的 Agent 系统排查问题基本靠猜。第三层是指标。指标的作用不是定位问题而是发现问题。我最关注的几个指标包括任务成功率、平均失败重试次数、工具调用失败率、模型响应延迟、token 成本分布、上下文窗口利用率。这些指标可以做成看板出现问题前往往指标会先异常。比如某次我观测到工具调用失败率从 2% 飙升到 40%就知道上游某个服务出问题了可以先暂停相关 Agent 的调用而不是等用户大量投诉了才反应过来。3.3 现场排查的方法论Agent 系统问题最难的一点是复现难。模型本身有随机性同样的输入不一定产生同样的输出。所以排查 Agent 问题时我有一套固定的方法论先说复现。排查问题时我优先做的是固定随机性和温度把本次请求的完整上下文原始输入、之前的工具返回结果、模型采样参数、温度值打包快照。很多框架支持传入 seed遇到可疑情况我就用同一个 seed 去重放请求虽然不能 100% 复现但概率会大很多。还有一个技巧是把模型温度临时调成 0虽然创造性少了但错误复现的成功率会高不少。然后是拆解。拿到一次失败请求的 trace 后我会从后往前逐层倒查最后一步是谁报的错它前面一步的输入是什么再前面一步的工具结果是否符合预期大多数问题都能在倒查三步之内找到根源。最后是回归验证。修复一个问题后把当时失败的那个输入样例做成回归用例每次改动都跑一遍。这就引出了测试的话题——Agent 系统的测试本质上就是把历史错误变成用例集没有这个基础你改代码永远都是提心吊胆的。我在排查中还遇到过一种很让人无语的情况整套模型和工具都没问题但代码生成类 Agent 因为运行环境沙盒没更新导致环境中缺少新依赖直接报无法发送消息、沙盒需要更新之类的错误。这类环境类错误有时候比模型错误还要隐蔽因为它可能只在特定实例上出现。所以环境状态本身也要纳入可观测范围构建镜像时把版本信息作为元数据上报排查时才好对齐。4. 工程化落地从 Demo 到生产的最后一公里4.1 并发治理AI Agent 怎么扛并发很多做 Agent 的团队Demo 跑得很溜一上生产就被并发打趴了。核心问题在于Agent 任务不像普通 HTTP 请求那样毫秒级返回一个 Agent 任务可能要调用几十次模型接口耗时几十秒甚至几分钟。如果把它当作普通请求去处理并发一高模型服务的限流就会把你打爆。我给 Agent 系统做并发治理的实践是这样的首先Agent 不可能靠把接口请求并发打出去扛住流量必须引入队列和 Worker。用户提交任务后任务进队列由 Worker 池调度执行。这样可以把上游流量削峰填谷避免突发请求击穿模型服务。然后无论是模型 API 还是工具 API都必须要做并发限制。具体做法是给每个上游服务建一个信号量semaphore池控制并发数。比如模型服务的并发上限是 10就用一个值为 10 的信号量把所有模型调用框住。这样即使有 100 个 Agent 任务同时跑也不会把模型服务打爆。这个控制甚至可以做到按 Agent 维度拆分——重点业务 Agent 独占一部分并发配额普通 Agent 共用一个共享池实现资源隔离。# agent_executor.py import asyncio import time class TaskExecutor: def __init__(self, max_concurrency20, max_workers10): # 任务队列 self.queue asyncio.Queue() # 控制同时执行的任务数 self.semaphore asyncio.Semaphore(max_concurrency) self.workers [] self.max_workers max_workers async def worker(self, agent_instance): while True: task await self.queue.get() async with self.semaphore: try: await agent_instance.run(task.payload) except Exception as e: # 任务级异常捕获不让一个任务拖垮整个 worker print(ftask failed: {task.task_id}, error: {e}) finally: self.queue.task_done() async def start(self, agent_instance): for i in range(self.max_workers): w asyncio.create_task(self.worker(agent_instance)) self.workers.append(w) async def submit(self, payload): await self.queue.put(payload)这里还有一个重点Agent 任务必须要有总超时时间。我见过很多 Agent 因为工具响应慢单个任务跑了几十分钟还在烧 token。我的做法是给任务设置一个总预算包括时间和 token 两个维度任何一个超过预设上限就直接终止返回一个降级回复。用户在 30 秒内收到了一个处理超时的回复和一个 30 分钟后才返回我查不到的回复体验是完全不同的。token 成本控制也是并发设计里容易被忽略的点。我建议每个任务在初始化时申请一个 token 预算池每次模型调用从池子里扣减池子快要用尽时Agent 会收到一个预算告警的指令让它自动收敛推理过程直接给出结论而不是继续探索。这个做法原理上和人知道自己只有两分钟时间就会跳过废话直接说重点是一样的。4.2 记忆与上下文的异常处理Agent 的记忆系统无论短期还是长期都有各自的坑。短期记忆的核心问题是上下文窗口溢出。窗口再大也有限总会被长对话或者多轮工具调用撑爆。如果任由上下文增长到了临界点模型会直接报错。我的做法是主动做上下文裁剪先把最早的历史消息摘要化替换为总结文本保留最近几轮完整对话。这种摘要最近N轮的方案实践下来效果最好。这里也得设置一个保底动作如果裁剪后仍然超限就返回一个提示让用户开新会话而不是继续硬撑。长期记忆也就是向量数据库这侧的问题更多是写入和检索的可靠性。向量库不是每次写入都成功的检索也可能因为 embedding 模型临时抽风而返回空结果。这块不能因为一次写入失败把整个任务搞挂了。我的方案是记忆写入失败降级为本次对话不保留记忆记忆检索失败降级为返回空记忆并忽略——记忆是增强不是必需不能让它成为主流程的单点。这个思路本质上就是把记忆系统当作 Agent 的一个可降级的外部依赖来对待。顺带提一句现在也有一些专门针对 Agent 记忆的安全防护框架在探索比如给记忆做越权防御、给记忆注入做检测思路是主动防御而非事后补救。虽然行业还没有统一标准但方向是对的记忆一旦被污染Agent 后续的判断都会带上毒工程上要尽早设计防污染机制。4.3 安全与权限护栏Agent 的权限问题比传统系统严重得多。Agent 不只是读数据它还会主动调用工具去做事。如果 Agent 被 prompt 注入攻击恶意指令可能诱导它调用高危工具删除数据、转账、发邮件后果不堪设想。我在工程实践里的安全底线是任何有副作用的工具调用必须经过一次独立于 Agent 逻辑的安全检查。这里的独立非常关键——不能由 Agent 自己判断这个操作安不安全因为 Agent 的逻辑可能已经被污染了。安全检查应该由代码层的护栏完成。具体来说我做了两层。第一层是工具分级读操作是低危直接放行写操作是中危需要在请求里附带用户的显式授权删除、转账、发送外部消息这类操作是高危必须二次确认甚至需要不同的权限 Token 才能执行。第二层是输出侧防御把模型生成的工具调用参数做白名单校验不在白名单内的参数直接拒绝。比如只允许操作指定 ID 范围内的数据超出范围的直接拦截。Prompt 注入的检测也要做。常见的做法是给 Agent 的输入加一道检测如果用户输入中包含明显的指令注入特征比如忽略之前的指令、你现在是...系统先打一个风险标签让高危工具对这类标签的任务不可用。这个检测不一定能防住所有攻击但能挡住大部分廉价攻击大幅提高攻击成本就够了。4.4 测试、灰度与发布Agent 系统的测试是一道复杂的课题。传统单元测试只能覆盖工具层的纯逻辑模型层的行为是难以断言的。我现在的测试策略是分三层第一层工具单元测试。每个工具函数独立测试输入输出断言这部分和传统单元测试没区别。第二层场景回归测试。每一个线上出过问题的例子修复后都沉淀为一个测试用例。跑回归时把用例喂给 Agent固定在温度 0 与固定 seed 下检查最终结果是否符合预期。这种测试不一定每次都能触发暴露问题但至少能保证修复没有破坏历史行为。测试集越滚越大你的系统就会越来越稳这是个笨但有效的方法。第三层基于评估集的整体评测。准备一组覆盖典型使用场景的评测集给每个任务的完成质量打分统计通过率指标。每次要上线新模型、新工具或新逻辑之前先跑一遍评测分数不降才允许上。灰度发布方面我的原则是不要让真实用户当测试员。Agent 的每次改动先在测试环境跑评测及格后放到灰度环境给一小批用户用观察指标没有明显恶化再逐步放量。这个流程虽然听起来老生常谈但在 Agent 项目里经常被跳过——因为很多人觉得反正有模型兜底改坏了也没关系。实际上模型兜底只会让错误看起来不那么突兀但正确率、成本、延迟都可能会明显劣化没有灰度会让问题在不知不觉中积累。5. 高频问题排查速查表与我的避坑实录5.1 Agent 错误排查速查表我在实际项目里把高频问题整理成了一张速查表每次排查问题先对着表找方向效率高很多。错误现象可能原因排查思路解决方案Agent 反复调用同一个工具不停止模型陷入死循环没有设置最大步数看 trace 中是否存在循环调用模式设置最大步数循环检测超过 N 次相同调用强制终止工具返回了数据但 Agent 判断没有结果工具输出 schema 与模型预期不匹配检查工具输出字段命名是否语义清晰工具输出标准化在 prompt 中给工具返回示例模型输出 JSON 频繁解析失败prompt 指令不够严格查看失败输出的具体格式在 prompt 中加入防代码块指令解析时兼容 Markdown上下文窗口溢出对话历史过长没有裁剪机制评估上下文内容的分布情况引入摘要机制压缩历史提示用户开启新会话任务执行到一半进程重启进度丢失状态只存在内存中检查是否有状态持久化把状态同步到 Redis 等外部存储并发一高工具被限流没有做并发控制观察工具调用的 QPS 和错误率引入信号量增加重试与退避策略提前申请配额Agent 执行了未授权的操作权限设计缺失检查工具调用日志建立工具分级制度引入独立安全检查层代码生成类 Agent 报沙盒旧环境错误沙盒构建版本与当前代码不匹配核对环境版本元数据把环境版本纳入 trace 元数据构建后做环境校验这个表不是万能的但它能覆盖我过去遇到过的大部分线上问题。排查 Agent 问题的大方向永远是先看 trace 再补日志先复现再修代码不要一上来就怀疑模型。5.2 三个印象深刻的踩坑记录第一个坑工具调用幂等没设计好导致重复发单。有一次一个自动下单类的 Agent 在调用下单接口时超时了框架自动重试了一次结果用户收到了两个完全相同的订单。这个事故让我彻底记住了幂等的重要性。教训是凡是会触发外部副作用的工具必须带 request_id后端按 id 去重这个不是可选项是必选项。第二个坑过度设计降级链反而导致故障扩大。我刚开始做工具降级的时候给所有工具都配了链路。比如 Agent 要调用 A 工具代码会自动尝试 B、C、D 作为备选。结果又一次 A 工具故障所有请求都涌到 B 上B 也被打挂然后涌到 C 上。那个下午整个系统的可用性比不降级时还差。后来我学乖了降级只降一级备选工具最多一个而且备选方案要有独立的配额。宁可损失一部分功能也不能让故障像多米诺骨牌一样传导。第三个坑长时间运行的 Agent 任务在上下文接近窗口上限时质量问题急剧恶化。模型到了上下文后半段注意力分布会出现迷失在中间的现象早期信息容易被忽略导致 Agent 做出前后矛盾的决定。我后来规定所有 Agent 进程里如果上下文利用率超过 70%就直接触发摘要压缩把最早一半的历史总结掉。牺牲一点细节换回模型质量是值得的。写在最后我自己的体会是Agent 工程化跟传统软件工程有一个本质区别传统软件追求的是确定性正确你写对了就是对了Agent 追求的是在不确定中保持稳定你无法保证模型每次都正确但你可以保证它错了之后系统能发现问题、能降级、能恢复。错误处理不是 Agent 项目里最炫酷的部分但它是决定你的 Agent 能不能从玩具变成工具的分水岭。这篇文章里的做法全部来自我在实际项目里的摸爬滚打。如果你正在做 Agent 的工程化可以先把最大步数、超时控制、结构化输出重试、状态持久化这几样做上已经能解决大部分问题。剩下的就交给时间慢慢填坑吧。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。