这一话先泼冷水:能用确定性编排解决的,绝不让模型自由发挥。 然后给你五种编排模式在 Spring 里的落地写法、Agent 循环的终止条件、状态持久化方案,以及官方"没有图编排"这件事该怎么补救。
| 维度 | Workflow(确定性编排) | Agent(模型决定下一步) |
|---|---|---|
| 控制流 | 代码写死分支与顺序 | 模型输出决定 |
| 可测性 | 每一步可单独单测 | 只能端到端评测 |
| 成本 | N 次调用,可预测 | 1~∞ 次,可能失控 |
| 延迟 | 可并行,可控 | 串行推理,通常更慢 |
| 适用 | 流程已知、步骤固定的任务(90% 场景) | 步骤未知、需要探索与试错(排障、调研、开放式分析) |
先画流程图,再看哪些节点需要"智能"。只有那些"无法用规则描述"的节点才交给模型; 其余节点用普通 Java 代码实现。这样你会得到一个「大部分是确定性代码 + 少数几个 LLM 节点」的系统 —— 这才是能上生产的形态。
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();
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();
}
}
路由的核心价值是成本分层:分类用最便宜的模型(甚至用规则/关键词先过一遍),只有真正需要推理的请求才交给贵模型。
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();
工作者 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();
}
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();
}
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 运行的完整状态 —— 建议持久化,支持断点续跑与事后复盘
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 失败后主流程直接崩 | 每个节点都要有超时、降级与兜底输出 |
最终话 —— 代码写完只完成了一半。下一话讲可观测、评估、护栏、成本与可靠性, 并把前十话所有零件拼成一个能跑的「运维排障 Agent」—— 那是这套教程的终点,也是你项目的起点。