Spring AI Agent 漫画实战
第 09 话 SYSTEM

从独狼到团队:多 Agent 编排与工作流 一个 Agent 干不完的活,怎么拆给一群 Agent —— 以及什么时候根本不该拆

这一话先泼冷水:能用确定性编排解决的,绝不让模型自由发挥。 然后给你五种编排模式在 Spring 里的落地写法、Agent 循环的终止条件、状态持久化方案,以及官方"没有图编排"这件事该怎么补救。

老 K
我要搞个"全能 Agent":接需求、写代码、跑测试、提 PR,全自动。
小 S
这就是典型的"Agent 过度设计"。
旁白:老 K 想做的东西,本质是一条有明确先后顺序的流水线:解析需求 → 生成代码 → 跑测试 → 提 PR。 这条链路不需要模型来决定"下一步干什么" —— 它只需要模型在每一步里把活干好。 用代码写死顺序,比让模型"规划"要快 3 倍、便宜 5 倍、稳定 10 倍。

一、第一原则:Workflow 优先,Agent 兜底

维度Workflow(确定性编排)Agent(模型决定下一步)
控制流代码写死分支与顺序模型输出决定
可测性每一步可单独单测只能端到端评测
成本N 次调用,可预测1~∞ 次,可能失控
延迟可并行,可控串行推理,通常更慢
适用流程已知、步骤固定的任务(90% 场景)步骤未知、需要探索与试错(排障、调研、开放式分析)
实践心法

先画流程图,再看哪些节点需要"智能"。只有那些"无法用规则描述"的节点才交给模型; 其余节点用普通 Java 代码实现。这样你会得到一个「大部分是确定性代码 + 少数几个 LLM 节点」的系统 —— 这才是能上生产的形态。

二、五种编排模式(附 Spring 落地写法)

1. Prompt Chaining(链式) 输入 LLM 1 契约对象 LLM 2 输出 每步之间用结构化输出做"契约",中间可插入校验门(不合格就中断) 2. Routing(路由) 输入 分类器 LLM 退款专家 物流专家 转人工 用廉价模型 + 枚举结构化输出做分类,走 Map<枚举, ChatClient> 分发 3. Parallelization(并行)     4. Orchestrator-Workers(编排者-工作者)   5. Evaluator-Optimizer(评估优化) 输入 专家 A 专家 B 聚合 目标 主 Agent Worker 1 Worker 2 生成 打分 不达标 → 带反馈重写(最多 N 轮)

模式 1:Prompt Chaining —— 用结构化输出当"接口契约"

public record Requirement(String module, List<String> acceptance, String risk) {}

// 第一步:把自然语言需求解析成结构化对象(用廉价模型即可)
Requirement req = parserClient.prompt()
        .user("""
              请把下面的需求解析为 JSON:
              {requirement}
              只输出 JSON,不要解释。
              """)
        .user(u -> u.param("requirement", rawText))
        .call()
        .entity(Requirement.class);     //!hl 结构化输出 = 两个节点之间的 DTO

// 第二步:契约校验(纯 Java,不烧 token)
if (req.acceptance().isEmpty()) throw new InvalidRequirementException();  //!hl

// 第三步:拿着契约对象去生成代码(用强模型)
String code = coderClient.prompt()
        .user(u -> u.text("按以下验收标准实现:{a}").param("a", req.acceptance()))
        .call().content();

模式 2:Routing —— 枚举分类 + 策略分发

public enum Intent { REFUND, LOGISTICS, BILLING, HUMAN }

record ClassifyResult(Intent intent, double confidence) {}

@Service
class RouterService {
    private final Map<Intent, ChatClient> experts;   //!hl

    public String handle(String question) {
        ClassifyResult r = cheapClient.prompt()
                .user("判断意图,只输出 JSON:{q}")
                .user(u -> u.param("q", question))
                .call().entity(ClassifyResult.class);

        if (r.confidence() < 0.6 || r.intent() == Intent.HUMAN) {  //!hl 低置信度兜底
            return transferToHuman(question);
        }
        return experts.get(r.intent())          //!hl
                .prompt().user(question).call().content();
    }
}

路由的核心价值是成本分层:分类用最便宜的模型(甚至用规则/关键词先过一遍),只有真正需要推理的请求才交给贵模型。

模式 3:Parallelization —— 并发跑多个专家

