AI Agent 的容错与韧性:从错误处理到生产级可靠性保障的 Java 实战

AI Agent 的容错与韧性:从错误处理到生产级可靠性保障的 Java 实战

系列文章

  1. 理解 AI Agent 的大脑:ReAct 模式从入门到实战
  2. AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
  3. AI Agent 的记忆力是怎么实现的——LangChain4j Memory 机制深度解析
  4. AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
  5. AI Agent 的灵魂对话:Prompt Engineering 系统提示词设计的艺术与工程
  6. AI Agent 的规划大脑:从任务分解到自适应执行策略
  7. AI Agent 的推理引擎:从 Chain-of-Thought 到推理模型的深度解析与 Java 实战
  8. AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
  9. AI Agent 的可观测性:从链路追踪到成本监控的 Java 实战
  10. AI Agent 的安全防线:Prompt 注入防御与生产级安全防护实战
  11. AI Agent 评估与优化:从基准测试到生产环境的质量守护实战
  12. AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战
  13. AI Agent 的容错与韧性:从错误处理到生产级可靠性保障的 Java 实战(本文)

前言:你的 Agent 翻过车吗?

你精心设计了一个 AI Agent,Demo 演示行云流水,老板拍手叫好。上线第一天,Agent 就给你来了个”惊喜”:

  • 调用天气 API 超时,整个对话卡死 30 秒后返回一个莫名其妙的错误
  • LLM 偶尔返回了格式错误的 JSON,下游解析直接 NPE
  • 用户输入了一段超长文本,Token 爆了,Agent 直接”失忆”
  • 连续 50 个请求都是恶意 Prompt 注入,Agent 被”玩坏了”

这不是段子,这是每一个把 Agent 部署到生产环境的开发者都会遇到的真实场景。

传统软件的错误处理相对简单——网络超时就重试,数据库挂了就切主从,代码 Bug 就 Hotfix。但 AI Agent 的错误模式完全不同:它的核心”大脑”是一个概率系统,LLM 的输出天然具有不确定性。同样的输入,两次调用可能得到不同的结果;格式化要求再严格,偶尔也会输出不符合 JSON Schema 的文本。

这篇文章就来聊聊:如何让 AI Agent 在各种”翻车”场景下依然能优雅地工作。我们会从 Agent 的独特失败模式讲起,逐步深入到重试策略、Fallback 降级、断路器模式、Human-in-the-Loop 等生产级容错机制,并用 LangChain4j 和 Spring AI 给出完整的 Java 实战代码。

AI Agent 的独特失败模式

在设计容错方案之前,我们需要先理解 Agent 到底会怎么”挂”。和传统微服务相比,Agent 的失败模式有三个显著不同:

1. LLM 调用的不确定性

传统 RPC 调用是确定性的——给定相同输入,总是返回相同输出。但 LLM 不一样:

1
2
3
// 同样的 Prompt,两次调用可能返回不同结果
String result1 = chatModel.call("今天星期几?"); // "今天是星期六"
String result2 = chatModel.call("今天星期几?"); // "让我想想,今天应该是周六"

这种不确定性带来的问题远不止”答案不一样”。更严重的是,LLM 可能返回结构上就不对的内容——你要求 JSON,它给你返回了一段自然语言;你要求枚举值,它给你返回了一个不在枚举中的词。

2. 级联失败的放大效应

Agent 的调用链通常很长:用户输入 → LLM 理解 → 选择工具 → 执行工具 → LLM 总结 → 返回结果。任何一环出错,后续全部失败。而且由于 LLM 的不确定性,重试不一定能解决问题——甚至可能让问题更糟(比如 LLM 在重试时改变了工具选择策略)。

3. 资源消耗的不可预测性

传统 API 调用的资源消耗基本恒定,但 Agent 的 Token 消耗完全不可预测。一个看似简单的用户问题,可能触发 Agent 进入一个复杂的推理循环,消耗大量 Token 和时间。

理解了这些独特性,我们就能有针对性地设计容错方案了。

重试策略:不只是 for 循环加 sleep

重试是最基础的容错手段,但 Agent 场景下的重试远比传统场景复杂。

