[AI工程] Spring AI 第十四篇:Agent 五种模式在 2.0 里怎么写

💡 前几篇把零件拆完了:ChatClient、Tools、MCP、RAG、评测、可观测。接下来自然的问题是——“那我什么时候算是写了一个 Agent?”

我见过两种极端。一种把任何 .tools(...) 都叫 Agent,结果一次简单查询被模型循环调三次工具;另一种坚持"Agent 太不可控",把所有流程写成十几段 prompt 串起来的硬编码链,改一次要回归全部。两边都不对:真正的问题不是"要不要 Agent",而是这个任务的哪种不确定性值得交给模型*。***

Anthropic 那份被引用很多的《Building Effective Agents》把可控的编排归纳成几种模式:Chain、Parallelization、Routing、Orchestrator-Workers、Evaluator-Optimizer。Spring AI 2.0 没有把"Agent"做成一个抽象类,它给的是更底层的积木:ChatClient + Advisor 链 + 结构化输出 + 工具循环。所以模式要自己拼,但拼法有标准答案。

这一篇按五种模式各给一段能跑的 Java 代码,说清各自的适用与不适用、常见的翻车点,最后给一张"五种模式 ↔ Spring AI 2.0 落点"的对照表(含 Spring AI Alibaba Agent Framework 里已经做好的那几个 FlowAgent),并回答一个更实际的问题:哪些需求根本不该用 Agent。API 事实按本地 Spring AI 2.0.11.1.2 与 Spring AI Alibaba 1.1.2.0 sources jar 核对。

下面开启: 【AI工程】Spring AI 第十四篇:Agent 五种模式在 2.0 里怎么写(实战代码 + 什么时候别用)]

下面开启: 【AI工程】Spring AI 第十四篇:Agent 五种模式在 2.0 里怎么写(实战代码 + 什么时候别用)


1. Workflow 还是 Agent,先看谁决定路径

判断标准只有一条:执行路径是写死的,还是模型当场决定的。

1.1 一张表划清边界

维度Workflow(工作流)Agent(智能体)
路径由谁决定开发者写在代码里模型按中间结果动态决定
步数可预估不可预估(可能循环)
可测试性高,能断言每一步低,只能断言最终产物
成本线性可控会被循环放大
失败形态某一步报错,定位清楚跑偏、绕圈、提前收工
适合流程已知的业务分解方式事先说不清的任务

一句话理解:Workflow 是"我把步骤写清楚,模型只负责填空";Agent 是"我给目标和安全边界,模型自己决定下一步"。

1.2 Spring AI 2.0 里哪部分是哪种

ChatClient 请求
   │
   ├─ Advisor 链(顺序、次数由你写死)           ← Workflow:Memory → RAG → ToolCalling → Logger
   │
   └─ ToolCallingAdvisor 的 do { … } while 循环   ← Agent 特性:调不调工具、调哪个、几轮,模型说了算
             ↑
        唯一的"自主性"来源:工具返回值重新进 prompt

也就是说,Spring AI 的 Agent 能力就一个抓手:工具。所谓五种模式,本质是"你在工具循环外面套什么样的编排"。

1.3 五种模式速查

模式核心机制复杂度什么时候用什么时候别用
Chain 链式顺序执行,前步输出 = 后步输入步骤固定、要拿延迟换准确步骤之间其实无依赖
Parallelization 并行化并发多路 + 聚合⭐⭐可切分的独立子任务、要多视角投票子任务互相依赖
Routing 路由先分类,再交给专门链⭐⭐类别清晰、每类 prompt 差异大类别边界模糊(会变成乱分)
Orchestrator-Workers模型动态拆解 + 并行执行 + 合成⭐⭐⭐事先不知道要拆成几块拆错代价高、需要强审计
Evaluator-Optimizer生成 → 评估 → 带反馈重写⭐⭐⭐⭐有可判的质量标准标准主观、用户在等首 Token
Q:我到底该不该上 Agent?
你的情况建议
能用一条 SQL / 一次 if-else 解决别调模型,业务代码更快更稳
流程固定,只是每步要生成文本Chain 或 Parallelization(Workflow)
输入类型混杂但类别可枚举Routing,且保留 OTHER 兜底分支
任务分解方式依赖中间结果Agent(工具循环 / Orchestrator)
出错了要能追责到"哪一步谁做的"用 Workflow 编排 + 少量工具,别放全自主 Agent