List<CompletableFuture<ExpertOpinion>> futures = List.of(
        CompletableFuture.supplyAsync(() -> logExpert.analyze(incident), pool),   //!hl
        CompletableFuture.supplyAsync(() -> metricExpert.analyze(incident), pool),
        CompletableFuture.supplyAsync(() -> deployExpert.analyze(incident), pool));

// 注意:要设超时,否则一个慢专家拖垮全局
List<ExpertOpinion> opinions = futures.stream()
        .map(f -> f.completeOnTimeout(ExpertOpinion.timeout(), 20, TimeUnit.SECONDS)) //!hl
        .map(CompletableFuture::join)
        .toList();

// 最后用一个"聚合 Client"把多路意见综合成结论
String conclusion = summarizer.prompt()
        .user(u -> u.text("综合以下专家意见给出结论:{o}").param("o", opinions))
        .call().content();

模式 4:Orchestrator-Workers —— 把专家 Agent 包装成工具

关键技巧

工作者 Agent 不必是"另一个服务",它可以就是一个 @Tool 方法: 主 Agent 看到的是一个工具,工具内部跑的是一整个子 Agent(有自己的 system prompt、记忆隔离、甚至不同的模型)。 这样主 Agent 可以自己决定"先问谁、问几次",同时你又能在代码侧控制每个子 Agent 的能力边界。

@Service
public class ExpertTools {

    private final ChatClient logExpert;     // 独立的 ChatClient:不同的 system prompt / 模型 / 工具
    private final ChatClient deployExpert;

    @Tool(description = "交给日志分析专家:给定服务名与时间窗,返回异常模式、TOP 错误与初步判断") //!hl
    public String askLogExpert(
            @ToolParam(description = "服务名") String service,
            @ToolParam(description = "时间窗,如 2026-10-15 14:00~14:30") String window) {
        return logExpert.prompt()                       //!hl
                .user(u -> u.text("分析 {s} 在 {w} 的日志")
                            .param("s", service).param("w", window))
                .call().content();                      // 子 Agent 独立会话,不污染主上下文 //!hl
    }

    @Tool(description = "交给发布变更专家:查询该时间窗内的发布/配置变更记录并判断是否相关")
    public String askDeployExpert(
            @ToolParam(description = "服务名") String service,
            @ToolParam(description = "时间窗") String window) {
        return deployExpert.prompt()
                .user(u -> u.text("查询 {s} 在 {w} 的变更").param("s", service).param("w", window))
                .call().content();
    }
}

// 主 Agent 只挂载"专家工具",自己不直接碰数据
@Bean
ChatClient orchestrator(ChatClient.Builder builder, ExpertTools experts) {
    return builder
            .defaultSystem("""
                你是排障总指挥。必须:
                1) 先通过专家工具取证,至少调用 2 个专家;
                2) 证据不足时继续追问,但总调用次数不超过 8 次;
                3) 最终输出:根因(带证据)、影响面、处置建议、置信度。
                禁止在缺少证据时下结论。
                """)                                   //!hl
            .defaultToolCallbacks(MethodToolCallbackProvider.builder()
                    .toolObjects(experts).build())
            .build();
}
为什么这样更好

模式 5:Evaluator-Optimizer —— 让另一个模型当评审

String draft = writer.prompt().user(topic).call().content();

for (int i = 0; i < 3; i++) {                            //!hl 最多迭代 3 轮
    Review review = judge.prompt()                        //!hl
            .user(u -> u.text("""
                    评审下面的方案,输出 JSON:{score}(1-10) 与 {issues} 列表。
                    只输出 JSON。
                    方案:{draft}
                    """).param("draft", draft))
            .call().entity(Review.class);

    if (review.score() >= 8) break;                       //!hl 达标即停

    draft = writer.prompt()                               //!hl 带反馈重写
            .user(u -> u.text("""
                    根据评审意见修改方案。
                    原方案:{draft}
                    意见:{issues}
                    """).param("draft", draft).param("issues", review.issues()))
            .call().content();
}

三、Agent 循环骨架:终止条件比循环本身重要

