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 选型决策树

Java Agent 编排选型

步骤数 ≤ 5 且无 HITL?

Spring AI ChatClient
内置 Tool 循环

需要跨重启恢复?

需要多 Agent 协作?

流程以天计?

Plan-Execute
自研编排层

Supervisor 模式
§5

Spring Statemachine
§3

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 编排层设计

HITL Queue Redis Checkpoint @Tool Methods ExecutorService PlannerService Controller User HITL Queue Redis Checkpoint @Tool Methods ExecutorService PlannerService Controller User 人工审批(可能 4 小时) alt [step.requiresHitl] alt [step 失败且 retryExhausted] loop [每个 step] 退款请求 orderId=O-123 generatePlan(request) RefundPlan [查订单, 查支付, 风险评估, 退款] save(planId, plan, CREATED) executeStep(plan, stepIndex) invoke @Tool observation save(planId, stepResult, STEP_N_DONE) enqueue(planId, stepIndex, context) approved / rejected save(planId, hitlResult) escalate(planId, error) save(planId, FAILED) refund_id=R-456

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

用户提交退款

plan 生成完毕

金额 > 阈值

下一步

审批通过

审批拒绝

所有步骤完成

重试耗尽

PLANNING

EXECUTING

WAITING_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 三种模式

③ A2A 跨语言

gRPC/A2A

Java Agent

Python Agent

② Pipeline

Researcher

Writer

Reviewer

① Supervisor

Supervisor Agent

Order Agent

Payment Agent

Notify Agent

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 四层测试金字塔

金字塔

端到端 · 真实模型 · 少量

单元测试 · 每个 @Tool 方法

合约测试 · Tool I/O Schema

轨迹 Eval · Golden Set · 100+

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 层次

Agent Span

Execute Loop

Step 1: query_order
@Tool span

Step 2: query_payment
@Tool span

Step 3: create_refund
@Tool span

Plan Span
planner LLM call

HITL Span
wait + approval

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) 架构推演

用户

Gateway

Controller

PlannerService
LLM 生成 Plan

ExecutorService
逐步执行 Tool

@Tool
幂等 + 超时

Redis
Checkpoint + Plan

HITL Queue
人工审批

(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(机制修复)

  1. 幂等键createRefund 增加 orderId:date:amount 幂等键,Redis SETNX
  2. Tool 去重:Agent 循环中,同一 Tool + 同一参数 hash → 第二次直接返回首次结果
  3. maxSteps=5:超过 5 步强制熔断,转人工
  4. 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 循环失控怎么紧急处理?

  1. 熔断:max_iterations 触发后返回兜底文案
  2. 限流:per-user 每分钟 3 次 Agent 调用
  3. 降级:一键 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 个指标?

  1. loop_rate(循环率)> 0.5% 告警
  2. cost_per_successful_task > $0.50 告警
  3. 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 · 论文 / 开放规范

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