Flowable集成Spring AI实战:让工作流引擎在审批节点长出智能力
在OA里点了一单合同审批流程走到部门主管过了、法务也过了卡在“是否进入财务复核”这个节点上。你说写规则吧业务同学能给你列三十条真写进BPMN下个季度业务一变又得发版。那段时间我一直在琢磨能不能让Flowable这个流程引擎在决策点上多个“脑子”而不是把所有判断逻辑硬编码进流程模型。最后落到实处的方案就是标题里“面向flowable的Spring AI”这句话。先把这个项目到底是什么说清楚。Flowable是Java生态里最常用的开源工作流引擎之一擅长处理BPMN流程定义、任务状态流转、事务持久化这些“确定性”的东西。Spring AI则是Spring官方在2024年开始主推的人工智能集成框架把模型调用、提示词模板、向量检索、工具调用这些AI能力封装成了Spring风格的应用组件。所谓“面向flowable的Spring AI”并不是要改Flowable的内核而是把Spring AI的能力作为“智能层”接到Flowable的节点、监听器和命令链上让流程引擎在关键决策点能调用大模型去理解文本、做判断、生成意见甚至接收自然语言指令直接操作流程。这个方向适合正在做审批系统、工单平台、低代码流程引擎的人看也适合对Spring AI实战落地感兴趣但不想只停留在“写个聊天机器人”的Java开发者。下面我会直接拆解它的设计思路、架构决策、可复现的代码片段还有我实际落地过程中踩过的坑。1. 标题拆解到底把AI放在流程的哪个环节1.1 Flowable管确定性AI管非确定性Flowable本身是一台状态机。流程定义是确定的节点流转是预设的审批链路是写死的它最擅长的就是把这些业务规则变成可持久化、可审计、可追溯的流程实例。Spring AI则相反它处理的全是非确定性的东西同一句话可以有不同理解同一份合同草稿可以给出不同风险提示同一个工单可以对应不同分派策略。传统工作流做决策的方式是“规则引擎”把所有分支条件写成表达式。问题是真实业务流程里“该不该继续往下走”这件事往往依赖大量非结构化的信息合同的自然语言条款、客户的历史合作记录、工单里一段描述性的故障现象。规则引擎处理这类输入要么写成满屏的if-else要么写一堆正则去匹配关键词维护成本极高而且业务一变就要重新发版。所以我当时给自己定了一个原则Flowable继续负责状态机、事务、权限、审计这些“确定性”能力一分钟都不让它含糊Spring AI只出现在需要“理解、判断、生成”的环节负责把非结构化输入转化成结构化决策。这个边界如果不划清楚后面会遇到很多可怕的问题比如流程跑到一半模型超时导致事务回滚或者模型幻觉给了个不存在的节点ID直接让流程卡死。1.2 四个集成层级从“AI建议”到“AI操作流程”“面向flowable的Spring AI”可以落成四个完整的集成层级我按复杂度从低到高排一下。层级落地位置典型场景核心成本L1 AI服务节点服务任务Service Task内调用Spring AI合同初审、工单内容分类、自动摘要提示词设计 JSON输出校验L2 AI路由决策监听器/Delegate输出流程变量排他网关消费智能分流、动态审批人推荐路由入参保障 兜底分支L3 AI生成流程定义模型输出BPMN XML人工确认后动态部署让业务用自然语言描述流程自动建模生成结果校验 部署权限控制L4 Agent操作流程Spring AI Tool调用Flowable API自然语言查询待办、批量完成审批、发起流程权限校验 工具方法防滥用这四个层级不是互斥的实际项目里往往是混合使用。我最早是从L1开始的做的是“AI合同初审”模型读合同标题和金额返回风险等级和审批建议流程继续往下走。做到L2之后发现智能路由才是业务同学最买账的功能因为以前改一个分派规则要排队等开发排期现在只需要调整提示词。L3和L4属于进阶玩法尤其是L4一旦你允许模型直接调用TaskService.complete()这类API就相当于给AI装了一双手权限控制必须同步到位。我特别想强调一点这四个层级共同构成了一种“面向Flowable的AI集成方式”而不是“用AI替代流程引擎”。流程模型依然是权威来源AI只是被接进流程的结构化决策点里。这样设计的好处是AI挂了你还有兜底分支可以走流程定义还是人能看懂、能手工改的BPMN不会变成黑盒。2. 架构设计的三个关键决策2.1 AI放在服务任务和监听器里而不是改造命令引擎刚开始做技术方案的时候我认真研究过Flowable的CommandExecutor扩展机制琢磨是不是要自定义Command拦截器在命令执行到某个环节时自动触发AI。调研之后我放弃了这个想法原因很现实Flowable的命令执行默认是包裹在数据库事务里的ServiceTask里的JavaDelegate执行完毕后流程状态才会被提交。如果你在命令链里同步等待一个LLM的HTTP响应平均耗时三到五秒极端情况十几秒数据库连接会被你一直占着。用户量大一点连接池分分钟被打满。所以我的结论是AI调用应当发生在“流程业务逻辑层”也就是ServiceTask、ExecutionListener、TaskListener里面而不是“命令基础层”。ServiceTask本身是一个明确的流程节点有完整的上下文对象DelegateExecution可以方便地读取流程变量、设置流程变量、触发后续流转。它出错时Flowable也提供了重试机制哪怕AI返回了坏数据你也能在Delegate里捕获异常并路由到指定的补偿节点。举个例子BPMN里指定一个服务任务serviceTask idaiCheck nameAI初审 flowable:delegateExpression${aiApprovalDelegate} /然后在Java代码里用Spring Bean实现JavaDelegate接口Flowable会自动从Spring容器里找到名为aiApprovalDelegate的Bean来执行。这样AI逻辑和Flowable引擎的耦合度极低你可以单独对Delegate做单元测试也可以随时替换成另一套算法而流程定义完全不用改。2.2 CommandExecutor与CommandInterceptorAI审计的切入点虽然我反对在命令链里同步调用AI但CommandExecutor这个扩展点在我做AI审计日志的时候发挥了很大价值。Flowable是一个典型的命令模式架构所有操作比如启动流程、完成待办、驳回任务都会包装成一个个Command被CommandExecutor执行。CommandInterceptor则是在Command执行前和后插入的统一拦截通道。我做AI操作审计时会定义这样一个拦截器把它加进Flowable的拦截器链Component public class AiAuditInterceptor extends AbstractCommandInterceptor { private final AiAuditLogService auditLogService; public AiAuditInterceptor(AiAuditLogService auditLogService) { this.auditLogService auditLogService; } Override public T T execute(CommandConfig config, CommandT command) { boolean aiRelated command instanceof CompleteTaskCmd || command instanceof StartProcessInstanceCmd || command instanceof SetProcessDefinitionCategoryCmd; if (!aiRelated) { return next.execute(config, command); } long start System.currentTimeMillis(); try { T result next.execute(config, command); long cost System.currentTimeMillis() - start; auditLogService.record(command.getClass().getSimpleName(), SUCCESS, cost); return result; } catch (Exception e) { auditLogService.record(command.getClass().getSimpleName(), FAIL, System.currentTimeMillis() - start, e.getMessage()); throw e; } } }这个拦截器不干预业务流程只记录哪些命令是AI链路触发的、耗时多少、成功还是失败。它的价值在于审计溯源当运营同学质疑“为什么AI会把任务自动完成了”的时候你能精确地拿出时间线日志看到命令类型、耗时、责任人。需要说明的是注册自定义拦截器在Flowable Spring Boot里并不复杂。实现ProcessEngineConfigurationConfigurer接口在configure方法里往配置对象的自定义拦截器集合追加即可。但有一个细节要注意自定义拦截器必须放在合适的命令链位置否则会影响Flowable内部的事务边界。如果你只是做日志和审计可以完全不动引擎的默认拦截器链。2.3 Spring AI的组件与Flowable上下文的对接方式Spring AI的核心对象是ChatClient它把所有模型调用封装成了统一的接口。与Flowable集成时我一般不直接写底层的ChatModel而是用ChatClient加上提示词模板。流程序列化的关键就是把DelegateExecution里的流程变量组织成结构化的模型输入。比如你有一个processInstanceId、operatorName、approvalContent你可以用文本块格式化String prompt 你是企业内部审批助手请对以下申请单给出风险评估。 单号%s 申请人%s 审批内容%s 你只需要返回结果不要补充其他说明。 .formatted(processInstanceId, operatorName, approvalContent); String response chatClient.prompt(prompt).call().content();模型返回的结果再解析成JSON反向写回流程变量。这样整个流程实例在模型调用前后都有完整的数据记录即便流程中途失败你也能从流程历史里复盘模型当时看到了什么、输出了什么。Spring AI的Advisor机制也是一个很有用的切入点。它类似AOP可以在模型调用前后注入额外行为比如做敏感词过滤、自动拼装检索结果、记忆最近对话等。在审批场景里我会把历史操作记录通过Advisor自动追加到上下文里让模型知道这个单子之前被驳回过一次而不是把每次节点调用都当作完全独立的请求。3. 三个可以直接复制的实操Demo3.1 Demo1AI合同初审服务任务加JSON结构化输出先跑通这个最基础的Demo后面两个都是它的变体。首先在Maven里引入必要依赖。dependency groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version7.1.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependencySpring AI的模型接入支持很多大厂家的接口。我这里用了OpenAI兼容协议然后把Base URL指向智谱AI的兼容端点模型名用glm-4-flash这样代码层面不需要绑定某个特定厂商换模型只改配置。spring: ai: model: chat: openai: base-url: https://open.bigmodel.cn/api/paas/v4 api-key: ${ZHIPU_API_KEY} options: model: glm-4-flash在Spring配置类里创建ChatClient BeanConfiguration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder.build(); } }接着写JavaDelegate。这是整个Demo的核心我建议按“拼Prompt、调模型、解析JSON、回写变量”四步来组织代码。Component(aiApprovalDelegate) public class AiApprovalDelegate implements JavaDelegate { private final ChatClient chatClient; private final ObjectMapper objectMapper new ObjectMapper(); public AiApprovalDelegate(ChatClient chatClient) { this.chatClient chatClient; } Override public void execute(DelegateExecution execution) throws Exception { String contractName execution.getVariable(contractName).toString(); String amount execution.getVariable(contractAmount).toString(); String summary execution.getVariable(contractSummary).toString(); String prompt 你是企业合同初审助手。请阅读下面合同信息输出风险评估结论。 合同名称%s 合同金额%s 合同摘要%s 必须返回严格JSON不要输出其他内容 {riskLevel:LOW或MEDIUM或HIGH,riskPoint:一句话风险点,suggestion:处理建议} .formatted(contractName, amount, summary); String response chatClient.prompt(prompt).call().content(); JsonNode json objectMapper.readTree(extractJson(response)); execution.setVariable(aiRiskLevel, json.get(riskLevel).asText()); execution.setVariable(aiRiskPoint, json.get(riskPoint).asText()); execution.setVariable(aiSuggestion, json.get(suggestion).asText()); } private String extractJson(String content) { int start content.indexOf({); int end content.lastIndexOf(}); if (start 0 end start) { return content.substring(start, end 1); } return {}; } }BPMN里的服务任务指定这个BeanserviceTask idaiCheck nameAI初审 flowable:delegateExpression${aiApprovalDelegate} /流程定义部署后启动流程实例时传入上述三个变量执行到该节点时aiRiskLevel等变量会自动写入流程实例。后续排他网关可以直接用这些变量做分支。这套代码最大的价值是形成了“AI回写变量”这个闭环。模型不直接控制流程它只提供数据流程走向还是由流程引擎基于数据做判断。有一说一JSON结构化输出这个环节需要花点心思我会在后面“常见问题”里专门讲。3.2 Demo2智能路由排他网关消费AI变量审批流里最容易被问到的另一个需求是“这个工单该分给A组还是B组”。以前写死规则现在可以让模型理解工单描述后给一个路由信号。Delegate写法与Demo1类似核心是返回一个targetDept变量Component(aiRouterDelegate) public class AiRouterDelegate implements JavaDelegate { private final ChatClient chatClient; public AiRouterDelegate(ChatClient chatClient) { this.chatClient chatClient; } Override public void execute(DelegateExecution execution) { String content execution.getVariable(workOrderContent).toString(); String prompt 你是工单分派助手。根据工单描述判断应由哪个部门处理。 可选部门技术部、财务部、客服部、运维部。 工单描述%s 只返回一个部门名称例如技术部 .formatted(content); String dept chatClient.prompt(prompt).call().content(); execution.setVariable(aiRouteDept, dept.trim()); } }在BPMN里通过排他网关做分支exclusiveGateway idrouteGateway / sequenceFlow idtoTech sourceRefrouteGateway targetReftechTask conditionExpression${aiRouteDept 技术部} / sequenceFlow idtoFinance sourceRefrouteGateway targetReffinanceTask conditionExpression${aiRouteDept 财务部} / sequenceFlow idtoDefault sourceRefrouteGateway targetRefmanualAssignTask /这里我特别强调兜底分支。模型返回的结果一定不要直接作为唯一依据default分支必须存在路由不到任何匹配条件时把工单拉到人工分派列表。我的做法是模型输出部门名称之前先做一个枚举校验不在预设部门列表内就强制走default。说白了模型给的是“建议值”流程引擎的规则才是“最终裁决”。实际生产里我还会把模型计算的置信度也写进流程变量比如让模型同时返回confidence字段。当置信度低于0.6时直接走人工分派不冒险让低质量判断驱动流程。3.3 Demo3用自然语言操作流程Spring AI Tool机制这是四个层级里最“惊艳”的一个但也是最考验工程能力的一个。思路是让大模型通过工具调用Flowable的API用户直接用自然语言就能查询待办、完成审批、发起流程。先定义一个业务服务把Flowable的TaskService封装成工具方法Service public class FlowableToolService { private final TaskService taskService; public FlowableToolService(TaskService taskService) { this.taskService taskService; } Tool(description 按处理人查询所有待办任务返回任务ID、标题、流程定义名称) public ListMapString, Object listTodoTasks(String assignee) { return taskService.createTaskQuery() .taskAssignee(assignee) .list() .stream() .map(task - Map.of( taskId, task.getId(), name, task.getName(), processDefinitionName, task.getProcessDefinitionId() )) .toList(); } Tool(description 根据任务ID完成审批approval传true表示通过false表示驳回) public void completeTask(String taskId, boolean approval) { taskService.setVariable(taskId, aiApproved, approval); taskService.complete(taskId); } }然后在构建ChatClient时把工具方法注册进去Bean public ChatClient agentChatClient(ChatClient.Builder builder, FlowableToolService toolService) { return builder .defaultTools(toolService) .build(); }这样做完之后用户可以这样问“帮我查一下张三当前待办的所有合同审批金额超过十万的全部同意。”模型会解析意图先调用listTodoTasks(张三)拿到任务列表筛选出金额超过十万的任务再逐个调用completeTask完成审批。我只能说这个Demo跑通的那一刻非常爽但立刻会意识到权限问题有多严重。completeTask一旦被模型调用相当于任何能发起对话的人都可以在这个prompt里操作流程哪怕流程引擎根本没有给他这个权限。所以实际落地的工具方法内部必须做二次校验从当前登录信息里取操作人再对比任务assignee对不上就直接抛异常回绝而不是让模型自由裁量。4. 让AI更懂业务上下文RAG与流程知识的结合4.1 审批场景为什么不能只靠模型记忆我最早直接用基础模型做审批建议效果不太理想。原因倒不是模型笨是因为它不了解你们公司的报销标准、合同审批授权额度、供应商准入规则这些规章制度在模型训练完成时根本不存在或者只在通用层面存在。比如业务问“这笔1万5的推广费该不该走总部审批”模型不知道你们内部规定“超过1万就要总部备案”给出的建议自然不可靠。RAG检索增强生成正好解决这个问题。做法是先把你公司内部的制度文档、历史审批记录、常见风险提示切块向量化存进向量数据库每次调用模型前先把当前的审批内容拿去检索把最相关的几个制度片段取回来一起拼进Prompt。这样模型的输出就有了内部知识依据而且知识可以随时更新制度变了重新跑一遍文档入库就行不用改代码。4.2 一个最小可用的RAG增强流程用Spring AI做RAG的链路不长。引入向量存储依赖比如用Redis或PgVector存向量dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-vector-store-redis/artifactId /dependency初始化向量存储和文档加载器后通过Advisor把它接进ChatClientimport org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; ChatClient chatClient ChatClient.builder(chatModel) .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) .build();这样在调用模型时它会根据用户的提问自动检索向量库把命中结果注入上下文。在审批流的实际使用中我会把Prompt模板调整成“请基于以下公司规定和合同信息给出建议”并把检索结果放在规定位置而不是让模型自由发挥。有一点必须提醒RAG提升的是模型输出的上下文相关性不代表你可以完全信任它。检索到的制度片段可能本身已过期向量库里也可能存在相互矛盾的旧规定。我的经验是AI产出的是“初审意见”最终审批决策仍保留给流程中的实际审批人。这个边界在UI和业务规则层都要明确体现否则出了争议很难解释。5. 常见问题与排查技巧实录5.1 模型输出不稳定JSON解析失败怎么办这是最频繁的坑没有之一。模型确实会返回带多余文字的JSON比如好的这是结果{riskLevel:HIGH}或者干脆用中文括号。你不能假设它每次都遵守“只返回JSON”的指令。我的处理思路有三层。第一层是在Prompt里强调格式约束并给出示例格式这能显著提高命中率。第二层是在代码里提取JSON子串extractJson方法先找第一个左大括号和最后一个右大括号再做Jackson解析。第三层是兜底如果解析失败不直接让流程报错而是设置一个aiRiskLevelDEFAULT变量把任务转到人工处理节点同时记录一条模型原始输出到日志表方便排查。如果你的模型服务支持JSON Schema可以更进一步在模型层约束响应结构但要用OpenAI协议的话这部分能力取决于上游兼容程度需要单独验证。5.2 同步等待大模型响应导致事务超时这是架构层面的坑。我前面说过不建议在CommandInterceptor里同步调AI但即使在ServiceTask里同步调也需要关注Flowable引擎的事务边界。当非异步的ServiceTask在执行时当前流程实例的数据库事务还是打开状态LLM的耗时会被计入这个事务里。一旦数据库连接等待时间超过连接池阈值就会出现大批量的超时回滚。解决方案是给服务任务开启Flowable的异步执行在BPMN里加flowable:asynctrue或者把模型调用放到消息队列里异步完成完成后通过消息驱动流程继续流转。我在生产环境用后者居多因为AI调用本身可能要重试放入队列后由消费者统一管理重试策略不会把压力打在流程引擎上。5.3 AI工具被滥用绕过权限操作流程L4层级的Agent操作流程是我实际推进中安全性顾虑最大的一块。模型本身不懂权限它只知道用户在Prompt里告诉它“把这个任务全部完成”它就会尽量去完成。如果你在Tool实现里直接调用taskService.complete(taskId)相当于给所有能访问聊天入口的人开了一个后台接口。我在实践里的做法是每个Tool方法内先从当前上下文取出身份信息再校验该身份是否有权限操作目标任务同时给Tool方法加操作清单和限流不允许模型一次性完成超过五个任务必须分批提交。另外所有AI发起的Flowable操作都强制打审计日志方便追踪。5.4 Spring AI版本迭代快网上教程很容易对不上Spring AI从早期的spring-ai-openai-spring-boot-starter一路改名到现在的spring-ai-starter-model-openai配置前缀也从早期的spring.ai.openai.*调整到了spring.ai.model.chat.openai.*。网络上很多教程的时间线很混乱你照着旧配置很可能启动即报错。我的建议是优先用Spring官方的BOM锁版本不要自己指定一堆starter的小版本。并在项目里查清当前版本对应的官方文档配置前缀以文档为准。如果项目里有多个AI功能模块统一放在一个配置类里管理避免散落在各业务代码里后面升级时想改都找不到地方。常见问题出现阶段应对建议模型返回格式不符合JSON代码调试Prompt加示例代码提取JSON子串失败走兜底节点流程事务超时集成测试/生产ServiceTask加async或者AI调用异步化避免占用事务模型幻觉导致错误路由功能验证加入部门枚举校验和default分支低置信度走人工工具方法绕过权限安全评审方法内二次校验身份限制批量操作全程审计版本兼容问题依赖升级用BOM统一版本配置前缀查官方文档集中配置管理6. 落地半年后的几点体会这个方案跑了半年我最大的体会是AI在流程里的定位必须是一个“带建议权的参与者”而不是“拍板者”。你可以让模型去起草风险提示、生成审批意见、提供路由倾向但流程的最终决定权还是得保留在人或者明确的业务规则手里。这个原则写进了我们团队后续所有流程设计规范里每次新增AI能力先问一句这个环节允许AI犯错吗如果答案是不允许那就别让AI直接控制流转。另外我还想分享一个比较细的经验凡是模型生成的流程变量命名上我都加了ai前缀比如aiRiskLevel、aiRouteDept。这样做的好处是你在BPMN条件表达式里能一眼区分哪些变量来自AI哪些来自业务侧排查的时候思路会很清晰。万一AI功能需要灰度下架后端只需要停用对应Bean老的变量也不会和业务变量混在一起。最后一个小技巧Prompt里一定要写清楚“只输出结果不要解释”否则模型会用大段话回复你浪费token还增加解析成本。和模型打交道多了你会发现明确约束输出格式往往比换更强的模型更有用。这个方向如果继续往下走我自己更期待的是流程版本和提示词版本绑定流程模型升级时对应的AI判断逻辑也一起做版本化管理而不是让新旧流程实例去适配同一套AI行为。这也是我下一步准备在团队内部推进的事情。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →