让 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 的理解能力和自动化能力。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