public AgentResult run(String goal, String conversationId) {
    int step = 0;
    long tokenUsed = 0;
    Deque<String> recentActions = new ArrayDeque<>();

    while (true) {
        if (++step > MAX_STEPS)              return AgentResult.abort("达到最大步数 " + MAX_STEPS);   //!hl
        if (tokenUsed > TOKEN_BUDGET)        return AgentResult.abort("超出 token 预算");             //!hl
        if (costService.exceeded(goal))      return AgentResult.abort("超出成本上限");                 //!hl
        if (isLooping(recentActions))        return AgentResult.abort("检测到重复动作,疑似死循环");     //!hl
        if (approvalService.isInterrupted(conversationId)) return AgentResult.abort("被人工中断");      //!hl

        Step s = planner.prompt()
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
                .user(goal)
                .call().entity(Step.class);       // {thought, action, args, isFinal}

        recentActions.add(s.action());
        tokenUsed += s.tokens();

        if (s.isFinal()) return AgentResult.done(s.output());     //!hl
        String observation = actionExecutor.execute(s);           // 工具执行(含审批/沙箱)
        memory.add(conversationId, List.of(new UserMessage("观察结果:" + observation)));
    }
}

// 连续 3 步动作相同 = 卡住了
private boolean isLooping(Deque<String> q) {
    return q.size() >= 3 && new HashSet<>(q).size() == 1;    //!hl
}
五个必须有的闸门

步数上限 / token 预算 / 成本上限 / 死循环检测 / 人工中断。 缺任何一个,你的 Agent 迟早会在半夜烧掉一笔钱或写坏一批数据。循环是 Agent 的心脏,闸门是它的保险丝。

四、状态管理:Agent 不是无状态服务

// 一次 Agent 运行的完整状态 —— 建议持久化,支持断点续跑与事后复盘
public record AgentState(
        String threadId,        // 会话/任务 ID
        String goal,            // 目标
        List<Step> history,     // 已执行的步骤(每一步:thought / action / args / observation)
        Map<String, Object> scratchpad,  // 中间产物(如查到的数据)
        AgentStatus status,     // RUNNING / WAITING_APPROVAL / DONE / ABORTED
        long tokenUsed,
        Instant expireAt) {}
状态类型存哪说明
对话消息ChatMemoryRepository(JDBC / Redis)第 05 话已述
执行步骤与中间产物业务表(JSON 列)+ 版本字段支持断点续跑、支持"回放"排查
审批中的动作独立审批表 + 状态机人工介入的唯一入口
工具副作用业务系统自身幂等键必须落库

五、图编排:官方没有,怎么办?

Spring AI 1.x 官方不提供类似 LangGraph 的图编排框架(没有 StateGraph / CompiledGraph)。 这是事实,不要硬找。三条可行路线:

方案做法适用
自研状态机用 enum State + switch 或 Spring Statemachine,节点 = Spring Bean,边 = 条件表达式流程可控、团队可控、依赖最少,中小团队首选
社区图框架如 spring-ai-alibaba-graph 提供的 StateGraph(节点 + 边 + 条件 + 编译执行),与 Spring AI 生态兼容需要复杂分支、循环、并行、检查点时;注意它是社区方案,版本跟随风险自担
流程引擎Flowable / Camunda 编排,Agent 作为"服务任务"节点已有 BPM 基建、需要可视化流程与人工审批流的传统企业
选型建议

先手写状态机。当且仅当你的图出现「条件分支 + 回环 + 并行 + 需要检查点恢复」这四样东西中的三样时,再引入图框架。 过早引入图编排框架,等于用复杂度换一个你还没遇到的需求。

六、反模式清单(看到就该重构)

反模式为什么危险 / 怎么改
把 30 个工具全塞给一个 Agent选择准确率暴跌、token 暴涨。按场景动态挂载,或拆成多个专家 Agent
循环没有步数与预算上限一次异常输入就能烧掉一天预算。加五道闸门
让模型直接写 SQL 连生产库注入 + 越权 + 慢查询三重暴击。改为"参数化查询工具 + 只读账号 + 行数上限"
所有子 Agent 共享一份无限增长的上下文越跑越贵越跑越笨。子 Agent 独立上下文,主 Agent 只拿结论
用 Agent 做有明确 SOP 的事用 Workflow,成本降一个数量级
子 Agent 失败后主流程直接崩每个节点都要有超时、降级与兜底输出
小 S
记住这张图的判断顺序:能写死就写死 → 写不死就分类路由 → 路由不了才让模型自己规划。自由度是奢侈品,按克买。
← 上一话:MCP 目录 最终话:生产化与综合实战 →