指数退避 + 抖动

对于瞬时故障(如 API 限流、网络抖动),指数退避是最经典的策略。但在 Agent 场景中,我们需要加上抖动(Jitter),避免多个 Agent 实例在同一时刻重试导致”惊群效应”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
public class ResilientLlmClient {

private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
private static final double JITTER_FACTOR = 0.3;

private final ChatModel chatModel;
private final Random random = new Random();

public String callWithRetry(String prompt) {
Exception lastException = null;

for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
return chatModel.call(prompt);
} catch (Exception e) {
lastException = e;

if (!isRetryable(e) || attempt == MAX_RETRIES) {
break;
}

// 指数退避 + 随机抖动
long baseDelay = BASE_DELAY_MS * (1L << attempt);
long jitter = (long) (baseDelay * JITTER_FACTOR * random.nextDouble());
long delay = baseDelay + jitter;

log.warn("LLM 调用失败,第 {} 次重试,等待 {}ms", attempt + 1, delay, e);
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}

throw new LlmCallFailedException("LLM 调用在 " + MAX_RETRIES + " 次重试后仍然失败", lastException);
}

private boolean isRetryable(Exception e) {
// 可重试的异常类型
if (e instanceof HttpTimeoutException) return true;
if (e instanceof RateLimitException) return true;
if (e instanceof ServiceUnavailableException) return true;

// 429 Too Many Requests
if (e instanceof HttpClientErrorException httpEx) {
return httpEx.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS
|| httpEx.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE;
}

return false;
}
}

模型降级重试

比普通重试更聪明的策略是模型降级:主力模型失败时,切换到备用模型。这就像传统架构中的主从切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component
public class ModelFallbackChain {

// 模型优先级链:高质量 → 中等 → 低成本
private final List<ModelCandidate> modelChain;

public ModelFallbackChain(
@Qualifier("primaryModel") ChatModel primary,
@Qualifier("fallbackModel") ChatModel fallback,
@Qualifier("emergencyModel") ChatModel emergency) {
this.modelChain = List.of(
new ModelCandidate("gpt-4o", primary, 0.005),
new ModelCandidate("gpt-4o-mini", fallback, 0.00015),
new ModelCandidate("gpt-3.5-turbo", emergency, 0.0005)
);
}

public ModelResult callWithFallback(String systemPrompt, String userPrompt) {
List<String> errors = new ArrayList<>();

for (ModelCandidate candidate : modelChain) {
try {
long start = System.currentTimeMillis();
String result = candidate.model().call(systemPrompt + "\n\n" + userPrompt);
long latency = System.currentTimeMillis() - start;

return new ModelResult(result, candidate.name(), latency,
candidate != modelChain.get(0)); // 标记是否降级
} catch (Exception e) {
errors.add(candidate.name() + ": " + e.getMessage());
log.warn("模型 {} 调用失败,尝试下一个: {}", candidate.name(), e.getMessage());
}
}

throw new AllModelsFailedException("所有模型均调用失败: " + errors);
}

record ModelCandidate(String name, ChatModel model, double costPer1kTokens) {}
record ModelResult(String content, String modelUsed, long latencyMs, boolean wasDegraded) {}
}

关键设计点:降级链中的模型应该是”能力递减但稳定性递增”的。GPT-4o 推理能力最强但偶尔超时,GPT-4o-mini 稍弱但更稳定,GPT-3.5-turbo 最快最便宜。根据业务场景决定降级深度——对于客服场景,降级到 mini 完全可以接受;对于医疗诊断场景,宁可失败也不能降级。

参数调整重试