2. 模式一:Chain —— 把"想清楚"拆成几步

最被低估的模式:它不做任何花哨事,但准确率提升最直接。

2.1 实战:报告生成链

record Outline(List<String> sections) {}
record Report(String title, String body, List<String> openQuestions) {}

@Service
class ReportChain {

    private final ChatClient writer;
    private final ChatClient editor;

    // 注意:注入 ChatClient.Builder(第十四篇的 NOOP 陷阱),不要 ChatClient.builder(chatModel)
    ReportChain(ChatClient.Builder writerBuilder, @Qualifier("editorClient") ChatClient editor) {
        this.writer = writerBuilder.clone().defaultSystem(WRITER_PROMPT).build();
        this.editor = editor;
    }

    Report generate(String topic) {
        // ① 结构先行:让模型只做骨架,输出可断言
        Outline outline = writer.prompt()
                .user("为「%s」拟一份技术报告大纲,3~6 节,不要正文。".formatted(topic))
                .call().entity(Outline.class);

        // ② 逐节成文:每节都把"已写内容"作为上下文传下去
        StringBuilder draft = new StringBuilder();
        for (String section : outline.sections()) {
            draft.append(writer.prompt()
                    .user("大纲:%s%n已写:%s%n请只写这一节:%s".formatted(outline.sections(), draft, section))
                    .call().content());
            draft.append("\n\n");
        }

        // ③ 编辑收敛:换一个角色、换一份 prompt,专挑毛病
        return editor.prompt()
                .user("校对下面报告:删除重复表述、统一术语、标出事实性风险;不要重写全文。%s".formatted(draft))
                .call().entity(Report.class);
    }
}

Chain 的关键不在"多调几次",而在每一步的输出形状被约束:① 和 ③ 用 .entity(record),② 用自由文本。这样任何一步失败你都能定位,而不是拿到一坨看不出问题的长文。

2.2 误差累积与缓解

症状原因缓解
越到后面越离谱前面的小错被后续步骤当事实继承每步结构化输出 + 关键中间产物落库,可重放
延迟线性增长N 次串行往返无依赖的步骤改用并行(模式二)
成本不可控每步都携带全文上下文传"摘要 + 当前节要求",别把全文塞回 prompt
Q:Chain 和 Orchestrator-Workers 差在哪?

拆不拆、拆成几份,是 Chain 里你写死的;Orchestrator 是模型当次决定的。 前者可回归测试,后者的输出集合每次都可能不同——这条差别决定了后面所有工程成本。


3. 模式二:Parallelization —— 分段与投票

两种变体,解决两类完全不同的问题。

3.1 变体分工

变体做法解决什么典型场景
Sectioning 分段不同子任务并发(各用不同 prompt)缩短总耗时、职责隔离合同从法务/财务/安全三个视角各评一次
Voting 投票同一任务跑多次(换措辞或换模型)降低单次抖动、提高稳定性代码审查结论、风险等级判定

3.2 实战:并发风险评估 + 聚合

private static final ExecutorService VIRTUAL = Executors.newVirtualThreadPerTaskExecutor();

public RiskReport assess(String contract) {
    List<CompletableFuture<RiskItem>> jobs = List.of("法务", "财务", "安全").stream()
            .map(angle -> CompletableFuture.supplyAsync(
                    () -> riskOf(angle, contract), VIRTUAL))       // 三路并发
            .toList();

    List<RiskItem> items = jobs.stream().map(CompletableFuture::join).toList();   // gather
    return merge(items);
}

