Subagents错误处理最佳实践

子代理错误处理指南

一、防御性prompt设计

在使用子代理架构时,防御性prompt设计是第一道防线。通过在prompt中明确告知子代理如何处理错误和边界情况,可以从源头减少异常行为的发生。防御性prompt的核心思想是"预防胜于治疗"——在子代理执行任务之前,就为其设定清晰的行为边界。

1. 明确告知子代理如何处理错误情况

在子代理的system prompt中,明确描述各种错误场景的应对策略。例如,当子代理无法获取所需数据时,是应该重试、返回空值还是上报异常?提前定义这些规则可以避免子代理在错误发生时做出不可预期的行为。

错误处理prompt示例: // 当遇到API调用失败时 // 1. 自动重试最多3次,间隔1秒 // 2. 重试后仍然失败,返回明确的错误标志 // 3. 在response中附加详细的错误信息 if (api_call_failed) { retry(3, 1000); if (still_failed) { return { error: true, type: "API_ERROR", detail: error_message }; } }

2. 提供错误处理的示例和模板

在prompt中嵌入标准的错误处理代码模板,子代理在遇到类似场景时可以直接复用。这不仅提高了代码的一致性,也降低了子代理自行发明错误处理逻辑可能引入的风险。

// 标准错误处理模板 function safeExecute(task) { try { return { success: true, data: task() }; } catch (error) { return { success: false, errorType: error.constructor.name, message: error.message, timestamp: new Date().toISOString() }; } }

3. 指定子代理在不确定时的默认行为

当子代理面对模糊或不完整的指令时,必须有一个明确的默认行为。推荐的做法是:优先询问澄清,而不是自行猜测。可以在prompt中设置一个"不确定阈值",当置信度低于该阈值时,自动触发询问流程。

在prompt中明确声明:"如果你对用户意图的置信度低于80%,请列出你的假设并请求用户确认,不要自行假设并继续执行。"

4. 要求子代理报告遇到的任何问题

在prompt中明确要求子代理记录并上报所有异常情况,即使这些异常最终被成功处理。完整的错误报告可以帮助主代理和开发者了解系统的运行状况,发现潜在的风险点。

核心要点:防御性prompt不是限制子代理的能力,而是为子代理提供清晰的决策边界。好的防御性prompt应该像交通规则一样——不告诉司机怎么开车,但告诉司机在各种路况下应该怎么做。

二、错误传播控制

在多子代理协作系统中,错误传播控制是保证系统稳定性的关键。一个子代理的失败如果不加控制地传播,可能导致整个系统的连锁崩溃。合理的错误传播策略需要在信息透明和系统隔离之间取得平衡。

1. 子代理的错误应传播给主代理而不是静默忽略

最常见的错误是子代理在遇到异常时静默吞掉错误,返回一个看似正常但实际上不完整的结果。这种行为会误导主代理做出错误的决策。正确的做法是:子代理应当将错误信息完整地传递给主代理,让主代理掌握系统的真实状态。

// 错误:静默忽略错误 function processBad() { try { return fetchData(); } catch (e) { return { data: [] }; // 静默返回空数据,主代理以为一切正常 } } // 正确:错误传播给主代理 function processGood() { try { return { status: "ok", data: fetchData() }; } catch (e) { return { status: "error", errorType: e.name, message: e.message, recovered: false, data: null }; } }

2. 错误的传播范围应受控

错误的传播应该遵循"最小传播原则"——错误只传递给直接相关的组件,不广播给不相关的子代理。可以使用层级化的错误处理机制:子代理的错误先由父级代理处理,只在必要时才继续向上一级传播。

// 层级化错误传播 class AgentError { constructor(sourceAgent, error, severity) { this.sourceAgent = sourceAgent; this.error = error; this.severity = severity; // "low" | "medium" | "high" | "critical" this.propagatedTo = []; } shouldPropagateToParent() { return this.severity === "critical"; } propagateTo(targetAgent) { this.propagatedTo.push(targetAgent.id); targetAgent.receiveError(this); } }

3. 主代理接收错误后决定是否重新分配

当主代理收到子代理的错误报告后,应该根据错误类型和严重程度做出判断:对于临时性错误,可以尝试重新分配任务给同一子代理;对于持续失败的情况,应该将任务重新分配给其他子代理并标记原子代理为异常状态。

function handleSubAgentError(task, error, subAgent) { if (error.isRetryable && subAgent.retryCount < MAX_RETRIES) { subAgent.retryCount++; return reassignTask(task, subAgent); } if (hasAlternativeAgent(task)) { return reassignTask(task, getAlternativeAgent(task)); } return { status: "failed", task, error, requiresUserAttention: true }; }

4. 避免错误的级联传播

级联传播是多代理系统中最危险的错误模式之一。当子代理A的错误导致子代理B异常,B的异常又影响到C,形成连锁反应。避免级联传播的关键在于:在每个子代理的边界设置"错误隔离区",确保一个子代理的失败不会影响其他子代理的正常运行。

最佳实践:为每个子代理设置独立的超时时间和错误上下文。当一个子代理超时或出错时,立即终止其执行并回收资源,不等待其他子代理的完成状态。同时,使用断路器模式(Circuit Breaker)防止连续失败的子代理被反复调用。

三、自动重试和降级

在实际运行中,许多错误是暂时性的,如网络波动、服务短暂不可用等。合理的自动重试机制可以大幅提高系统的整体可用性。同时,当重试无法解决问题时,系统应该优雅地降级,提供部分可用的结果。

1. 可恢复错误自动重试

