Java-Agent编排-状态机与多Agent
Java Agent 编排 · 状态机 · 多 Agent · 生产工程
定位:14 讲 Spring AI / LangChain4j 的 基础 API(ChatClient / Tool / Advisor);本篇讲 Agent 编排层——多步循环、状态持久化、HITL、Multi-Agent 协作、测试与可观测——Staff 面试的白板设计母题。
不重复:框架选型见 04;12 能力域见 13;七视图见 27;Python Agent 见 29。
风格:沿 L1 概念 → L2 原理 → L3 生产 → L4 Staff 答辩四层递进;每层有 ⚠ 难点 / 🔥 高频 / 💀 陷阱标注。
§0 面试前 30 分钟 Checklist(Staff / Architect)
| 时间盒 | 动作 | 产出 |
|---|---|---|
| 5 min | 背 §1 编排生态六选型 + 决策树 | 能按条件点名推荐 |
| 5 min | 白板 §2 Plan-Execute 序列图 | plan → execute → observe → decide |
| 5 min | 口述 §5 Supervisor 三板斧 | Fan-out + idempotency + token budget |
| 5 min | 画 §3 状态机五状态 | PLANNING → EXECUTING → HITL → DONE / FAILED |
| 5 min | 准备 1 个 STAR-M-P 事故(§13) | 含 M(机制修复)与 P(指标) |
| 5 min | 过 §Checklist 标红 3 项 | 知道回哪章补课 |
开场金句(60s):
「Spring AI ChatClient 的 Tool Calling 循环足以覆盖 80% 的 Java Agent 场景。当需要 HITL 等待、跨重启恢复、多 Agent 协作时,我会在 ChatClient 之上叠加 Plan-Execute 编排层 + Redis checkpoint + Supervisor 消息总线——能画出来、能讲清 trade-off、能写测试。」
§1 Java Agent 编排生态全景(L1)
1.1 六大编排方案一张表 🔥
| 方案 | 核心抽象 | 甜区 | 生产就绪 | 主要风险 |
|---|---|---|---|---|
| Spring AI ChatClient loop | while (hasToolCalls) 内置循环 |
单 Agent + ≤5 步简单工具链 | ⭐⭐⭐⭐⭐ | 无 checkpoint;HITL 需自行实现 |
| Plan-Execute on Spring AI | PlannerService + ExecutorService 自研 | 退款/对账/审批等多步 + HITL | ⭐⭐⭐⭐ | 需自己管状态;研发成本中 |
| Spring Statemachine + Agent | StateMachine<States, Events> |
长流程(小时/天)+ 审计合规 | ⭐⭐⭐⭐ | 配置繁琐;与 AI 概念有阻抗 |
| LangGraph4j | StateGraph<S>.addNode().compile() |
复杂条件分支 + checkpoint | ⭐⭐(社区) | API 不稳定;文档少 |
| LangChain4j AiService | AiServices.builder().tools().build() |
快速原型 + 声明式 Agent | ⭐⭐⭐ | 无 checkpoint;循环控制弱 |
| Semantic Kernel Java Planner | Kernel + Plugin + Planner |
微软生态 / 多语言统一 | ⭐⭐ | Java 版滞后;社区小 |
1.2 选型决策树
1.3 与 Python 生态的映射
| Java 方案 | Python 等价 | 差异 |
|---|---|---|
| Spring AI ChatClient loop | LangChain AgentExecutor | Spring AI 自动 loop;Python 需显式 executor |
| Plan-Execute on Spring AI | LangGraph StateGraph | Java 需自研;Python 有官方 LangGraph |
| Spring Statemachine | Temporal / Prefect | Java 更偏传统工作流 |
| Supervisor (Spring Event) | LangGraph Multi-Agent | Java 用 Spring Event;Python 用 StateGraph |
§2 Spring AI Agent 多步编排深度(L2-L3)🔥
2.1 ChatClient 内置 Tool Calling 循环
Spring AI 1.0 的 ChatClient.call() 内部已有循环:
用户消息 → ChatModel.call() → 检查 response
↓ ↓
↓ hasToolCalls = true? ←──── 执行 @Tool 方法
↓ ↓ 否 ↓ 是
↓ 返回最终回复 把 tool result 追加到 messages
↓ ↓
↓ 再次 ChatModel.call()
↓ ↓
↓ (循环直到无 toolCalls 或超限)
限制:
- 无
maxSteps硬限(需在 Advisor 层实现) - 无 checkpoint(worker 重启丢失上下文)
- 无 HITL interrupt(Tool 执行是同步的)
- 无 Plan 阶段(直接 ReAct 循环)
2.2 Plan-Execute 编排层设计
2.3 核心代码:Plan-Execute Refund Agent
// 1. Plan 结构化输出
public record RefundPlan(
String planId,
String orderId,
List<PlanStep> steps
) {
public sealed interface PlanStep permits QueryStep, AssessStep, RefundStep {}
public record QueryStep(String tool, Map<String, Object> params) implements PlanStep {}
public record AssessStep(String rule, boolean requiresHitl) implements PlanStep {}
public record RefundStep(String idempotentKey, BigDecimal amount, boolean requiresHitl)
implements PlanStep {}
}
// 2. Planner: 用 LLM 生成结构化 Plan
@Service
public class RefundPlannerService {
private final ChatClient chatClient;
public RefundPlan generatePlan(String orderId, String reason) {
return chatClient.prompt()
.system("""
你是退款规划器。根据用户请求生成退款计划。
输出 JSON 格式的 RefundPlan,每步标注是否需要 HITL。
金额 > 200 元的退款步骤必须标注 requiresHitl=true。
""")
.user("订单 %s 退款原因: %s".formatted(orderId, reason))
.call()
.entity(RefundPlan.class);
}
}
// 3. 编排循环 + Checkpoint + HITL
@Service
public class AgentLoopService {
private final RedisTemplate<String, AgentCheckpoint> redis;
private final ToolRegistry toolRegistry;
private final HitlQueue hitlQueue;
private static final int MAX_STEPS = 8;
private static final Duration STEP_TIMEOUT = Duration.ofSeconds(30);
public AgentResult execute(RefundPlan plan) {
var checkpoint = loadOrCreate(plan.planId());
for (int i = checkpoint.currentStep(); i < plan.steps().size(); i++) {
if (i >= MAX_STEPS) {
return AgentResult.failed(plan.planId(), "MAX_STEPS exceeded");
}
var step = plan.steps().get(i);
// HITL 拦截
if (step instanceof RefundPlan.RefundStep rs && rs.requiresHitl()) {
hitlQueue.enqueue(plan.planId(), i, rs);
save(checkpoint.withStatus(WAITING_HITL).withStep(i));
return AgentResult.pendingHitl(plan.planId());
}
// 执行 Tool(带超时 + 幂等)
try {
var result = Mono.fromCallable(() -> toolRegistry.invoke(step))
.timeout(STEP_TIMEOUT)
.block();
save(checkpoint.withStep(i + 1).withObservation(i, result));
} catch (TimeoutException e) {
save(checkpoint.withStatus(FAILED).withError(i, e));
return AgentResult.failed(plan.planId(), "Step %d timeout".formatted(i));
}
}
save(checkpoint.withStatus(COMPLETED));
return AgentResult.success(plan.planId());
}
/** HITL 审批回调后恢复执行 */
public AgentResult resumeAfterHitl(String planId, boolean approved) {
var checkpoint = load(planId);
if (!approved) {
save(checkpoint.withStatus(REJECTED));
return AgentResult.rejected(planId);
}
// 从 checkpoint 恢复,继续下一步
return execute(checkpoint.plan());
}
}
// 4. Checkpoint 记录
public record AgentCheckpoint(
String planId,
RefundPlan plan,
int currentStep,
AgentStatus status,
Map<Integer, String> observations,
Instant updatedAt
) {
public AgentCheckpoint withStep(int step) {
return new AgentCheckpoint(planId, plan, step, status, observations, Instant.now());
}
public AgentCheckpoint withStatus(AgentStatus s) {
return new AgentCheckpoint(planId, plan, currentStep, s, observations, Instant.now());
}
}
public enum AgentStatus { CREATED, EXECUTING, WAITING_HITL, COMPLETED, FAILED, REJECTED }
2.4 幂等 Tool 设计 ⚠
@Component
public class RefundTools {
private final RefundService refundService;
private final IdempotencyStore idempotencyStore;
@Tool(description = "创建退款。幂等键格式: orderId:date:amount")
public RefundResult createRefund(
String orderId, BigDecimal amount, String reason) {
String idempotentKey = "%s:%s:%s".formatted(
orderId, LocalDate.now(), amount.toPlainString());
// 幂等检查
return idempotencyStore.executeIdempotent(idempotentKey, () -> {
var result = refundService.createRefund(orderId, amount, reason);
return new RefundResult(result.refundId(), result.status(), idempotentKey);
});
}
}
2.5 量化账本
Refund Agent(Plan-Execute + 3 tools + HITL):
Plan: 1× GPT-4o 6k in + 800 out ≈ $0.04
Execute: 3× GPT-4o 4k in + 300 out ≈ $0.06
HITL: 等待不计 token
合计: ≈ $0.10–0.15 / 成功路径
MAX_STEPS=8 防护:最坏 8× ≈ $0.32
大促峰值 800 QPS,1% 进 Agent:
8 QPS × $0.15 × 3600s = $4,320/h Agent 成本
→ 需 per-user 日预算 + 语义缓存
§3 Spring Statemachine + Agent(L2-L3)
3.1 何时用 Statemachine
| 条件 | ChatClient 循环 | Plan-Execute | Spring Statemachine |
|---|---|---|---|
| 流程时长 | 秒级 | 分钟级 | 小时/天级 |
| 状态持久化 | 无 | Redis 自研 | 内置 Redis/JPA |
| 审计合规 | 需自研 | 需自研 | 状态转换日志内置 |
| 并行分支 | 不支持 | 自研 | Region 原生支持 |
| 学习成本 | 低 | 中 | 高(状态机概念) |
3.2 状态图
状态机核心状态与转换:
- PLANNING:接收用户退款请求,LLM 生成执行 plan(步骤列表 + 预期状态)
- EXECUTING:按 plan 逐步执行 Tool(查询订单、计算金额、调用退款 API)
- WAITING_HITL:金额 > 阈值时暂停,等待人工审批(异步事件驱动)
- COMPLETED / FAILED / REJECTED:终端状态,写入审计日志
转换条件:
PLAN_READY→ plan 生成完毕,进入执行HITL_REQUIRED→ 金额 > 阈值,触发人工审批HITL_APPROVED / HITL_REJECTED→ 审批结果返回- 所有步骤完成 → COMPLETED;重试耗尽 → FAILED
3.3 核心代码
@Configuration
@EnableStateMachineFactory
public class RefundStateMachineConfig
extends EnumStateMachineConfigurerAdapter<AgentState, AgentEvent> {
@Override
public void configure(StateMachineStateConfigurer<AgentState, AgentEvent> states)
throws Exception {
states.withStates()
.initial(AgentState.PLANNING)
.state(AgentState.EXECUTING, executeAction(), null)
.state(AgentState.WAITING_HITL)
.end(AgentState.COMPLETED)
.end(AgentState.FAILED)
.end(AgentState.REJECTED);
}
@Override
public void configure(StateMachineTransitionConfigurer<AgentState, AgentEvent> transitions)
throws Exception {
transitions
.withExternal().source(PLANNING).target(EXECUTING).event(PLAN_READY)
.action(ctx -> {
RefundPlan plan = ctx.getExtendedState().get("plan", RefundPlan.class);
log.info("Plan ready: {} steps", plan.steps().size());
})
.and()
.withExternal().source(EXECUTING).target(WAITING_HITL).event(HITL_REQUIRED)
.guard(ctx -> {
BigDecimal amount = ctx.getExtendedState().get("amount", BigDecimal.class);
return amount.compareTo(new BigDecimal("200")) > 0;
})
.and()
.withExternal().source(WAITING_HITL).target(EXECUTING).event(HITL_APPROVED)
.and()
.withExternal().source(WAITING_HITL).target(REJECTED).event(HITL_REJECTED)
.and()
.withExternal().source(EXECUTING).target(COMPLETED).event(ALL_DONE)
.and()
.withExternal().source(EXECUTING).target(FAILED).event(STEP_FAILED);
}
@Bean
public StateMachineRuntimePersister<AgentState, AgentEvent, String> persister(
RedisConnectionFactory factory) {
return new RedisStateMachineContextRepository<>(factory);
}
}
// 使用
@Service
public class RefundAgentService {
private final StateMachineFactory<AgentState, AgentEvent> factory;
public String startRefund(String orderId) {
var sm = factory.getStateMachine(orderId);
sm.getExtendedState().getVariables().put("orderId", orderId);
sm.sendEvent(Mono.just(MessageBuilder.withPayload(PLAN_READY).build())).blockLast();
return orderId;
}
public void approveHitl(String orderId) {
var sm = factory.getStateMachine(orderId);
sm.sendEvent(Mono.just(MessageBuilder.withPayload(HITL_APPROVED).build())).blockLast();
}
}
§4 LangGraph4j(L2,实验性)
4.1 概述
LangGraph4j 是 LangGraph 的社区 Java 移植,提供 StateGraph 状态图编排。
⚠ 生产建议:2026 年仍为社区项目,API 不稳定。推荐了解概念,生产用 Spring AI 原生编排(§2)。
4.2 核心 API
// LangGraph4j 概念代码(API 可能变化)
var graph = new StateGraph<>(AgentState::new)
.addNode("planner", plannerNode)
.addNode("executor", executorNode)
.addNode("evaluator", evaluatorNode)
.addEdge(START, "planner")
.addConditionalEdges("planner", state -> {
if (state.plan().isEmpty()) return "end";
return "executor";
})
.addEdge("executor", "evaluator")
.addConditionalEdges("evaluator", state -> {
if (state.allStepsDone()) return END;
if (state.currentStep().requiresHitl()) return "interrupt";
return "executor";
})
.compile(checkpointSaver); // Postgres / SQLite
var result = graph.invoke(new AgentState(userRequest));
4.3 三方案对比
| 维度 | Spring AI 原生 | Plan-Execute 自研 | LangGraph4j |
|---|---|---|---|
| 步骤可视化 | 日志 | 自研 UI | 图原生 |
| Checkpoint | 无 | Redis 自研 | 内置 Postgres |
| HITL | 无 | 自研 Queue | interrupt 原生 |
| 生产稳定 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 学习成本 | 零 | 中 | 高 |
| 推荐 | 简单场景 | 生产首选 | 实验/学习 |
§5 Java Multi-Agent 编排(L3,Staff)🔥
5.1 三种模式
5.2 Supervisor 模式完整代码
// 1. Agent 消息契约
public record AgentMessage(
String supervisorId,
String targetAgent,
String taskDescription,
Map<String, Object> context,
Instant timestamp
) {}
public record AgentResult(
String agentName,
boolean success,
Map<String, Object> output,
int tokensUsed
) {}
// 2. Worker Agents
@Component
public class OrderAgent {
private final ChatClient chatClient;
@EventListener
public AgentResult handle(AgentMessage msg) {
if (!"order".equals(msg.targetAgent())) return null;
var response = chatClient.prompt()
.system("你是订单查询专家。只查询,不修改。")
.user(msg.taskDescription())
.tools(new OrderTools())
.call()
.content();
return new AgentResult("order", true, Map.of("response", response), 0);
}
}
@Component
public class PaymentAgent {
private final ChatClient chatClient;
@EventListener
public AgentResult handle(AgentMessage msg) {
if (!"payment".equals(msg.targetAgent())) return null;
var response = chatClient.prompt()
.system("你是支付查询专家。只查询支付状态,不发起退款。")
.user(msg.taskDescription())
.tools(new PaymentQueryTools())
.call()
.content();
return new AgentResult("payment", true, Map.of("response", response), 0);
}
}
// 3. Supervisor 编排器
@Service
public class SupervisorAgent {
private final ChatClient supervisorClient;
private final ApplicationEventPublisher publisher;
private final List<CompletableFuture<AgentResult>> pendingResults = new CopyOnWriteArrayList<>();
private static final int MAX_ROUNDS = 3;
private static final int MAX_TOTAL_TOKENS = 50_000;
public String orchestrate(String userRequest) {
int totalTokens = 0;
for (int round = 0; round < MAX_ROUNDS; round++) {
// Supervisor 决定分派给哪些 worker
var dispatch = supervisorClient.prompt()
.system("""
你是 Supervisor。分析用户请求,决定分派给哪些 Agent。
可用 Agent: order(订单查询)、payment(支付查询)、notify(通知)。
输出 JSON 数组: [{"agent":"order","task":"..."},...]
如果所有信息已收集完毕,输出 {"done":true,"summary":"..."}
""")
.user("用户请求: %s\n已有结果: %s".formatted(userRequest, collectResults()))
.call()
.entity(SupervisorDecision.class);
if (dispatch.done()) {
return dispatch.summary();
}
// Fan-out 分派
var futures = dispatch.tasks().stream()
.map(task -> CompletableFuture.supplyAsync(() -> {
var msg = new AgentMessage(
UUID.randomUUID().toString(), task.agent(), task.task(),
Map.of(), Instant.now());
publisher.publishEvent(msg);
return waitForResult(msg.supervisorId());
}))
.toList();
// Fan-in 收集
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.orTimeout(30, TimeUnit.SECONDS)
.join();
totalTokens += futures.stream()
.mapToInt(f -> f.join().tokensUsed())
.sum();
if (totalTokens > MAX_TOTAL_TOKENS) {
return "Token budget exceeded, returning partial results: " + collectResults();
}
}
return "Max rounds reached: " + collectResults();
}
}
5.3 Multi-Agent 成本建模
Supervisor 退款场景(3 Worker Agent):
Supervisor: 3 轮 × 4k in + 500 out = $0.07
OrderAgent: 1 轮 × 3k in + 200 out = $0.02
PaymentAgent: 1 轮 × 3k in + 200 out = $0.02
NotifyAgent: 1 轮 × 2k in + 100 out = $0.01
合计: ≈ $0.12 / 成功路径
vs 单 Agent: ≈ $0.10(省 Supervisor 开销但 prompt 更长)
结论: 3 Worker 以下单 Agent 更划算;5+ Worker 时 Supervisor 省上下文
5.4 反模式 💀
| 反模式 | 为什么危险 | 正确做法 |
|---|---|---|
| GroupChat(多 LLM 互聊)做退款决策 | 不可审计、不可回放、成本爆炸 | Supervisor 集中分派 |
| 共享全量 prompt context | N Agent × R 轮 → context 超窗口 | 每 Worker 只接收必要 context |
| 写操作 Agent 无幂等 | 并行 Fan-out → 重复退款 | 每个写 Tool 幂等键 |
| 跳 Stage 1 直上 Multi-Agent | 成本翻倍,completion 下降 | 先单 Agent 优化到极致 |
§6 LangChain4j Agent 深度(L2-L3)
6.1 AiService Agent 完整示例
// 1. 声明式接口
interface RefundAssistant {
@SystemMessage("""
你是退款客服助手。禁止口算金额。
所有金额来自 Tool 返回值。
""")
@UserMessage("{{message}}")
String chat(@MemoryId String sessionId, @V("message") String message);
}
// 2. 构建 Agent
@Configuration
public class LangChain4jConfig {
@Bean
RefundAssistant refundAssistant(ChatLanguageModel model) {
return AiServices.builder(RefundAssistant.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.tools(new OrderQueryTool(), new RefundTool())
.contentRetriever(EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3)
.build())
.build();
}
}
// 3. 使用
@RestController
public class RefundController {
private final RefundAssistant assistant;
@PostMapping("/chat")
public String chat(@RequestBody ChatRequest req) {
return assistant.chat(req.sessionId(), req.message());
}
}
6.2 Spring AI vs LangChain4j Agent 对比
| 维度 | Spring AI Agent | LangChain4j Agent |
|---|---|---|
| 编排控制 | Advisor 链拦截 | AiServiceListener 回调 |
| 步数限制 | 需自研 | 需自研 |
| Checkpoint | 需自研 | 需自研 |
| HITL | 需自研 | 需自研 |
| MCP 支持 | 内置 McpClient | 社区插件 |
| 可观测 | Micrometer 自动 | 手动 |
| 推荐 | 生产核心链路 | 快速原型 / Quarkus |
§7 Quarkus + LangChain4j(L2)
// Quarkus 声明式 AI Service
@RegisterAiService
interface OrderAssistant {
@SystemMessage("你是订单查询助手")
String chat(@UserMessage String message);
}
@Path("/ai")
public class AiResource {
@Inject OrderAssistant assistant;
@POST @Path("/chat")
public String chat(String message) {
return assistant.chat(message);
}
}
选型理由:Quarkus native image → Agent 容器 启动 < 100ms、内存 < 128MB。适合边缘 / IoT / Serverless Agent 场景。
§8 MCP Server Java 开发(L2-L3)
8.1 用 Spring AI 构建 MCP Server
@Configuration
public class OrderMcpServerConfig {
@Bean
McpServer orderMcpServer(OrderService orderService) {
return McpServer.builder()
.name("order-mcp-server")
.version("1.0.0")
.tool("query_order", "查询订单详情",
Map.of("orderId", "string"),
params -> {
var order = orderService.findById(params.get("orderId").toString());
return Map.of("order", order);
})
.tool("list_refunds", "查询退款记录",
Map.of("orderId", "string"),
params -> {
var refunds = orderService.listRefunds(params.get("orderId").toString());
return Map.of("refunds", refunds);
})
.build();
}
}
用法:Cursor / Claude Code 通过 MCP 协议直接查询生产 OMS(只读 Tool),与 Agent 共用 Tool 契约。
§9 Java 本地推理 DJL / ONNX Runtime(L2)
9.1 ONNX 意图分类器
@Component
public class IntentClassifier {
private final OrtEnvironment env = OrtEnvironment.getEnvironment();
private final OrtSession session;
public IntentClassifier(@Value("${intent.model.path}") String modelPath)
throws OrtException {
this.session = env.createSession(modelPath);
}
/** 本地推理 ~5ms vs API 调用 ~500ms */
public IntentResult classify(String userMessage) throws OrtException {
float[][] inputIds = tokenize(userMessage);
var tensor = OnnxTensor.createTensor(env, inputIds);
var results = session.run(Map.of("input_ids", tensor));
float[] logits = ((float[][]) results.get(0).getValue())[0];
int intent = argMax(logits);
return new IntentResult(Intent.values()[intent], softmax(logits)[intent]);
}
}
// 路由:意图 → 对应 Agent
@Service
public class AgentRouter {
private final IntentClassifier classifier;
private final Map<Intent, ChatClient> agents;
public String route(String message) throws OrtException {
var intent = classifier.classify(message); // ~5ms 本地
var agent = agents.get(intent.intent()); // 路由到对应 ChatClient
return agent.prompt().user(message).call().content();
}
}
| 方案 | 延迟 | 成本 | 适用 |
|---|---|---|---|
| API 意图分类 | ~500ms | $0.001/次 | 准确率要求极高 |
| ONNX 本地 | ~5ms | $0 | 路由分流、PII 检测、简单分类 |
| 规则 + keyword | ~1ms | $0 | 固定分支 |
§10 Java Agent 测试方法论(L3)
10.1 四层测试金字塔
10.2 Stub LLM
public class StubChatModel implements ChatModel {
private final Queue<String> responses;
public StubChatModel(String... responses) {
this.responses = new LinkedList<>(List.of(responses));
}
@Override
public ChatResponse call(Prompt prompt) {
String response = responses.poll();
if (response == null) throw new IllegalStateException("No more stub responses");
return new ChatResponse(List.of(new Generation(response)));
}
}
10.3 Trajectory Eval
# golden-set.yaml
- name: "退款-正常路径"
input: "订单 O-123 申请退款"
expected_tools: ["query_order", "query_payment", "create_refund"]
expected_tool_count: 3
must_contain: ["退款成功", "R-"]
must_not_contain: ["口算", "大概"]
max_steps: 5
- name: "退款-超额触发HITL"
input: "订单 O-456 退款 500 元"
expected_tools: ["query_order", "query_payment"]
expected_hitl: true
max_steps: 3
@SpringBootTest
class TrajectoryEvalTest {
@Autowired AgentLoopService agent;
@ParameterizedTest
@MethodSource("loadGoldenSet")
void evalTrajectory(GoldenCase tc) {
var result = agent.execute(tc.input());
assertThat(result.toolsCalled()).containsExactlyElementsOf(tc.expectedTools());
assertThat(result.stepCount()).isLessThanOrEqualTo(tc.maxSteps());
tc.mustContain().forEach(s -> assertThat(result.output()).contains(s));
tc.mustNotContain().forEach(s -> assertThat(result.output()).doesNotContain(s));
}
}
10.4 CI 门禁
| 指标 | 阈值 | 动作 |
|---|---|---|
trajectory_success_rate |
< 95% | 阻断发布 |
loop_rate(陷入循环) |
> 0.5% | 阻断发布 |
avg_steps |
> 6 | 告警 |
duplicate_tool_call_rate |
> 1% | 告警 |
cost_per_task_p95 |
> $0.50 | 告警 |
§11 Java Agent 可观测深度(L3)
11.1 Trace 层次
11.2 Micrometer 指标
@Component
public class AgentMetrics {
private final MeterRegistry registry;
public void recordStep(String agentName, String toolName, Duration duration, boolean success) {
registry.timer("ai.agent.step.duration",
"agent", agentName, "tool", toolName, "success", String.valueOf(success))
.record(duration);
registry.counter("ai.agent.step.total",
"agent", agentName, "tool", toolName)
.increment();
}
public void recordCompletion(String agentName, int steps, int tokens, AgentStatus status) {
registry.summary("ai.agent.steps", "agent", agentName).record(steps);
registry.summary("ai.agent.tokens", "agent", agentName).record(tokens);
registry.counter("ai.agent.completion",
"agent", agentName, "status", status.name())
.increment();
}
}
11.3 告警规则
| 指标 | 告警阈值 | 含义 |
|---|---|---|
ai.agent.step.duration{tool="create_refund"} P99 |
> 10s | 退款 API 慢 |
rate(ai.agent.completion{status="FAILED"}[5m]) |
> 5% | Agent 失败率高 |
ai.agent.steps avg |
> 6 | 可能陷入循环 |
sum(ai.agent.tokens) by (agent) per hour |
> 1M | 成本异常 |
§12 大厂面试题(5 道)
12.1 🟦 字节 —「Java Agent 怎么做多步编排?Spring AI 够用吗?」
(1) 标准答案
Spring AI ChatClient 内置 Tool Calling 循环覆盖 80% 场景(≤5 步、无 HITL)。需要 HITL / checkpoint / 多 Agent 协作时,在 ChatClient 之上叠加 Plan-Execute 编排层:Planner 生成结构化 Plan → Executor 逐步执行 → Redis checkpoint 持久化 → HITL Queue 拦截写操作。
(2) 架构推演
(3) 量化权衡
- ChatClient 循环:$0→ 无额外开销,但 HITL 等待 4h 后 worker 重启丢上下文
- Plan-Execute + Redis:+20ms checkpoint 写入延迟,换来跨重启恢复
- Spring Statemachine:配置复杂度 +3×,适合以天计的审批流
(4) 落地清单
maxSteps=8硬限- 写操作 Tool 幂等键 =
orderId:date:amount - HITL 审批 SLA < 4h;超时自动 escalate L2 人工
(5) 追问
- Q:为什么不用 LangGraph4j?
A:社区项目 API 不稳定,生产用 Spring AI 原生 + 自研编排层更可控。
12.2 🟧 阿里 —「Multi-Agent 在支付场景如何保证一致性?」
(1) 标准答案
支付场景 Multi-Agent 禁止 GroupChat(不可审计)。用 Supervisor 模式:一个 Supervisor Agent 集中分派 → Worker Agent 只做读操作 → 写操作统一回 Supervisor → Supervisor 调用幂等 API。跨 Agent 一致性靠 Saga + 幂等键 + 单一写入点。
(2) 核心规则
| 规则 | 实现 |
|---|---|
| 单一写入点 | 只有 Supervisor 可调写 Tool |
| 幂等键 | orderId:action:date |
| Saga 补偿 | 退款失败 → 取消通知 → 恢复订单状态 |
| Token 预算 | MAX_TOTAL_TOKENS=50k per session |
| 审计日志 | 每个 Agent 每步写 append-only log |
12.3 🟪 蚂蚁 —「Agent 写操作如何做到幂等?」
(1) 答案要点
幂等键 = 业务键 + 时间窗口 + 操作类型。实现:@Tool 方法入口查 Redis SETNX,命中直接返回首次结果。数据库层加 unique constraint 作兜底。Agent 循环中同一 Tool + 同一参数第二次调用直接返回缓存。
12.4 🔵 Google —「How do you test a multi-step Java Agent?」
(1) Answer
Four-layer pyramid: Unit test each @Tool → Contract test JSON schema → Trajectory eval with golden set YAML → Nightly integration with real model sample. CI gates: success_rate ≥ 95%, loop_rate ≤ 0.5%, duplicate_tool_call ≤ 1%.
12.5 🟢 美团 —「Agent 状态机用 Spring Statemachine 还是自研?」
(1) 标准答案
简单场景(≤5 步):ChatClient 循环 + Redis 自研 checkpoint → 研发成本低、与 Spring AI 无缝。复杂场景(跨天审批 + 并行分支 + 审计合规):Spring Statemachine + Redis 持久化 → 状态转换日志内置。不建议从零自研状态机——Spring Statemachine 已有 10 年沉淀。
§13 STAR-M-P 真实事故:Spring AI Agent 循环重复退款
背景
- S:电商客服 Agent,日均 5000 退款请求,单日退款 ~¥200 万
- T:Agent 使用 Spring AI ChatClient +
@Tool createRefund - A:上线第 2 天发现:同一订单在同一会话中
createRefund被调用 2-3 次 - R:截获重复退款 ¥15 万;3 天内修复上线
M(机制修复)
- 幂等键:
createRefund增加orderId:date:amount幂等键,Redis SETNX - Tool 去重:Agent 循环中,同一 Tool + 同一参数 hash → 第二次直接返回首次结果
- maxSteps=5:超过 5 步强制熔断,转人工
- token 预算:per-session 30k tokens 上限
P(指标)
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 重复退款率 | 0.3% | 0% |
| Agent 循环率 | 1.2% | 0.1% |
| MTTR | — | 15 min |
| 退款金额异常 | ¥15 万/天 | ¥0 |
§14 速记卡
| 主题 | 一句话 |
|---|---|
| Spring AI 循环 | ChatClient 内置 while(hasToolCalls) 自动循环,够用但无 checkpoint/HITL |
| Plan-Execute | Planner LLM 生成 Plan JSON → Executor 逐步执行 → Redis checkpoint → HITL Queue |
| Statemachine | 长流程(小时/天)→ Spring Statemachine + Redis 持久化 + 审计日志 |
| LangGraph4j | 社区实验性,了解概念,生产不推荐 |
| Multi-Agent | Supervisor 集中分派 + 单一写入点 + 幂等 + Token 预算 |
| LangChain4j | AiService 接口代理,适合原型和 Quarkus |
| MCP Server | Spring AI 暴露 @Tool 为 MCP,Cursor 可查生产 OMS |
| 本地推理 | ONNX 意图分类 ~5ms vs API ~500ms,路由分流用 |
| 测试 | Unit → Contract → Trajectory → E2E 四层金字塔 |
| 幂等 | 业务键 + 时间窗口 + 操作类型 = 幂等键 |
§99 冲刺 Q&A
J01 · Spring AI ChatClient 内部 Tool Calling 是同步还是异步?
同步。call() 内部 while(hasToolCalls) 阻塞循环。异步用 stream() 但 Tool 执行仍同步。需要异步 Agent 用 CompletableFuture 包装或 Reactor Mono。
J02 · Plan-Execute 模式中 Plan 漂移怎么处理?
Plan 生成后 embedding 存储,每步执行前 goal embedding 相似度检查,< 0.8 拒绝执行并 replan。max_replan_count ≤ 3。
J03 · Redis checkpoint 用什么数据结构?
Hash:key = agent:checkpoint:{planId},field = plan/step/status/observations。TTL 7 天。HITL 等待期间不过期。
J04 · 多 Agent Fan-out 写操作如何避免重复退款?
单一写入点:只有 Supervisor 可调写 Tool。Worker Agent 只做读操作返回建议。Supervisor 汇总后调一次幂等 createRefund。
J05 · Spring Statemachine 和自研状态机的分界线?
≤ 5 状态 + 无并行分支 → 自研(enum + switch);> 5 状态 / 并行 Region / 需审计日志 → Spring Statemachine。
J06 · LangGraph4j 生产能用吗?
2026 年不建议。API 不稳定,文档少,社区小。了解概念用于面试交流,生产用 Spring AI 原生 + 自研编排层。
J07 · LangChain4j 和 Spring AI 选哪个做 Agent?
Spring Boot 团队 → Spring AI(原生集成、Micrometer 自动)。Quarkus → LangChain4j(原生扩展)。两者都无 checkpoint/HITL,需自研。
J08 · MCP Server 和 @Tool 什么时候拆?
同团队单体 → @Tool(零延迟)。多团队 / Cursor 需要访问 → MCP Server(标准协议 + 独立鉴权)。详见 14 §12。
J09 · ONNX 本地推理准确率够吗?
意图分类(10 类以内):ONNX 与 API 基本持平(F1 > 0.95)。复杂推理/生成:不够,必须用 LLM API。
J10 · Agent 测试覆盖率怎么衡量?
不用代码覆盖率,用 轨迹覆盖率:golden set 覆盖了多少条 expected_tools 路径。目标 ≥ 90% 路径覆盖。
J11 · Agent 循环失控怎么紧急处理?
- 熔断:
max_iterations触发后返回兜底文案 - 限流:per-user 每分钟 3 次 Agent 调用
- 降级:一键
agent_mode=rag_only关闭 Tool Calling
J12 · Supervisor 模式 token 爆炸怎么解?
每轮只把 Worker 结果的 摘要(structured JSON,非 raw text)传回 Supervisor。设 MAX_ROUNDS=3 + MAX_TOTAL_TOKENS=50k。
J13 · Java Agent 怎么对接 Python Agent?
gRPC / A2A 协议。Java Supervisor 通过 A2A Task 发请求 → Python Agent 返回 Artifact → Java 侧 JSON 反序列化。详见 28 §4。
J14 · Agent 可观测最重要的 3 个指标?
loop_rate(循环率)> 0.5% 告警cost_per_successful_task> $0.50 告警tool_error_rate> 5% 熔断
J15 · Quarkus Agent 的优势?
Native image 启动 < 100ms、内存 < 128MB。适合边缘 / Serverless / IoT Agent。LangChain4j 原生 Quarkus 扩展,CDI 注入。
§Checklist(考前 15 分钟)
- 能画 Plan-Execute 序列图(§2.2)
- 能写 AgentLoopService 核心循环(§2.3)
- 能说 幂等键三要素:业务键 + 时间窗口 + 操作类型(§2.4)
- 能比较 ChatClient 循环 vs Plan-Execute vs Statemachine(§1.1)
- 能画 State 五状态图(§3.2)
- 能写 Supervisor Fan-out 代码(§5.2)
- 能说 Multi-Agent 成本公式(§5.3)
- 能列 4 个反模式(§5.4)
- 能写 LangChain4j AiService 声明式 Agent(§6.1)
- 能说 MCP Server vs @Tool 分界线(§8)
- 能说 ONNX 本地推理场景(§9)
- 能画 测试金字塔(§10.1)
- 能写 TrajectoryEvalTest(§10.3)
- 能说 3 个告警指标(§11.3)
- 能讲 STAR-M-P 重复退款事故(§13)
- 能口述 J01–J15 任意 3 题(§99)
一句话速记:Spring AI ChatClient 循环覆盖 80% 场景;HITL / checkpoint → Plan-Execute + Redis;长流程 → Statemachine;多 Agent → Supervisor + 单一写入点 + 幂等。写操作幂等是 Java Agent P0 事故源。
官方文档与源码(一级依据)
AI Engineering · 正文机制应来自下方 官方文档(L1) 与 官方源码仓库(L2);
禁止用教程站/博客充当机制依据。本章 QPS/延迟/STAR 为面试示意。
写作规范:docs/official-sources-registry.md §0
L1 · 官方文档
L2 · 官方源码
L3 · 论文 / 开放规范
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)