Agent 工具调用的异常处理框架:超时、重试与优雅降级
Agent 工具调用的异常处理框架超时、重试与优雅降级一、工具调用失败在 Agent 中的传播特征一个典型的 Agent 任务涉及多次工具调用先调用搜索工具获取信息再调用计算工具处理数据最后调用发送工具输出结果。如果搜索工具因网络抖动超时了 5 秒Agent 可能产生两种不良行为幻觉替代Agent 在未获取搜索结果的情况下基于自身训练数据编造了搜索结果后续的计算和发送环节在虚假数据上运行静默丢失Agent 报告搜索工具调用成功实际超时未获取数据生成的最终结果缺失了关键信息。这两种行为都比直接告诉用户搜索失败更危险。幻觉替代让用户误信虚假信息静默丢失让用户遗漏关键数据。Agent 工具调用的异常处理核心原则是宁可明确失败不可静默降质。二、三层防护的工具调用异常体系从工具调用到 Agent 响应建立三层异常防护graph TB AGENT[Agent 决策层] -- TOOL[工具调用层] subgraph 第一层工具级防护 T1[超时控制br/单工具 ≤ 30s] T2[结果校验br/Schema 验证] T3[重试策略br/指数退避 max 3 次] end subgraph 第二层编排级防护 O1[依赖检查br/前置工具失败则跳过后继] O2[替代路由br/查询工具失败→告知用户] O3[部分成功br/3/5 工具成功仍可返回] end subgraph 第三层Agent 级防护 A1[失败通知br/向用户说明哪些数据缺失] A2[降级模式br/关闭失败工具切换简化模式] A3[熔断机制br/连续 5 次失败暂停该工具] end TOOL -- T1 T1 -- T2 T2 -- T3 T3 -- O1 O1 -- A1 A1 -- USER[用户]第一层在单个工具内部做超时、校验和重试。第二层在工具编排层面管理依赖关系——如果搜索失败后续的数据分析步骤应该跳过而非在空数据上运行。第三层在 Agent 层面将工具失败信息转化为对用户透明的状态说明。三、工具调用的安全封装实现// 工具调用的结果类型——区分成功、降级、失败三种状态 type ToolResultT | { status: success; data: T; toolName: string; elapsedMs: number } | { status: degraded; data: PartialT; reason: string; toolName: string } | { status: failed; error: string; toolName: string }; // 工具函数签名——Agent 通过此接口调用任何工具 type ToolFunctionTParams, TResult (params: TParams) PromiseTResult; interface ToolConfig { name: string; timeoutMs: number; maxRetries: number; // 重试的基础延迟毫秒 retryBaseDelayMs: number; // 是否允许返回部分结果降级模式 allowPartial: boolean; } class SafeToolExecutor { // 连续失败计数器——用于熔断 private failureCounts: Mapstring, number new Map(); // 熔断阈值 private readonly circuitBreakerThreshold 5; // 熔断后的冷却时间毫秒 private readonly cooldownMs 60000; // 熔断器状态 private circuitOpenUntil: Mapstring, number new Map(); async executeTParams, TResult( config: ToolConfig, toolFn: ToolFunctionTParams, TResult, params: TParams, // Schema 校验函数——验证输出是否符合预期结构 validator?: (data: unknown) data is TResult, ): PromiseToolResultTResult { const startTime Date.now(); // 检查熔断器状态 if (this.isCircuitOpen(config.name)) { return { status: failed, error: 工具 ${config.name} 已熔断请稍后重试, toolName: config.name, }; } // 带重试的执行循环 let lastError: Error | null null; for (let attempt 0; attempt config.maxRetries; attempt) { try { const result await this.callWithTimeout( toolFn, params, config.timeoutMs, ); // 结果校验 if (validator !validator(result)) { // Schema 校验失败——这可能是模型输出格式问题 // 不同于一般的执行错误重试的意义不大 return { status: degraded, data: {} as PartialTResult, reason: ${config.name} 输出格式校验失败, toolName: config.name, }; } // 成功——重置失败计数器 this.failureCounts.set(config.name, 0); return { status: success, data: result, toolName: config.name, elapsedMs: Date.now() - startTime, }; } catch (error) { lastError error as Error; // 最后一次重试也失败了 if (attempt config.maxRetries) { this.recordFailure(config.name); break; } // 指数退避等待后重试 const delay config.retryBaseDelayMs * Math.pow(2, attempt); // 加入随机抖动——防止惊群效应 const jitter Math.random() * 200; await new Promise(resolve setTimeout(resolve, delay jitter)); } } // 所有重试均已耗尽——决定返回降级还是失败 if (config.allowPartial) { return { status: degraded, data: {} as PartialTResult, reason: ${config.name} 执行失败: ${lastError?.message}, toolName: config.name, }; } return { status: failed, error: ${config.name} 重试 ${config.maxRetries} 次后仍失败: ${lastError?.message}, toolName: config.name, }; } private async callWithTimeoutTParams, TResult( toolFn: ToolFunctionTParams, TResult, params: TParams, timeoutMs: number, ): PromiseTResult { return new PromiseTResult((resolve, reject) { const timer setTimeout( () reject(new Error(工具调用超时 (${timeoutMs}ms))), timeoutMs, ); toolFn(params) .then((result) { clearTimeout(timer); resolve(result); }) .catch((error) { clearTimeout(timer); reject(error); }); }); } private recordFailure(toolName: string): void { const count (this.failureCounts.get(toolName) || 0) 1; this.failureCounts.set(toolName, count); if (count this.circuitBreakerThreshold) { // 触发熔断——在冷却期内拒绝所有调用 this.circuitOpenUntil.set(toolName, Date.now() this.cooldownMs); } } private isCircuitOpen(toolName: string): boolean { const until this.circuitOpenUntil.get(toolName); if (!until) return false; return Date.now() until; } }四个关键设计决策指数退避 随机抖动——防止所有重试同时发起造成下游压力Schema 校验失败不走重试——因为模型输出格式问题不会因重试而改善熔断机制保护下游服务——单个工具连续失败 5 次后暂停 60 秒allowPartial标志区分降级和失败——搜索工具的失败可以让 Agent 凭知识回答支付工具的失败则必须阻断。四、异常处理在 Agent 决策中的反馈机制失败信息对 Agent Prompt 的注入。当工具调用失败后不应仅返回调用失败这个笼统信息。需要将失败类型超时/校验失败/熔断、失败原因和建议的替代方案写入下次 Agent 调用的上下文。示例失败反馈工具 search_knowledge_base 调用失败 - 原因连接超时30s - 影响无法检索相关知识 - 建议基于已获取的信息继续回答并告知用户检索功能暂时不可用这种结构化的失败反馈让 Agent 能做出合理的降级决策而非在缺少上下文的情况下猜测下一步。部分成功的处理。当 5 个工具中有 2 个成功、2 个降级、1 个失败时Agent 应聚合所有结果并告知用户以下信息基于部分数据得出。这个告知不应淹没在冗长的回复中而应该作为回复的首句。五、总结Agent 工具调用的异常处理框架三层防护——工具级超时/重试/校验、编排级依赖/替代/部分成功、Agent 级通知/降级/熔断熔断器保护下游——连续失败 5 次暂停工具 60 秒结构化失败反馈——让 Agent 理解失败原因并做出合理降级决策。落地建议为每个工具定义清晰的能力边界和失败场景SafeToolExecutor 封装所有工具的调用统一管理超时和重试将失败信息以结构化格式注入 Agent 上下文监控各工具的失败率和熔断触发次数持续优化超时阈值。