引言
从"会用 GPT"到"生产级 Agent",这条路比你想象的要颠簸得多。
单个 LLM 调用像个优秀实习生:能干,但得手把手指挥;真正的 Agent 系统更像一个自主工程团队------能规划、能分工、能纠错、能汇报。
本文带你搭一套可在生产落地的 AI Agent 编排系统 :以 LangGraph4j 驱动状态机工作流,以 Spring AI 封装工具调用,配以背压控制、可观测性链路追踪和优雅降级,把 Agent 从 Demo 推向真正的生产环境。
关键数据
| 指标 | 数值 |
|---|---|
| 企业 AI 项目因缺少编排层而失败(Gartner 2025) | 73% |
| 多 Agent 系统相比单 Agent 平均任务完成率提升 | 8× |
| 生产 Agent 需要可观测的延迟分位 | P99 |
| 完整编排系统的降级防护层数 | 3 层 |
一、整体架构:分层编排模型
生产级 Agent 系统不是一个大 Prompt,而是一套分层编排架构:
┌─────────────────────────────────────────┐
│ 外部输入层 │
│ HTTP API (REST/SSE) | MQ | 定时触发 │
└───────────────────┬─────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 编排层(LangGraph 状态机) │
│ Supervisor(规划分派) │
│ Router(条件分支) │
│ State Store(Redis 持久化) │
└───────────────────┬─────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 执行层(专家 Agent) │
│ Search Agent | Code Agent │
│ Data Agent | Writer Agent │
└───────────────────┬─────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 工具层(Spring AI Tool Calling) │
│ Tool Registry | 外部 API │
│ OpenTelemetry 可观测性 │
└─────────────────────────────────────────┘
状态机节点流转
START → Planner → Router → Executor → Critic
│
ok ────────┤
retry ─────┘(最多 3 次)
超限 → 强制 END
二、LangGraph4j 状态机工作流
2.1 核心状态定义与图构建
java
// ── AgentState:贯穿整个工作流的共享状态 ──
@Data
@Builder
public class AgentState {
private String taskId;
private String userQuery;
private List<SubTask> planSteps; // Planner 生成的子任务列表
private int currentStepIdx;
private Map<String, Object> stepResults; // 各步骤执行结果
private int retryCount;
private String finalAnswer;
private AgentStatus status; // RUNNING / SUCCESS / FAILED
// 上下文传播:确保 traceId 在子任务中可见
private String traceId;
private Instant startedAt;
}
// ── 图构建:注册节点 + 定义边 ──
@Configuration
public class AgentGraphConfig {
@Bean
public CompiledGraph<AgentState> agentGraph(
PlannerNode planner,
RouterNode router,
ExecutorNode executor,
CriticNode critic) throws GraphStateException {
return StateGraph.builder(AgentState.schema())
// 注册节点
.addNode("planner", planner)
.addNode("router", router)
.addNode("executor", executor)
.addNode("critic", critic)
// 定义边
.addEdge(START, "planner")
.addEdge("planner", "router")
.addEdge("router", "executor")
.addEdge("executor","critic")
// 条件边:Critic 决定结束还是重试
.addConditionalEdges("critic",
state -> {
if (state.getStatus() == AgentStatus.SUCCESS)
return END;
if (state.getRetryCount() >= 3)
return "force_end"; // 超过重试上限,强制终止
return "planner"; // 重新规划
},
Map.of(END, END, "force_end", END, "planner", "planner")
)
.compile(
CompileConfig.builder()
// Redis 持久化检查点:支持断点续跑
.checkpointer(redisCheckpointer())
.build()
);
}
}
2.2 Planner 节点:LLM 驱动的任务分解
java
@Component
@Slf4j
public class PlannerNode implements NodeAction<AgentState> {
private final ChatClient chatClient;
private final MeterRegistry meterRegistry;
private static final String PLANNER_PROMPT = """
你是一个任务规划专家。将用户任务分解为可执行的子步骤。
输出 JSON 格式:{"steps": [{"id": "s1", "type": "search|code|data|write", "instruction": "..."}]}
限制:最多 5 步,避免重复,确保步骤间依赖关系明确。
当前任务:{query}
重试原因(如有):{retryReason}
""";
@Override
public Map<String, Object> apply(AgentState state) {
Timer.Sample sample = Timer.start(meterRegistry);
try (var ignored = MDC.putCloseable("traceId", state.getTraceId())) {
log.info("[Planner] 开始规划, retry={}", state.getRetryCount());
String prompt = PLANNER_PROMPT
.replace("{query}", state.getUserQuery())
.replace("{retryReason}", getRetryReason(state));
// 调用 LLM,带超时控制(规划不应超过 30s)
String planJson = chatClient.prompt()
.user(prompt)
.options(ChatOptions.builder()
.temperature(0.1f) // 低温度,确保确定性输出
.maxTokens(1024)
.build())
.call()
.content();
List<SubTask> steps = parsePlan(planJson);
// 埋点:记录规划步骤数
meterRegistry.gauge("agent.planner.steps",
Tags.of("taskId", state.getTaskId()), steps.size());
return Map.of(
"planSteps", steps,
"currentStepIdx", 0,
"status", AgentStatus.RUNNING
);
} finally {
sample.stop(meterRegistry.timer("agent.planner.duration"));
}
}
}
三、Spring AI 工具调用:生产级工具注册
3.1 工具定义:带熔断与限流
java
@Component
public class WebSearchTool {
private final SearchApiClient searchClient;
private final CircuitBreaker circuitBreaker;
private final RateLimiter rateLimiter; // Resilience4j
private final MeterRegistry meterRegistry;
@Tool(name = "web_search",
description = "搜索互联网获取最新信息。输入:搜索关键词,输出:搜索摘要列表。")
public List<SearchResult> search(
@ToolParam(description = "搜索关键词,精简到核心词") String query) {
Counter callCounter = meterRegistry.counter("tool.web_search.calls",
"query_length", String.valueOf(query.length() / 10 * 10));
callCounter.increment();
// 限流:每秒最多 5 次搜索(防止 LLM 滥用工具)
if (!rateLimiter.acquirePermission()) {
log.warn("[WebSearch] 限流触发, query={}", query);
return List.of(SearchResult.fallback("搜索频率过高,请稍后重试"));
}
// 熔断:搜索 API 故障时快速失败
return circuitBreaker.executeSupplier(() -> {
try {
return searchClient.search(
SearchRequest.builder()
.query(query)
.maxResults(5)
.timeout(Duration.ofSeconds(10))
.build()
);
} catch (Exception e) {
meterRegistry.counter("tool.web_search.errors").increment();
throw e;
}
});
}
@Tool(name = "code_execution",
description = "执行 Python 代码片段,返回执行结果。仅用于数据计算和分析。")
public CodeResult executeCode(
@ToolParam(description = "Python 代码,禁止网络访问和文件系统操作")
String pythonCode) {
// 安全沙箱执行:隔离容器 + 资源限制
return sandboxExecutor.run(SandboxRequest.builder()
.code(pythonCode)
.language("python")
.timeoutMs(5000L)
.memoryLimitMb(128)
.networkDisabled(true)
.build());
}
}
// ── 将工具注入到 ChatClient ──
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
WebSearchTool searchTool,
CodeExecutionTool codeTool) {
return builder
.defaultTools(searchTool, codeTool)
.defaultAdvisors(
new MessageChatMemoryAdvisor(redisChatMemory()),
new TracingAdvisor() // OpenTelemetry 链路追踪
)
.build();
}
}
3.2 Executor 节点:并发子任务执行 + 背压控制
java
@Component
@Slf4j
public class ExecutorNode implements NodeAction<AgentState> {
private static final int MAX_CONCURRENT_TASKS = 3; // 背压:最大并发子任务
private static final int SEMAPHORE_TIMEOUT_S = 30;
private final Semaphore concurrencyGuard =
new Semaphore(MAX_CONCURRENT_TASKS);
@Override
public Map<String, Object> apply(AgentState state) {
SubTask task = state.getPlanSteps().get(state.getCurrentStepIdx());
// 背压控制:并发超限时等待,而不是直接失败
try {
if (!concurrencyGuard.tryAcquire(SEMAPHORE_TIMEOUT_S, TimeUnit.SECONDS)) {
throw new AgentBackpressureException("执行器并发饱和,任务: " + task.getId());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AgentExecutionException("执行器被中断", e);
}
try (var ignored = MDC.putCloseable("taskStep", task.getId())) {
meterRegistry.gauge("agent.executor.active",
MAX_CONCURRENT_TASKS - concurrencyGuard.availablePermits());
// 根据任务类型路由到专用 Prompt
String systemPrompt = buildSystemPrompt(task.getType());
String contextJson = buildContext(state.getStepResults(), task);
String result = chatClient.prompt()
.system(systemPrompt)
.user(task.getInstruction() + "\n\n上下文:\n" + contextJson)
.options(ChatOptions.builder()
.temperature(0.3f)
.maxTokens(2048)
.build())
.call()
.content();
// 更新状态:保存执行结果,步骤推进
Map<String, Object> updatedResults =
new HashMap<>(state.getStepResults());
updatedResults.put(task.getId(), result);
int nextIdx = state.getCurrentStepIdx() + 1;
boolean allDone = nextIdx >= state.getPlanSteps().size();
return Map.of(
"stepResults", updatedResults,
"currentStepIdx", nextIdx,
"status", allDone ? AgentStatus.PENDING_REVIEW : AgentStatus.RUNNING
);
} finally {
concurrencyGuard.release();
}
}
}
四、生产级可观测性:三支柱全覆盖
可观测性三支柱
| 支柱 | 技术栈 | 存储 |
|---|---|---|
| Metrics(指标) | Micrometer | Prometheus → Grafana |
| Tracing(链路追踪) | OpenTelemetry | Jaeger |
| Logging(日志) | Logback JSON | ELK Stack |
OpenTelemetry 全链路追踪
java
// TracingAdvisor:在每次 LLM 调用前后注入 Span
@Component
public class TracingAdvisor implements RequestResponseAdvisor {
private final Tracer tracer;
@Override
public AdvisedRequest adviseRequest(AdvisedRequest req, Map<String, Object> ctx) {
Span span = tracer.nextSpan()
.name("llm.call")
.tag("llm.model", req.chatOptions().getModel())
.tag("llm.tokens", "input")
.start();
ctx.put("llm.span", span);
ctx.put("llm.start_ms", System.currentTimeMillis());
return req;
}
@Override
public ChatResponse adviseResponse(ChatResponse resp, Map<String, Object> ctx) {
Span span = (Span) ctx.get("llm.span");
long durationMs = System.currentTimeMillis()
- (Long) ctx.get("llm.start_ms");
Usage usage = resp.getMetadata().getUsage();
span.tag("llm.prompt_tokens", usage.getPromptTokens())
.tag("llm.completion_tokens", usage.getCompletionTokens())
.tag("llm.duration_ms", durationMs)
.end();
// Prometheus 指标:Token 费用估算
meterRegistry.counter("llm.tokens.total",
"type", "prompt").increment(usage.getPromptTokens());
return resp;
}
}
五、生产踩坑:别让 Demo 毁了你的 KPI
🔥 高频踩坑清单
1. 工具调用死循环
LLM 反复调用同一工具但得不到满意结果,导致无限循环消耗 Token。
解法 :给每个工具维护 callCount,单轮超过 3 次后注入 "stop using this tool" 指令到下一轮 Prompt。
2. 状态爆炸 ------ stepResults 无限膨胀
每步都把完整结果写入 AgentState,传入 LLM 的 Context Window 超限。
解法:对 stepResults 做摘要压缩(Summary Agent),只保留关键信息而不是原始输出。
3. Checkpoint 序列化失败
AgentState 中包含不可序列化字段(如 Span 对象),导致 Redis 持久化崩溃。
解法 :Span/MDC 等运行时上下文不要放进 AgentState,用 ThreadLocal 传递,仅在节点入口处恢复。
4. 并发幻觉问题
多 Agent 并发修改同一份 AgentState,导致数据竞争。
解法 :AgentState 设计为不可变快照,每次节点返回的是 delta 字段更新,由框架负责 merge,绝不让两个节点写同一个字段。
5. LLM 输出 JSON 解析失败
LLM 输出的 JSON 有时含 Markdown 代码块包裹或末尾多余字符,导致解析异常。
解法 :统一用正则 \{[\s\S]*\} 提取 JSON 主体,再用 Jackson 的 readTree(),包一层重试逻辑(最多 2 次)。
6. Token 成本失控
没有 Token 预算,Critic 节点反复重规划,一次任务消耗数万 Token。
解法 :给每个 taskId 设置 maxTokenBudget(如 20000),超限后触发降级:直接用当前最佳结果输出,记录告警。
六、框架选型对比
| 维度 | LangGraph4j + Spring AI | 纯 LangChain4j | 自研状态机 |
|---|---|---|---|
| 图编排能力 | ✅ 原生 DAG + 循环 | ⚠️ 链式,不支持循环 | ✅ 完全自定义 |
| Spring 生态集成 | ✅ 开箱即用 | ✅ 成熟 | 🔧 需自行适配 |
| 断点续跑 | ✅ Checkpointer 机制 | ❌ 不支持 | 🔧 需自行实现 |
| 可观测性 | ✅ 内置 OTel 支持 | ⚠️ 需手动埋点 | 🔧 需完整实现 |
| 学习曲线 | 中(需理解 StateGraph) | 低 | 高 |
| 生产案例 | 增长中 | 多 | 视团队而定 |
七、生产部署要点
-
Redis 持久化 AgentState,支持任务断点续跑
使用 LangGraph4j 的 RedisSaver 作为 Checkpointer,任务中途宕机后可从最后一个成功的 Checkpoint 恢复,避免从头重跑的 Token 浪费。
-
异步流式输出:SSE 推送中间进度
用 Spring WebFlux 的 Flux 接收 LLM 流式输出,通过 SSE 实时推送给前端,避免用户等待黑屏(Agent 任务普遍 10-60 秒)。
-
双层 Token 预算:节点级 + 任务级
每个节点设置
maxTokens,同时在 AgentState 记录累计消耗;超过任务预算时触发 Early-Stop,返回当前最优结果。 -
工具执行隔离:超时 + 沙箱 + 熔断
所有外部工具调用必须有超时限制(建议 10-30s),代码执行类工具须在沙箱容器中运行,熔断阈值设置为失败率 50% 触发开路。
-
Grafana 告警:P99 延迟 + Token 消耗 + 失败率
关键指标:任务 P99 延迟、每任务平均 Token 消耗、Agent 失败率、工具调用成功率。失败率 > 5% 立即告警。
总结
把 AI Agent 推向生产,需要的不仅是"让 LLM 能调用工具",而是一套完整的工程化体系:
- 🗺️ LangGraph 状态机:有向图驱动的可控工作流,支持反思重试
- 🌱 Spring AI 工具链:生产级工具注册、熔断、限流一体化
- 🛡 三层容错:限流 → 熔断 → Token 预算,防止故障扩散
- 📊 全链路可观测:Metrics + Tracing + Logging,故障 5 分钟内定位
- 💾 Redis Checkpoint:断点续跑,避免长任务从头重来
- 🔥 踩坑经验:状态爆炸、工具死循环、JSON 解析失败......提前规避