Spring AI + Flowable 工作流深度整合
让 AI 当流程节点的执行者:Spring AI + Flowable 工作流深度整合
前两篇我们把 Agent 做出来了,也让多个 Agent 协作起来了。但企业里还有一个绕不开的现实:流程已经在跑了。OA 审批、工单系统、合同管理,背后大多是 Flowable 或 Activiti 驱动的工作流引擎。本文解决的问题是:怎么把 Agent 嵌进去,而不是推倒重来。
配套阅读:第一篇《从 ReAct 到 MCP》、第二篇《多 Agent 协作》。
一、两种错误的整合思路
在动手之前,先说两条弯路,省得你走。
错误一:用 Agent 替换整个工作流引擎。
有人看完 Agent 演示,第一反应是"那我还要 Flowable 干嘛"。这个想法很危险。工作流引擎解决的是流程持久化、状态恢复、超时重试、审计日志这些问题——这些 Agent 一个都不擅长。Agent 擅长的是非结构化判断:读一段文字、理解意图、调工具。两者是互补关系,不是替代关系。
错误二:在 Agent 里硬编码流程逻辑。
上一篇的 ExpenseSupervisor 是 Java 硬编码的 Pipeline。这在流程固定时没问题。但一旦业务说"这个客户走特殊通道"、“节假日前后审批人不同”,你就得改代码重新部署。工作流引擎存在的意义,就是让这些变化不需要改代码。
正确姿势:Flowable 管流程骨架,Agent 管节点内的智能判断。
Flowable 工作流
├── 开始事件
├── [ServiceTask] → OcrAgent.extract() ← AI 节点
├── [UserTask] → 人工复核(金额 > 1万)
├── [ServiceTask] → PolicyAgent.check() ← AI 节点
├── [ExclusiveGateway] → 合规 / 不合规
├── [ServiceTask] → BookingAgent.book() ← AI 节点
└── 结束事件
流程图在 Flowable Designer 里画,AI 节点在 Java 里实现。业务改流程,改 BPMN 文件就够了,Agent 代码不动。
二、Flowable 基础:三个概念够用
不熟悉 Flowable 的读者,三个概念就能上手:
ProcessDefinition:流程定义,就是那张 BPMN 图,描述"流程长什么样"。
ProcessInstance:流程实例,一次具体的流程执行,比如"张伟 2026-04-10 提交的报销单"。
Task:流程里的一个节点,可以是人工任务(UserTask)或自动任务(ServiceTask)。
Agent 要做的事,就是实现 ServiceTask 背后的 Java 逻辑。
三、项目搭建
3.1 依赖
<dependencies>
<!-- Flowable Spring Boot Starter -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>7.1.0</version>
</dependency>
<!-- Spring AI(同前两篇) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
3.2 配置
spring:
datasource:
url: jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
ai:
openai:
base-url: https://api.deepseek.com
api-key: ${DEEPSEEK_API_KEY}
chat:
options:
model: deepseek-chat
temperature: 0.1
flowable:
database-schema-update: true
async-executor-activate: true
四、核心:把 Agent 包装成 JavaDelegate
Flowable 的 ServiceTask 通过 JavaDelegate 接口执行 Java 逻辑。把每个 Agent 包一层 JavaDelegate,就能挂进流程节点。
4.1 OcrDelegate
@Component("ocrDelegate")
public class OcrDelegate implements JavaDelegate {
private final OcrAgent ocrAgent;
public OcrDelegate(OcrAgent ocrAgent) {
this.ocrAgent = ocrAgent;
}
@Override
public void execute(DelegateExecution execution) {
String rawText = (String) execution.getVariable("rawText");
InvoiceInfo invoice = ocrAgent.extract(rawText);
// 把结果写回流程变量,供后续节点使用
execution.setVariable("invoiceAmount", invoice.amount());
execution.setVariable("invoiceMerchant", invoice.merchant());
execution.setVariable("invoiceDate", invoice.date());
execution.setVariable("invoiceNo", invoice.invoiceNo());
}
}
关键点:流程变量(ProcessVariable)就是多 Agent 之间的"黑板"。上一篇我们用 ExpenseContext Record 传递状态,这里换成 Flowable 的变量存储——它会持久化到数据库,进程重启也不丢。
4.2 PolicyDelegate
@Component("policyDelegate")
public class PolicyDelegate implements JavaDelegate {
private final PolicyAgent policyAgent;
public PolicyDelegate(PolicyAgent policyAgent) {
this.policyAgent = policyAgent;
}
@Override
public void execute(DelegateExecution execution) {
var info = InvoiceInfo.from(execution); // 从流程变量重建对象
String userId = (String) execution.getVariable("userId");
PolicyCheck check = policyAgent.check(info, userId);
execution.setVariable("policyPassed", check.passed());
execution.setVariable("policyReason", check.reason());
}
}
4.3 BookingDelegate
@Component("bookingDelegate")
public class BookingDelegate implements JavaDelegate {
private final BookingAgent bookingAgent;
public BookingDelegate(BookingAgent bookingAgent) {
this.bookingAgent = bookingAgent;
}
@Override
public void execute(DelegateExecution execution) {
var ctx = ExpenseContext.from(execution);
String voucherNo = bookingAgent.book(ctx);
execution.setVariable("voucherNo", voucherNo);
}
}
五、BPMN 流程定义
在 src/main/resources/processes/expense-approval.bpmn20.xml 创建流程文件:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="expense">
<process id="expenseApproval" name="报销审批流" isExecutable="true">
<startEvent id="start"/>
<!-- AI 节点 1:OCR 识别 -->
<serviceTask id="ocrTask" name="发票识别"
flowable:delegateExpression="${ocrDelegate}"/>
<!-- AI 节点 2:合规校验 -->
<serviceTask id="policyTask" name="合规校验"
flowable:delegateExpression="${policyDelegate}"/>
<!-- 网关:合规与否 -->
<exclusiveGateway id="policyGateway" name="是否合规"/>
<!-- 人工节点:金额超限复核 -->
<userTask id="manualReview" name="人工复核"
flowable:assignee="${approver}"/>
<!-- AI 节点 3:入账 -->
<serviceTask id="bookingTask" name="财务入账"
flowable:delegateExpression="${bookingDelegate}"/>
<!-- 结束 -->
<endEvent id="approved"/>
<endEvent id="rejected"/>
<!-- 连线 -->
<sequenceFlow sourceRef="start" targetRef="ocrTask"/>
<sequenceFlow sourceRef="ocrTask" targetRef="policyTask"/>
<sequenceFlow sourceRef="policyTask" targetRef="policyGateway"/>
<sequenceFlow id="passFlow" sourceRef="policyGateway" targetRef="manualReview">
<conditionExpression>${policyPassed == true}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="rejectFlow" sourceRef="policyGateway" targetRef="rejected">
<conditionExpression>${policyPassed == false}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="manualReview" targetRef="bookingTask"/>
<sequenceFlow sourceRef="bookingTask" targetRef="approved"/>
</process>
</definitions>
注意 flowable:delegateExpression="${ocrDelegate}" 这里的 ocrDelegate 就是 Spring Bean 的名字,Flowable 会自动从 Spring 容器里找。
六、启动和推进流程
@Service
@RequiredArgsConstructor
public class ExpenseWorkflowService {
private final RuntimeService runtimeService;
private final TaskService taskService;
public String startExpense(String userId, String rawText) {
Map<String, Object> vars = new HashMap<>();
vars.put("userId", userId);
vars.put("rawText", rawText);
vars.put("approver", resolveApprover(userId)); // 查审批人
ProcessInstance instance = runtimeService
.startProcessInstanceByKey("expenseApproval", vars);
return instance.getId();
}
public String getStatus(String instanceId) {
// 查当前停在哪个节点
var tasks = taskService.createTaskQuery()
.processInstanceId(instanceId)
.list();
if (tasks.isEmpty()) {
// 流程已结束,查结果变量
var vars = runtimeService.getVariables(instanceId);
return (String) vars.getOrDefault("voucherNo", "rejected");
}
return "waiting:" + tasks.get(0).getName();
}
public void completeManualReview(String taskId, boolean approved) {
taskService.complete(taskId, Map.of("manualApproved", approved));
}
private String resolveApprover(String userId) {
// 查组织架构,返回直属主管工号
return "manager_" + userId;
}
}
接口层
@RestController
@RequestMapping("/api/expense")
@RequiredArgsConstructor
public class ExpenseController {
private final ExpenseWorkflowService workflowService;
@PostMapping("/submit")
public Map<String, String> submit(@RequestBody SubmitRequest req) {
String instanceId = workflowService.startExpense(req.userId(), req.rawText());
return Map.of("instanceId", instanceId);
}
@GetMapping("/{instanceId}/status")
public Map<String, String> status(@PathVariable String instanceId) {
return Map.of("status", workflowService.getStatus(instanceId));
}
@PostMapping("/task/{taskId}/complete")
public void complete(@PathVariable String taskId,
@RequestBody Map<String, Boolean> body) {
workflowService.completeManualReview(taskId, body.get("approved"));
}
record SubmitRequest(String userId, String rawText) {}
}
调用示例:
# 提交报销
curl -X POST http://localhost:8080/api/expense/submit \
-H "Content-Type: application/json" \
-d '{"userId":"zhangwei","rawText":"[发票内容] 金额3860元 上海出行 2026-04-10"}'
# 返回:{"instanceId":"abc-123"}
# 查状态
curl http://localhost:8080/api/expense/abc-123/status
# 返回:{"status":"waiting:人工复核"}
# 主管审批通过
curl -X POST http://localhost:8080/api/expense/task/task-456/complete \
-H "Content-Type: application/json" \
-d '{"approved":true}'
七、三个让整合更稳的工程细节
7.1 Agent 异常不能让流程卡死
JavaDelegate 里抛出未捕获异常,Flowable 会把流程实例标记为 ERROR 状态,卡在那个节点。正确做法是捕获异常,写入流程变量,让网关决定走人工还是重试:
@Override
public void execute(DelegateExecution execution) {
try {
InvoiceInfo invoice = ocrAgent.extract(rawText);
execution.setVariable("ocrSuccess", true);
execution.setVariable("invoiceAmount", invoice.amount());
} catch (Exception e) {
log.error("OCR failed for instance {}", execution.getProcessInstanceId(), e);
execution.setVariable("ocrSuccess", false);
execution.setVariable("ocrError", e.getMessage());
// 流程继续,由网关判断走人工补录
}
}
7.2 异步 Agent 调用
LLM 调用耗时 3–10 秒,同步阻塞 Flowable 线程池不合适。用 flowable:async="true" 让节点异步执行:
<serviceTask id="ocrTask" name="发票识别"
flowable:async="true"
flowable:delegateExpression="${ocrDelegate}"/>
Flowable 会把任务放进异步执行器队列,不阻塞主线程。
7.3 流程变量别存大对象
Flowable 的流程变量会序列化存数据库。把整个 InvoiceInfo 对象塞进去,序列化反序列化容易出问题,而且查询慢。
原则:流程变量只存基础类型(String、Long、Boolean、Double),复杂对象存业务数据库,流程变量只存 ID:
// 不推荐
execution.setVariable("invoice", invoiceInfo); // 整个对象
// 推荐
String invoiceId = invoiceRepo.save(invoiceInfo).getId();
execution.setVariable("invoiceId", invoiceId); // 只存 ID
八、整合后的架构全貌
用户 / 前端
│
▼
ExpenseController
│
▼
Flowable 工作流引擎
├── ServiceTask → OcrDelegate → OcrAgent → [OCR 工具]
├── ServiceTask → PolicyDelegate → PolicyAgent → [制度库 / 预算 API]
├── UserTask → 人工审批(钉钉 / 企微 通知)
├── ServiceTask → BookingDelegate → BookingAgent → [ERP API]
└── 流程变量持久化 → MySQL
每一层职责清晰:
- Flowable:流程状态、持久化、超时、审计
- Delegate:胶水层,连接 Flowable 和 Agent
- Agent:智能判断,调工具,处理非结构化输入
- 工具:真实业务系统 API
九、写在最后
AI Agent 不是要替换企业现有系统,而是要嵌入它。
工作流引擎管"流程走到哪了",Agent 管"这一步该怎么判断"。两者结合,才是企业 AI 落地的正确姿势——既保留了流程的可审计性和可配置性,又获得了 AI 的理解能力和自动化能力。
更多推荐




所有评论(0)