第2005篇:AI Agent的错误恢复——任务失败后的自我纠正机制设计
2026/4/30大约 5 分钟
第2005篇:AI Agent的错误恢复——任务失败后的自我纠正机制设计
适读人群:构建生产级AI Agent系统的工程师 | 阅读时长:约19分钟 | 核心价值:让Agent在遇到错误时优雅恢复,而不是直接崩溃
Agent上线后三天,我收到了一个用户投诉。
用户让Agent帮他修改三个订单的交货期。Agent执行到第二个订单时,那个订单刚好被锁定了(系统在做批量处理),工具返回了"订单被锁定,无法修改"的错误。
Agent看到这个错误,直接就放弃了,输出了"操作失败"。第一个和第三个订单都没有被处理。
用户不满意很正常——第一和第三个订单完全可以处理的,为什么因为中间一个失败就全部放弃?
这就是没有错误恢复机制的Agent的典型问题:一遇到错误就停下来,而不是想办法绕过去继续完成能完成的部分。
错误的类型与对应策略
不是所有错误都一样,需要分类处理:
public enum ErrorType {
/**
* 瞬时错误:稍后重试可能成功
* 例:网络超时、临时的系统繁忙
*/
TRANSIENT,
/**
* 资源冲突:需要等待或换个方式
* 例:订单被锁定、文件被占用
*/
RESOURCE_CONFLICT,
/**
* 输入错误:参数有问题,LLM需要纠正
* 例:日期格式错误、参数缺失
*/
INPUT_ERROR,
/**
* 权限错误:该操作没有授权
* 例:尝试修改其他用户的数据
*/
PERMISSION_ERROR,
/**
* 不可恢复错误:需要终止并告知用户
* 例:目标资源不存在
*/
FATAL
}
@Data
@Builder
public class ToolError {
private String toolName;
private ErrorType errorType;
private String message;
private Map<String, Object> context; // 帮助LLM理解错误的上下文
private List<String> suggestions; // 可能的恢复建议
private boolean isRetryable;
private int retryAfterSeconds; // TRANSIENT错误时的建议等待时间
}带自我纠正的工具执行器
@Service
@Slf4j
@RequiredArgsConstructor
public class SelfCorrectingToolExecutor {
private final ToolRegistry toolRegistry;
private final ChatClient correctionClient;
// 同一工具在同一任务中的最大重试次数
private static final int MAX_RETRIES_PER_TOOL = 3;
/**
* 执行工具,支持自动重试和错误纠正
*/
public ToolExecutionResult executeWithCorrection(
String toolName,
Map<String, Object> params,
AgentContext agentContext) {
int attempt = 0;
ToolError lastError = null;
while (attempt < MAX_RETRIES_PER_TOOL) {
attempt++;
try {
String result = toolRegistry.executeTool(toolName, params);
// 检查结果是否包含错误标志(有些工具用返回值而非异常来表示错误)
if (isErrorResponse(result)) {
lastError = parseErrorFromResponse(toolName, result);
} else {
return ToolExecutionResult.success(result);
}
} catch (Exception e) {
lastError = classifyException(toolName, e, params);
}
// 根据错误类型决定下一步
if (lastError == null) break;
switch (lastError.getErrorType()) {
case TRANSIENT:
// 等待后重试
if (attempt < MAX_RETRIES_PER_TOOL) {
log.info("工具{}瞬时错误,{}秒后重试(第{}次)",
toolName, lastError.getRetryAfterSeconds(), attempt);
sleepSeconds(lastError.getRetryAfterSeconds());
continue;
}
break;
case INPUT_ERROR:
// 请LLM纠正参数
Map<String, Object> correctedParams =
requestParameterCorrection(toolName, params, lastError, agentContext);
if (correctedParams != null) {
params = correctedParams;
continue;
}
break;
case RESOURCE_CONFLICT:
// 这类错误通常需要人工干预或换策略,不要傻等
return ToolExecutionResult.skippable(lastError);
case PERMISSION_ERROR:
case FATAL:
// 不可恢复,立即终止
return ToolExecutionResult.fatal(lastError);
}
}
return ToolExecutionResult.failed(lastError);
}
/**
* 让LLM根据错误信息纠正参数
*/
private Map<String, Object> requestParameterCorrection(
String toolName,
Map<String, Object> originalParams,
ToolError error,
AgentContext context) {
String correctionPrompt = """
工具调用失败了,需要你纠正参数:
工具名称: %s
原始参数: %s
错误信息: %s
错误类型: %s
建议: %s
请分析错误原因,返回修正后的参数(JSON格式),如果无法修正请返回null。
""".formatted(
toolName,
params2json(originalParams),
error.getMessage(),
error.getErrorType(),
String.join("; ", error.getSuggestions())
);
try {
String response = correctionClient.prompt()
.user(correctionPrompt)
.call()
.content();
if (response.trim().equalsIgnoreCase("null")) return null;
return parseParamsFromResponse(response);
} catch (Exception e) {
log.warn("参数纠正失败", e);
return null;
}
}
private ToolError classifyException(String toolName, Exception e, Map<String, Object> params) {
String message = e.getMessage();
if (message == null) {
return ToolError.builder()
.toolName(toolName).errorType(ErrorType.FATAL)
.message("未知错误").isRetryable(false).build();
}
// 根据异常信息分类
if (message.contains("timeout") || message.contains("connection refused")) {
return ToolError.builder()
.toolName(toolName).errorType(ErrorType.TRANSIENT)
.message(message).isRetryable(true).retryAfterSeconds(2).build();
}
if (message.contains("locked") || message.contains("conflict")) {
return ToolError.builder()
.toolName(toolName).errorType(ErrorType.RESOURCE_CONFLICT)
.message(message).isRetryable(false)
.suggestions(List.of("等待锁释放后再试", "跳过该项处理其他项"))
.build();
}
if (message.contains("invalid") || message.contains("format")) {
return ToolError.builder()
.toolName(toolName).errorType(ErrorType.INPUT_ERROR)
.message(message).isRetryable(true)
.suggestions(List.of("检查参数格式", "确认参数值的合法性"))
.build();
}
if (message.contains("permission") || message.contains("unauthorized")) {
return ToolError.builder()
.toolName(toolName).errorType(ErrorType.PERMISSION_ERROR)
.message(message).isRetryable(false).build();
}
return ToolError.builder()
.toolName(toolName).errorType(ErrorType.FATAL)
.message(message).isRetryable(false).build();
}
}带部分成功处理的批量Agent
针对最开始说的问题(处理多个订单时中间失败),设计了"部分成功"模式:
@Service
@RequiredArgsConstructor
@Slf4j
public class BatchTaskAgent {
private final SelfCorrectingToolExecutor executor;
private final ChatClient plannerClient;
/**
* 执行批量任务,允许部分项目失败
* 失败的项目会被收集并在最终报告中说明
*/
public BatchTaskResult executeBatch(
List<String> items,
String operationTemplate,
AgentContext context) {
List<ItemResult> results = new ArrayList<>();
for (String item : items) {
log.info("处理批量任务项: {}", item);
ItemResult itemResult = processItem(item, operationTemplate, context);
results.add(itemResult);
// 如果是权限错误,整个批次终止(权限问题会影响所有项)
if (itemResult.getErrorType() == ErrorType.PERMISSION_ERROR) {
log.warn("权限错误,终止批量任务");
break;
}
}
// 汇总结果
long succeeded = results.stream().filter(ItemResult::isSucceeded).count();
long failed = results.size() - succeeded;
String summary = buildBatchSummary(results);
return BatchTaskResult.builder()
.totalItems(items.size())
.succeededCount((int) succeeded)
.failedCount((int) failed)
.itemResults(results)
.summary(summary)
.build();
}
private String buildBatchSummary(List<ItemResult> results) {
StringBuilder sb = new StringBuilder();
List<ItemResult> succeeded = results.stream()
.filter(ItemResult::isSucceeded).collect(Collectors.toList());
List<ItemResult> failed = results.stream()
.filter(r -> !r.isSucceeded()).collect(Collectors.toList());
sb.append("处理完成,共 ").append(results.size()).append(" 项\n");
sb.append("✅ 成功: ").append(succeeded.size()).append(" 项\n");
if (!succeeded.isEmpty()) {
succeeded.forEach(r -> sb.append(" - ").append(r.getItemId())
.append(": ").append(r.getMessage()).append("\n"));
}
if (!failed.isEmpty()) {
sb.append("❌ 失败: ").append(failed.size()).append(" 项\n");
failed.forEach(r -> sb.append(" - ").append(r.getItemId())
.append(": ").append(r.getErrorMessage()).append("\n"));
}
return sb.toString();
}
}错误恢复的边界
不是所有错误都应该尝试恢复。有几条原则我坚持:
永远不要静默忽略错误:就算某个子任务失败了,最终回复里必须明确告知用户哪些失败了
不要无限重试:每个工具最多重试3次,防止雪崩
权限错误不重试:权限问题不是时序问题,重试只是浪费时间和资源
向用户透明:用户应该知道"有3个订单处理成功了,有1个因为系统锁定没处理",而不是看到一个笼统的"操作失败"
做好错误恢复,才能让Agent在复杂的现实世界里可靠地工作。