private RiskItem riskOf(String angle, String contract) {
    // 每个视角单独一条链:system prompt 里限定"只从该角度评价,不要综合结论"
    return reviewerClient.prompt()
            .user("角度:%s%n条款:%s%n输出该角度下的最高风险点与依据条款号。".formatted(angle, contract))
            .call().entity(RiskItem.class);
}

Java 版本要先确认:Executors.newVirtualThreadPerTaskExecutor() 需要 JDK 21+,而 Spring Boot 4 的最低要求是 Java 17。spring.threads.virtual.enabled=true 只影响 Boot 自己管理的执行器(Web 容器、@AsyncTaskExecutor),不会让你手工 new 的线程池自动变虚拟线程,两件事别混。

投票变体只多两点:同一入参跑 N 次、结果用枚举做多数决。

record Judgment(String verdict, String reason) {}    // verdict ∈ {PASS, REWRITE}

public String vote(String code) {
    var votes = IntStream.range(0, 3).mapToObj(i -> CompletableFuture.supplyAsync(
                    () -> judgeClient.prompt().user(JUDGE_PROMPTS[i % 3] + code).call().entity(Judgment.class),
                    VIRTUAL))
            .map(CompletableFuture::join).toList();
    long rewrite = votes.stream().filter(v -> "REWRITE".equals(v.verdict())).count();
    return rewrite >= 2 ? "需改:" + votes.stream().map(Judgment::reason).distinct().toList()
                        : "可合入";
}

有个已存在的并发点值得利用:RetrievalAugmentationAdvisor 内部默认就带一个 ThreadPoolTaskExecutor(core 4 / max 16,线程名前缀 ai-advisor-,并挂 ContextPropagatingTaskDecorator)来并行处理多路查询扩展。你换掉它时要一起换 decorator,否则并行分支的 trace 会从主链上断掉(第十四篇 3.3 提过)。


4. 模式三:Routing —— 先分类,再交给专门的链

最实用、也最容易被忽略"兜底分支"的一种。

4.1 实战:客服分流

enum Route { POLICY, ORDER, REFUND, OTHER }
record Classification(Route route, String reason) {}

@Service
class AirlineRouter {

    private final ChatClient classifier;
    private final Map<Route, ChatClient> handlers;      // 每个分支一条独立链

    String answer(String question, String userId) {
        Classification c = classifier.prompt()
                .user("把下面问题归类为 POLICY(条款)/ORDER(订单查询)/REFUND(退改操作)/OTHER。%n%s".formatted(question))
                .call().entity(Classification.class);

        ChatClient target = handlers.getOrDefault(c.route(), handlers.get(Route.OTHER));   // ← 兜底必须有
        return target.prompt()
                .user(question)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId + ":" + c.route()))
                .toolContext(Map.of("userId", userId))
                .call().content();
    }
}

注意最后一行:路由决定了走哪条链,但身份仍然从 ToolContext,不要让分类模型来决定权限(第十一篇 5.4 的三层隔离在这里同样适用)。

4.2 规则路由还是 LLM 路由

依据用什么理由
有明确关键词/字段(订单号前缀、按钮来源)业务代码路由零成本、可单测、不会漂移
语义类别(投诉 vs 咨询)LLM 分类,输出枚举规则写不全
类别边界模糊、混类多别路由,改用带工具的单一链分错比不分更糟
每条链 prompt 差异极大(长度、工具集)LLM 路由收益明显省 Token,也避免巨型 prompt
Q:路由为什么不直接用 Advisor 实现?

可以:写一个 CallAdvisor,在 before() 里分类、把结果塞进 context,再让后续 Advisor 按 context 决定是否生效。但**"换一整条 ChatClient"这种分支,用 Advisor 表达会很别扭**——Advisor 适合横切能力(记忆、检索、日志),不适合互斥的业务分派。我的经验:分支 ≤ 2 且共享大部分配置 → Advisor;分支是不同系统 → 独立 Bean + 显式路由。


5. 模式四:Orchestrator-Workers —— 让模型决定怎么拆

