Agent 应用的异常处理和普通接口的异常处理不是一回事。一个 Agent 调用链路上可能同时出现模型超时、工具执行失败、返回格式解析错误、上下文超限、异步任务中断等问题如果只在入口写一个统一的 try-catch很难把异常恢复、重试、兜底消息这些逻辑放到正确的位置。这篇内容会围绕 Agent 异常处理的三种方式展开同步调用中的显式异常处理、异步编排中的 CompletableFuture 异常回调、以及 Agent 执行器层面的超时、重试和兜底。理解这三种方式的边界之后再去看具体的 Agent 框架异常处理 API 的作用就很容易对号入座。1. 先把 Agent 异常分成三份才能设计处理策略1.1 Agent 运行链路的异常来源一个典型的 Agent 任务执行链路通常包含四类节点接收用户输入、规划下一步动作、调用模型、执行工具。每一类节点都有各自的失败方式。模型调用失败最典型。可能是模型服务超时、限流、上下文超长也可能是返回内容不符合 JSON 结构导致解析失败。工具执行失败也很常见比如 HTTP 接口返回 500、数据库连接超时、文件路径不存在、权限不足。另外还有 Agent 框架自身的问题例如规划器循环次数过多、运行器超过执行时长、异步任务被中断或者父子任务之间异常传播断裂。这些异常如果混在一起处理会出现两个问题。第一个问题是定位困难日志里只有一个外层异常用户无法知道是模型超时还是工具失败。第二个问题是恢复策略无法差异化重试一个不可重试的解析错误没有意义对限流错误不重试又会白白浪费可用额度。所以在设计异常处理之前先要把异常分类。按照“在哪里抛出、由哪一层处理”来划分比按照“对用户展示什么”来划分更实用。1.2 三种处理方式的职责边界Agent 异常处理的三种方式并不是三个等价方案而是三个层次。第一层是同步调用中的显式异常处理。在代码中直接调用模型接口、工具方法、解析函数时使用 try-catch、自定义异常、异常包装等手段把原始异常转换成语义清晰的业务异常。这一层解决的问题是“这个异常是什么、出在哪一步”。第二层是异步编排中的异常处理。Agent 任务经常并行执行多个工具或者把大任务拆成多个子任务这就会用到 CompletableFuture 或类似的异步编程模型。此时异常不会主动出现在当前线程而是挂在 Future 链上需要用 exceptionally、handle、whenComplete 等方法处理。这一层解决的问题是“异步失败后如何恢复、如何传递、如何感知”。第三层是 Agent 执行器层面的保护。无论同步还是异步最终都需要一个运行器把任务整体跑起来。执行器负责设置总超时、重试策略、熔断降级、兜底回复。这一层解决的问题是“任务整体卡死或失败时如何给用户一个确定结果”。三者不是替代关系。只有第一层异步失败会漏掉只有第二层同步入口的异常会污染外层逻辑只有第三层具体错误信息会丢失。生产环境通常是三层叠加使用。1.3 三种方式与异常类型的对应关系异常处理方式针对场景典型异常处理目标同步显式异常处理模型调用、工具调用、解析过程格式错误、连接异常、状态码错误明确异常语义保留现场CompletableFuture 异常处理并行工具、异步子任务、结果合并执行异常、取消异常、超时恢复默认值、传播异常、记录结果执行器层保护整个 Agent 任务总超时、循环过深、不可恢复错误限时、重试、熔断、兜底可以先在纸上把你项目里的 Agent 调用链路画出来标出哪些节点是同步方法哪些节点是异步任务哪个对象是总执行器。标完之后三种方式的落点自然就清楚了。2. 方式一用显式异常处理管住同步调用2.1 在模型调用点捕获异常最简单的 Agent 流程是用户输入后直接调用一次模型然后把结果返回。此时模型调用是同步方法异常可以用 try-catch 处理。下面是一段通用示意代码不绑定某个具体 Agent 框架。在实际项目中把modelClient.chat(...)换成你所使用的框架 API 即可。public class AgentService { private final ModelClient modelClient; public AgentService(ModelClient modelClient) { this.modelClient modelClient; } public String chat(String userInput) { try { // 调用模型返回可能是普通文本也可能是结构化 JSON String raw modelClient.chat(buildPrompt(userInput)); return parseResult(raw); } catch (ModelTimeoutException e) { // 超时通常可以重试也可以直接降级 return 当前模型响应较慢请稍后重试。; } catch (ModelParseException e) { // 返回内容无法解析重复尝试大概率还是失败 log.error(模型返回内容解析失败: {}, rawLog(e)); return 模型返回内容无法理解已记录日志。; } catch (Exception e) { // 最后兜底避免异常穿透到接口层 log.error(Agent 调用失败, e); return 服务暂时不可用请稍后再试。; } } }这段代码有几个关键点。第一catch (ModelTimeoutException e)和catch (ModelParseException e)必须放在catch (Exception e)前面因为子类异常先捕获父类最后兜底。第二虽然这是一个同步方法但异常类型必须由模型客户端或者 Agent 框架明确抛出否则catch永远接不到对应分支。第三兜底返回的字符串要分场景设计超时提示、解析失败提示、系统异常提示不能完全一样否则用户无法判断是自己输入问题还是系统问题。2.2 用自定义异常包装工具调用错误Agent 执行工具时异常信息往往包含底层细节比如第三方接口的原始报文、HTTP 状态码、超时时间。直接把这些内容抛给上层既不安全也不方便做策略判断。推荐的做法是定义少量业务异常在工具执行处做一次包装。public class AgentToolExecutionException extends RuntimeException { private final String toolName; private final String action; // 例如 QUERY_ORDER、CALL_API private final boolean retryable; public AgentToolExecutionException(String toolName, String action, boolean retryable, String message, Throwable cause) { super(message, cause); this.toolName toolName; this.action action; this.retryable retryable; } public boolean isRetryable() { return retryable; } public String toolName() { return toolName; } }这里最重要的不是异常类的数量而是retryable这个标记。同样是工具失败网络超时可能是可重试的参数校验失败是不可重试的。把是否可重试放在异常类上后续做重试策略时就不需要去解析字符串也不需要用“异常类型名是否包含 Timeout”这种脆弱判断。包装时要注意保留原始异常。比如下面的写法。public String callExternalApi(String param) { try { return httpClient.post(/api/query, param); } catch (SocketTimeoutException e) { throw new AgentToolExecutionException( externalApi, QUERY, true, 外部接口查询超时, e ); } catch (IllegalArgumentException e) { throw new AgentToolExecutionException( externalApi, QUERY, false, 外部接口参数错误, e ); } }使用cause把原始异常传入日志里可以看到完整的堆栈链路同时对外保留了一个稳定的业务异常。好的异常设计通常不是“异常越少越好”而是“异常类型有限、语义清晰、携带足够上下文”。大量用new RuntimeException(失败了)会抹掉所有排查线索后续定位成本很高。2.3 对不可变结果使用 fail-fast同步异常处理里还有一个经常被忽略的问题有些错误发生之后重试没有意义但代码却没有快速失败反而继续往下执行。比如模型返回的 JSON 缺少必要字段、工具返回的数据结构不对这些属于“数据契约错误”。建议在解析层做显式校验校验失败直接抛异常。public AgentResult parseResult(String raw) { JsonNode node JsonUtils.parse(raw); if (!node.has(intent)) { throw new ModelParseException(模型返回缺少 intent 字段, raw); } if (!node.get(intent).isTextual()) { throw new ModelParseException(模型返回 intent 字段类型错误, raw); } return new AgentResult(node.get(intent).asText(), node); }这样的代码虽然看起来多但把错误从“后续某个地方空指针”提前到了“解析这个节点就报错”错误位置离问题根源更近。排查一个 NPE 比排查一个明确的字段校验异常要慢得多。2.4 同步方式常见的三个坑第一个坑是捕获范围过大。有人把整个 Agent 执行流程放在一个 try-catch 里所有异常都变成“系统异常”。结果是模型超时、工具失败、解析错误、用户输入错误全部混在一起后续无法做重试判断。推荐做法是在异常源头附近做捕获和包装入口只做兜底。第二个坑是吞掉异常。catch (Exception e) { return 失败; }且不写日志是最危险的写法。异常发生后系统仍然“正常”返回日志里没有任何记录问题只能靠用户投诉发现。无论哪种异常都要至少记录一条日志并保留异常堆栈。第三个坑是异常类爆炸。每个工具类都建一个独占异常类最后维护成本很高。推荐做法是围绕 Agent 的少数关键边界设计异常类型用toolName、action、retryable等字段区分具体场景而不是为每个方法单独建异常。3. 方式二用 CompletableFuture 处理异步编排中的异常3.1 Agent 异步编排为什么需要专门处理异常当 Agent 需要并行调用多个工具、同时查询多个数据源、或者把一个大任务拆成多个子任务时同步 try-catch 就不够用了。因为异常发生在线程池中的某个子任务里当前主线程不会立刻感知到。如果处理不当异常会被 Future 包装并在线程池中“安静”地结束调用方拿到的是一个ExecutionException但内部失败细节常常丢失。CompletableFuture 是 Java 中常用的异步编排工具。它提供了三种典型的异常处理方法exceptionally、handle、whenComplete。三者都能拿到异常但语义不同。理解差异才能真正用好。exceptionally只在异常发生时执行返回一个恢复值。handle无论成功还是失败都执行可以同时拿到结果和异常并返回一个新结果。whenComplete无论成功还是失败都执行但返回结果由原 Future 决定不能改变结果。3.2 用 exceptionally 在失败后给出默认结果exceptionally适合“失败后给一个默认值”的场景。比如并行查询多个工具时某个工具失败不应该导致整个 Agent 失败而是用缓存值或默认值补齐。CompletableFutureDouble queryScore(String userId) { return CompletableFuture.supplyAsync(() - { // 模拟远程查询 return riskService.queryScore(userId); }, agentExecutor) .exceptionally(ex - { log.warn(查询 {} 评分失败使用默认分值, userId, ex); return 0.0D; }); }这里的agentExecutor是自定义线程池。使用supplyAsync时如果不指定线程池会走公共 ForkJoinPool在 Web 应用里容易和业务线程互相影响。建议异步任务都要显式传入线程池。exceptionally方法的返回值类型必须与上游的 CompletableFuture 结果类型一致。如果上游是CompletableFutureDouble恢复值也必须是Double。这会在编译期检查是最安全的异常恢复方式。3.3 用 handle 同时处理结果和异常handle能同时拿到正常结果和异常对象。适合需要根据“成功但结果不满足条件”和“直接失败”做不同处理的场景。CompletableFutureString executeToolAsync(ToolCall call) { CompletableFutureString future CompletableFuture.supplyAsync(() - invokeTool(call), agentExecutor); return future.handle((result, ex) - { if (ex ! null) { Throwable cause (ex instanceof CompletionException) ? ex.getCause() : ex; if (cause instanceof AgentToolExecutionException toolEx toolEx.isRetryable()) { return TOOL_RETRYABLE; } return TOOL_FAILED; } if (result null || result.isBlank()) { return TOOL_EMPTY; } return result; }); }这里要注意CompletionException的拆解。CompletableFuture 在内部会把异常包装成CompletionException直接判断异常类型会失真。建议先看future.isCompletedExceptionally()或者对ex做一次拆包装再判断真实类型。handle返回的 CompletableFuture 永远不会以异常结束因为内部已经处理了 ex。如果你想保留异常给后续链使用就不要在 handle 里覆盖异常状态而是要重新抛出或者传递。3.4 用 whenComplete 记录状态不改变结果whenComplete适合“只观测不修改”的场景。比如异步任务结束后记录耗时、记录成功失败状态、写审计日志但最终结果仍然沿用上游值。CompletableFutureAgentResult future CompletableFuture .supplyAsync(() - agentRunner.run(userInput), agentExecutor) .whenComplete((result, ex) - { if (ex ! null) { log.error(Agent 异步执行失败input{}, userInput, ex); } else { log.info(Agent 异步执行成功cost{}ms, result.costMs()); } });这里最容易被绕过的地方是whenComplete所在的上游 Future 如果异常结束那么这个CompletableFuture仍然是异常结束的。也就是说whenComplete不等于“捕获异常”它只是“看一眼异常”。要真正处理异常后面还需要接exceptionally或handle。如果只是希望失败时记录日志正确姿势就是whenComplete之后再接exceptionally。CompletableFutureAgentResult resultFuture CompletableFuture .supplyAsync(() - agentRunner.run(userInput), agentExecutor) .whenComplete((result, ex) - { if (ex ! null) { log.warn(异步失败准备降级, ex); } }) .exceptionally(ex - AgentResult.fallback(系统繁忙));3.5 异步链异常传播和 get 超时CompletableFuture 的异常传播有一个让新手困惑的点如果链上某个节点异常后续节点默认不会执行异常会沿着链继续向后传递直到被某个处理节点捕获或者最终被join()/get()抛出。比如下面这个链CompletableFuture .supplyAsync(() - toolA(), executor) .thenApply(a - toolB(a)) .thenAccept(r - save(r)) .exceptionally(ex - { log.error(执行失败, ex); return null; });如果toolA()失败thenApply和thenAccept都不会执行异常会直接跳到exceptionally。这个设计符合“失败即短路”的语义但如果你期待每个节点都有日志就必须在每个节点内部自行 try-catch或者在每个节点后面接单独的异常处理。阻塞获取结果时也容易踩坑。get()会抛出ExecutionException而join()会抛出CompletionException。很多代码直接捕获ExecutionException但对join()无效。更稳妥的方式是把异常拆开再判断。try { AgentResult result future.get(5, TimeUnit.SECONDS); return result; } catch (TimeoutException e) { future.cancel(true); return AgentResult.timeout(); } catch (ExecutionException e) { Throwable cause e.getCause(); if (cause instanceof AgentToolExecutionException toolEx) { return AgentResult.retryable(toolEx.isRetryable()); } return AgentResult.error(cause.getMessage()); }用get(timeout, TimeUnit)是异步任务最基础的兜底否则一个永远不结束的工具调用会让整个 Agent 卡死。3.6 常见异步异常处理误区CompletableFuture 的异常处理有一个常见误区以为whenComplete能改变结果。它不能。如果whenComplete内想返回其他值需要用handle。另一个误区是使用completeExceptionally之后没有消费异常。比如CompletableFutureString f new CompletableFuture(); f.completeExceptionally(new IllegalStateException(boom));如果后续没有人调用get()、join()或exceptionally这个异常相当于被吞掉。Java 不会自动记录它。生产环境建议在任务结束点统一注册whenComplete或exceptionally确保异常都有消费者。还有一个误区是混合使用orTimeout和completeOnTimeout。orTimeout会让 Future 以TimeoutException结束之后可以接exceptionally恢复completeOnTimeout则直接给一个默认值Future 会以正常状态结束。两者语义不同不要混用。orTimeout和completeOnTimeout需要 JDK 9 及以上使用时注意项目 JDK 版本。4. 方式三在执行器层做超时、重试和兜底4.1 执行器层为什么不能省同步异常处理和异步异常处理解决的是“调用点”的问题。但 Agent 任务通常不是单次方法调用而是一个有循环、有步骤、有模型推理、有工具执行的完整过程。循环可能失控模型可能反复给出同一个错误动作工具可能连续超时。这些情况需要有一个更高的层次来踩刹车。执行器层做的就是这件事对整体任务设置执行时长上限对可重试异常做有限次数重试对不可恢复错误提供兜底回复同时把运行中的错误、状态、耗时记录下来。学习阶段可以忽略执行器层因为任务简单跑一次就结束。但进入生产环境后没有执行器保护的 Agent 会非常脆弱一个模型调用的超时可能让用户请求挂 30 秒一个工具重试可能让整体任务重复执行三次导致订单接口被重复调用。4.2用带超时的 get 拦截卡死执行器层最简单的保护是给整个 Agent 调用设置超时。先提交一个 Callable 到线程池再用 future.get(timeout) 等待。public AgentResult executeWithTimeout(CallableAgentResult task, long timeoutMs) { FutureAgentResult future threadPool.submit(task); try { return future.get(timeoutMs, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); log.error(Agent 整体执行超时已取消任务); return AgentResult.timeout(任务执行超时已中断); } catch (ExecutionException e) { Throwable cause e.getCause(); if (cause instanceof AgentToolExecutionException toolEx) { return AgentResult.retryable(toolEx.retryable()); } return AgentResult.error(Agent 执行失败: cause.getMessage()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return AgentResult.error(当前线程被中断); } }这段代码的三个 catch 都要写。TimeoutException意味着任务没有在限定时间内返回需要取消任务。ExecutionException意味着任务内部抛出了异常需要拆开getCause()才能看到真实异常。InterruptedException意味着当前线程被外部中断需要恢复中断标记不能让线程状态被抹掉。future.cancel(true)并不是一定能杀死正在运行的线程。如果任务内部不响应中断线程会继续执行但至少不会再被主流程等待。生产环境还要注意线程池的回收策略避免因任务卡死导致线程池线程耗尽。4.3 对可重试错误做有限次数重试重试不是越多越好。模型接口限流时稍等一会儿重试可能成功但如果是参数错误重试一万次也失败。建议只在异常携带retryabletrue时重试并且设置最大次数和间隔。public AgentResult executeWithRetry(Task task, int maxRetries, long delayMs) { int retryCount 0; while (true) { try { return task.run(); } catch (AgentToolExecutionException e) { if (!e.isRetryable() || retryCount maxRetries) { throw e; } retryCount; log.warn(任务第 {} 次执行失败将在 {}ms 后重试, retryCount, delayMs); sleepQuietly(delayMs); } } }这里要注意两点。第一重试间隔最好有退避比如第一次间隔 200ms第二次 500ms第三次 1s。固定间隔在限流场景下容易再次触发限流。第二重试必须保证幂等。Agent 执行工具时如果工具本身不是幂等的比如创建订单、发送短信、扣减库存重试可能导致重复业务操作。这需要在工具设计时考虑或者在重试前通过业务唯一 ID 去重。4.4 对不可重试错误做 fallback有些异常重试没有意义比如模型返回内容无法解析、工具参数不符合规则、用户输入不合法。这时候执行器需要提供 fallback 逻辑返回一个对用户有意义的兜底结果。public AgentResult runWithFallback(String userInput) { try { return agent.run(userInput); } catch (AgentToolExecutionException e) { if (!e.isRetryable()) { log.warn(工具不可重试失败tool{}, e.toolName()); return AgentResult.fallback(该操作暂时无法完成请检查参数后重试); } throw e; } catch (ModelParseException e) { log.error(模型输出无法解析, e); return AgentResult.fallback(模型输出格式异常请重新提问); } }fallback 的关键是“给用户一个确定结果同时不隐藏问题”。兜底消息要友好但也要区分场景不能让所有失败都返回“系统繁忙”。用户输入问题、模型问题、工具问题、系统问题应该有不同的提示。4.5 框架内置异常策略与自定义扩展很多 Agent 框架自带执行器和异常策略。有的提供 maxIterations限制规划循环次数有的提供 timeout 配置控制执行总时间有的提供 retry 模板对模型调用自动重试。使用框架时优先使用框架内置能力不要重复造轮子。但内置策略通常只能处理框架层异常。对于你自己工具里的业务异常仍然需要沿用前面两种方式做好包装和语义化。框架内置异常策略适合作为第三层兜底而不是替代第一层和第二层。如果框架的兜底策略不符合要求可以通过实现框架的执行器接口或异常处理接口扩展。自定义时要注意不要覆盖框架已有的超时配置也不要把所有异常都转成通用错误否则日志可观测性会下降。5. 一套代码把三种方式串起来5.1 分层职责划分一个可工作的 Agent 服务建议按三层划分异常处理。业务服务层负责同步异常处理。模型调用、工具调用、结果解析都在这一层做 try-catch 和异常包装保证异常类型语义明确。异步编排层负责处理并行任务。使用 CompletableFuture 的 exceptionally、handle、whenComplete 处理子任务失败并保证异常链不中断。执行器层负责总体兜底。设置整体超时执行有限重试提供 fallback形成给用户的最终结果。5.2 核心代码AgentService 和 AgentExecutor下面是一个简化的组合示例。该类先定义工具执行的异步编排再由执行器统一控制超时。public class AgentFacade { private final AgentService agentService; private final ExecutorService executor; public AgentFacade(AgentService agentService, ExecutorService executor) { this.agentService agentService; this.executor executor; } public AgentResult run(String userInput) { FutureAgentResult future executor.submit( () - agentService.executeWithRetry(userInput, 2, 300L) ); try { return future.get(10, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); return AgentResult.timeout(任务处理超时); } catch (ExecutionException e) { Throwable cause e.getCause(); if (cause instanceof AgentToolExecutionException toolEx) { return AgentResult.toolError(toolEx.toolName(), toolEx.getMessage()); } return AgentResult.error(系统繁忙); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return AgentResult.error(任务被中断); } } }这段代码把执行器超时作为最外层保护。agentService.executeWithRetry内部再处理具体的同步异常、异步编排和重试逻辑。在生产项目中这一层还会加入熔断、监控埋点、审计日志。5.3 统一错误响应和恢复策略为了让上层不关心异常细节可以定义一个统一的错误码枚举。public enum AgentErrorCode { SUCCESS(SUCCESS, 成功), MODEL_TIMEOUT(MODEL_TIMEOUT, 模型调用超时), MODEL_PARSE_ERROR(MODEL_PARSE_ERROR, 模型输出解析失败), TOOL_ERROR(TOOL_ERROR, 工具执行失败), AGENT_TIMEOUT(AGENT_TIMEOUT, Agent 整体执行超时), INTERRUPTED(INTERRUPTED, 任务被打断), ERROR(ERROR, 系统异常); private final String code; private final String desc; AgentErrorCode(String code, String desc) { this.code code; this.desc desc; } public String code() { return code; } public String desc() { return desc; } }错误码的好处是上层可以根据 code 决定 HTTP 状态码、用户提示和是否需要重试而不用依赖异常类的具体实现。日志系统也可以直接用 code 聚合统计快速看出哪类错误最多。5.4 运行验证与预期输出假设创建一个简单的 Agent 服务模型模拟正常返回工具模拟超时运行后可以在日志中看到如下输出[main] WARN AgentService - 工具 externalApi 第一次执行超时2 秒后重试retry1 [main] WARN AgentService - 工具 externalApi 第二次执行超时3 秒后重试retry2 [main] ERROR AgentFacade - Agent 整体执行超时任务已取消如果只想快速验证异步异常处理可以写一个最小单元测试。Test void testAsyncExceptionRecovery() { CompletableFutureString future CompletableFuture .supplyAsync(() - { throw new IllegalStateException(boom); }) .exceptionally(ex - recovered); assertEquals(recovered, future.join()); }验证时不要只看程序不崩溃还要看异常是否被正确记录、恢复值是否符合预期、重试次数是否正确、超时是否真的取消了任务。只有这些行为都符合预期异常处理才算是真正生效。6. Agent 异常处理高频问题排查6.1 现象与排查顺序表遇到 Agent 异常时先不要急着改代码按下面表格从现象倒推原因。问题现象常见原因检查方式处理建议Agent 请求长时间不返回未设置整体超时模型或工具卡住查看线程池状态、jstack执行器层增加 future.get(timeout)日志里没有异常但结果错误异常被吞掉catch 后未记录检查 catch 块是否只有 returncatch 中至少记录 error 日志异步任务失败但主流程不知道未接 exceptionally/handle/whenComplete检查 CompletableFuture 链是否有消费者在链末尾加 whenComplete 或 exceptionally返回“系统繁忙”但无法定位所有异常都进入统一兜底检查异常是否在源头包装在源头区分超时、解析、工具、系统异常重试导致工具重复执行工具非幂等未做重试去重检查工具调用是否传业务唯一 ID重试前加入幂等控制线程池线程耗尽大量任务卡死未取消监控线程池活跃线程数增加超时合理设置队列和拒绝策略6.2 日志关键字与定位手段排查 Agent 异常时日志中优先关注这些关键字AgentToolExecutionException工具执行失败重点看 toolName 和 retryable 字段。CompletionException/ExecutionException异步包装异常重点拆 getCause() 看真实原因。TimeoutException要么是单次调用超时要么是整体任务超时。ModelParseException模型输出结构不符合预期检查 prompt 和解析器。InterruptedException线程被中断检查是否有线程池关闭或任务取消。排查时可以用 jstack 查看线程状态jcmd pid Thread.print如果发现大量WAITING状态的线程集中在某几个方法说明任务在那个位置没有响应超时优先给对应调用点补超时。6.3 最容易忽略的幂等和资源释放问题第一个容易忽略的是重试期间的幂等。Agent 重试不是单纯地重跑一个函数而是可能重新执行模型调用、重新调度工具。如果工具是“发通知”“创建订单”重试会造成重复操作。建议在工具执行前生成 traceId并在下游接口使用该 id 做幂等校验。第二个容易忽略的是线程池资源释放。使用 CompletableFuture 时如果没有指定线程池会使用公共 ForkJoinPool。公共池被多个任务占用后其他异步任务会被排队。生产环境要单独创建线程池并在应用关闭时优雅关闭。第三个容易忽略的是中断状态。捕获InterruptedException后如果不调用Thread.currentThread().interrupt()线程的中断状态会被清除后续调用方无法感知线程被中断。规范做法是捕获后立即恢复中断标记。7. 生产环境落地建议与检查清单7.1 生产环境不能只写 try-catch学习环境的 Agent 示例通常是一次调用、一次返回异常处理可以非常粗糙。生产环境至少要补齐五件事。第一配置外置。超时时间、重试次数、重试间隔、熔断阈值不要写死在代码里放到配置中心或环境变量中方便运维动态调整。第二日志与监控。每个 Agent 任务要有 traceId核心节点都要打印耗时和状态。监控指标至少包括任务成功率、平均耗时、P99 耗时、工具失败率、重试次数分布、超时次数。第三权限与安全。Agent 执行的工具通常涉及外部接口和数据库异常信息中包含的原始报文不能直接返回给前端防止内部信息泄露。第四回滚方案。如果模型服务或工具服务出现问题异常处理策略要能快速降级。比如关闭某些高风险工具或者把模型切到备用服务。第五异常审计。异常处理不只是“给用户一个提示”还要沉淀成异常数据。每类错误码出现的次数、影响用户数、平均处理时长都应该可以在报表中看到。7.2 可复用的 Agent 异常处理检查清单在代码合并前可以按下面的清单逐项确认。检查项是否满足模型并发地调用接口是否有超时兜底是 / 否每个工具执行异常是否包装为带 retryable 标记的业务异常是 / 否CompletableFuture 链异常是否有消费者是 / 否是否区分 whenComplete 和 handle 的语义是 / 否获取异步结果是否使用 get(timeout)是 / 否重试次数是否有限重试间隔是否退避是 / 否可重试工具是否考虑幂等控制是 / 否整体 Agent 执行器是否设置总超时是 / 否异常日志是否保留原始 cause是 / 否用户提示是否区分超时、解析、工具、系统异常是 / 否中断异常是否恢复线程中断标记是 / 否错误码是否可用于统计聚合是 / 否没有全部满足时可以先上线最核心的两项整体超时和异常日志。这两项能避免 Agent 服务在故障时无响应、无记录。7.3 后续扩展方向如果 Agent 异常处理已经形成稳定体系下一步可以继续做几件事。接入重试框架或熔断框架。通过注解或配置统一管理重试次数、退避策略、熔断阈值减少手写 while 循环。建立异常演练机制。定期模拟模型超时、工具 500、限流、线程池耗尽验证异常处理链路是否真的按预期工作。把异常分为“用户可恢复”和“系统可恢复”两类分别设计交互逻辑。用户可恢复的异常可以引导用户重新输入或修改参数系统可恢复的异常可以自动重试或降级。Agent 应用越复杂异常处理越要前置设计。不要等项目上线后才在入口堆 try-catch那只能解决“看不到报错”解决不了“报错定位慢、恢复策略混乱、重复执行风险高”这些本质问题。把三种方式按层落实是让 Agent 服务具备基础稳定性的第一步。