有时候 LLM 返回错误不是因为服务不可用,而是因为输出格式不对。这时候不应该原样重试,而应该调整参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public String callWithFormatRetry(String prompt, Class<?> expectedType) {
// 第一次:正常调用
String result = chatModel.call(prompt);

if (isValidJson(result, expectedType)) {
return result;
}

// 第二次:降低 temperature,增加格式约束
ChatOptions strictOptions = ChatOptions.builder()
.temperature(0.0) // 降低随机性
.responseFormat(ResponseFormat.builder()
.type(ResponseFormat.Type.JSON_SCHEMA)
.schema(extractSchema(expectedType))
.build())
.build();

result = chatModel.call(new Prompt(prompt, strictOptions)).getResult().getOutput().getContent();

if (isValidJson(result, expectedType)) {
return result;
}

// 第三次:用更强的模型 + 更严格的提示词
String strictPrompt = prompt + "\n\n注意:你必须且只能返回有效的 JSON,不要包含任何其他文字、解释或 markdown 标记。";
return modelFallbackChain.callWithFallback("你是一个严格的 JSON 输出器。", strictPrompt).content();
}

这种”逐步加约束”的重试策略,比简单重复调用有效得多。

Fallback 降级:多层兜底方案

当重试也无法解决问题时,我们需要 Fallback——提供一个”次优但可用”的结果。Fallback 的核心思想是:有总比没有好

分层 Fallback 架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Service
public class AgentFallbackService {

private final ChatModel chatModel;
private final CacheService cacheService; // 历史对话缓存
private final RuleEngine ruleEngine; // 规则引擎兜底
private final TemplateEngine templateEngine;

/**
* 分层 Fallback:
* L1: LLM 正常调用
* L2: LLM 简化调用(短 Prompt、低 Token)
* L3: 缓存命中(相似问题的历史回答)
* L4: 规则引擎(基于关键词的预设回答)
* L5: 模板兜底(通用话术)
*/
public String handleWithFallback(UserRequest request) {
// L1: 正常 LLM 调用
try {
return chatModel.call(buildFullPrompt(request));
} catch (Exception e) {
log.warn("L1 失败: {}", e.getMessage());
}

// L2: 简化调用(减少 Token 消耗,降低超时概率)
try {
String simplifiedPrompt = "请简要回答:" + request.getQuestion();
return chatModel.call(simplifiedPrompt);
} catch (Exception e) {
log.warn("L2 失败: {}", e.getMessage());
}

// L3: 缓存命中
Optional<String> cached = cacheService.findSimilarAnswer(request.getQuestion());
if (cached.isPresent()) {
log.info("L3 缓存命中");
return cached.get();
}

// L4: 规则引擎
Optional<String> ruleResult = ruleEngine.match(request.getQuestion());
if (ruleResult.isPresent()) {
log.info("L4 规则引擎命中");
return ruleResult.get();
}

// L5: 模板兜底
log.info("L5 模板兜底");
return templateEngine.render("fallback-response", Map.of(
"question", request.getQuestion(),
"supportEmail", "support@example.com"
));
}
}

为什么 L3(缓存)放在 L2(简化调用)之后? 因为 L2 的成功率通常很高(短 Prompt 更不容易超时),而且返回的是实时生成的内容,比缓存更”新鲜”。缓存只有在 LLM 完全不可用时才使用。

工具调用的 Fallback

Agent 不只是调用 LLM,还要调用各种工具。工具调用同样需要 Fallback:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ToolExecutionWithFallback {

private final Map<String, ToolExecutor> primaryTools;
private final Map<String, ToolExecutor> backupTools;

public ToolResult executeWithFallback(String toolName, Map<String, Object> params) {
ToolExecutor primary = primaryTools.get(toolName);
ToolExecutor backup = backupTools.get(toolName);

// 尝试主工具
try {
return primary.execute(params);
} catch (ToolExecutionException e) {
log.warn("主工具 {} 执行失败: {}", toolName, e.getMessage());

// 尝试备用工具
if (backup != null) {
try {
ToolResult result = backup.execute(params);
result.setDegraded(true); // 标记为降级结果
return result;
} catch (Exception backupEx) {
log.error("备用工具 {} 也失败了: {}", toolName, backupEx.getMessage());
}
}

// 返回"软失败"结果,让 LLM 决定下一步
return ToolResult.softFailure(
"工具 " + toolName + " 暂时不可用,请基于你的知识尝试回答," +
"或者告诉用户该功能暂时不可用。"
);
}
}
}

注意最后的 ToolResult.softFailure()——这不是抛异常,而是返回一个包含错误信息的结果给 LLM。这样 LLM 可以自主决定是用自身知识回答,还是告知用户功能暂时不可用。这就是 Agent 和传统软件的根本区别:Agent 有能力理解和应对失败

