AI Agent 多智能体编排生产实战


引言

从"会用 GPT"到"生产级 Agent",这条路比你想象的要颠簸得多。

单个 LLM 调用像个优秀实习生:能干,但得手把手指挥;真正的 Agent 系统更像一个自主工程团队------能规划、能分工、能纠错、能汇报。

本文带你搭一套可在生产落地的 AI Agent 编排系统 :以 LangGraph4j 驱动状态机工作流,以 Spring AI 封装工具调用,配以背压控制、可观测性链路追踪和优雅降级,把 Agent 从 Demo 推向真正的生产环境。


关键数据

指标 数值
企业 AI 项目因缺少编排层而失败(Gartner 2025) 73%
多 Agent 系统相比单 Agent 平均任务完成率提升
生产 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)
生产案例 增长中 视团队而定

七、生产部署要点

  1. Redis 持久化 AgentState,支持任务断点续跑

    使用 LangGraph4j 的 RedisSaver 作为 Checkpointer,任务中途宕机后可从最后一个成功的 Checkpoint 恢复,避免从头重跑的 Token 浪费。

  2. 异步流式输出:SSE 推送中间进度

    用 Spring WebFlux 的 Flux 接收 LLM 流式输出,通过 SSE 实时推送给前端,避免用户等待黑屏(Agent 任务普遍 10-60 秒)。

  3. 双层 Token 预算:节点级 + 任务级

    每个节点设置 maxTokens,同时在 AgentState 记录累计消耗;超过任务预算时触发 Early-Stop,返回当前最优结果。

  4. 工具执行隔离:超时 + 沙箱 + 熔断

    所有外部工具调用必须有超时限制(建议 10-30s),代码执行类工具须在沙箱容器中运行,熔断阈值设置为失败率 50% 触发开路。

  5. Grafana 告警:P99 延迟 + Token 消耗 + 失败率

    关键指标:任务 P99 延迟、每任务平均 Token 消耗、Agent 失败率、工具调用成功率。失败率 > 5% 立即告警。


总结

把 AI Agent 推向生产,需要的不仅是"让 LLM 能调用工具",而是一套完整的工程化体系

  • 🗺️ LangGraph 状态机:有向图驱动的可控工作流,支持反思重试
  • 🌱 Spring AI 工具链:生产级工具注册、熔断、限流一体化
  • 🛡 三层容错:限流 → 熔断 → Token 预算,防止故障扩散
  • 📊 全链路可观测:Metrics + Tracing + Logging,故障 5 分钟内定位
  • 💾 Redis Checkpoint:断点续跑,避免长任务从头重来
  • 🔥 踩坑经验:状态爆炸、工具死循环、JSON 解析失败......提前规避

相关推荐
niuniuyi~2 小时前
科研阶段记录2-上
人工智能·知识图谱
astragin2 小时前
Wolfram Mathematica汉化版试用版下载入口
人工智能
Omics Pro2 小时前
3种蛋白结构输入方式!已申报欧洲发明专利
数据库·人工智能·python·机器学习·plotly
声光界2 小时前
《声音与音乐中的情感理解及人机交互设计》
人工智能·人机交互·声学
voidmort2 小时前
13. 强化学习中的评估、奖励设计与 Reward Hacking
人工智能
Studying 开龙wu2 小时前
16位工业灰度图的深度学习预处理:从方法选择到ImageJ实战
人工智能·深度学习
烟雨江南7852 小时前
特高压输电线路带电作业直升机吊篮与强电磁感应放电:基于“灵声智库”空间自适应滤波与声纹授权的离线语音控制指令方案
人工智能·ffmpeg·webrtc·语音识别·ai质检
清辞8533 小时前
入门大模型工程师第十课----学习总结
大数据·人工智能·深度学习·学习·语言模型
zhangfeng11333 小时前
那nvidia orim车载gpu tee安全飞地 和天垓 100 gpgpu的 飞地 ,大概有多大存储量 ,解密流程
人工智能·深度学习·安全·语言模型·gpu算力·芯片