对于网络超时、服务暂时不可用、资源竞争等可恢复的错误,应该配置自动重试机制。重试策略需要包含:最大重试次数、重试间隔(可以指数退避)、重试条件(仅对特定类型的错误重试)。

class RetryStrategy { constructor(maxRetries = 3, baseDelay = 1000, backoffFactor = 2) { this.maxRetries = maxRetries; this.baseDelay = baseDelay; this.backoffFactor = backoffFactor; } async execute(task) { let lastError; for (let i = 0; i < this.maxRetries; i++) { try { return await task(); } catch (error) { lastError = error; if (!this.isRetryable(error)) throw error; await this.delay(this.baseDelay * Math.pow(this.backoffFactor, i)); } } throw lastError; } isRetryable(error) { return ['TimeoutError', 'NetworkError', 'RateLimitError'] .includes(error.name); } }

2. 不可恢复错误直接上报

逻辑错误、权限拒绝、数据校验失败等不可恢复的错误不应该重试。这类错误反映了系统设计或输入数据的根本问题,重试只会浪费资源。这类错误应该立即上报给主代理或开发者,附带完整的上下文信息以便诊断根因。

注意:区分可恢复和不可恢复错误的标准是——如果重试后错误的根本原因没有改变,那么这个错误就是不可恢复的。例如,权限拒绝错误不会因为重试而自动获得权限,所以不应该重试。

3. 部分结果可用时继续工作

当子代理的某个子任务失败,但部分结果仍然可用时,系统应该支持使用部分结果继续执行。这一策略称为"部分成功"(Partial Success)模式,可以显著提高系统的鲁棒性和用户体验。

function processWithPartialResults(items, processor) { const results = []; const errors = []; for (const item of items) { try { results.push(processor(item)); } catch (error) { errors.push({ item, error: error.message }); continue; // 跳过失败项,继续处理剩余项 } } return { successCount: results.length, failureCount: errors.length, results, errors, // 告知调用方哪些项失败了 partial: errors.length > 0 // 标记是否为部分结果 }; }

核心要点:自动重试和降级策略的选择需要权衡系统的可用性和资源消耗。重试次数过多可能放大系统负载,不重试又可能降低可用性。推荐采用"快速失败+受控重试"的策略:对于明显不可恢复的错误立即失败,对于临时性错误有节制地重试。

四、错误日志 / Audit

没有日志的错误处理是不完整的。完善的错误日志系统不仅帮助排查问题,还能通过趋势分析发现系统的薄弱环节,指导架构优化。对于多子代理系统,日志的粒度、结构和可查询性尤为重要。

1. 记录所有子代理的错误到审计日志

每一个错误,无论是否被成功恢复,都应该被记录到审计日志中。记录的标准应该包含完整的上下文信息,而不是简单的错误消息。完整的日志对于事后分析和系统改进至关重要。

2. 日志包含的关键字段

每条错误日志至少应该包含以下字段,以便后续的查询和分析:

{ timestamp: "2026-05-08T10:23:43.000Z", // 精确到毫秒的时间戳 agentId: "subagent-data-fetcher-01", // 报错的子代理ID taskId: "task-xyz-789", // 当前执行的任务ID errorType: "TimeoutError", // 错误类型 severity: "warning", // 严重程度: fatal/error/warning/info detail: "API call timeout after 5000ms", // 错误详情 recoveryAction: "retry(3)->success", // 恢复操作及结果 context: { // 上下文信息 requestPayload: { ... }, responseStatus: 504, duration: 5123 } }

3. 定期分析错误模式和趋势

日志的真正价值在于分析。通过定期(如每日或每周)的错误分析,可以发现以下有价值的信息:

推荐做法:建立错误仪表盘(Error Dashboard),将错误日志可视化。通过图表展示错误率趋势、Top-N错误类型、各子代理的错误分布等关键指标。当错误率超过预设阈值时,自动触发告警通知。

五、用户通知

错误处理的最终目标是保证用户体验。不是所有的错误都需要通知用户,但那些影响最终结果的错误必须及时、清晰地向用户呈现。好的用户通知应该提供足够的信息让用户了解发生了什么、影响范围有多广、以及接下来可以做什么。

1. 影响结果的错误及时通知用户

当子代理的错误导致最终输出不完整、不准确或延迟时,必须通知用户。通知的时机应该是在错误发生并且系统无法自动恢复之后。避免在系统还在自动重试时就通知用户,这会导致不必要的用户困扰。

2. 提供错误详情和影响范围

用户通知应该包含以下信息,帮助用户理解问题的性质和严重程度:

// 用户通知格式示例 { notificationType: "partial_result", title: "部分数据加载失败", message: "在获取市场数据时,美股部分(AAPL、GOOGL) 遇到临时连接问题,已自动重试3次仍未成功。", impact: "已完成:中国市场数据(正常) 待补全:美股市场数据(加载失败)", suggestions: [ "请稍后刷新页面查看美股数据", "如需即时数据,可手动输入股票代码查询" ], estimatedRecovery: "系统将在5分钟后自动重试" }

3. 提供修复建议或替代方案

仅仅告知用户出了问题是不够的,优秀的错误处理还应该为用户提供可操作的下一步建议。这些建议应该基于错误的类型和系统的当前状态,给出切实可行的解决方案。例如:

核心要点:用户通知的设计原则是"该知道的必须知道,不该知道的不要打扰"。对于系统内部的技术细节(如堆栈跟踪、服务名称、内部IP等),不应该展示给终端用户。用清晰的中文描述问题,并始终提供解决方案或下一步行动建议。