断路器模式:保护你的 LLM 预算

如果说重试和 Fallback 是”事后补救”,那断路器就是”事前预防”。当 LLM 服务连续失败时,断路器会”跳闸”,直接拒绝后续请求,避免无意义的重试消耗资源和预算。

三态断路器

断路器有三个状态:关闭(正常)打开(拒绝请求)半开(试探恢复)。用 Resilience4j 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Configuration
public class CircuitBreakerConfig {

@Bean
public CircuitBreaker llmCircuitBreaker() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率超过 50% 触发跳闸
.slowCallRateThreshold(80) // 慢调用率超过 80% 触发跳闸
.slowCallDurationThreshold(Duration.ofSeconds(15)) // 超过 15 秒算慢调用
.waitDurationInOpenState(Duration.ofSeconds(30)) // 跳闸后等 30 秒进入半开
.permittedNumberOfCallsInHalfOpenState(3) // 半开状态允许 3 次试探
.slidingWindowSize(20) // 滑动窗口:最近 20 次调用
.minimumNumberOfCalls(5) // 至少 5 次调用才开始计算
.recordExceptions(
HttpTimeoutException.class,
RateLimitException.class,
ServiceUnavailableException.class
)
.ignoreExceptions(
ValidationException.class, // 参数错误不算服务故障
PromptTooLongException.class // Token 超限是调用方的问题
)
.build();

return CircuitBreaker.of("llm-service", config);
}

@Bean
public CircuitBreaker toolCircuitBreaker() {
// 工具调用的断路器配置更宽松
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(70) // 工具容错率更高
.waitDurationInOpenState(Duration.ofSeconds(10)) // 恢复更快
.slidingWindowSize(10)
.build();

return CircuitBreaker.of("tool-service", config);
}
}

@Service
public class CircuitBreakerProtectedAgent {

private final CircuitBreaker llmCircuitBreaker;
private final CircuitBreaker toolCircuitBreaker;
private final ModelFallbackChain fallbackChain;

public AgentResponse process(UserRequest request) {
// LLM 调用受断路器保护
String llmResult = CircuitBreaker.decorateSupplier(llmCircuitBreaker, () -> {
return fallbackChain.callWithFallback(
buildSystemPrompt(), request.getQuestion()
).content();
}).apply();

// 工具调用受独立断路器保护
if (needsToolCall(llmResult)) {
ToolResult toolResult = CircuitBreaker.decorateSupplier(toolCircuitBreaker, () -> {
return toolExecutor.execute(parseToolCall(llmResult));
}).apply();

return composeResponse(llmResult, toolResult);
}

return AgentResponse.of(llmResult);
}
}

为什么 LLM 和工具需要独立的断路器? 因为它们的故障模式不同。LLM 可能因为限流而不可用,但工具(如数据库查询)可能正常工作;反过来也一样。独立断路器让它们互不影响。

断路器 + Fallback 的组合

断路器跳闸后,请求直接走 Fallback 路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public AgentResponse processWithFullResilience(UserRequest request) {
Supplier<String> llmCall = () -> chatModel.call(buildPrompt(request));
Supplier<String> fallback = () -> fallbackService.getCachedOrTemplate(request);

// 断路器保护 + Fallback 兜底
String result = Try.ofSupplier(
CircuitBreaker.decorateSupplier(llmCircuitBreaker, llmCall)
).recover(CallNotPermittedException.class, e -> {
log.warn("断路器跳闸,走 Fallback 路径");
return fallback.get();
}).recover(Exception.class, e -> {
log.error("LLM 调用异常: {}", e.getMessage());
return fallback.get();
}).get();

return AgentResponse.of(result);
}

Human-in-the-Loop:让人类守住最后防线

有些场景下,Agent 不能”自作主张”——比如涉及资金操作、数据删除、医疗建议等高风险决策。这时候需要 Human-in-the-Loop(人在回路),让人类确认后再执行。

设计原则

