💡 前几篇把零件拆完了: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.1、1.1.2 与 Spring AI Alibaba 1.1.2.0 sources jar 核对。
下面开启: 【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 容器、@Async、TaskExecutor),不会让你手工 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();
}
三个必须加的护栏:
- 子任务数量上界(
.entity()的 schema 只能约束格式,不能约束数量,所以要在提示词里写死并在代码里if (plan.subtasks().size() > 6) throw …)。 - 每个 worker 单独超时与重试,一个卡住不能拖死整批——
parallelStream()跑在公共ForkJoinPool上,阻塞式模型调用会把公共池占满,拖慢同进程里其他并行任务,压测时很容易复现;生产建议换成显式虚拟线程执行器 +orTimeout(...)。 - 合成器不许静默取舍:让冲突显式可见,才有复盘的抓手。
5.3 和 Routing 到底怎么分
| 特性 | Routing | Orchestrator-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() | SequentialAgent(extends FlowAgent) |
| Parallelization | 虚拟线程 / CompletableFuture + 聚合 | ParallelAgent(注释即写明 Fan-Out/Gather 并合并结果) |
| Routing | 分类 → Map 分派 | LlmRoutingAgent(builder 可设做路由决策的 ChatModel) |
| Orchestrator-Workers | planner / worker / synthesizer 三段 | SupervisorAgent(由主 ReactAgent 产出决策,决定调哪个子智能体或结束) |
| Evaluator-Optimizer | 评审循环 / StructuredOutputValidationAdvisor | LoopAgent:COUNT / CONDITION / JSON_ARRAY 三种循环模式,可实现 LoopStrategy 自定义 |
说明:这张表的右列我没有实际跑过,只是按本地
spring-ai-alibaba-agent-framework:1.1.2.0sources 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.2 | Spring AI 2.0.1 |
|---|---|---|
| 工具循环 Advisor | ToolCallAdvisor(默认 order HIGHEST_PRECEDENCE + 300) | ToolCallingAdvisor(DEFAULT_ORDER 同值),ToolCallAdvisor 变成 @Deprecated(since="2.0.0", forRemoval=true) 的子类 |
| 工具注册 | defaultTools(Object...) + defaultToolCallbacks(...) | 只有 defaultTools(Object...),defaultToolCallbacks 已 forRemoval |
| 重复注册防护 | — | 新增标记接口 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 跑阻塞调用?并行度上限?部分失败怎么处理? |
| Routing | OTHER 分支真的存在且被统计过占比?分类错例有没有进评测集? |
| Orchestrator | 子任务数量上界与超时?成本是否按 1+N+1 预估过? |
| Evaluator | maxRounds 出口是否带"未通过"标记?判卷模型是否比生成模型便宜? |
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,哪些能力值得直接拿来用。
参考资料 & 致谢
[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 框架 - 官网
408

被折叠的 条评论
为什么被折叠?