Manus 那类通用智能体的骨架;灵活度和风险同时最高。

5.1 三个角色

需求 ──► Orchestrator(LLM 动态拆) ──► [Worker1, Worker2, …WorkerN](并发执行) ──► Synthesizer(LLM 合并)──► 交付物
          输出子任务列表                     各自独立上下文/工具/模型                    冲突要显式列出
组件职责输出约束
编排器决定拆成几块、每块要什么record Plan(List<String> subtasks),并限死上下界(2~6)
工作者完成单个子任务结构化输出,带 subtask 回填便于归因
合成器合并、消解冲突要求"冲突逐条列出并给结论",禁止静默取舍

5.2 实战:设计一个考勤系统方案

record Plan(List<String> subtasks) {}
record SubResult(String subtask, String deliverable) {}

public String design(String requirement) {
    Plan plan = planner.prompt()
            .user("把需求拆成 2~6 个互不重复、可独立完成的子任务,只输出子任务描述。%s".formatted(requirement))
            .call().entity(Plan.class);

    // 并行执行;worker 只看到"整体需求 + 自己这一块",避免上下文互串
    List<SubResult> results = plan.subtasks().parallelStream()
            .map(task -> new SubResult(task, worker.prompt()
                    .user("整体需求:%s%n你只负责:%s%n给出该部分的可落地设计与接口草案。".formatted(requirement, task))
                    .call().content()))
            .toList();

    return synthesizer.prompt()
            .user("合并下列交付物为一份方案;发现互相矛盾处必须单列并给出取舍理由。%s".formatted(results))
            .call().content();
}

三个必须加的护栏:

  1. 子任务数量上界.entity() 的 schema 只能约束格式,不能约束数量,所以要在提示词里写死并在代码里 if (plan.subtasks().size() > 6) throw …)。
  2. 每个 worker 单独超时与重试,一个卡住不能拖死整批——parallelStream() 跑在公共 ForkJoinPool 上,阻塞式模型调用会把公共池占满,拖慢同进程里其他并行任务,压测时很容易复现;生产建议换成显式虚拟线程执行器 + orTimeout(...)
  3. 合成器不许静默取舍:让冲突显式可见,才有复盘的抓手。

5.3 和 Routing 到底怎么分

特性RoutingOrchestrator-Workers
分支/子任务预定义枚举模型动态生成
数量固定每次可变
合并不需要(单链返回)需要 LLM 合成
可回归性高(分类正确即可断言)低(要断言的是产物质量)
成本≈ 1 条链1 + N + 1 次调用起

6. 模式五:Evaluator-Optimizer —— 循环必须有出口

第十三篇已经把评测器讲透,这一节只补"编排"这一层。

6.1 最小实现:两个角色一个上限

record Verdict(boolean pass, String feedback) {}

public String refine(String task, int maxRounds) {
    String draft = generator.prompt().user(task).call().content();
    for (int round = 1; round <= maxRounds; round++) {
        Verdict v = checker.prompt()
                .user("按【能否直接交付客服坐席】评审,输出 pass 与一句可执行反馈:%s".formatted(draft))
                .call().entity(Verdict.class);
        if (v.pass()) {
            return draft;
        }
        draft = generator.prompt()
                .user("%s%n上一版:%s%n反馈:%s%n只修反馈指出的问题。".formatted(task, draft, v.feedback()))
                .call().content();
    }
    return draft + "\n\n[未通过自动评审,转人工]";   // 出口:交出去但标清楚
}

三个容易写错的地方:没通过时必须带标记返回(否则会静默交付劣质结果)、反馈必须具体到可执行("再优化一下"只会绕圈)、maxRounds 要与成本预算对齐(每轮至少 2 次模型调用)。

6.2 格式收敛用现成的那个 Advisor

如果只是要保证输出符合 schema,不必自己写循环:

.defaultAdvisors(StructuredOutputValidationAdvisor.builder()
        .outputType(RefundSummary.class)
        .maxRepeatAttempts(3)                 // 默认值;源码里总上限是 1 + 3 次调用
        .build())
事实(2.0.1 源码)对编排的含义
默认 order = BaseAdvisor.LOWEST_PRECEDENCE - 2000,极靠内重试只重跑模型,不重跑记忆与工具循环
chatResponse.hasToolCalls() 时跳过校验工具轮次不会被格式校验卡住
校验失败把错误原因追加进用户消息再来一轮反馈是"格式错在哪",不含上一版错误输出
它是 Evaluator-Optimizer 的特例只管格式;内容质量仍要靠第十三篇的两个评估器

7. 五种模式的落地:自己写,还是用现成编排

7.1 对照表:模式 → Spring AI 2.0 落点 → 生态里已有的实现

模式自己写的抓手Spring AI Alibaba Agent Framework(1.1.2.0 源码里有这些类)
Chain顺序 ChatClient 调用 + .entity()SequentialAgentextends FlowAgent
Parallelization虚拟线程 / CompletableFuture + 聚合ParallelAgent(注释即写明 Fan-Out/Gather 并合并结果)
Routing分类 → Map 分派LlmRoutingAgent(builder 可设做路由决策的 ChatModel
Orchestrator-Workersplanner / worker / synthesizer 三段SupervisorAgent(由主 ReactAgent 产出决策,决定调哪个子智能体或结束)
Evaluator-Optimizer评审循环 / StructuredOutputValidationAdvisorLoopAgentCOUNT / CONDITION / JSON_ARRAY 三种循环模式,可实现 LoopStrategy 自定义

说明:这张表的右列我没有实际跑过,只是按本地 spring-ai-alibaba-agent-framework:1.1.2.0 sources jar 的类名与 javadoc 列举,供你判断"要不要自己造"。它们底层是 Graph 编排(spring-ai-alibaba-graph-core),代价是多一套状态图心智模型;如果你的编排已经复杂到要画状态机,那说明自研五段代码也撑不住了,这时候用它更划算。

7.2 版本兼容:1.1.x 与 2.0.x 的差别集中在名字

这点务必注意:spring-ai-alibaba-*:1.1.2.0 依赖的是 Spring AI 1.1.2(其 spring-ai-autoconfigure-model-chat-client 等坐标版本可在 pom 里看到),不是 2.0。

Spring AI 1.1.2Spring AI 2.0.1
工具循环 AdvisorToolCallAdvisor(默认 order HIGHEST_PRECEDENCE + 300ToolCallingAdvisorDEFAULT_ORDER 同值),ToolCallAdvisor 变成 @Deprecated(since="2.0.0", forRemoval=true) 的子类
工具注册defaultTools(Object...) + defaultToolCallbacks(...)只有 defaultTools(Object...)defaultToolCallbacksforRemoval
重复注册防护新增标记接口 ToolAdvisor:链路里已有它的实现就不再自动注册工具 Advisor;同时声明两个会在构建时抛 IllegalStateException: At most one ToolAdvisor is allowed in the advisor chain
ChatClient.builder(ChatModel)NOOP registry同样 NOOP(第十四篇 2.2)
StructuredOutputValidationAdvisor存在存在,行为一致

好消息是:第十一篇那张 Advisor 顺序表在两个版本上都成立(order 数值没变),只是类名要从 ToolCallAdvisor 改成 ToolCallingAdvisor

7.3 选型决策图

任务来了
  │
  ├─ 不需要生成/判断? ── 是 ──► 写业务代码,别碰模型
  │
  ├─ 步骤已知且固定? ── 是 ──► Chain(要快就把无依赖步骤改 Parallelization)
  │
  ├─ 输入类型可枚举? ── 是 ──► Routing(规则优先,语义才用 LLM;必带 OTHER)
  │
  ├─ 分解方式要看中间结果? ── 是 ──► Orchestrator-Workers / 工具循环 Agent
  │                                      └─ 加三护栏:子任务上界、单任务超时、合成器显式列冲突
  ├─ 有明确质量标准? ── 是 ──► Evaluator-Optimizer(格式问题交给 SOVA;内容问题交给评估器)
  │
  └─ 都不像 ──► 先做 Chain 版本上线,拿到失败样本再决定升级到哪种

7.4 上线前自检

模式我必查的几件事
通用每次调用有没有 span(第十四篇);单请求模型调用数是否在预算内
Chain中间产物可重放?某一步失败能单独重试而不是整链重来?
Parallelization有没有用公共 ForkJoinPool 跑阻塞调用?并行度上限?部分失败怎么处理?
RoutingOTHER 分支真的存在且被统计过占比?分类错例有没有进评测集?
Orchestrator子任务数量上界与超时?成本是否按 1+N+1 预估过?
EvaluatormaxRounds 出口是否带"未通过"标记?判卷模型是否比生成模型便宜?

7.5 知识脉络图

Agent 五种模式(Spring AI 2.0)
├── 边界:Workflow(路径写死)vs Agent(工具循环里模型决定)
│   └── Spring AI 的自主性只有一个抓手:ToolCallingAdvisor 的 do-while
├── Chain        :顺序 + 每步 .entity() 约束;误差会累积 → 中间产物落库可重放
├── Parallel     :Sectioning(独立子任务)/ Voting(同任务多视角)
│   └── JDK 21 虚拟线程;spring.threads.virtual.enabled 只管 Boot 自己的 executor
├── Routing      :Classification(record) → Map<Route, ChatClient> + OTHER 兜底;身份仍走 ToolContext
├── Orchestrator :Plan(List<String>) → 并发 worker → Synthesizer;护栏=上界/超时/冲突显式
├── Evaluator    :生成↔评审循环,maxRounds 必须有出口标记
│   └── 特例:StructuredOutputValidationAdvisor(order=LOWEST-2000,最多 1+3 次调用,跳过 tool 轮次)
└── 落地选择:自己拼积木 vs spring-ai-alibaba-agent-framework(Sequential/Parallel/LlmRouting/Supervisor/Loop Agent)

最后总结

  • 如果只是想稳定地把活干完:Chain 是性价比最高的模式,别急着谈 Agent。
  • 如果需要快或稳:可切分就 Parallelization(虚拟线程 + 显式并行度),要抗抖动就 Voting,但记得别把阻塞调用扔进公共 ForkJoinPool
  • 如果输入天然分几类:Routing,规则能分就别问模型,且必须留 OTHER 分支并统计它占比。
  • 如果分解方式事先说不清:才上 Orchestrator-Workers,并且先加三道护栏(子任务上界、单任务超时、合成器显式列冲突)。
  • 如果有明确质量标准:Evaluator-Optimizer;只要格式,交给 StructuredOutputValidationAdvisor 就行,别自己写循环。
  • 对后端 / 架构开发者,我的判断是:这五种模式的工程成本不是均等的。前三种是"写代码",后两种是"养系统"——一旦让模型决定拆几步、循环几轮,你就必须有评测集(第十三篇)和链路观测(第十四篇)兜底,否则出问题只能靠猜。

一句话结论:Agent 不是"更高级的架构",而是"把一部分决策权换成成本和不确定性";只有当任务分解方式真的无法预先写出时,这笔交换才划算。

下一篇换个视角:看看社区把 Claude Code 的那套工具(文件、Shell、Grep、Skills、子智能体、长期记忆)用 Spring AI 重写成了 spring-ai-agent-utils,哪些能力值得直接拿来用。

参考资料 & 致谢

[1] Agent 5 种模式详细应用 2.0 - 语雀

[2] Building Effective Agents - Anthropic(五种模式的出处)

[3] Chat Client API :: Spring AI Reference

[4] Advisors :: Spring AI Reference

[5] Tools / Function Calling :: Spring AI Reference

[6] Spring AI Alibaba Agent 框架 - 官网

[7] Spring AI Alibaba Graph / Agent Framework - GitHub

[8] OpenJDK Virtual Threads (JEP 444)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OxYGC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值