尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Agent开发异常处理实战:重试、状态机与人工接管

Agent开发异常处理实战:重试、状态机与人工接管 兄弟们做 Agent 开发最头疼的事情其实不是模型能力不够而是模型不按套路出牌。你用ChatClient调模型模型开始“自由发挥”返回了一堆设计之外的 JSON 格式你让 Agent 连续调用工具结果第一轮就报了个依赖服务超时整个任务链路就卡死在那里了。传统 Java 三板斧try-catch-finally能解决代码层面的异常但在 Agent 场景里很多“异常”根本不是代码抛出来的而是模型输出结果不符合预期、外部工具调用失败、上下文超出窗口限制甚至 Agent 在某个步骤里自己“绕晕了”循环出不来。这篇文章我从工程落地的角度梳理出一套 Agent 异常处理的思考框架。重点会围绕三种最常用、也最有效的处理方式展开基于重试与退避的异常处理解决模型服务瞬时故障、限流、超时问题。基于 Agent 内部状态机流转的异常处理解决“本地代码没报错、但 Agent 流程走偏了”的异常情况。基于人工接管与异步编排的异常处理解决单次 Agent 无法恢复、需要兜底或需要人机协同的问题。这篇文章适合已经做过一个简单的 Agent Demo、现在想迈向生产级应用的开发者。如果你对 Agent 的理解还停留在“用 LangChain 调一个模型喂一个 Prompt”那这篇文章能帮你补齐一个很关键的能力如何让 Agent 在纷繁复杂的真实环境中稳定地跑完一个任务。下面直接开始。1. Agent 异常处理的背景与痛点1.1 什么是 Agent 异常处理在传统后端开发里异常处理无外乎就是捕获异常。根据异常类型做日志记录。做补偿或回滚。返回一个友好的错误信息给调用方。但在 Agent 开发里“异常”的范围被放大了。一个 Agent 的标准执行链路通常是这样的接收用户指令 ↓ 规划子任务Plan ↓ 调用工具或模型Action ↓ 观察结果并反思结果是否符合预期Observation ↓ 输出最终答案Answer在这个链路里任何环节都可能产生“异常”模型服务超时或限流。模型返回的 JSON 格式不符合约定。外部工具返回了错误数据。Agent 在某个子任务上陷入死循环。上下文 token 长度超过模型窗口限制。这些异常只在很少数的情况下对应着 Java 代码层面的Exception。更多时候Agent 已经“正常运行”了但它没有按照我们的预期去运行。所以 Agent 异常处理的本质是在保持主流程可执行的前提下为模型、工具、外部依赖的不可靠性提供一个可恢复、可观测、可兜底的处理机制。1.2 Agent 异常与普通程序异常的区别很多从传统 Java 转过来做 Agent 开发的同学会下意识地写这样的代码try { String result chatClient.call(prompt); } catch (Exception e) { log.error(调用模型失败, e); }这种做法在普通 API 调用里没什么问题但在 Agent 场景里远远不够原因有两个。第一异常粒度不对。普通异常处理关心的是“这个请求是否成功”而 Agent 异常处理关心的是“这个任务是否完成”。一个任务可能需要多次模型调用、多次工具调用才能完成其中任何一次模型输出不符合预期都可能导致整个任务失败。如果你只是在最外层包了一个try-catch那你很难定位是哪一个子任务出了问题。第二异常恢复策略不同。普通异常处理通常是“报错后直接返回”但对于 Agent 来说最简单的解决方式根本不需要报错。比如模型第一次返回的 JSON 格式坏了我们完全可以把错误信息拼进新的 Prompt让模型自己“修正一下输出”。这种“用提示词去修复模型输出”的思路属于 Agent 异常处理里非常重要的补充手段。1.3 为什么需要系统化的处理机制如果你只是写一个 Demo那确实不需要考虑这些调一次模型返回一个结果Demo 就结束了。但如果你要做的是生产级 Agent你必须考虑模型服务是有概率性故障的。模型输出是有随机性的。外部工具是不可靠的。Agent 的执行链路是漫长且复杂。这四点叠加在一起结果就是Agent 在生产环境中的失败率远高于常规后端接口。在这种情况下如果没有系统化的异常处理机制你的 Agent 看起来就像是一台随时会罢工的机器。我们下面要讲的三种方式就是分别为“瞬时故障”“流程错误”“最终兜底”这三类问题设计的。2. 环境准备与工程结构2.1 技术栈与版本说明这篇文章的示例代码以 Java 17 Spring Boot 3 为主同时会涉及 Spring AI 和 CompletableFuture。需要注意Agent 框架本身迭代非常快不要盲目相信某个固定版本的使用方式。本文示例代码的核心思路不依赖特定版本你完全可以根据自己的项目实际情况调整。如果你使用的是 Python Agent 框架比如 LangChain、CrewAI 或自研 Agent这三种异常处理方式是通用的你需要做的只是把 Java 示例替换成对应框架的 API。但本着规则优先的原则我先给你一个相对清晰的版本假设JDK17 或更高版本 Spring Boot3.x Spring AI0.8.x 或当前最新稳定版 构建工具Maven 3.8如果您的项目还没有引入 Spring AI在pom.xml中做如下配置即可dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version${spring-ai.version}/version /dependency一定记得设置环境变量或配置项spring.ai.openai.api-key${OPENAI_API_KEY} spring.ai.model.chatopenai不同模型厂商的依赖不同你的环境里如果使用的是国产模型或本地部署模型需要替换为对应 Starter。2.2 项目结构设计一个包含异常处理的 Agent 项目我建议按下面目录结构来组织src/main/java/com/example/agent ├── AgentApplication.java ├── common │ ├── AgentException.java │ └── AgentResponse.java ├── llm │ └── LlmClient.java ├── plan │ └── TaskPlanner.java ├── state │ ├── AgentPhase.java │ ├── AgentContext.java │ └── AgentStateMachine.java └── handler ├── RetryHandler.java ├── InterruptHandler.java └── HumanTakeoverHandler.java我们在后面的实战案例中会逐一填充这些文件。3. Agent 运行中的异常类型拆解在设计异常处理机制之前先把“Agent 运行中的异常”分成三类。这个分类是整个处理机制的核心因为不同类别的异常对应的处理手段完全不同。3.1 外部依赖异常这是最容易理解的异常类别也是传统try-catch最擅长处理的类别。外部依赖异常包括模型 API 调用超时。模型 API 触发限流Rate Limit。模型 API 返回 5xx 错误。工具调用时目标服务不可用。数据库连接中断。这类异常的特点在于它是可重试的。因为这类异常往往属于瞬时故障服务可能在几秒后恢复正常。3.2 行为与内容异常行为与内容异常是 Agent 开发中最常见的“隐雷”。它的典型表现是代码没有报错但 Agent 的行为不符合预期。比如模型返回的文本不是合法的 JSON 格式。模型返回的 JSON 字段缺失。模型没有调用应该调用的工具。在一轮优化过程中模型不断重复相同的输出没有收敛。这类异常光靠try-catch是抓不住的必须要从“语义层面”去判断 Agent 是否运行正常。3.3 状态与循环异常状态与循环异常指的是 Agent 在运行时进入了“死循环”或“跑飞”状态Agent 规划了 10 个子任务但执行了 20 轮还没有结束。Agent 在同一个工具调用上反复失败且没有新进展。Agent 的上下文长度已经接近模型窗口限制。这类异常的特征是需要一个外部观察者去打断 Agent 的执行不能依赖 Agent 内部自己解决。4. 方式一基于重试与退避的异常处理4.1 适用场景第一种方式适用于处理【3.1 外部依赖异常】。最常见的场景就是调用大模型时模型服务返回超时或者限流。注意这里有两个关键点并不是每次失败都适合重试。如果是模型返回内容不合法重试也大概率不合法如果是 4xx 客户端错误重试也大概率报错。只有服务端错误5xx、网络超时、限流429这类瞬时故障才值得重试。重试不能无限重试。如果没有设置最大重试次数和退避策略重试请求会反过来加剧模型服务的压力导致雪崩。4.2 重试策略设计在生产环境里我推荐使用“指数退避 抖动”策略。先解释一下指数退避第 1 次失败后等待 1 秒再重试。 第 2 次失败后等待 2 秒再重试。 第 3 次失败后等待 4 秒再重试。 第 4 次失败后等待 8 秒再重试。“抖动”的意思是每次等待时间增加一个随机值避免多个请求同时重试造成流量峰值。在 Spring Boot 中可以直接使用 Spring Retry 库。先引入依赖dependency groupIdorg.springframework.retry/groupId artifactIdspring-retry/artifactId /dependency再在启动类加上EnableRetrySpringBootApplication EnableRetry public class AgentApplication { public static void main(String[] args) { SpringApplication.run(AgentApplication.class, args); } }4.3 代码实现我们先定义一个简单的LlmClient它负责调用模型接口// 文件路径src/main/java/com/example/agent/llm/LlmClient.java Component public class LlmClient { private final RestTemplate restTemplate; public LlmClient(RestTemplate restTemplate) { this.restTemplate restTemplate; } public String callModel(String prompt) { // 简化处理实际项目中这里会构造模型请求体 String url https://api.example.com/v1/chat/completions; return restTemplate.postForObject(url, prompt, String.class); } }然后创建一个重试包装类// 文件路径src/main/java/com/example/agent/handler/RetryHandler.java Component public class RetryHandler { private final LlmClient llmClient; public RetryHandler(LlmClient llmClient) { this.llmClient llmClient; } Retryable( retryFor {LLMTimeoutException.class, LLMThrottleException.class}, noRetryFor {IllegalArgumentException.class}, maxAttempts 3, backoff Backoff(delay 1000, multiplier 2) ) public String callModelWithRetry(String prompt) { return llmClient.callModel(prompt); } }上面这段代码的核心要点retryFor指定哪些异常需要触发重试。这里定义了LLMTimeoutException超时和LLMThrottleException限流。noRetryFor指定哪些异常不需要重试。比如参数错误直接抛错。maxAttempts最大尝试次数3 次意味着“首次执行 2 次重试”。backoff延迟策略delay 1000表示首次重试等待 1 秒multiplier 2表示倍数增长。如果你不喜欢注解的方式Spring Retry 也支持编程式重试。但整体来说注解方式更简洁也更容易在团队内统一规范。4.4 适用边界与局限重试方案最大的局限在于它只能解决“服务临时性故障”不能解决“Agent 行为错误”。举一个反例如果模型返回了一串内容你希望在内容中提取 JSON但模型返回的是纯文本。此时无论你重试多少次这个结果都是错的。因为问题不在网络而在于模型没有按照格式返回。这种情况下正确的做法不是重试而是把“结果错误”作为提示词重新喂给模型或者启用结构化输出。所以在设计 Agent 异常处理时永远不要只依赖重试这一招。5. 方式二基于 Agent 内部状态机流转的异常处理5.1 适用场景第二种方式解决的是“行为与内容异常”和部分“状态循环异常”。核心思路是把 Agent 的执行流程抽象成一组有限的状态并在状态转换后校验结果是否可用。如果不可用就触发状态回退或重新规划。这种方式特别适合以下场景Agent 需要分阶段完成一个复杂任务。每个阶段之间必须拿到上一阶段的结果才能进入下一阶段。模型在某个阶段可能输出不合法数据。5.2 状态模型设计以一个“撰写行业研究报告”的 Agent 为例我们需要三个子任务INIT初始状态 ↓ 规划任务 PLANNING规划中 ↓ 生成报告大纲 OUTLINE_GENERATED大纲已生成 ↓ 校验大纲 OUTLINE_VALIDATED大纲校验通过 ↓ 生成报告正文 REPORT_GENERATED报告已生成 ↓ 校验报告 COMPLETED完成现在定义AgentPhase枚举// 文件路径src/main/java/com/example/agent/state/AgentPhase.java public enum AgentPhase { INIT, PLANNING, OUTLINE_GENERATED, OUTLINE_VALIDATED, REPORT_GENERATED, COMPLETED, FAILED }再定义AgentContext用它携带整个 Agent 运行过程中的数据// 文件路径src/main/java/com/example/agent/state/AgentContext.java public class AgentContext { private AgentPhase currentPhase; private AgentPhase previousPhase; private String originalUserRequest; private String outline; private String report; private int retryCount 0; // getter / setter 省略实际项目中可借助 Lombok Data public int getRetryCount() { return retryCount; } public void incrementRetryCount() { this.retryCount; } }5.3 状态流转核心代码接下来定义一个AgentStateMachine。它的核心职责是驱动 Agent 在不同的阶段之间流转。在每次状态流转后校验结果。如果结果不满足要求返回上一个状态让 Agent 在上一状态中“自我纠正”。// 文件路径src/main/java/com/example/agent/state/AgentStateMachine.java Component public class AgentStateMachine { private static final int MAX_PHASE_RETRY 2; private final LlmClient llmClient; public AgentStateMachine(LlmClient llmClient) { this.llmClient llmClient; } public void execute(AgentContext context) { context.setCurrentPhase(AgentPhase.INIT); while (context.getCurrentPhase() ! AgentPhase.COMPLETED context.getCurrentPhase() ! AgentPhase.FAILED) { // 防止同一个阶段无限重试 if (context.getRetryCount() MAX_PHASE_RETRY) { context.setCurrentPhase(AgentPhase.FAILED); break; } switch (context.getCurrentPhase()) { case INIT: handleInit(context); break; case PLANNING: handlePlanning(context); break; case OUTLINE_GENERATED: handleValidateOutline(context); break; case OUTLINE_VALIDATED: handleGenerateReport(context); break; case REPORT_GENERATED: handleValidateReport(context); break; default: context.setCurrentPhase(AgentPhase.FAILED); } } } private void handleInit(AgentContext context) { // 初始化将阶段切到规划 context.setPreviousPhase(context.getCurrentPhase()); context.setCurrentPhase(AgentPhase.PLANNING); } private void handlePlanning(AgentContext context) { String prompt 请根据用户需求生成报告大纲直接输出JSON数组不要输出其他内容 context.getOriginalUserRequest(); String response llmClient.callModel(prompt); // 这里做“行为校验”尝试把模型输出解析成 JSON if (!isValidJson(response)) { // 模型输出不符合预期触发重试 context.incrementRetryCount(); log.warn(模型输出不是合法 JSON当前第 {} 次重试, context.getRetryCount()); return; } // 解析成功进入下一个阶段 context.setOutline(response); context.setPreviousPhase(context.getCurrentPhase()); context.setCurrentPhase(AgentPhase.OUTLINE_GENERATED); context.setRetryCount(0); } private void handleValidateOutline(AgentContext context) { // 校验大纲是否满足用户需求这里可以调用另一个“校验模型” boolean valid validateOutline(context.getOutline()); if (valid) { context.setPreviousPhase(context.getCurrentPhase()); context.setCurrentPhase(AgentPhase.OUTLINE_VALIDATED); } else { // 回到规划阶段让模型重新规划这里实际上就是“状态回退” context.incrementRetryCount(); context.setCurrentPhase(AgentPhase.PLANNING); } } private void handleGenerateReport(AgentContext context) { String prompt 请基于以下大纲生成完整报告\n context.getOutline(); String report llmClient.callModel(prompt); context.setReport(report); context.setPreviousPhase(context.getCurrentPhase()); context.setCurrentPhase(AgentPhase.REPORT_GENERATED); } private void handleValidateReport(AgentContext context) { boolean valid validateReport(context.getReport()); if (valid) { context.setCurrentPhase(AgentPhase.COMPLETED); } else { context.incrementRetryCount(); context.setCurrentPhase(AgentPhase.REPORT_GENERATED); } } }这段代码里最值得注意的部分在于handleValidateOutline方法的“状态回退”。当模型生成的大纲不满足要求时我们没有直接抛异常而是把状态从OUTLINE_GENERATED回退到PLANNING让 Agent 重新生成一份大纲。这个过程比你直接重试模型的单个调用要合理得多因为它重新执行的是“生成大纲这一整个动作”而不是简单地把同样的请求发一遍。5.4 结构化输出与结果校验在状态机设计过程中结果校验是关键中的关键。一个很常见的坑是模型输出文本时习惯带上 Markdown 代码块标记导致 JSON 解析失败。解决办法有两个在isValidJson方法里先对文本做清洗去掉 json 代码块标记再解析。使用模型的结构化输出能力。Spring AI 中可以在调用时指定BeanOutputConverter让模型只能输出与目标结构匹配的 JSON。这里给出一个简单的清洗与校验方法private boolean isValidJson(String text) { if (text null || text.isBlank()) { return false; } String cleaned text .replace(json, ) .replace(, ) .trim(); try { new ObjectMapper().readTree(cleaned); return true; } catch (JsonProcessingException e) { return false; } }这个方法不是最终的“万能解法”但在大多数情况下够用。在企业项目中建议把清洗 校验封装成独立的ModelOutputValidator方便复用和测试。6. 方式三基于人工接管与异步编排的异常处理6.1 适用场景第三种方式解决的是“Agent 自身处理不了”的异常。这里的“异常”可能是已经重试多次但 Agent 仍然无法完成当前任务。Agent 需要访问一些权限受限的资源。Agent 的输出结果需要人工审核以后才能生效。Agent 在执行过程中需要用户确认某个关键步骤。在这些场景下我们不能让 Agent 继续“硬刚”而是应该把异常状态暴露给外部由人类介入处理。6.2 设计思路人工接管的核心设计目标是在 Agent 执行异常时保留上下文并提供一个能被外部系统识别、可以被恢复继续执行的状态。最简单的做法是在AgentResponse中增加一个结果状态字段// 文件路径src/main/java/com/example/agent/common/AgentResponse.java public class AgentResponseT { private boolean success; private T data; private String errorCode; private String errorMessage; private AgentPhase currentPhase; private String taskId; public static T AgentResponseT failure(String errorCode, String errorMessage, AgentPhase phase) { AgentResponseT response new AgentResponse(); response.setSuccess(false); response.setErrorCode(errorCode); response.setErrorMessage(errorMessage); response.setCurrentPhase(phase); return response; } }当 Agent 判定自己无法继续时就返回AgentResponse.failure(...)同时把taskId、currentPhase和上下文存储在数据库中。这样运维人员或业务人员可以拿到一个“半成品的任务”在后台重新提交或人工修改。6.3 CompletableFuture 异步编排在 Agent 场景下人工接管往往和异步编排是紧密结合的。因为 Agent 任务执行时间长通常不会同步阻塞等待结果而是通过异步方式提交任务然后通过回调通知。CompletableFuture 是 Java 8 中引入的异步编程工具。它的exceptionally方法非常适合用来处理 Agent 异步任务中的异常。下面是一个典型的使用示例// 文件路径src/main/java/com/example/agent/handler/HumanTakeoverHandler.java Component public class HumanTakeoverHandler { private final TaskService taskService; private final NotifyService notifyService; public HumanTakeoverHandler(TaskService taskService, NotifyService notifyService) { this.taskService taskService; this.notifyService notifyService; } public CompletableFutureAgentResponseString executeAsync(AgentContext context) { return CompletableFuture .supplyAsync(() - { // 实际执行 Agent 主流程 if (context.getCurrentPhase() AgentPhase.FAILED) { throw new AgentExecutionException(Agent 在手写本次任务执行达到最大重试次数, context); } return 任务完成; }) .exceptionally(ex - { // 异步异常处理逻辑记录异常、保存上下文、通知人工接管 AgentContext errorContext extractContext(ex); taskService.persistHalfDoneTask(errorContext); NotifyMessage message new NotifyMessage( Agent 任务执行失败需要人工介入, errorContext.getTaskId(), errorContext.getCurrentPhase() ); notifyService.notifyOperator(message); return AgentResponse.failure( AGENT_NEED_HUMAN, ex.getMessage(), errorContext.getCurrentPhase()); }); } }这段代码想说明的并不是某个特定框架而是异常处理的两个原则失败不是终点要保留现场。即把AgentContext保存下来方便人工恢复。失败要主动通知而不是默默吞掉。即调用notifyService通知人工处理。6.4 异步编排的异常传播坑点使用 CompletableFuture 时有两点非常容易被忽略第一exceptionally只对CompletableFuture链条上的异常生效。如果你在supplyAsync内部自己捕获了异常并返回了null那exceptionally是感知不到的。第二多个 CompletableFuture 并行编排时需要注意依赖关系CompletableFutureAgentResponseString future1 executeAsync(context1); CompletableFutureAgentResponseString future2 executeAsync(context2); CompletableFutureVoid all CompletableFuture.allOf(future1, future2); all.exceptionally(ex - { // 这里能捕获 future1 或 future2 的异常 return null; });allOf本身不会抛出异常它只负责等所有任务结束。真正拿到异常需要在后面的join或whenComplete中处理。7. 三种方式对比与选型建议7.1 三种方式对比表对比维度方式一重试与退避方式二状态机流转方式三人工接管与兜底解决的核心问题模型服务瞬时故障、超时、限流模型输出不符合预期、Agent 行为错误Agent 无法自主恢复、需要人为干预是否需要外部通知不需要不需要需要处理时效秒级自动恢复秒级自动纠正分钟级人工介入恢复能力弱只能重试同一次调用强可以重新规划或状态回退中需要人为参与代码成本低中高适合阶段单次模型调用、短链路任务多步骤、多工具长链路任务生产环境必备兜底机制7.2 选型决策树在实际项目中我建议按下面的规则决定采用哪种方式判断问题类型 是否是网络超时、限流、5xx └─ 是 → 方式一重试 退避 是否是模型输出格式不合法、结果不符合预期 └─ 是 → 方式二状态机回退 重新规划 是否重试多次仍然失败 └─ 是 → 方式三人工接管兜底三种方式并不互斥。在一个真正生产级的 Agent 系统中通常是三种方式同时存在、层层递进的先是重试重试不行就状态回退状态回退还不行就上人工接管。8. 常见问题与排查思路8.1 高频问题汇总问题现象常见原因解决思路the agent execution provider did not respond in time模型服务调用超时常见于网络波动或模型负载过高设置合理超时时间对超时采用重试 退避记录耗时用于后续分析模型返回的 JSON 解析失败模型输出被 Markdown 代码块包裹或直接输出纯文本清洗输出文本后再解析使用结构化输出把解析失败信息回传让模型重新输出Agent 任务执行到某一步后不再继续异常被捕获后被直接吞掉没有记录上下文检查异常日志确保exceptionally或全局异常处理器返回了可追踪的信息异步任务异常没有被捕获异常发生在子线程中主线程的try-catch捕获不到使用 CompletableFuture 的exceptionally、handle方法处理异步异常Agent 执行轮数过多模型陷入循环多次生成相同结果在状态机中增加MAX_PHASE_RETRY在循环开始前设置最大轮数上下文 token 超限多轮对话内容不断累积对历史消息做摘要清理过老的消息或把不必要的前置上下文截断8.2 一个完整的排查案例结合这次搜索热词里比较典型的报错信息来演示排查思路现象Agent 在执行一次长任务时控制台报了一个类似 “the agent execution provider did not respond in time. this may indicate the provider is overloaded or the request took too long to complete” 的错误。排查步骤先看错误类型。这个报错指向的是“执行提供方没有及时响应”通常发生在模型提供方或者某个外部 Agent 执行引擎服务上。查看请求耗时。如果耗时就接近超时阈值那核心原因大概率是模型服务单次响应太慢。查看是偶尔发生还是持续发生。偶尔发生优先用“方式一重试 退避”解决持续发生则需要检查当前模型服务的负载情况或者考虑更换模型服务。检查重试配置。如果你的代码中超时时间设置得过短比如 5 秒而模型服务平均耗时就需要 10 秒那无论怎么重试都会失败。这种情况下应该先调大超时时间再配置重试。解决方案在模型调用时设置合理超时时间比如 30 秒。增加重试 退避机制。对耗时进行监控并设置告警。9. 工程化最佳实践9.1 异常分类与错误码统一我强烈建议你在项目初期就统一异常分类和错误码规范AGENT_LLM_TIMEOUT - LLM 调用超时 AGENT_LLM_RATE_LIMIT - LLM 限流 AGENT_LLM_FORMAT - 模型输出格式错误 AGENT_TOOL_ERROR - 工具调用异常 AGENT_STATE_ERROR - Agent 状态流转异常 AGENT_HUMAN_NEEDED - 需要人工介入有了错误码前后端联调、日志监控、告警规则都会舒服很多。9.2 日志记录必须带上上下文在 Agent 异常处理里最忌讳的日志是“只记录异常消息不记录上下文”。因为 Agent 链路太长如果日志里没有taskId、当前阶段、当前 Prompt排查起来基本只能靠猜。推荐日志格式[taskId12345][phaseREPORT_GENERATED] 模型输出校验失败重试次数19.3 重试必须考虑幂等性如果你的 Agent 在调用外部工具时操作不是幂等的比如“创建订单”“发送短信”那么重试前一定要判断当前操作是否已经生效。比较稳妥的做法是在AgentContext中记录每个工具调用的唯一 ID重试时带上idempotency-key头让外部服务做去重。9.4 熔断与降级对于生产环境的重型 Agent我建议结合熔断降级方案。如果模型服务持续不可用与其让所有请求都进入长时间重试不如直接熔断快速失败并把任务转发到人工处理队列。一种轻量级降级方案是设置“兜底模型”。当主模型不可用时自动切换到备选模型虽然效果可能有差异但至少能保证任务尽量往下走。9.5 安全与权限边界涉及 Agent 调用外部工具时一定要做好权限控制。Agent 只是执行者不应该拥有过高的系统权限。人工接管机制本身就是一道安全闸门当 Agent 无法确认结果是否符合预期时宁可让任务停下来等人处理也不要让 Agent 自主执行高风险动作。9.6 测试策略异常处理代码一定要配测试。你可以用 Mock 方法模拟各种异常场景模拟模型接口超时验证重试策略是否生效。模拟模型输出非法 JSON验证状态回退是否生效。模拟连续多次失败验证人工接管是否被触发。对于 Java 项目推荐的测试方法是 Mockito 加上单元测试对于包含异步编排的代码还需要测试exceptionally分支是否被覆盖。10. 总结Agent 异常处理并不是单纯地写几个catch块而是要从整个任务的视角出发设计一套多层级的兜底体系。面对模型服务的瞬时故障我们采用重试与退避策略快速恢复执行。面对模型输出不符合预期我们采用状态机回退让 Agent 在上一阶段自我纠正。面对 Agent 无法自愈的严重异常我们采用人工接管与异步编排保留现场、通知人员介入。三者层层递进共同构成一个完整的异常处理闭环。在 AI Agent 开发这个领域模型的推理能力会越来越强但模型的不确定性永远存在。作为开发者我们能做的不是祈祷模型永远正确而是设计一套足够稳健的机制让出错之后仍然可控、可恢复、可审计。建议你挑选一个现有 Agent 项目先梳理当前执行链路再对照这篇文章里三种方式看看哪些环节还没有兜底。只有动手实践一遍才能真正理解异常处理在 Agent 工程中的分量。
返回列表