尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Agent异常处理的可选性设计:从CompletableFuture到策略配置

Agent异常处理的可选性设计:从CompletableFuture到策略配置 Agent开发里最容易被低估的一块就是异常处理。很多团队把Agent链路搭起来之后第一版只保证了“主流程能跑通”一旦某个工具调用超时、模型接口返回异常、下游服务报错整个Agent要么卡死要么直接失败要么用写死的默认值继续往下跑后面出了问题根本不知道是从哪一步开始歪的。这里真正值得做的是把异常处理做成“可选的”。也就是说Agent在什么场景下应该快速失败什么场景下可以重试什么场景下要降级兜底什么场景下允许忽略继续都应该是可配置、可组合、能按任务动态切换的而不是写死在代码里的一套逻辑。这篇文章就围绕“Agent异常处理的可选性”这个主题把我在实际项目里拆过、踩过、改过的思路完整整理一遍。适合正在做Agent开发、想把任务编排做得更稳的人读也适合刚接触Agent框架、不知道异常该在哪一层处理的人看。1. 先理解Agent的异常处理为什么不能只写一套逻辑1.1 Agent失败的形式比传统接口多得多传统业务接口的异常处理核心就两类同步调用返回错误码或者抛出异常。调用方拿到结果后决定是重试、报错还是走兜底。这套逻辑在Agent里也能用但远远不够。Agent的失败形态非常杂至少包括下面几种模型调用失败网络超时、限流、上下文超长、内容被安全策略拦截。工具调用失败外部API返回5xx、参数校验不过、鉴权失效、结果格式不符合预期。多步任务中断前面某一步得到的结果不满足下一步的条件整条链路没法继续。异步任务超时使用CompletableFuture、消息队列或独立任务节点时某个分支长时间没有返回。输出校验失败Agent给出了回答但缺少关键字段、JSON格式错误、内容为空。这些失败形式之间没有统一规律。有的需要立刻终止有的重试一次就好有的可以直接给个默认结果继续。强行用同一套try-catch包住所有逻辑最后只会得到两种结果要么异常被吞掉要么整个任务被一个不重要的子步骤拖垮。1.2 “可选性”到底可选的什么我理解的“可选性”有三个层面。第一层策略可选。同一个Agent任务里不同的错误类型可以对应不同的处理动作。超时走重试限流走等待结果格式错误走重新解析致命错误才走终止。第二层任务可选。不是所有任务都需要同样严格的异常策略。内部实验任务可以失败快速暴露问题线上生产任务要尽量兜底批量跑批任务则要记录失败、跳过、继续处理后面的数据。第三层链路可选。异常处理本身不应该是所有代码的必经之路。有的错误发生后后面的步骤需要知道有的错误发生后后面步骤应该完全感知不到。这决定了异常是向上抛、向下传递还是转换成默认值。说到底Agent异常处理的可选性本质上是把“失败后的行为”从代码里抽出来变成一套可以按场景选择的规则。这样Agent才能真正适应复杂任务而不是一遇到异常就停摆。2. 异常处理策略清单快速失败、重试、降级、忽略、熔断2.1 五种基础策略先分清它们各自解决什么问题在做任何配置化之前先把策略本身梳理清楚。下面是Agent开发里最常用的五种异常处理动作以及它们的适用场景和风险。策略核心动作适用场景主要风险快速失败遇到异常立即终止并抛出调试阶段、数据校验失败、无法恢复的致命错误可能因为单个子步骤误伤整个任务重试等待后重新执行失败步骤网络抖动、临时超时、服务端5xx重试次数过多会拖慢任务非幂等操作会重复执行降级使用备用方案替代失败步骤主数据源不可用、主模型超时、工具暂不可用降级结果质量下降需要记录标志忽略继续跳过失败步骤继续后续任务非关键步骤、日志类旁路操作、可选信息采集错误被隐藏后续结果可能不完整熔断连续失败后暂停该路径一段时间某个工具或模型持续故障、批量任务高峰期熔断参数设置不当会影响正常流量这五种策略不是互斥的真正的Agent异常处理通常是组合使用。比如先重试两次再降级降级也失败才快速失败。或者批量任务里单条记录失败后忽略继续但整批任务的失败率达到阈值就熔断停止。2.2 重试别乱用先确认幂等性重试是Agent开发里用得最多也最容易出问题的一个策略。很多初学者看到超时的报错第一反应就是加个循环重试。但这里有一个前提必须确认被重试的操作是不是幂等的。举一个很常见的例子。Agent调用支付或扣减库存的工具第一次请求其实已经成功了只是响应超时客户端没收到结果。这时候如果直接重试就会造成重复扣减或重复下单。这种场景下正确的做法不是无脑重试而是先查询状态确认之前的请求是否真的失败。如果Agent拿不到幂等键或查询接口那宁可快速失败把问题交给告警和人工处理也不要让重试制造出更大的问题。反过来对于查询类、纯计算类、幂等的写入操作重试就非常安全。像读取天气数据、调用翻译接口、搜索知识库这类操作重试两三次基本不会有什么副作用。重试本身也要设计参数。我一般会关注三个值最大重试次数单次任务内不要超过3次超过之后大概率不是临时抖动。重试间隔固定间隔适合快速抖动指数退避适合服务端压力大的场景。最大重试耗时重试是为了恢复不是为了无限等待要给整个重试过程设置总时间上限。2.3 降级的关键是“降得够明显”降级策略在Agent里很实用但有个容易被忽略的点降级结果必须在最终输出中留下痕迹。比如Agent本来要调用一个实时汇率接口接口挂了降级成使用昨天的缓存汇率。这个结果在业务上可能是可接受的但如果你不告诉调用方“你用的是缓存数据”后面做财务计算就麻烦了。所以降级策略一定要额外输出一个标志例如返回结果里带一个isFallback字段或者在日志里打一条明确的降级记录。降级的另一个原则是“备用方案要提前准备”。不要等到接口挂了才开始想替代方案。在设计Agent任务时就应该把每个关键步骤的降级方案列出来。比如主模型超时可以用备用模型实时数据失败可以用预计算数据外部API失败可以尝试本地规则引擎。2.4 忽略和熔断是两种容易被误用的策略忽略继续听起来很省事但使用前提非常苛刻被忽略的步骤对最终结果必须是非关键性的。举个例子Agent在生成周报时要附带获取团队成员的在线状态。这个数据获取失败不影响周报主体内容那就可以忽略。但如果是获取销售额统计失败那整个报告的核心数字就是缺失的不能忽略。熔断则是为了应对“群体性失败”。当一个外部服务连续失败达到一定阈值比如最近一分钟内失败率超过50%再继续调用没有意义只会浪费时间。这时候应该直接进入熔断状态对应的调用路径在短暂时间内不再执行而是直接走降级或失败逻辑。过一段时间后再放少量请求试探恢复。熔断参数有几个常见坑阈值设置太低容易被一次小抖动触发恢复时间太短会让服务在还没恢复时就被反复试探熔断过程中如果直接全部失败会给用户造成很差的体验最好配合降级策略使用。3. CompletableFuture异步编排里异常怎么“可选地”接住3.1 三个核心方法行为差异要分清Agent任务的很多流程是异步编排的Java项目里最常用的是CompletableFuture。之前很多热搜和搜索里都在问“CompletableFuture异步编程异常处理”说明这块确实是普遍痛点。CompletableFuture提供了三个看着很像、实际行为完全不同的方法exceptionally、handle、whenComplete。三者的差别恰好对应了异常处理的可选性设计。CompletableFutureString task CompletableFuture.supplyAsync(() - { if (System.currentTimeMillis() % 2 0) { throw new RuntimeException(模拟调用失败); } return 成功结果; }); // 方式一exceptionally异常时返回替代值正常时不受影响 CompletableFutureString r1 task.exceptionally(ex - 默认兜底结果); // 方式二handle不管成功失败都会执行需要自己判断 CompletableFutureString r2 task.handle((result, ex) - { if (ex ! null) { return 异常时的结果; } return result; }); // 方式三whenComplete只感知结果不改变结果 CompletableFutureString r3 task.whenComplete((result, ex) - { if (ex ! null) { System.out.println(记录异常信息但不处理: ex.getMessage()); } });这段代码里最关键的区别是exceptionally 只有在异常时才执行而且它的返回值会替换掉之前阶段的异常。换句话说异常被“接住”了后面的链路上看到的是一段正常数据。handle 无论是否异常都会执行适合“不管结果怎样都要做统一处理”的逻辑。whenComplete 不改变结果也不吞异常适合做日志记录、指标埋点这类旁路操作。在Agent开发里这三个方法对应的策略完全不同。比如Agent调模型失败后你想要的是“失败后使用备用模型重新生成”那应该用exceptionally。如果你想记录“本次调用模型失败”的日志但不想影响后续逻辑那用whenComplete。如果你想统计成功率同时把失败情况转换成业务可读的错误信息那用handle。3.2 异常被吞掉是异步链路最大的坑CompletableFuture有一个很隐蔽的行为如果你在链路的最后没有调用get()或join()异常会被静默丢弃。也就是说任务失败了你可能完全不知道。这在Agent开发里非常危险因为Agent的异步任务通常埋得很深可能是某个子工具调用、某一步数据清洗、某一次并行分支。我见过一个真实案例。Agent里有一个并行分支负责拉取公司公告信息代码写得没问题但最后一个节点没有主动get()导致公告拉取失败时整个分支被忽略。Agent照常返回了报告只是报告里少了一大段内容。用户不问根本发现不了。所以异步链路的异常处理第一步不是选策略而是确保异常一定会被某个地方感知到。推荐的做法是每个异步链路的末端都要有一个终结方法用whenComplete记录日志或者用join()获取结果并处理异常。不要让CompletableFuture的异常静默消失。CompletableFutureListString fetchTask CompletableFuture.supplyAsync(() - { // 模拟拉取公告 throw new RuntimeException(公告服务不可用); }); // 在链路末端显式使用 exceptionally并明确处理 CompletableFutureListString safeTask fetchTask .exceptionally(ex - { // 记录日志返回空列表或者抛出业务异常 System.out.println(公告拉取失败进入降级逻辑: ex.getMessage()); return Collections.emptyList(); }); ListString result safeTask.join(); System.out.println(最终结果条数: result.size());这里有个细节值得注意exceptionally返回空列表之后后面的链路会认为这一步是成功的。如果下游逻辑依赖公告数据非空那么返回空列表反而会产生一个“看似成功但实际上数据缺失”的结果。所以异常处理的“可选性”不仅要在异常发生时做选择还要考虑异常处理之后的结果是否满足后续步骤的输入要求。如果不满足就不要返回默认值而是明确抛出一个新的业务异常。3.3 超时必须要单独处理异步任务另一个常见问题是“没有超时”。CompletableFuture本身不会因为你等得够久就自动停止它没有内置的超时机制。如果你调用的工具服务一直不返回你的Agent任务就会一直挂在那边。处理方式要给任务设置显式的超时时间。Java 9之后CompletableFuture有两个方法orTimeout和completeOnTimeout。前者超时后让任务进入异常完成状态后者超时后返回一个默认值。CompletableFutureString future CompletableFuture.supplyAsync(() - { // 模拟一个可能长时间不返回的工具调用 try { Thread.sleep(10000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return 结果; }); // 方式一5秒超时后任务异常完成后续可以感知 CompletableFutureString withTimeout future.orTimeout(5, TimeUnit.SECONDS); // 方式二5秒超时后返回默认值任务正常完成 CompletableFutureString withDefault future.completeOnTimeout(默认结果, 5, TimeUnit.SECONDS);这两种方式的差异本质上也是一种“可选性”。如果超时之后你想要的是“重试”或“快速失败”那就用orTimeout让调用方感知异常。如果超时之后你想要的是“继续跑下去”那就用completeOnTimeout但要配合降级标志确保下游知道这个结果是超时兜底产物。在实际项目里我倾向于把orTimeout配合exceptionally一起用而不是直接用completeOnTimeout。原因是completeOnTimeout返回默认值后你无法从结果里区分“正常返回”和“超时兜底”除非你额外包一层对象。而orTimeout加exceptionally可以让异常处理逻辑保持统一所有非正常返回都走同一个策略分支。4. 把策略做成配置一套可复用的Agent异常处理链路4.1 策略注册表比if-else更值得做当异常处理策略多起来之后最忌讳的就是在业务代码里堆if-else。今天加一个重试条件明天加一个降级分支代码很快就会变成一坨没法维护的“异常沼泽”。更好的做法是把异常处理策略做成注册表。每种策略对应一个实现类通过配置或工厂方法选择使用哪个策略。以Java为例可以定义一个统一的策略接口public interface ExceptionHandlingStrategy { boolean supports(Throwable exception, AgentTaskContext context); AgentTaskResult handle(Throwable exception, AgentTaskContext context); }接口里两个方法职责很清晰supports方法判断当前异常和任务上下文是否适用这个策略。handle方法执行具体的处理动作返回处理后的结果。然后实现具体策略比如RetryStrategy、FallbackStrategy、FailFastStrategy、IgnoreStrategy。每个策略都单独一个类互不干扰。使用的时候通过一个策略链把它们串起来public class ExceptionHandlingChain { private final ListExceptionHandlingStrategy strategies; public ExceptionHandlingChain(ListExceptionHandlingStrategy strategies) { this.strategies strategies; } public AgentTaskResult handle(Throwable exception, AgentTaskContext context) { for (ExceptionHandlingStrategy strategy : strategies) { if (strategy.supports(exception, context)) { return strategy.handle(exception, context); } } throw new AgentExecutionException(未找到适用的异常处理策略, exception); } }这个链路的好处是顺序决定优先级。你可以在前面放快速失败策略处理致命错误中间放重试策略处理超时错误最后放忽略策略处理非关键错误。新增策略时只需要实现接口并加入列表不需要改动业务代码。4.2 用配置来决定“这次任务怎么处理异常”策略注册表解决的是“代码结构问题”配置化解决的是“运行时切换问题”。同一个异常在不同任务里可能希望走不同策略。这时候最合理的做法是通过配置文件或配置中心下发策略规则。下面是一个示例配置结构实际字段可以根据项目情况调整agent: task: report-generation: exceptions: - type: TimeoutException strategy: RETRY maxRetries: 2 backoff: FIXED intervalMs: 1000 - type: RateLimitException strategy: RETRY maxRetries: 3 backoff: EXPONENTIAL - type: DataFormatException strategy: FALLBACK fallback: 使用上次缓存数据 - type: PermissionDeniedException strategy: FAIL_FAST - type: ToolNotFoundException strategy: IGNORE logLevel: WARN看到这个例子你会发现同样是异常处理但规则完全是任务级别的。报周报的任务遇到超时可以重试两次做财务汇总的任务遇到超时可能必须快速失败。这些差异不应该写在业务代码里而是应该在配置里明确表达。配置化的另一个价值是可观测性。每次异常处理动作发生时把异常类型、命中策略、处理结果、耗时都记录成结构化日志。等Agent跑完一批任务你可以直接统计出哪种异常最多、哪个策略被触发最频繁从而判断是否调整策略参数。4.3 默认策略要“安全”而不是“聪明”配置化之后会有一个新问题用户没有配置的异常类型该怎么处理这里我的建议是默认策略一定要保守优先保证任务不会静默失败。比较稳妥的默认方案是默认使用快速失败同时把异常信息完整记录到日志。理由是与其让Agent带着不确定的结果继续跑不如让异常尽快暴露出来。当然如果你的Agent应用场景对连续性要求很高比如长时间批处理任务那默认策略可以改成“记录并继续”但必须在结果里标记失败项方便事后复查。默认策略的选择本质上是在“可用性”和“可靠性”之间做取舍。这个取舍没有标准答案但一定要被显式定义出来而不是靠运气。我见过很多项目默认策略是隐式的代码里某个位置顺手catch了一下结果就把该暴露的问题吞了。选择默认策略时问自己一句这个Agent任务如果失败用户更希望看到“任务失败了但我知道原因”还是希望看到“任务完成了但结果可能不准”答案不同默认策略就不同。5. 单任务、批量任务、队列任务要使用不同的异常策略5.1 单条任务失败可以更直接单条Agent任务的异常处理核心目标是“快速暴露问题”。因为单条任务通常是人发起、人等待的交互方式决定了用户需要明确的结果。要么成功返回结果要么失败给出原因不要让用户等半天拿到一个半成品。所以单条任务下策略选择优先级一般是致命错误、数据校验失败、权限问题快速失败。临时超时、网络抖动重试一到两次。关键数据获取失败降级但输出中标记降级。非关键数据失败忽略但日志记录。单条任务不太需要复杂的熔断机制因为单个请求触发熔断的意义不大。熔断更多是保护性措施适合系统长期运行、需要防御外部服务持续故障的场景。5.2 批量任务失败重试、输出命名、断点续跑才是重点批量Agent任务和单条任务完全不同。批量任务里单条记录失败是常态不可能因为一条数据出问题就把整个批次都停掉。但也不能无限跳过错下去那样结果会不完整。批量任务至少要处理三类问题第一失败记录要单独保存。每一条失败的任务都要写入失败列表包含输入内容、异常信息、失败步骤。这样跑完之后可以通过失败列表定位原因。第二要有断点续跑能力。批量任务跑了一半程序重启或者服务崩溃重新开始是最低效的方案。更好的做法是任务处理完后标记状态恢复时只处理未完成任务。第三输出要可以区分批次。多条任务放在一起输出的文件、记录、日志都要带上任务ID或批次号否则后续没法追踪。下面是批量任务中比较常见的一个处理框架遍历任务列表 - 单条执行 - 成功写入结果集 - 失败 - 重试未到上限加入重试队列 - 重试已达上限写入失败列表 - 每处理N条持久化一次进度 结束后 - 汇总成功数、失败数、跳过数 - 输出失败文件路径批量任务的异常处理原则可以概括为不追求每条都成功但要做到每条结果都可追踪。5.3 队列任务死信和人工介入不能少如果Agent任务是通过消息队列异步消费的那异常处理还要多考虑一层消息消费失败怎么处理。常见的消息队列消费失败处理方案有三种死信队列、定时重投、人工介入。死信队列用于处理多次重试仍然失败的消息避免消息在队列里无限循环。定时重投适合临时性故障给消息设置延迟时间过一段时间再消费。人工介入则是把失败告警发给负责人由人来决定是修复重投还是丢弃任务。在Agent场景里还要注意消息幂等。Agent任务往往不是纯计算它会调用外部服务、写数据库、生成文件。消息重投后同样的任务会不会被执行两次这需要在入队时就设计好任务ID和去重逻辑。队列任务的异常处理还有一个特点异常处理发生的时间点往往比实际失败时间晚。比如消息投递失败后过了10分钟才重试成功。这时候Agent拿到的上下文环境可能已经变了外部服务可能已经恢复但业务数据可能已经发展到另一个状态。所以队列任务的异常重试一定要考虑数据的时间有效性而不是机械地重放。6. Agent异常处理最常踩的坑和排查顺序6.1 五个高频问题先对照一下你的项目我在实际排查Agent异常时发现很多问题不是出在“没有异常处理”而是出在“异常处理的姿势不对”。下面五个问题最常见。第一异常被吞了。最常见的原因是异步任务没有显式获取结果或者某个catch块只打印了日志没有继续抛。排查方法很简单在日志系统里搜异常关键字看有没有对应记录。如果没有记录就说明异常在某个环节被静默吞掉了。第二重试导致的重复执行。前面讨论过的幂等问题集中在非查询类操作上。排查方法是检查操作的唯一标识确认是否使用了业务幂等键。第三降级结果没有任何标记。降级数据混在正常数据里用户和调用方都不知道。排查方法是在结果对象里增加来源字段区分“实时数据”“缓存数据”“默认值”。第四熔断恢复之后仍然失败。这可能是因为熔断参数设置得太激进或者恢复探测时仍然用原来的高并发流量。排查时先看熔断期间的失败率再确认恢复请求是否限流。第五异常处理策略本身抛异常。有些策略代码写得不严谨比如fallback逻辑里又调用了同一个外部服务导致二次失败。策略代码要保证不依赖失败源否则异常处理链路就失效了。6.2 异常处理的排查应该按什么顺序来遇到Agent异常不要上来就改代码。我建议按下面这个顺序排查第一步看现象。确认是整条任务失败还是某个步骤失败还是结果不正确但没报错。结果不正确往往比直接失败更危险因为它意味着异常处理“成功”地掩盖了一个问题。第二步看日志。重点找异常栈、策略命中记录、降级标记。如果日志里没有任何异常信息先怀疑是执行链路的末端没有终结方法。第三步看配置。确认当前任务类型使用的是哪套策略配置重试次数、熔断阈值、降级启用状态是否符合预期。很多时候不是代码错了是配置没覆盖到新加的异常类型。第四步看数据。确认输入数据是否合法、是否包含特殊字符、是否超过模型上下文限制。Agent的很多异常表面上是工具调用失败实际上是输入格式和内容的问题。第五步看外部依赖。确认工具服务、模型接口、下游API的状态。如果外部服务本身在故障再好的策略也只是延迟失败时间。6.3 设计Agent异常处理时最后一定要想的三个问题写到这里把异常处理“可选性”这件事收个尾。无论你用什么语言、什么框架设计Agent异常处理时都要先想清楚三个问题。第一个问题每种异常发生后后续步骤还需要继续吗需要继续的要考虑降级结果是否满足下游输入要求不需要继续的要确保异常能被正确终止和记录。第二个问题你这个Agent任务是给谁用的给内部调试用的失败越直接越好给业务方用户用的要有兜底方案给批量跑批用的要有失败追踪和断点恢复。第三个问题异常处理动作本身可观测吗每次重试、降级、熔断、忽略有没有留下结构化日志降级结果有没有标记如果没有那你的异常处理就是“黑盒”出了问题只能靠猜。做过几个Agent项目之后你会发现异常处理不是写一个try-catch那么简单而是一个覆盖策略设计、配置管理、异步编排、批量追踪、可观测性的完整链路。把这部分做扎实了Agent才能真正稳定地跑在生产环境里。如果只是学习阶段先把单任务的异常处理跑通再逐步扩展重试、降级和批量策略别指望一上来就交付一个完美的异常处理体系。
返回列表