Human-in-the-Loop 不是简单地”每步都问用户”,而是要智能地判断何时需要人类介入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public enum RiskLevel {
LOW, // 直接执行:查询天气、搜索文档
MEDIUM, // 执行后通知:发送邮件、创建任务
HIGH, // 执行前确认:删除数据、修改配置
CRITICAL // 必须人工审批:资金操作、医疗建议
}

@Component
public class RiskAssessor {

private static final Map<String, RiskLevel> TOOL_RISK_MAP = Map.of(
"search", RiskLevel.LOW,
"send_email", RiskLevel.MEDIUM,
"delete_record", RiskLevel.HIGH,
"transfer_money", RiskLevel.CRITICAL
);

public RiskLevel assess(String toolName, Map<String, Object> params) {
RiskLevel baseRisk = TOOL_RISK_MAP.getOrDefault(toolName, RiskLevel.MEDIUM);

// 参数级别的风险调整
if ("delete_record".equals(toolName)) {
Integer count = (Integer) params.getOrDefault("limit", 1);
if (count > 100) {
return RiskLevel.CRITICAL; // 批量删除需要最高级审批
}
}

if ("send_email".equals(toolName)) {
List<String> recipients = (List<String>) params.getOrDefault("to", List.of());
if (recipients.size() > 50) {
return RiskLevel.HIGH; // 群发邮件需要确认
}
}

return baseRisk;
}
}

确认机制实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Service
public class HumanInTheLoopAgent {

private final RiskAssessor riskAssessor;
private final ConfirmationService confirmationService;

public AgentResponse executeWithGuardrails(AgentContext context, String toolName,
Map<String, Object> params) {
RiskLevel risk = riskAssessor.assess(toolName, params);

return switch (risk) {
case LOW -> {
// 直接执行
ToolResult result = toolExecutor.execute(toolName, params);
yield AgentResponse.of(result);
}

case MEDIUM -> {
// 执行后异步通知
ToolResult result = toolExecutor.execute(toolName, params);
confirmationService.notifyAsync(context.getUserId(),
"Agent 已执行 " + toolName + ",结果: " + result.summary());
yield AgentResponse.of(result);
}

case HIGH -> {
// 同步确认(设置超时)
ConfirmationRequest confirmation = ConfirmationRequest.builder()
.userId(context.getUserId())
.action(toolName)
.params(params)
.timeout(Duration.ofMinutes(5))
.riskLevel(risk)
.build();

ConfirmationResponse response = confirmationService.confirm(confirmation);

if (response.isApproved()) {
ToolResult result = toolExecutor.execute(toolName, params);
yield AgentResponse.of(result);
} else {
yield AgentResponse.of("操作已取消。" + response.getReason());
}
}

case CRITICAL -> {
// 人工审批(可能需要多人审批)
ApprovalRequest approval = ApprovalRequest.builder()
.userId(context.getUserId())
.action(toolName)
.params(params)
.requiredApprovals(2) // 需要两人审批
.timeout(Duration.ofHours(24))
.build();

ApprovalResponse approvalResponse = confirmationService.requestApproval(approval);

if (approvalResponse.isApproved()) {
ToolResult result = toolExecutor.execute(toolName, params);
yield AgentResponse.of(result);
} else {
yield AgentResponse.of("操作需要人工审批,已提交审批流程。" +
"审批ID: " + approvalResponse.getApprovalId());
}
}
};
}
}

关键设计点:超时机制。用户可能不会立即响应确认请求,Agent 不能无限等待。设置合理的超时时间,超时后自动取消操作并通知用户。

Guardrails:输入输出的安全护栏

在之前的文章 AI Agent 的安全防线 中,我们详细讨论了 Prompt 注入防御。这里从容错的角度补充 Guardrails 的设计——它们不仅是安全机制,更是可靠性保障。

输入护栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
public class InputGuardrails {

private static final int MAX_INPUT_LENGTH = 10000;
private static final Pattern INJECTION_PATTERN = Pattern.compile(
"ignore previous|忽略之前的|disregard|forget your instructions",
Pattern.CASE_INSENSITIVE
);

public GuardrailResult validate(String userInput) {
// 长度检查
if (userInput.length() > MAX_INPUT_LENGTH) {
return GuardrailResult.rejected("输入过长,请精简到 " + MAX_INPUT_LENGTH + " 字以内");
}

// 注入检测
if (INJECTION_PATTERN.matcher(userInput).find()) {
return GuardrailResult.rejected("检测到潜在的安全风险,请重新表述您的问题");
}

// 空输入检查
if (userInput.isBlank()) {
return GuardrailResult.rejected("请输入您的问题");
}

return GuardrailResult.accepted();
}
}

输出护栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Component
public class OutputGuardrails {

private final JsonSchemaValidator schemaValidator;

public GuardrailResult validate(String llmOutput, OutputConstraints constraints) {
// 1. 空输出检查
if (llmOutput == null || llmOutput.isBlank()) {
return GuardrailResult.rejected("模型返回了空结果");
}

// 2. JSON 格式验证(如果要求结构化输出)
if (constraints.requiresJson()) {
try {
JsonNode json = objectMapper.readTree(llmOutput);
if (constraints.getJsonSchema() != null) {
schemaValidator.validate(json, constraints.getJsonSchema());
}
} catch (Exception e) {
return GuardrailResult.rejected("输出格式不符合要求: " + e.getMessage());
}
}

// 3. 内容安全检查
if (containsSensitiveContent(llmOutput)) {
return GuardrailResult.rejected("输出包含敏感内容,已拦截");
}

// 4. 长度检查
if (llmOutput.length() > constraints.getMaxLength()) {
return GuardrailResult.rejected("输出过长,已截断");
}

return GuardrailResult.accepted(llmOutput);
}

private boolean containsSensitiveContent(String content) {
// 检查是否包含 PII(个人身份信息)
// 检查是否包含有害内容
// 可以用正则、关键词或专门的内容审核 API
return false; // 简化实现
}
}

LangChain4j 和 Spring AI 的内置容错机制

在实际开发中,我们不一定需要从零实现所有容错逻辑。LangChain4j 和 Spring AI 都提供了一些内置的容错能力。

LangChain4j 的自动重试

LangChain4j 的 AiServices 在底层集成了自动重试:

1
2
3
4
5
6
7
8
9
10
11
12
13
// LangChain4j 内置的重试机制
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(
OpenAiChatModel.builder()
.apiKey(apiKey)
.modelName("gpt-4o")
.maxRetries(3) // 内置重试次数
.timeout(Duration.ofSeconds(30))
.logRequests(true)
.logResponses(true)
.build()
)
.build();

但 LangChain4j 的内置重试比较基础,不支持模型降级、断路器等高级特性。生产环境中建议结合 Resilience4j 使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
public class LangChain4jResilienceConfig {

@Bean
public ChatLanguageModel resilientModel() {
ChatLanguageModel delegate = OpenAiChatModel.builder()
.apiKey(apiKey)
.modelName("gpt-4o")
.build();

// 用 Resilience4j 包装
CircuitBreaker cb = CircuitBreaker.of("langchain4j", CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.build());

Retry retry = Retry.of("langchain4j", RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.retryExceptions(HttpTimeoutException.class, RateLimitException.class)
.build());

return new ResilientChatModel(delegate, cb, retry);
}
}

Spring AI 的 Advisor 链容错

Spring AI 的 Advisor Chain 提供了优雅的错误处理扩展点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
public class ResilientAdvisor implements CallAroundAdvisor {

private final ModelFallbackChain fallbackChain;

@Override
public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
try {
return chain.nextAroundCall(request);
} catch (Exception e) {
log.warn("Advisor 链执行失败,进入 Fallback: {}", e.getMessage());

// 降级到备用模型
String fallbackResult = fallbackChain.callWithFallback(
request.adviseContext().get("system").toString(),
request.userText()
).content();

return new AdvisedResponse(
new Generation(fallbackResult),
Map.of("fallback", true, "originalError", e.getMessage())
);
}
}

@Override
public String getName() {
return "ResilientAdvisor";
}

@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE; // 最高优先级,包裹整个链
}
}

生产环境最佳实践

1. 超时分层设置

不同调用应该有不同的超时时间:

1
2
3
4
5
6
7
# application.yml
agent:
timeout:
llm-call: 30s # 单次 LLM 调用
tool-call: 10s # 单次工具调用
total-agent-run: 120s # 整个 Agent 运行(含多轮推理)
human-confirmation: 300s # 等待人类确认

2. 资源预算控制

防止 Agent “烧钱”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class BudgetGuard {

private final MeterRegistry meterRegistry;
private static final double MAX_COST_PER_REQUEST = 0.10; // 单次请求最大成本 $0.10
private static final double MAX_DAILY_COST = 50.0; // 每日最大成本 $50

public BudgetCheckResult checkBudget(String userId) {
double dailyCost = meterRegistry.get("agent.cost.daily")
.tag("user", userId)
.counter().count();

if (dailyCost >= MAX_DAILY_COST) {
return BudgetCheckResult.exceeded("今日使用额度已用完,请明天再来");
}

return BudgetCheckResult.ok(MAX_DAILY_COST - dailyCost);
}

public void recordCost(String userId, double cost) {
meterRegistry.counter("agent.cost.daily", "user", userId).increment(cost);
}
}

3. 优雅降级的用户提示

当 Agent 进入降级模式时,要透明地告知用户,而不是默默返回低质量结果:

1
2
3
4
5
6
7
8
9
10
public String formatDegradedResponse(String content, DegradationReason reason) {
String notice = switch (reason) {
case MODEL_UNAVAILABLE -> "⚠️ 当前 AI 服务繁忙,以下回答基于简化模型生成,可能不如平时准确。";
case TOOL_UNAVAILABLE -> "⚠️ 部分功能暂时不可用,以下回答可能不完整。";
case BUDGET_EXCEEDED -> "⚠️ 今日使用额度即将用完,已切换到轻量模式。";
case RATE_LIMITED -> "⚠️ 请求过于频繁,请稍后再试。";
};

return notice + "\n\n" + content;
}

4. 失败事件的可观测性

结合之前文章 AI Agent 的可观测性 中的方案,为每次失败建立完整的追踪链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class FailureEventPublisher {

private final ApplicationEventPublisher eventPublisher;

public void publishFailure(AgentContext context, Exception error, RecoveryAction action) {
AgentFailureEvent event = AgentFailureEvent.builder()
.traceId(context.getTraceId())
.userId(context.getUserId())
.agentName(context.getAgentName())
.failedStep(context.getCurrentStep())
.errorType(error.getClass().getSimpleName())
.errorMessage(error.getMessage())
.recoveryAction(action)
.timestamp(Instant.now())
.build();

eventPublisher.publishEvent(event);

// 严重失败实时告警
if (action == RecoveryAction.FALLBACK_TO_TEMPLATE
|| action == RecoveryAction.HUMAN_INTERVENTION) {
alertService.sendAlert(AlertLevel.HIGH, event);
}
}
}

总结

AI Agent 的容错不是简单地套用传统微服务的容错模式。由于 LLM 的不确定性、级联失败的放大效应和资源消耗的不可预测性,Agent 需要一套多层次、智能化的容错体系:

层次 机制 适用场景
第一层 重试(指数退避 + 参数调整) 瞬时故障、格式错误
第二层 Fallback 降级(模型降级、缓存、规则引擎) 服务不可用
第三层 断路器(Resilience4j) 连续失败、保护预算
第四层 Human-in-the-Loop 高风险决策、关键操作
第五层 Guardrails(输入输出护栏) 安全防护、质量保障

核心设计原则:

  1. 让 Agent 自己处理失败——返回错误信息给 LLM,让它决定下一步,而不是硬编码错误处理逻辑
  2. 渐进式降级——从高质量结果逐步降到”有总比没有好”
  3. 透明告知用户——降级时明确告知,不要默默降低质量
  4. 预算守卫——防止 Agent 在故障重试中”烧钱”
  5. 可观测性——每次失败都应该有迹可循,为优化提供数据支撑

容错做得好不好,决定了你的 Agent 是只能在 Demo 中表演,还是能在生产环境中扛住真实用户的考验。


相关阅读推荐