Agent异常处理实战:三种策略保障项目稳定

Agent异常处理实战:三种策略保障项目稳定 在做 Agent 项目时我逐渐发现一个规律业务逻辑的难点在于设计但真正让项目稳定性拉开差距的往往是异常处理。Agent 不像普通接口一次调用结束后就断开连接它通常要经历“模型推理 → 工具调用 → 结果解析 → 多轮循环”的完整链路链路越长异常越难复现也越难排查。本文围绕 Agent 开发中最常见的三类异常场景总结三种实用处理方式本地捕获与结构化记录、重试与指数退避、CompletableFuture 异步编排中的异常恢复。内容以 Java 技术栈为主同时也适用于 Spring Boot、Spring AI 等框架下的 Agent 项目。1. Agent 异常处理的背景与难点1.1 什么是 Agent为什么异常更复杂Agent智能体可以理解为一个具备“感知、决策、执行、反馈”能力的程序实体。在 AI Agent 开发中Agent 通常负责拆解用户任务、选择工具、调用外部 API、整合结果最后再把答案返回给用户。它与普通函数的本质区别在于Agent 的执行路径不是固定的模型会根据输入动态决定下一步动作所以同一个看起来相同的请求可能在内部走向完全不同的分支。正因为执行路径不固定异常处理也变得更复杂。普通程序里异常通常来自代码逻辑错误、资源不足、网络抖动链路短、可复现性强而在 Agent 项目里异常来源至少包括模型 API 超时、限流、输出内容不符合格式要求、工具链调用失败、上下文窗口溢出、并行子任务竞争等等。可以说异常处理已经成为 Agent 工程化落地中最容易被低估、却最影响稳定性的一环。1.2 Agent 项目中异常的高频来源结合线上 Agent 项目的踩坑经验异常主要集中在这几类模型调用异常模型接口超时、限流、连接池耗尽导致 Agent 无法获取推理结果。输出格式异常模型返回内容没有被正确解析比如要求返回 JSON却返回了额外说明文字导致反序列化失败。工具调用异常Agent 内部执行的 Function/Tool 出现业务错误比如查询订单失败、数据库连接断开。上下文溢出多轮对话后 Token 数超过模型上下文窗口限制Agent 被迫截断或直接失败。异步任务异常多个工具并行调用时其中一个子任务失败导致整个编排链路异常。很多框架中还会看到 Harness 与 Agent 的区别Agent 是任务执行的主体负责思考和调用工具Harness 则是承载 Agent 运行的调度与控制层负责生命周期、日志、超时管理。Agent 内部产生的异常最终需要上报到 Harness 层统一处理这也是异常处理设计时要考虑的分层问题。1.3 传统异常处理为什么不够用传统开发中我们习惯用 try-catch 包裹容易出错的代码块捕获后打日志、抛异常、返回错误码。这套思路在 Agent 项目中依然重要但不够用。原因是 Agent 的执行过程往往带有“自动性”一次工具调用失败后Agent 可能还有能力换一种工具继续完成任务一个子任务失败后也可能不影响整体结果返回。如果所有异常都直接向上抛出Agent 的“智能”就发挥不出来如果所有异常都静默吞掉用户只会得到一个莫名其妙的回答。所以Agent 异常处理需要一套更贴近执行链路的策略性方案。下面要介绍的三种方式就是按这个思路展开的。2. Agent 异常处理的三种方式总览在正式写代码之前先用一张表格把三种方式放在一起对比。这样你在阅读后续示例时能更清楚每种方式的定位。处理方式核心思路适用场景复杂度本地捕获与结构化记录每个关键节点捕获异常转成内部错误码并记录日志所有 Agent 任务的兜底能力必须要有低重试与指数退避对暂时性异常自动重试采用退避策略避免加重下游压力网络抖动、API 超时、限流、瞬时故障中CompletableFuture 异步编排恢复在并行子任务中单独处理异常失败的子任务返回降级结果多工具并行调用、多模型协同、耗时任务聚合较高三种方式不是互斥关系而是层层递进。一般建议先给 Agent 加上本地捕获与记录保证异常可观测再对远程调用增加重试机制减少临时故障的影响最后在异步编排链路上使用 CompletableFuture 的异常处理函数保障整体流程稳定。下面逐个展开。3. 环境准备与项目结构3.1 开发环境说明本文示例以 Java 环境为主适合已经接触过 Spring Boot 或普通 Java 项目的开发者。版本上我没有绑定某一个具体版本因为不同公司项目差异较大。示例代码使用 JDK 17 的语法如果你的项目还在 JDK 8需要把部分写法降级调整。话虽如此核心思路是一致的。在依赖方面为了不让示例过于臃肿我会尽量使用 JDK 自带能力只加一个日志框架和 JSON 解析库。如果你的项目是 Spring Boot 3.x可以直接复用以下依赖。3.2 Maven 依赖配置先创建一个 Maven 项目并添加必要依赖dependencies dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version2.0.9/version /dependency dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId version1.4.11/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.30/version scopeprovided/scope /dependency /dependenciesslf4j 负责日志输出Jackson 用于处理模型返回的 JSON 格式Lombok 用来简化实体类代码。实际项目中如果你的 Agent 框架已经自带日志和 JSON 能力可以去掉对应依赖。3.3 示例项目结构为了后续演示方便我按下面的结构组织代码src/main/java/com/example/agent ├── AgentApplication.java ├── agent │ ├── AgentRunner.java │ ├── ToolExecutor.java │ ├── AgentException.java │ └── RetryTemplate.java └── tool ├── WeatherTool.java └── WeatherResult.java其中AgentRunner是 Agent 执行入口ToolExecutor负责调用具体工具并处理异常RetryTemplate是通用重试组件WeatherTool模拟底层远程服务。这个结构虽然简单但和真实项目中的分层思路是一致的。4. 方式一本地捕获与结构化记录4.1 为什么先做本地捕获本地捕获是 Agent 异常处理的地基。很多 Agent 项目出问题后定位困难不是因为日志不够多而是因为每个节点上的异常没有被整理成统一格式。模型 API 超时、工具返回格式错误、数据库异常在日志里可能表现为完全不同的输出排查时像无头苍蝇。所以方式一的重点不是“捕获异常”而是“捕获后统一记录”。每个工具调用、每次模型请求、每一步状态流转都应该用统一的结构化日志输出包含任务 ID、节点名称、错误码、耗时、原始异常摘要等关键字段。4.2 自定义 Agent 异常类型在 Agent 项目里建议先定义自己的异常基类便于上层识别错误类型和错误码// 文件路径src/main/java/com/example/agent/agent/AgentException.java public class AgentException extends RuntimeException { private final String code; public AgentException(String code, String message) { super(message); this.code code; } public AgentException(String code, String message, Throwable cause) { super(message, cause); this.code code; } public String getCode() { return code; } }定义code字段的意义在于上层可以根据错误码决定是否需要重试、降级或直接终止任务。比如TOOL_TIMEOUT可以触发重试而MODEL_PARSE_ERROR可能需要修正提示词后重新调用模型。4.3 在工具执行节点捕获并记录下面模拟一个工具执行器它是 Agent 内部调用外部能力的统一入口// 文件路径src/main/java/com/example/agent/agent/ToolExecutor.java import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ToolExecutor { private static final Logger log LoggerFactory.getLogger(ToolExecutor.class); public String execute(String toolName, String param) { String requestId TraceIdHolder.get(); long startTime System.currentTimeMillis(); try { String result doInvoke(toolName, param); log.info(工具调用成功, requestId{}, tool{}, param{}, cost{}ms, requestId, toolName, param, System.currentTimeMillis() - startTime); return result; } catch (TimeoutException e) { log.warn(工具调用超时, requestId{}, tool{}, param{}, cost{}ms, requestId, toolName, param, System.currentTimeMillis() - startTime, e); throw new AgentException(TOOL_TIMEOUT, toolName 调用超时, e); } catch (Exception e) { log.error(工具调用失败, requestId{}, tool{}, param{}, cost{}ms, requestId, toolName, param, System.currentTimeMillis() - startTime, e); throw new AgentException(TOOL_ERROR, toolName 调用失败, e); } } private String doInvoke(String toolName, String param) { // 这里调用具体工具例如天气API、订单服务、数据库等 return mock_result; } }实际开发中TraceIdHolder是链路追踪 ID可以从日志上下文或 ThreadLocal 中获取。每一步工具调用都带上 requestId才能把异常定位到具体任务。4.4 本地捕获的注意事项本地捕获最容易犯的错误是“捕获后只打日志不处理”。比如 catch 之后什么都不做上层拿不到任何信息Agent 就会把上一层的结果当成正常结果继续执行。正确做法是至少做到“捕获 → 记录 → 转换错误码 → 继续抛出或返回降级结果”保证异常信息在链路上有效传递。另一个容易被忽略的点是日志级别。远程 API 超时这类暂时性错误通常用 WARN 级别就可以不必打 ERROR否则监控系统会频繁报警只有连续重试失败、或无法恢复的业务异常才应该用 ERROR 级别。5. 方式二重试与指数退避5.1 为什么需要重试Agent 项目中最常见的异常是远程调用超时或限流。这种异常有一个特点故障往往是暂时性的下一秒再调用可能就成功了。如果直接失败Agent 会终止当前任务用户只能重新发起请求而如果加上重试机制很多问题在用户无感知的情况下就能自动恢复。但重试不是越多次越好。无限制重试会给下游服务造成更大压力限流可能从“瞬时抖动”变成“持续雪崩”如果工具非幂等重试还可能导致重复下单、重复扣款等严重问题。所以重试策略必须配合退避策略和最大重试次数一起来设计。5.2 简单重试模板实现这里我实现了一个通用重试模板支持最大重试次数、初始延迟、最大延迟和重试异常类型// 文件路径src/main/java/com/example/agent/agent/RetryTemplate.java import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.function.Supplier; public class RetryTemplate { private static final Logger log LoggerFactory.getLogger(RetryTemplate.class); public static T T withRetry( SupplierT supplier, int maxAttempts, long initialDelayMs, long maxDelayMs) { int attempt 1; long delayMs initialDelayMs; while (true) { try { return supplier.get(); } catch (Exception e) { if (attempt maxAttempts) { throw e; } log.warn(执行失败, 准备第 {} 次重试, 延迟 {}ms, error{}, attempt, delayMs, e.getMessage()); sleep(delayMs); delayMs Math.min(delayMs * 2, maxDelayMs); attempt; } } } private static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException(重试等待被中断, e); } } }使用方式很简单String result RetryTemplate.withRetry( () - weatherTool.query(北京), 3, 200, 2000 );这段代码会最多尝试 3 次第一次失败后等待 200ms第二次失败后等待 400ms第三次失败则向上抛出异常。delayMs在每次失败后翻倍但不会超过maxDelayMs这就是指数退避的基本思路。5.3 指数退避与抖动真实生产环境中指数退避往往还要加入“抖动”Jitter。抖动是给重试延迟增加一个随机范围避免大量 Agent 任务在同一时刻同时重试导致下游被瞬间打满。改进后的延迟计算可以这样写long jitter ThreadLocalRandom.current().nextLong(0, delayMs); delayMs Math.min(delayMs * 2, maxDelayMs) jitter;这样每次重试的等待时间都不同下游压力会被摊开。实际项目中也可以使用现成的重试库比如 Spring Retry、Resilience4j它们对退避策略、重试次数、异常类型都做了更完整的封装。5.4 重试的边界条件重试策略需要和异常类型绑定。比如网络超时可以重试但参数校验错误、认证失败、工具不存在这类异常重试多少次都不会成功反而会浪费资源。所以更严谨的重试封装应该允许调用方传入“哪些异常才重试”的判断条件public interface RetryPredicate { boolean canRetry(Throwable throwable); }此外一定要考虑幂等。如果工具本身不具备幂等性重试前建议为每个 Agent 任务分配唯一的 RequestId工具侧根据 RequestId 做去重。这个点在支付、订单、消息发送类场景尤其重要。6. 方式三CompletableFuture 异步编排中的异常处理6.1 Agent 为什么需要异步编排Agent 任务往往包含多个独立子任务。比如用户想规划一次出行Agent 需要同时查询天气、查询航班、查询酒店然后把所有结果整合给模型做最终推荐。如果这些查询串行执行响应时间会成倍增加更好的方式是并行执行最后合并结果。这里的难点在于多个并行子任务的异常状态各不相同。如果其中一个子任务失败就让整个 CompletableFuture 链路失败其他已成功的子任务结果也白白浪费了。因此我们需要在异步编排链路中把异常处理放在子任务一级而不是整个链路的末尾。6.2 CompletableFuture 异常处理的三个函数CompletableFuture 提供了三个核心异常处理函数很多 AI Agent 开发中的异步异常问题都是围绕它们展开的。方法触发时机能否改变结果适用场景exceptionally仅发生异常时可以返回替代结果子任务失败时提供兜底值handle无论正常还是异常可以统一处理需要同时处理成功和失败结果whenComplete无论正常还是异常不可以只做观察记录日志、统计耗时不修改结果先看一个最简单的 exceptionally 示例CompletableFutureString weatherFuture CompletableFuture .supplyAsync(() - weatherTool.query(北京), executor) .exceptionally(ex - { log.error(查询天气失败, ex); return 天气信息暂时不可用; });当weatherTool.query抛出异常时exceptionally会捕获异常并返回兜底字符串如果没有异常则正常返回查询结果。这样就保证了单个子任务失败不会影响其他任务的合并。6.3 handle 与 whenComplete 的差异handle和exceptionally的区别在于handle无论前一个阶段成功还是失败都会执行而且可以通过判断Throwable参数是否为 null 来区分状态CompletableFutureString weatherFuture CompletableFuture .supplyAsync(() - weatherTool.query(北京), executor) .handle((result, ex) - { if (ex ! null) { log.error(查询天气失败, ex); return 天气信息暂时不可用; } if (result null || result.isBlank()) { return 天气信息为空; } return result; });whenComplete则更偏向“观察者”角色适合记录日志和发送监控指标。它不能改变上一阶段的结果即使你在回调里返回其他值也不会生效CompletableFutureString weatherFuture CompletableFuture .supplyAsync(() - weatherTool.query(北京), executor) .whenComplete((result, ex) - { if (ex ! null) { log.error(查询天气失败, 仅记录日志, ex); } else { log.info(查询天气成功, result{}, result); } });如果whenComplete回调本身抛出了新异常这个异常会覆盖原始异常。所以回调里尽量避免抛出异常尤其是不要把业务校验逻辑放到whenComplete中。6.4 多工具并行聚合的异常处理下面是一个多子任务并行聚合的完整示例。三个工具并行获取天气、航班和酒店信息任何一个子任务失败都返回降级文案最后合并成一个结果CompletableFutureString weatherFuture CompletableFuture .supplyAsync(() - weatherTool.query(北京), executor) .exceptionally(ex - 天气信息暂时不可用); CompletableFutureString flightFuture CompletableFuture .supplyAsync(() - flightTool.query(北京到上海), executor) .exceptionally(ex - 航班信息暂时不可用); CompletableFutureString hotelFuture CompletableFuture .supplyAsync(() - hotelTool.query(上海酒店), executor) .exceptionally(ex - 酒店信息暂时不可用); CompletableFutureString resultFuture weatherFuture .thenCombine(flightFuture, (w, f) - w \n f) .thenCombine(hotelFuture, (total, h) - total \n h); String finalResult resultFuture.join();这段代码的核心思路是每个子任务都用自己的exceptionally处理异常避免异常在合并阶段才暴露。这样即使某个服务挂了Agent 仍然能返回一个部分结果体验比直接报错好很多。6.5 线程池与资源隔离线上 Agent 项目中CompletableFuture.supplyAsync不建议直接使用默认的 ForkJoinPool因为公共线程池一旦被阻塞任务占满整个 JVM 的异步任务都会受影响。最好创建独立线程池并做资源隔离ExecutorService agentExecutor new ThreadPoolExecutor( 4, 8, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(100), new ThreadFactoryBuilder().setNameFormat(agent-task-%d).build(), new ThreadPoolExecutor.CallerRunsPolicy() );线程池的核心线程数、队列大小、拒绝策略需要根据你的 Agent 任务量来调整。CallerRunsPolicy可以在队列满时由调用线程执行任务避免直接丢弃任务但也可能导致调用线程阻塞需要结合场景权衡。7. 完整实战模拟 Agent 调用天气工具7.1 功能目标现在我们把前面三种方式组合起来实现一个模拟 Agent用户输入“查询北京今天的天气并判断是否适合出行”Agent 调用天气工具工具以一定概率失败整个流程包含捕获记录、重试降级、并行等能力。这个示例足够简单但能完整展示三种异常处理方式如何协同工作。7.2 模拟天气工具先定义一个模拟天气工具让它有 40% 的概率抛出超时异常模拟真实网络抖动// 文件路径src/main/java/com/example/agent/tool/WeatherTool.java import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class WeatherTool { private static final Logger log LoggerFactory.getLogger(WeatherTool.class); public String query(String city) { long start System.currentTimeMillis(); // 模拟40%概率的超时异常 if (Math.random() 0.4) { throw new RuntimeException(模拟天气API超时); } String result 北京 晴 18~26℃ 微风; log.info(天气查询成功, city{}, result{}, cost{}ms, city, result, System.currentTimeMillis() - start); return result; } }这里把异常直接抛出来是为了让上层测试重试和降级逻辑。真实项目中工具内部可以完成自己的日志记录。7.3 Agent 执行器代码有了异常类型、重试模板和工具之后编写 Agent 执行器的核心逻辑。它的处理顺序是先记录调用上下文然后使用重试模板调用天气工具如果重试仍然失败降级返回固定文案最后将结果交给大模型生成回答。为了简洁这里用字符串拼接代替大模型// 文件路径src/main/java/com/example/agent/agent/AgentRunner.java import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AgentRunner { private static final Logger log LoggerFactory.getLogger(AgentRunner.class); private final WeatherTool weatherTool; public AgentRunner(WeatherTool weatherTool) { this.weatherTool weatherTool; } public String run(String userTask) { log.info(Agent 开始处理任务, task{}, userTask); String officialResult; try { officialResult loadWeatherWithRetry(北京); } catch (Exception e) { log.error(天气查询最终失败, 进入降级处理, task{}, userTask, e); officialResult 天气信息暂时不可用建议稍后查询; } // 实际项目中这里会把工具结果交给模型生成最终回答 String answer 根据当前天气信息 officialResult 为您生成出行建议。; log.info(Agent 任务处理完成, task{}, answer{}, userTask, answer); return answer; } private String loadWeatherWithRetry(String city) { return RetryTemplate.withRetry( () - weatherTool.query(city), 3, 200, 2000 ); } }在loadWeatherWithRetry中我们使用了第 5 节的重试模板在run方法的 catch 中我们使用了第 4 节的本地捕获与降级思路。到这里方式一和方式二已经完整落地。7.4 使用 CompletableFuture 并行处理多个工具前面示例只有一个工具无法体现 CompletableFuture 的优势。我们把场景扩展一下同时查询天气和城市限行信息两个任务并行执行单个任务失败不影响整体// 文件路径src/main/java/com/example/agent/agent/AgentRunner.java 追加方法 public String runParallel(String userTask) { ExecutorService executor Executors.newFixedThreadPool(4); CompletableFutureString weatherFuture CompletableFuture .supplyAsync(() - loadWeatherWithRetry(北京), executor) .exceptionally(ex - 天气信息暂时不可用); CompletableFutureString travelFuture CompletableFuture .supplyAsync(() - loadTravelLimit(北京), executor) .exceptionally(ex - 限行信息暂时不可用); String weather weatherFuture.join(); String travel travelFuture.join(); String answer 天气 weather 限行 travel; log.info(并行任务处理完成, answer{}, answer); executor.shutdown(); return answer; } private String loadTravelLimit(String city) { // 模拟限行查询 if (Math.random() 0.3) { throw new RuntimeException(限行查询超时); } return 今日限行尾号 3 和 8; }这段代码体现了方式三的典型用法。两个并行子任务都用exceptionally做了兜底后续可以在异步线程中完成合并也可以像示例里一样用join()获取结果后在主线程加工。7.5 主入口与运行结果最后编写一个可运行的主函数// 文件路径src/main/java/com/example/agent/AgentApplication.java public class AgentApplication { public static void main(String[] args) { WeatherTool weatherTool new WeatherTool(); AgentRunner runner new AgentRunner(weatherTool); String answer runner.run(查询北京今天的天气并判断是否适合出行); System.out.println(answer); String parallelAnswer runner.runParallel(查询北京天气和限行信息); System.out.println(parallelAnswer); } }多次运行程序时由于工具调用带了随机失败概率你可能会看到两种结果天气信息暂时不可用建议稍后查询或者根据当前天气信息北京 晴 18~26℃ 微风为您生成出行建议。出现第一种结果说明三次重试都失败了被降级逻辑兜住第二种结果说明重试成功或者第一次调用就成功。无论哪种情况Agent 进程都不会因为远程工具异常而崩溃这就是三种异常处理方式共同作用的效果。8. 常见问题与排查思路Agent 异常处理在落地过程中经常会遇到一些看起来奇怪的现象。下面整理几个高频问题如果你在项目中遇到类似报错可以参考排查。问题现象常见原因解决思路Agent 多次重复调用同一个工具重试机制没有考虑幂等性为每次任务生成 RequestId工具侧做幂等去重异步任务异常被静默吞掉CompletableFuture 回调中没有记录日志或者异常处理函数返回 null使用 exceptionally 统一兜底并在回调中输出完整异常日志模型返回内容无法解析系统提示词约束不够严格输出被截断或包含解释性文字增加输出格式校验解析失败后触发一次重试或要求模型重新输出出现 execution provider did not respond in time 类似报错Agent 执行器或模型 API 响应超时先调整超时时间再配置重试与降级策略最后检查线程池是否被占满线程池耗尽任务全部阻塞所有 Agent 任务共用默认 ForkJoinPool改为独立线程池配置合理队列和拒绝策略捕获异常后上层拿不到信息捕获后只记录日志没有重新抛出或转换成自定义异常使用 AgentException 包装原始异常并设置错误码关于“execution provider did not respond in time”这类报错很多 Agent 框架在底层执行器超过一定时间没有响应时就会抛出。它不一定代表模型服务挂了更可能是超时时间设置过短、上下文过长导致推理变慢或者线程池资源被耗尽。排查顺序建议是先看任务执行耗时再看模型 API 响应日志最后检查线程池活跃线程数。9. 最佳实践与工程建议9.1 异常要分层处理Agent 异常不能所有逻辑都堆在一个类里。建议按“工具层 → 服务层 → Agent 编排层”分层处理工具层负责捕获底层网络、数据库、API 异常记录明细日志抛出自定义 AgentException。服务层负责重试、降级、幂等去重决定哪些异常需要重试哪些直接返回兜底值。Agent 编排层负责整体状态流转汇总子任务结果记录最终日志。这样分层以后每一层的职责都清晰排查问题时也能快速定位。9.2 日志必须带上链路信息Agent 项目比普通接口复杂得多一次任务可能触发多个工具调用和多轮模型推理。日志如果只记录 message基本上没法串联整条链路。建议每个任务都生成全局唯一的 requestId 或 traceId并把它放到线程上下文和日志格式中。在并行任务中还要注意 ThreadLocal 的传递问题。CompletableFuture.supplyAsync会在其他线程执行默认情况下父线程的 ThreadLocal 无法直接透传。可以使用自定义线程池在提交任务时手动保存并还原上下文。9.3 重试策略要结合幂等与限流重试是提升 Agent 成功率的重要手段但不是万能的。明确的业务校验错误、鉴权失败、非法参数不应该重试网络超时、5xx 错误、限流错误可以重试但重试前必须判断工具是否幂等。如果是非幂等工具建议改用“去重重试”也就是带上唯一业务键让下游判断是否已经处理过。9.4 降级文案要面向用户友好当 Agent 所有恢复手段都失败时最终返回给用户的降级文案不要暴露异常堆栈和内部错误码。不要让用户看到“NullPointerException”或者“Tool call timeout”这类信息。正确做法是返回“天气信息暂时不可用建议稍后再试”同时把详细异常记录到日志和监控系统里。9.5 建立异常监控与指标异常处理做得再好如果不可观测线上依然会失控。建议至少监控以下指标Agent 总调用量、任务成功率、重试触发次数、重试成功率、降级触发次数、平均耗时和 P99 耗时。这些指标可以帮助你判断当前异常处理策略是否需要调整比如重试次数太少导致成功率低或降级触发过于频繁说明下游服务不稳定。10. 总结Agent 开发中异常处理不是简单的 try-catch而是一套从“捕获记录”到“自动恢复”再到“异步降级”的策略组合。本地捕获与结构化记录解决的是可观测性问题重试与指数退避解决的是暂时性故障问题CompletableFuture 的异常处理函数解决的是并行编排中的故障隔离问题。三种方式可以单独使用也可以像本文实战示例那样组合落地。如果你正在开发 Agent 相关项目建议先把这三种方式沉淀成公共组件再接入自己的业务工具这样后续扩展新的 Agent 能力时异常处理逻辑就可以直接复用项目稳定性也会明显提升。