多Agent协作-Spring-AI报销审批流
从 1 个 Agent 到 5 个 Agent:Spring AI 多智能体协作实战
用一个完整的自动报销审批流,讲清楚多 Agent 协作的三种主流模式。
一、单 Agent 的天花板:为什么必须拆?
单体 Agent 走到一定规模,必然撞到三堵墙:
第一堵:工具地狱。工具超过 20 个后,模型选错工具的概率显著上升。OpenAI 官方建议单次 Function Calling 工具数控制在 20 以内,超过就要分组。
第二堵:Prompt 膨胀。你需要给模型解释"什么时候用工具 A、什么时候用工具 B、A 和 B 同时满足时怎么办"……规则越多,幻觉越多。
第三堵:职责不清。一个 Agent 同时管 OCR、管规则、管财务计算、管审批路由,等于一个员工兼任五个岗位——出问题谁都说不清。
工程上的答案早就有了:分而治之。把一个胖 Agent 拆成若干个瘦 Agent,每个只懂自己那点事,再由一个调度者协调。
下面用一个真实场景看它怎么工作。
二、场景:一张发票的自动审批之旅
员工上传一张 ¥3,860 的差旅发票,附言:“上海出差,4 月 10–12 日”。
传统流程:员工提交 → 部门主管审 → 财务核 → 出纳付款。耗时 3–5 天。
多 Agent 流程跑起来是这样:
用户输入:[发票图片] + "上海出差4月10-12日"
│
▼
[OcrAgent] → 识别金额 ¥3,860、商户、日期、发票号
│
▼
[PolicyAgent] → 校验差旅标准:上海三日,住宿+餐补上限 ¥4,500,通过
│
▼
[FinanceAgent] → 拆分科目:交通 ¥1,200 / 住宿 ¥2,000 / 餐补 ¥660
│
▼
[RoutingAgent] → 金额 < ¥5,000,直接到部门主管;> ¥5,000 加一级总监
│
▼
[BookingAgent] → 写入 ERP,生成凭证号 PV-2026-04829
│
▼
返回:「已自动核算并提交至王经理审批,预计 1 个工作日内到账。」
整条链路 8–12 秒跑完。员工再也不用填那张 14 个字段的表单。
三、多 Agent 协作的三种模式
在写代码之前,先把脑子里的模型定清楚。业界 95% 的多 Agent 方案都是这三种或它们的组合:
3.1 Supervisor 模式(主管 - 下属)
主管 Agent 看用户意图,动态决定调哪个下属、调几次。最灵活,最贵(每次决策都要过一遍大模型)。适合开放式任务,比如"帮我安排下周差旅"。
3.2 Pipeline 模式(流水线)
顺序固定,每个 Agent 干完交给下一个。最便宜,最稳定。适合流程类业务——报销、审批、工单、KYC 全部是这类。
3.3 Group Chat 模式(圆桌讨论)
多个 Agent 围绕一个话题轮流发言,直到达成共识或达到轮次上限。适合需要"多视角辩论"的场景,比如方案评估、代码 Review。
怎么选? 默认 Pipeline。需要分支时上 Supervisor。需要多视角时才上 Group Chat。别上来就 Group Chat,那是炫技不是工程。
报销审批是典型 Pipeline + 局部 Supervisor 的组合,下面我们就这么实现。
四、动手:用 Spring AI 1.0 实现报销审批流
4.1 整体结构
expense-agents/
├── agent/
│ ├── OcrAgent.java
│ ├── PolicyAgent.java
│ ├── FinanceAgent.java
│ ├── RoutingAgent.java
│ ├── BookingAgent.java
│ └── ExpenseSupervisor.java ← 编排者
├── model/
│ └── ExpenseContext.java ← 共享上下文(黑板)
└── controller/
└── ExpenseController.java
核心思路:每个 Agent 是一个 ChatClient Bean,编排器按顺序调用,共享一份 ExpenseContext。
4.2 共享上下文:黑板模式
多 Agent 协作最容易翻车的点是状态传递。一个 Agent 算出来的数据,下一个 Agent 拿不到。
最干净的做法是定义一个不可变的"黑板"对象,每个 Agent 输出一份增量:
public record ExpenseContext(
String requestId,
String userId,
String rawText,
InvoiceInfo invoice, // OcrAgent 填
PolicyCheck policyCheck, // PolicyAgent 填
List<AccountEntry> entries, // FinanceAgent 填
ApprovalRoute route, // RoutingAgent 填
String voucherNo // BookingAgent 填
) {
public ExpenseContext withInvoice(InvoiceInfo info) {
return new ExpenseContext(requestId, userId, rawText,
info, policyCheck, entries, route, voucherNo);
}
// 其它 withXxx 同理
}
Record + with 方法,保持不可变。这比塞一个 Map<String,Object> 强一百倍——后者半年后没人看得懂。
4.3 每个子 Agent
每个 Agent 都是一个轻量 ChatClient,只配自己需要的 System Prompt 和工具。
OcrAgent(识别发票字段):
@Component
public class OcrAgent {
private final ChatClient client;
public OcrAgent(ChatClient.Builder builder, OcrTools tools) {
this.client = builder
.defaultSystem("""
你负责从发票图片或文本中提取结构化信息。
必须调用 ocrInvoice 工具完成识别,禁止猜测。
输出 JSON:{amount, merchant, date, invoiceNo}
""")
.defaultTools(tools)
.build();
}
public InvoiceInfo extract(String rawText) {
return client.prompt()
.user(rawText)
.call()
.entity(InvoiceInfo.class); // Spring AI 1.0 直接反序列化
}
}
.entity(Class) 是 Spring AI 1.0 的一个甜点——自动把模型输出 JSON 映射成对象,省掉手写 ObjectMapper。
PolicyAgent(规则校验):
@Component
public class PolicyAgent {
private final ChatClient client;
public PolicyAgent(ChatClient.Builder builder, PolicyTools tools) {
this.client = builder
.defaultSystem("""
你负责对照公司差旅制度,判断报销是否合规。
调用 getPolicy 工具读取制度,调用 checkBudget 工具核实预算。
输出 JSON:{passed: boolean, reason: string, breachItems: []}
""")
.defaultTools(tools)
.build();
}
public PolicyCheck check(InvoiceInfo info, String userId) {
String prompt = """
员工:%s
发票金额:%.2f 元
商户:%s
日期:%s
请核对差旅标准并给出结论。
""".formatted(userId, info.amount(), info.merchant(), info.date());
return client.prompt().user(prompt).call().entity(PolicyCheck.class);
}
}
剩下三个 Agent 套路完全一样:定 System Prompt、注入工具、.entity(...) 拿结构化结果。这就是 Spring AI 的好处——每个 Agent 都是 30 行以内的小类,看着就让人安心。
4.4 编排者:ExpenseSupervisor
Pipeline 模式下的编排器其实不需要 LLM,纯 Java 就够:
@Service
@RequiredArgsConstructor
public class ExpenseSupervisor {
private final OcrAgent ocrAgent;
private final PolicyAgent policyAgent;
private final FinanceAgent financeAgent;
private final RoutingAgent routingAgent;
private final BookingAgent bookingAgent;
public ExpenseContext process(String userId, String rawText) {
var ctx = new ExpenseContext(UUID.randomUUID().toString(),
userId, rawText, null, null, null, null, null);
ctx = ctx.withInvoice(ocrAgent.extract(rawText));
var check = policyAgent.check(ctx.invoice(), userId);
ctx = ctx.withPolicyCheck(check);
if (!check.passed()) {
return ctx; // 不合规直接退回
}
ctx = ctx.withEntries(financeAgent.split(ctx.invoice()));
ctx = ctx.withRoute(routingAgent.decide(ctx.invoice(), userId));
ctx = ctx.withVoucherNo(bookingAgent.book(ctx));
return ctx;
}
}
一句话总结:Pipeline 模式下,“编排” 就是 Java 方法的顺序调用。没有花架子。
4.5 什么时候才该用 LLM 当编排者?
当流程有分支判断且分支条件难以用代码穷举时。比如:
用户上传了一张图,可能是发票、可能是行程单、也可能就是张照片。
这种情况下让一个 SupervisorAgent 看一眼决定走哪条 Pipeline 才合适:
@Tool(description = "处理标准报销发票流程")
public String runExpensePipeline(String rawText, String userId) {
return expenseSupervisor.process(userId, rawText).voucherNo();
}
@Tool(description = "处理出差行程单匹配流程")
public String runTripMatch(String rawText, String userId) { /* ... */ }
@Tool(description = "标记为非财务文件,转人工")
public String escalateToHuman(String reason) { /* ... */ }
把这三个工具挂给一个 RouterAgent,它会按用户输入选合适的 Pipeline。LLM 只做决策,不做执行——这是性价比最高的多 Agent 用法。
五、少走弯路的生产化经验
5.1 并行能并的,不要傻等
报销流程里,PolicyAgent 和 FinanceAgent 其实没有依赖关系——前者校验合规、后者拆分科目,可以并发跑:
CompletableFuture<PolicyCheck> policyFut =
CompletableFuture.supplyAsync(() -> policyAgent.check(ctx.invoice(), userId));
CompletableFuture<List<AccountEntry>> financeFut =
CompletableFuture.supplyAsync(() -> financeAgent.split(ctx.invoice()));
ctx = ctx.withPolicyCheck(policyFut.join())
.withEntries(financeFut.join());
8 秒的链路压到 5 秒,用户体验显著上升。LLM 调用都是 IO 密集型,并行收益巨大。
5.2 每个 Agent 必须能"独立失败"
子 Agent 出错不能让整条链路崩。我习惯每个 Agent 包一层降级:
public InvoiceInfo extract(String rawText) {
try {
return client.prompt().user(rawText).call().entity(InvoiceInfo.class);
} catch (Exception e) {
log.warn("OcrAgent failed, fallback to manual", e);
return InvoiceInfo.manualRequired(rawText);
}
}
下游 Agent 看到 manualRequired 就知道要触发人工补录。这比抛异常优雅得多。
5.3 留好"人在回路"(Human-in-the-loop)的口子
任何涉及钱的流程,都不要让 Agent 全自动走到底。在 BookingAgent 之前加一个确认节点:
if (ctx.invoice().amount() > 10_000) {
return ctx.withRoute(ApprovalRoute.toHuman("金额超过 1 万,需人工复核"));
}
监管查起来你才说得清楚——关键节点保留人工卡点不是软弱,是合规。
5.4 全链路 trace 必须有
5 个 Agent 串起来,出问题排查比单 Agent 难 5 倍。最少要做到:
- 每个 Agent 入参出参打日志,关联同一个
requestId - LLM 调用走 Micrometer,能看 token 数和延迟
- 失败的 Agent 自动重试一次(注意幂等)
Spring AI 1.0 的 ChatClientObservation 已经把 Hook 留好了,配一下 SkyWalking 或 OpenTelemetry 就能用。
六、Java 多 Agent 的现状与 Python 对比
绕不开这个话题。截至 2026 年初,主流多 Agent 框架长这样:
| 框架 | 语言 | 协作模式 | 成熟度 | 适用场景 |
|---|---|---|---|---|
| Spring AI + 自编排 | Java | 全部 | 稳定 | 企业 Spring 项目 |
| LangChain4j Agentic | Java | Supervisor + Pipeline | 增长中 | Java 项目偏 LangChain 生态 |
| LangGraph | Python | 图编排 | 成熟 | 复杂状态机 |
| AutoGen | Python | Group Chat 见长 | 成熟 | 多角色辩论 |
| CrewAI | Python | 角色驱动 | 增长快 | 模拟团队协作 |
国内 Java 团队的现实是:业务系统是 Java 的,没人愿意单独起个 Python 服务。Spring AI + 手写编排(像本文这样)已经能覆盖 90% 企业场景。剩下 10% 真的需要图状态机的,再考虑接入 Python LangGraph 微服务。
不要为了用 LangGraph 而 LangGraph。一个 30 行的 Java Supervisor,可读性和稳定性都比花哨的 DAG 配置强。
七、后话
下一步是什么?是把这些 Agent 接入企业现有的工作流引擎(Activiti、Camunda、Flowable),让 Agent 在合适的节点接管,不合适的节点交还给人。
那才是企业 AI 真正落地的样子——不是 AI 取代流程,而是 AI 嵌入流程。
现在已经不是在"用大模型",而是在"用大模型重做业务系统"。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)