Java Agent 异常处理的可选性:从 Optional 到 CompletableFuture 的降级策略实践
如果把 Agent 的异常处理写成传统的try-catch,大概率会在第一个真实故障面前失灵。这不是危言耸听,而是 Agent 应用与传统后端服务的本质差异决定的:Agent 的执行结果不确定、调用链不固定、外部依赖多,而且它并不是每一次失败都值得“抛出异常”。同样是一次超时,有时应该重试,有时应该降级,有时应该返回部分结果,有时应该直接快速失败。这个“根据场景选择不同异常策略”的能力,就是我们说的异常处理的可选性。
这篇文章不打算只讲概念,而是把 Java 环境下实现 Agent 可选异常处理的几种常见方式拆开讲清楚:从 Optional/Result 的显式控制,到 CompletableFuture 的异步恢复,再到策略化的降级设计。你还会看到一个可以运行的完整示例,以及一套可以直接抄进项目的排查和最佳实践清单。
如果你正在做 Agent 开发、异步任务编排,或者正在排查“provider 超时未响应”“Agent 执行失败导致整条链路卡死”这类问题,这篇文章应该能帮上忙。
1. 为什么 Agent 异常处理不能只靠 try-catch
很多开发者对异常处理的理解还停留在“能捕就捕,捕不到就往上层扔”。这个思路在普通接口开发里没有问题,但在 Agent 场景里至少会遇到三个新的麻烦。
第一,Agent 的失败不一定是程序的 Bug。模型接口超时、工具服务返回 5xx、上下文过长被截断、上游限流,这些是运行常态,不是意料之外。传统 try-catch 默认把异常理解为“不应该发生的事件”,而 Agent 里恰恰相反,异常是常态的一部分。如果你把每次超时都当成重大事故来抛,系统会处于持续告警和反复重试的失控状态。
第二,Agent 的异常处理没有统一答案。同样一次“工具调用失败”,发生在用户咨询天气时可以直接返回“暂时查不到”,发生在自动化下单流程里就应该中止而不是猜一个结果。也就是说,处理策略必须可选、可配置、可替换,而不是把所有异常都塞进同一个 catch 块。
第三,Agent 调用链往往是异步和编排式的。Java 中常见的 CompletableFuture、响应式流、Agent 框架的任务编排,会让异常出现在完全不同的线程和阶段。某个子任务失败,不代表整条链路必须失败。我们需要的是“部分成功、局部补偿、选择性降级”的能力,而不是简单粗暴地 throw。
所以,Agent 异常处理的核心不是“捕获”,而是决策。可选性就是在异常发生后,系统能根据场景选择最合适的策略。
2. 异常处理的可选性到底是什么
“可选性”这个词听起来抽象,放进代码里就很具体。它至少包含三层含义。
第一层是处理方式可选。对于一个失败,系统可以选择重试、忽略、降级、终止、转人工、返回默认值,等等。不同异常类型、不同业务状态,可以选择不同策略。
第二层是处理粒度可选。可以针对整个 Agent 任务做处理,也可以细化到单次模型调用、单次工具调用、单步推理。粒度越细,系统的容错能力越强,但实现成本也越高。
第三层是策略本身可配置。生产环境中,运维和研发往往需要在不改代码的情况下调整超时时间、重试次数、降级开关。可选性的另一个面向,是把异常策略变成配置而不是硬编码逻辑。
我们用一张表对比传统异常处理和 Agent 可选异常处理:
| 维度 | 传统 try-catch | Agent 可选异常处理 |
|---|---|---|
| 异常定位 | 认为是 Bug | 认为是运行常态 |
| 处理方式 | 捕获、包装、抛出 | 选择、恢复、降级、终止 |
| 失败粒度 | 方法或接口 | 子任务、工具调用、模型调用 |
| 上下文依赖 | 较弱 | 强,依赖业务场景与执行进度 |
| 可配置性 | 低 | 高 |
| 代表结构 | try-catch-finally | Optional、Result、重试策略、降级策略 |
这张表想说明一个判断:“可选性”不是另一种语法,而是把异常处理从“语法层”提升到“策略层”。理解了这一层,再去看代码实现,思路就会清晰很多。
3. Java 中合法的异常处理结构:先打好语法地基
在进入 Agent 场景之前,值得先把 Java 本身提供的异常处理结构梳理一遍。很多 Agent 框架的异常处理底层就是这些基础语法,只是外面包了一层策略。
3.1 try-catch-finally
最基础的结构。finally 块保证资源清理或必要收尾工作执行,即使异常被抛出或捕获。
try { String result = agentClient.chat(prompt); return result; } catch (AgentTimeoutException e) { log.warn("agent timeout, use fallback"); return fallback(prompt); } catch (AgentExecutionException e) { log.error("agent execution failed", e); throw new BizException("AGENT_EXEC_ERROR", e); } finally { trace.endSpan(); }这是 Java 中最合法的异常处理结构之一,但它的问题也很明显:一旦异常场景变多,catch 块会逐渐膨胀,而且策略是写死的。
3.2 多异常捕获
Java 7 开始支持|合并多个异常类型。适合异常处理逻辑相同的情况。
catch (AgentTimeoutException | RateLimitException e) { // 都当作可重试异常处理 return retryOrFallback(prompt, e); }3.3 try-with-resources
适合需要自动关闭资源的场景。Agent 开发中比较典型的场景是 HTTP 客户端、数据库连接、文件会话。
try (AgentSession session = new AgentSession(userId)) { return session.run(prompt); }这个结构不仅合法,而且避免了手动关闭资源漏写的问题。Agent SDK 如果提供了 AutoCloseable 的会话对象,推荐优先使用。
3.4 方法声明 throws
把异常传递给调用方,由上层决定策略。适合“当前层无法判断业务语义”的情况。
public AgentResult run(AgentRequest request) throws AgentExecutionException { // 具体执行逻辑 }需要说明的是,throws不是逃避处理,而是在架构上把决策权上移。这在 Agent 编排中非常常见:底层工具不知道上层业务期望什么,上层才清楚该重试还是放弃。
这些基础语法,任何一种 Java 应用都绕不开。但在 Agent 场景中,我们还需要两个额外的能力:用返回值表达失败,以及在异步任务中恢复。
4. 用 Optional 和 Result 把异常变成可选值
Java 8 引入Optional后,很多开发者开始用它表达“可能没有值”。但 Optional 在异常处理中的价值往往被低估:它让“异常”从控制流变成数据流。
考虑一个最简单的场景:从工具服务读取配置,读取失败时返回空配置,而不是抛异常。
public Optional<AgentConfig> loadConfig(String agentId) { try { AgentConfig config = configClient.get(agentId); return Optional.ofNullable(config); } catch (IOException e) { log.warn("load config failed, agentId={}, fallback to empty", agentId, e); return Optional.empty(); } }调用方可以这样使用:
AgentConfig config = loadConfig(agentId) .orElseGet(() -> defaultConfig(agentId));这种写法的好处是:失败不再中断主流程,而是变成调用方可以选用的空值。Optional 让异常可以是“可选的”。
不过 Optional 有一个局限:它只能表达“有或没有”,无法表达“为什么没有”。如果下游故障的排查需要错误原因,Optional 就不够用。这时更推荐引入一个轻量级的 Result 类型。
public sealed interface Result<T> { record Success<T>(T value) implements Result<T> {} record Failure<T>(String code, String message, Throwable cause) implements Result<T> {} }这个结构在 Java 21 下可以正常编译。如果你的项目还停留在 Java 8,也可以用传统 abstract class 或直接复用第三方库里的 Either 类型。
使用示例:
public Result<AgentResponse> execute(AgentTask task) { try { AgentResponse response = agentExecutor.run(task); return new Result.Success<>(response); } catch (AgentTimeoutException e) { return new Result.Failure<>("TIMEOUT", "agent execute timeout", e); } catch (Exception e) { return new Result.Failure<>("UNKNOWN", e.getMessage(), e); } }调用方可以根据 Result 的情况选择策略:
Result<AgentResponse> result = execute(task); if (result instanceof Result.Success<AgentResponse> success) { return success.value(); } Result.Failure<AgentResponse> failure = (Result.Failure<AgentResponse>) result; if ("TIMEOUT".equals(failure.code())) { return retryLater(task); } return fallback(task);看到这里你可能会问:这不就是把异常换成返回值吗?是的。但关键在于,返回值天然支持多个分支决策,而且不会像异常那样打断栈帧语义。对于 Agent 这种“大部分子任务失败可以被局部补偿”的场景,用返回值表达失败往往比异常更合适。
5. CompletableFuture 异步编程中的异常处理与可选恢复
Agent 开发中,异步编排是绕不开的。CompletableFuture 提供了几个关键的异常处理方法:
exceptionally:为异常提供一个恢复值。handle:不管成功还是失败,都返回一个新值。whenComplete:感知结果但不改变结果。completeExceptionally:手动让 future 以异常完成。
其中exceptionally是“可选异常处理”最直接的体现:它把异常转换为一个可选的替代结果。
下面这段代码模拟了一个 Agent 并发调用两个模型的场景,一个失败时用另一个的结果兜底。
import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; public class AgentAsyncDemo { public static void main(String[] args) { CompletableFuture<String> modelA = CompletableFuture .supplyAsync(() -> callModel("model-a")) .exceptionally(ex -> { System.out.println("model-a 调用失败: " + ex.getMessage()); return null; }); CompletableFuture<String> modelB = CompletableFuture .supplyAsync(() -> callModel("model-b")) .exceptionally(ex -> { System.out.println("model-b 调用失败: " + ex.getMessage()); return null; }); CompletableFuture<String> result = modelA .thenCombine(modelB, (a, b) -> { if (a != null) { return "A: " + a; } if (b != null) { return "B: " + b; } throw new IllegalStateException("两个模型都失败了"); }) .exceptionally(ex -> "fallback: " + ex.getMessage()); System.out.println(result.join()); } private static String callModel(String modelName) { if (modelName.equals("model-a")) { throw new RuntimeException("timeout"); } return "ok from " + modelName; } }运行结果大致如下:
model-a 调用失败: java.lang.RuntimeException: timeout A: null这里我们要注意一个容易踩的坑:exceptionally返回null之后,后续thenCombine里确实可以通过判空来降级,但如果你在thenCombine中抛出了新异常,外层还需要再包一层exceptionally。也就是说,异步异常处理的每个阶段都可以重新产生失败,可选恢复不是一个终点,而是一条链。
更稳妥的做法是,用一个单独的handle来统一处理成功和失败:
CompletableFuture<String> safeResult = modelA .handle((value, ex) -> { if (ex != null) { log.warn("model-a failed", ex); return modelB.join(); } return value; });在 Agent 引擎开发中,超时控制往往比异常捕获更关键。一个常见错误写法是单方面依赖下游 SDK 的 timeout 参数,但整个编排链路的超时没有兜底。下面这段代码演示了为 future 设置超时并返回默认值:
CompletableFuture<String> future = CompletableFuture .supplyAsync(() -> callModel("model-a")); String result = future .completeOnTimeout("default-result", 5, TimeUnit.SECONDS) .join();Java 9 引入的completeOnTimeout让“超时即可选降级”变得很简洁。如果你的项目还在 Java 8,需要换成orTimeout加exceptionally:
String result = future .orTimeout(5, TimeUnit.SECONDS) .exceptionally(ex -> "default-result") .join();这两种写法都是合法的 Java 异步异常处理手段。关键不在于选哪个 API,而在于:每个异步步骤都要有一个明确的、可选的失败出口。
6. 环境准备与前置条件
如果想把上面的示例跑起来,建议准备以下环境:
- JDK 17 或更高版本,示例中使用了 Java 21 的 sealed interface 语法,如果使用 JDK 17,可以把 record 和 sealed 简单改成普通 class。
- Maven 3.8+ 或 Gradle 7+。
- Spring Boot 3.x,用于演示配置文件与策略注入。不强制,但本文的完整示例用 Spring Boot 承载。
- 一个可用的 Agent 执行器,可以是自研实现,也可以封装第三方 SDK。如果只是验证异常处理逻辑,可以直接使用 Mock 数据代替真实模型调用。
版本方面,以实际项目当前依赖为准。本文核心是通用思路,不依赖某个具体 Agent 框架的内部 API。
Maven 依赖建议:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>如果你确实在集成某个 Agent 框架,再自行补充对应 SDK 依赖。下面是完整示例的核心代码。
7. 完整示例:一个具有可选异常处理能力的 Agent 执行器
我把上面的概念合并成一个可运行的最小工程。目标是实现一个具备以下能力的 Agent 执行器:
- 模型调用超时可选处理。
- 工具调用失败可选降级。
- 重试次数可配置。
- 失败后可以选择返回默认结果、抛出异常或进入人工队列。
这里为了演示,我模拟了一个 Agent 执行过程:第一步调用模型,第二步调用一个工具接口,任意一步失败时进入策略选择。
7.1 项目结构
agent-demo/ ├── pom.xml └── src/main/java/com/example/agentdemo/ ├── AgentDemoApplication.java ├── core/ │ ├── Result.java │ ├── AgentExecutorService.java │ └── FallbackStrategy.java └── web/ └── AgentController.java7.2 定义 Result 类型
package com.example.agentdemo.core; public sealed interface Result<T> { record Success<T>(T value) implements Result<T> { } record Failure<T>(String code, String message, Throwable cause) implements Result<T> { } static <T> Result<T> ok(T value) { return new Success<>(value); } static <T> Result<T> fail(String code, String message, Throwable cause) { return new Failure<>(code, message, cause); } }这个类型是“可选异常”的载体。Agent 执行过程中的任何异常都可以先包装成 Result,再由上层决定如何处理。
7.3 定义可选处理策略
package com.example.agentdemo.core; @FunctionalInterface public interface FallbackStrategy<T> { T apply(Result.Failure<T> failure); }这个接口允许调用方传入不同的降级函数。在实际工程中,你可以为它提供多个实现,例如:
- 返回默认值。
- 重新执行一次简化流程。
- 写入人工处理队列。
- 抛出业务异常。
7.4 核心 Agent 执行服务
package com.example.agentdemo.core; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Service public class AgentExecutorService { private static final Logger log = LoggerFactory.getLogger(AgentExecutorService.class); private final ExecutorService executor = Executors.newCachedThreadPool(); public <T> Result<T> executeWithFallback( AgentTask<T> task, Duration timeout, FallbackStrategy<T> fallbackStrategy ) { CompletableFuture<Result<T>> future = CompletableFuture .supplyAsync(task::run, executor) .handle((value, ex) -> { if (ex != null) { log.warn("agent task failed, ex={}", ex.getMessage()); return Result.<T>fail("TASK_FAILED", ex.getMessage(), ex); } return Result.ok(value); }); try { Result<T> result = future.get(timeout.toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS); if (result instanceof Result.Success<T> success) { return Result.ok(success.value()); } Result.Failure<T> failure = (Result.Failure<T>) result; if (fallbackStrategy != null) { T fallbackValue = fallbackStrategy.apply(failure); return Result.ok(fallbackValue); } return failure; } catch (java.util.concurrent.TimeoutException e) { future.cancel(true); log.warn("agent task timeout after {} ms", timeout.toMillis()); if (fallbackStrategy != null) { Result.Failure<T> failure = Result.fail("TIMEOUT", "agent task timeout", e); return Result.ok(fallbackStrategy.apply(failure)); } return Result.fail("TIMEOUT", "agent task timeout", e); } catch (Exception e) { log.error("agent task interrupted", e); return Result.fail("INTERRUPTED", e.getMessage(), e); } } @FunctionalInterface public interface AgentTask<T> { T run(); } }这个服务的关键点在于:
handle把同步异常转换成了 Result。future.get(timeout)实现了整体超时控制。fallbackStrategy让调用方传入不同的失败恢复策略,这就是“可选性”。
7.5 使用示例:模拟模型调用超时
package com.example.agentdemo.web; import com.example.agentdemo.core.AgentExecutorService; import com.example.agentdemo.core.Result; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.Duration; @RestController @RequestMapping("/agent") public class AgentController { private final AgentExecutorService executorService; public AgentController(AgentExecutorService executorService) { this.executorService = executorService; } @GetMapping("/run") public String run() { AgentExecutorService.AgentTask<String> task = () -> { // 模拟模型调用:有一定概率超时或失败 if (System.currentTimeMillis() % 3 == 0) { throw new RuntimeException("model provider not respond in time"); } return "task completed"; }; // 可选策略1:失败时返回默认值 Result<String> result = executorService.executeWithFallback( task, Duration.ofSeconds(2), failure -> "fallback-default" ); // 可选策略2:失败时返回错误码,不抛异常 if (result instanceof Result.Failure<String> failure) { return "error code: " + failure.code(); } return result.value(); } }7.6 配置示例
Spring Boot 中可以把超时时间、重试次数等参数配置化:
agent: execution: timeout-ms: 3000 max-retry: 2 fallback-enabled: true fallback-value: "sorry, please try again later"@ConfigurationProperties(prefix = "agent.execution") @Component public class AgentExecutionProperties { private long timeoutMs = 3000; private int maxRetry = 2; private boolean fallbackEnabled = true; private String fallbackValue = "sorry"; // getter/setter 省略 }这样,选择哪种异常处理策略,从代码层上浮到了配置层。线上修改降级开关时,不需要重新发布应用,这也是可选性在工程层面的重要体现。
8. 运行结果与效果验证
启动 Spring Boot 应用后,访问:
http://localhost:8080/agent/run多次刷新,因为每次调用有三分之一的概率抛出“model provider not respond in time”,你会看到两种输出:
task completed或
fallback-default判断成功的标准很简单:
- 应用进程不崩溃。
- 超时或异常不会导致接口 500。
- 返回值要么是真实结果,要么是 fallback 值。
- 日志中能看到
agent task failed或agent task timeout的告警信息。
如果想验证失败策略为“抛出业务异常”的场景,可以在 FallbackStrategy 中直接抛异常:
Result<String> result = executorService.executeWithFallback( task, Duration.ofSeconds(2), failure -> { throw new BizException("AGENT_UNAVAILABLE", failure.message()); } );这样接口会以业务异常终止,而不是静默降级。可选性意味着你有权选择“不降级”。
如果运行失败,建议按以下顺序排查:
- 检查端口是否被占用。
- 检查 Spring Boot 是否成功启动。
- 检查是否添加了
@ConfigurationProperties的依赖支持,如果缺失,可以使用@Value代替。 - 检查 JDK 版本是否支持 sealed interface,如果不支持,把
Result改成普通 interface 或 class。
9. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 接口长时间不返回 | 未设置整体超时 | 查看线程栈,确认阻塞点 | 使用 future.get(timeout) 统一超时 |
| 模型调用失败后仍继续执行 | 缺少失败状态检查 | 查看日志,确认 Result 是否被忽略 | 在关键步骤强制检查 Result |
| 重试风暴,导致下游被打挂 | 重试策略不加限制 | 查看调用链和重试日志 | 限制重试次数,启用退避策略 |
| 降级后返回错误结果 | fallback 逻辑与业务语义不匹配 | 检查降级值来源 | 为不同场景配置不同 fallback |
| 本地运行正常,线上超时频繁 | 线上延迟更大 | 对比两端耗时分布 | 调大超时时间,或改为异步回调 |
| 异常被吞掉,无法定位 | 只在 handle 中打印 message | 检查完整堆栈日志 | 记录 cause 与 context 信息 |
| 多个可选策略相互冲突 | 策略优先级不明确 | 梳理代码分支 | 用配置中心统一控制策略开关 |
排查 Agent 异常问题,第一原则是把日志和状态记录下来,而不是先改代码。很多看似复杂的故障,日志里其实已经给出了答案。
10. 最佳实践与工程建议
结合前面这些示例,我在实际项目中会坚持以下几件事。
第一,优先用返回值表达业务可预期失败,用异常表达不可预期失败。模型超时、工具返回空、上游限流,这些应该走 Result 或 Optional 分支;代码 Bug、配置错误、序列化异常,才应该抛出异常。这个边界决定了整个系统的容错质量。
第二,每个异步任务都要有超时兜底。CompletableFuture 的orTimeout、completeOnTimeout,或者显式get(timeout),至少选一个。没有超时的异步编排,在 Agent 场景里就像没有保险丝的电路。
第三,把异常处理策略配置化。超时毫秒数、重试次数、fallback 开关、人工兜底开关,尽可能放到配置中心。Agent 系统的运行环境变化很快,今天适用的策略,下周可能就不适用了。
第四,为失败设计可观测性。在每次异常处理时记录:失败了哪一步、选择了什么策略、消耗了多少重试次数、降级结果是否命中。这些数据能为后续调优提供依据。
第五,注意幂等性。Agent 的重试可能造成重复消息或重复操作。建议在多次重试之间使用幂等键,尤其是涉及支付、下单、发送消息等场景时,不要假设重试只会发生一次。
第六,安全边界不能因为降级而放松。异常处理的可选性不意味着你可以随意绕过权限校验。模型调用失败后的 fallback 如果接入了非预期系统,必须走同样的授权流程。
11. 总结与后续学习方向
这篇文章围绕“Agent 异常处理之可选性”展开,核心判断是:Agent 的异常处理不应该只有“捕获并抛出”一条路,而应该是一套可选的策略集合。理解 Optional 和 Result 如何把异常变成值,理解 CompletableFuture 如何在异步链路中恢复,再配合一个可配置的降级入口,基本就能支撑大多数 Agent 工程需求。
如果你正在做 Agent 框架的二次开发,接下来值得继续深入的方向包括:重试退避算法的选取、分布式链路追踪在 Agent 调用链里的落地、熔断器模式在工具调用层的应用,以及如何把 LLM 返回格式错误纳入异常处理体系。
先把手头的最小示例跑通,再逐步把策略接入真实业务,这个顺序比直接抄一个大而全的框架代码要稳妥得多。建议收藏备用,后面踩坑的时候可以再翻回来对照。
