资讯动态

Flowable集成大模型:JavaDelegate实现AI预审节点

发布时间:2026/10/1 14:28:26 来源:尧图企业网站定制
先聊一个很实际的问题你手上已经有一套跑得好好的 Flowable 流程流程里全是人工审批、条件判断、外部服务调用现在突然要求“让大模型参与审批”怎么办最省事的办法当然是在流程里加一个“服务节点”然后在 Java 代码里硬调大模型 API。流程引擎不需要知道什么是 token、什么是 prompt它只需要知道这个节点的输入是流程变量输出是流程变量中间发生了什么由节点自己负责。Flowable 正好天然支持这种扩展Service Task 加一个自定义 Delegate 类前后都能接到流程上下文。就这么一个组合足以把绝大多数 LLM 能力嵌进审批流、工单流、报表流里。这篇内容适合做流程平台开发的工程师也适合正在做“AI 化系统改造”的架构师。我直接以一个“报销单 AI 预审”的实际案例来拆解从 BPMN 建模到 JavaDelegate 实现到超时降级和失败重试全都走一遍。里面包含的是我踩过坑之后的工程取舍不是官方文档的复述。1. 需求拆解与方案选型别一上来就写代码1.1 这个 LLM 节点到底要解决什么问题先别急着选技术方案把业务问题掰开。传统工作流擅长编排“确定性规则”比如金额大于 5000 走总监审批、部门是财务走财务复核这些用 Flowable 的表达式和网关就能做得很好。但有一类流程环节过去只能靠人判断比如报销摘要里写“客户招待”到底算业务招待费还是差旅费合同条款里有没有明显的风险表述工单描述里提到了“紧急”“宕机”是不是需要升级处理。这些都属于“非确定性判断”。大模型节点的价值就是把这类“需要语言理解和归纳”的环节从人工变成自动化。但它不是替代审批人而是做“预审助理”。我在实际项目里定位是LLM 节点只产出“判断建议 风险结构化数据”最终审批结论仍然由人或者人工规则决定。这个定位非常重要它能让你绕开一堆合规和责任归属的争议。所以接入 LLM 节点前先要把以下三个问题想清楚节点的输入是什么哪些流程变量可以被拼进 Prompt哪些字段涉及隐私不能出库节点的输出是什么是纯文本结论还是 JSON 结构化结果节点失败怎么办LLM 服务超时、返回格式不对、甚至内容涉敏拦截流程不能卡死。1.2 方案选型为什么是自定义 JavaDelegate实现一个 LLM 节点并不是只有一条路。我把主流做法列一下方便你对比方案优点缺点嵌入式 JavaDelegate 直接调 LLM API事务一致、变量传递顺、调试方便节点内网络延时阻塞事务需要额外做超时和重试先走 Service Task 发 HTTP 回调外部 AI 服务调用链更清晰适合已有独立 AI 服务多一次网络跳转回调结果更难写回流程变量直接把 LLM 能力做成 Flowable 外部 Rest APIFlowable 本身改动小要求有额外的流程平台 API适合大型中台但不适合单项目落地换一个原生支持 AI 的 BPM 引擎功能更“智能”迁移成本高现有流程资产几乎都要重做我的建议是选第一个在 Flowable 里定义 Service Task通过 Delegate 表达式指向一个 Spring Bean然后在这个 Bean 的 execute 方法里调用大模型。原因是这套方式和现有 Flowable 事务模型结合得最紧流程变量天然可用不需要额外设计“外部系统回调写流程变量”这套机制。自定义 JavaDelegate 的核心逻辑很简单读流程变量拼 Prompt调模型把返回的 JSON 解析后写回流程变量。但在生产环境里真正的难点不是“调通接口”而是“怎么让这个节点在流量进来时不拖垮事务、不重复扣费、不阻塞业务流程”。2. BPMN 建模与核心代码一个 AI 预审节点是怎么跑起来的2.1 流程设计serviceTask 放在哪个位置我用一个报销审批流程做示例。原始流程是员工提交报销单 - 部门经理审批 - 财务复核 - 出纳打款。现在要在“部门经理审批”之前插入一个“AI 预审节点”让 AI 先看看报销摘要、金额、票据信息输出一个风险等级和合规建议部门经理在审批面板上直接看到这些信息。对应的 BPMN XML 片段大概是这样process idexpenseApproval name报销审批流程 isExecutabletrue startEvent idstartEvent / serviceTask idaiPreAudit nameAI 预审 flowable:delegateExpression${llmDelegate} / userTask idmanagerApprove name部门经理审批 flowable:assignee${manager} / sequenceFlow sourceRefstartEvent targetRefaiPreAudit / sequenceFlow sourceRefaiPreAudit targetRefmanagerApprove / /process这里最关键的一行是flowable:delegateExpression${llmDelegate}。它表示当流程流转到这个节点时Spring 容器里找一个名为llmDelegate的 Bean执行它。用 Delegate 表达式而不是flowable:class最大的好处是可以注入 Spring 管理的组件比如 Redis、配置中心、HTTP 客户端后面的降级和限流都好做。放在这个位置的考虑是AI 预审不应该阻断主流程太久而且如果预审失败我希望它能自动降级为“不预审”让流程继续走到人工节点。所以我不建议在 AI 节点两边加排他网关来“强依赖结果”更合理的做法是让 AI 节点永远返回一个“可用/不可用”的结构化标记由后续网关决定分支。2.2 封装一个 OpenAI 兼容的 LLM 客户端大模型厂商很多各家 API 细节有差异但大部分都兼容 OpenAI 的 Chat Completions 协议。只要封装一个通用的客户端就同时适配了国内多家合规大模型服务商的接口也能适配很多私有化部署的模型。这样做的收益很明显后面换模型供应商只改配置流程节点代码一行不用动。下面这段代码用 Spring Boot 的 RestClient 实现了一个最小可用客户端没有引入额外 SDK方便你直接抄Component public class LlmClient { private final RestClient restClient; public LlmClient(Value(${llm.api-url}) String apiUrl, Value(${llm.api-key}) String apiKey) { this.restClient RestClient.builder() .baseUrl(apiUrl) .defaultHeader(Authorization, Bearer apiKey) .defaultHeader(Content-Type, application/json) .build(); } public LlmResult chat(String systemPrompt, String userPrompt, int maxTokens) { MapString, Object body new HashMap(); body.put(model, your-model-name); body.put(messages, List.of( Map.of(role, system, content, systemPrompt), Map.of(role, user, content, userPrompt) )); body.put(temperature, 0.2); body.put(max_tokens, maxTokens); body.put(response_format, Map.of(type, json_object)); MapString, Object resp restClient.post() .uri(/chat/completions) .body(body) .retrieve() .body(new ParameterizedTypeReferenceMapString, Object() {}); return parseResponse(resp); } }几个细节我想单独拎出来说因为它们都是实际运行之后才能发现的问题temperature调到 0.2 左右。审批预审这类场景要的是稳定判断而不是创意表达温度过高会导致同样的输入产生不一样的结果max_tokens一定要控制。报销预审输出几百字足够不限制的话容易被模型“自由发挥”Token 费用也会失控response_format用它约束 JSON 输出。现在主流服务商都支持这个参数务必使用后面解析字段会省很多事。2.3 JavaDelegate 实现读变量、拼 Prompt、回写结果JavaDelegate 本身不复杂真正要设计的是“输入输出契约”。我见过很多失败的接入案例都是因为没有设计好流程变量命名导致 AI 节点和后续人工节点各说各话。我的做法是固定一个前缀aiResult规定所有 AI 节点的结果都写成一个 JSON 字符串存进一个变量。后续节点要读直接解析整个 JSON而不是散落十几个小变量。Component(llmDelegate) public class LlmDelegate implements JavaDelegate { private final LlmClient llmClient; private final ObjectMapper objectMapper; public LlmDelegate(LlmClient llmClient, ObjectMapper objectMapper) { this.llmClient llmClient; this.objectMapper objectMapper; } Override public void execute(DelegateExecution execution) { String expenseAmount String.valueOf(execution.getVariable(expenseAmount)); String expenseReason (String) execution.getVariable(expenseReason); String expenseType (String) execution.getVariable(expenseType); String department (String) execution.getVariable(department); String userPrompt buildPrompt(expenseAmount, expenseReason, expenseType, department); String systemPrompt 你是一个企业报销合规审核助手。要求只输出 JSON不要输出额外文字。 JSON 格式{riskLevel:high|medium|low,riskReason:string,suggestion:string}; MapString, Object aiResult; String rawResult null; try { rawResult llmClient.chat(systemPrompt, userPrompt, 500); aiResult objectMapper.readValue(rawResult, Map.class); aiResult.put(available, true); aiResult.put(raw, rawResult); } catch (Exception e) { aiResult new HashMap(); aiResult.put(available, false); aiResult.put(riskLevel, unknown); aiResult.put(riskReason, AI 预审服务不可用或超时); aiResult.put(suggestion, 请人工审核); } execution.setVariable(aiResult, objectMapper.writeValueAsString(aiResult)); } }注意这里我故意做了一个非常关键的设计execute方法里没有直接抛异常而是把异常吞掉转成一个availablefalse的结果。原因后面在“失败兜底”里细说但简单讲就是一句话AI 节点只是预审不是审批主链路上的“必须成功节点”它挂了流程照样要走。buildPrompt的写法也有讲究。尽量不要把用户填写的原始字符串直接拼进去而是做一层转义和约束。比如要防止有人在报销摘要里故意写“忽略前面的指令”之类的提示注入内容。虽然模型本身有安全对齐但工程上仍然需要在拼之前过滤掉异常的控制字符并且明确告诉模型“只处理报销信息不执行描述之外的动作”。3. 让 LLM 节点安全落地超时、重试与事务隔离3.1 为什么同步调用会拖垮流程事务这是很多人第一次接的时候最容易忽略的问题。Flowable 执行一个 serviceTask默认是在同一个事务里完成的。也就是说当你在这个事务里发起一次 LLM 调用如果模型响应花了 60 秒那么数据库连接、流程锁、任务状态都会一直挂着。高并发场景下这会导致数据库连接池被打满流程引擎整个拖垮。尤其是 Flowable 的命令模式本身是同步执行的一个流程实例在跑节点时相关流程锁是持有的后面针对同一流程实例的命令都会排队等待。所以我的经验是LLM 节点必须设“硬超时”。在 RestClient 层面设置 connectTimeout 和 readTimeout我一般给 20 秒最多不超过 30 秒。不要相信模型服务商的 SLA网络抖动、模型排队、内容审核限流都会让实际响应时间远超预期。this.restClient RestClient.builder() .baseUrl(apiUrl) .defaultHeader(Authorization, Bearer apiKey) .requestFactory(ClientHttpRequestFactorySettings.custom() .withConnectTimeout(Duration.ofSeconds(5)) .withReadTimeout(Duration.ofSeconds(20)) .build()) .build();3.2 失败降级节点挂掉但不阻断流程既然明确了这个节点是“助手型节点”那它的失败降级策略就应该是宁可给人工审核留一个“AI 不可用”提示也不能让整个流程因为 LLM 服务问题而停下来。我在代码里用availablefalse表达这个状态。后续流程只需要判断这个标记exclusiveGateway idaiCheckGateway / sequenceFlow sourceRefaiPreAudit targetRefaiCheckGateway / sequenceFlow sourceRefaiCheckGateway targetRefmanagerApprove conditionExpression xsi:typetFormalExpression ![CDATA[${aiResult.contains(available:true)}]] /conditionExpression /sequenceFlow sequenceFlow sourceRefaiCheckGateway targetRefmanagerApprove conditionExpression xsi:typetFormalExpression ![CDATA[${aiResult.contains(available:false)}]] /conditionExpression /sequenceFlow两个分支都指向同一个审批节点但从操作日志里审批人能明确看到 AI 是否参与了预审。这种“尽力而为”的思路在大多数企业流程里是可接受的。只有极少数业务场景比如纯机器审核链路上不能有人工兜底才需要把 LLM 失败做成 hard fail。3.3 重试的幂等性问题小心重复扣费Flowable 对 serviceTask 抛出的异常默认会触发 Job 重试。这个机制本来是很好的但如果你的 Delegate 已经在执行过程中完成了 LLM 调用只是因为解析结果时报错Flowable 会重跑整个节点等于多花一次模型调用的钱。解决这个问题有两种方式第一种把“结果写入”和“后续依赖”做原子化。在 Delegate 里一旦拿到模型响应立刻把原始结果也存到流程变量里这样即使后续解析异常重试时可以先用一个“是否已调用过”标记判断跳过重复调用。第二种使用业务幂等键。在调用模型时传入一个request_id大部分大模型服务商收到相同 request_id 会返回同一个结果不会重复计费。我在实际项目里用的是“流程实例ID 节点ID”拼出来的唯一标识。String requestId execution.getProcessInstanceId() : execution.getCurrentActivityId(); body.put(request_id, requestId);需要注意的是并不是所有模型服务商都支持 request_id 幂等接入前先确认。如果不支持就只能靠“先写变量再决定是否重调”的方式自己做幂等。3.4 超时兜底脚本BPMN 定时边界事件除了代码层面的超时还可以在 BPMN 层面加一道保险给 serviceTask 挂一个 Timer Boundary Event比如 30 秒后如果节点还没执行完就中断并走人工兜底分支。serviceTask idaiPreAudit nameAI 预审 flowable:delegateExpression${llmDelegate} boundaryEvent idtimerBoundary attachedToRefaiPreAudit timerEventDefinition timeDurationPT30S/timeDuration /timerEventDefinition /boundaryEvent /serviceTask这个方案的好处是保险措施不依赖代码逻辑模型服务再慢也有个“物理上限”。缺点是要注意边界事件触发后Flowable 会中断节点的执行此时 Delegate 方法可能还在跑最终两个路径都会执行。所以代码里仍然要把降级写干净不能让模型结果覆盖掉超时兜底变量。我的建议是双保险都用代码里设置 20 秒超时BPMN 边界事件给 35 秒后者只作为最终防线。4. 再进一步让 LLM 节点做“智能路由”4.1 从预审到自动分单节点输出的第二层价值上面讲的 AI 预审只是把模型结果人工参考。但 LLM 节点的真正价值是能把非结构化文本转化成结构化路由条件。举个例子客户提交了一个工单描述是“系统登录后页面一直转圈重启了几次还是不行怀疑是认证服务有大面积故障”。传统规则路由很难从这段自然语言中提取“高优先级”“属于技术故障”“影响面可能较大”这几个标签。用 LLM 节点处理后输出一个 JSON{ category: 系统故障, priority: P1, impactScope: large, needOncall: true }然后 Flowable 排他网关直接根据这些字段做路由把工单分配给不同的工程师队列甚至可以通过变量动态指定 assignee。这样“理解文本 - 分发”就完全自动化了。4.2 动态从流程变量中构造 Prompt 的模板引擎现实中的案件描述、合同条款、报销摘要长度和格式五花八门。最好别在 Java 代码里用字符串拼接写死 Prompt而是做一个模板文件用 FreeMarker 或者 Thymeleaf 渲染。我建议把 Prompt 模板放在resources/prompt/目录下一个业务场景一个模板。比如报销预审的模板请对以下报销单进行合规预审。 部门${department} 金额${expenseAmount} 元 报销类型${expenseType} 报销摘要${expenseReason} 请按 JSON 格式输出风险等级、风险原因和审核建议。这样运维和业务人员可以在不修改 Java 代码的前提下调整 Prompt。模板文件放到配置中心后线上可以动态优化提示词不用重新发版。我在项目里就是这么做的效果比改代码强很多尤其当业务方反复提需求的时候非常省事。4.3 给 LLM 节点加一层“降级开关”接 LLM 节点最大的隐患不是技术不会而是模型输出不稳定。有些模型在同一个问题上的判断偏差很大尤其涉及金额、合规、法务场景时如果自动路由到一个完全错误的部门比不用 AI 还麻烦。所以我在所有 LLM 节点里都会加一个开关变量。流程定义里AI 节点前面接一个排他网关先判断一个叫aiEnabled的流程变量如果为 true 才走 LLM 节点否则直接跳人工。这个开关可以在流程启动时传入也可以由管理员在运行时通过 REST API 修改全局开关。凡是涉及财务、法务、供应商管理的流程我默认都先把开关关掉灰度跑一段时间效果稳定后再打开。5. 接入过程中的高频坑和排查方法5.1 流程变量太大导致序列化报错一次模型调用返回的原始 JSON 可能非常大特别是模型喜欢额外输出解释时一个节点的结果就可能超过几十 KB。Flowable 默认的变量序列化对长度有限制如果你把原始响应整个塞进流程变量很容易出现VariableValueIntegrityViolated之类的异常。我的经验是最多保留两层最终结构化结果放进aiResult原始完整响应不放进流程变量只记录在日志里或者经过截断后再存。如果确实要留完整记录建议把原始响应落到独立的日志表或者 ES流程变量只存一个摘要字段。5.2 delegateExpression 找不到 Bean这是刚上手最容易遇到的问题。flowable:delegateExpression${llmDelegate}依赖 Spring 容器里有对应的 Bean。如果你只写了一个Component类没指定名字Spring 默认名是类名首字母小写比如llmDelegate没问题。但如果类名比较长比如AiPreAuditDelegate默认 Bean 名就是aiPreAuditDelegate表达式里必须保持一致否则启动流程时直接报找不到表达式目标。还有个容易埋坑的点Bean 必须放在能被 Spring 扫描的包路径下。Flowable 本身是独立引擎如果你把 Delegate 类放在了流程引擎模块之外的某个扫描不到的位置同样会报Unknown property used in expression。排查时先看 Spring 容器里到底有没有这个 Bean我一般用 Spring Boot 的/actuator/beans端点直接查一遍。5.3 LLM 返回内容被解析成 JSON 失败即使你设置了response_format模型仍有可能在极端情况下返回一段 Markdown 或额外解释。所以解析逻辑不能太脆弱建议按下面这个顺序处理直接用正则在返回文本里查找第一个{到最后一个}之间的内容对这个片段做 JSON 解析解析失败时把整个返回文本原样写入降级结果并在日志里记录一句“LLM 返回格式异常”。不推荐用“如果包含json就截取”这种粗暴逻辑因为模型输出里经常带前后缀。正则提取花括号片段是当前兼容性最高的做法。5.4 并发调用导致限流和超时模型服务商的 API 都有 QPS 限制。流程引擎的并发能力很强一旦多个流程实例同时进入 LLM 节点瞬间几十个请求打过去很容易触发限流。这个问题的解法有两个第一在 Delegate 内部做信号量限流。比如用Semaphore(5)控制最多同时有 5 个请求在途超出就快速失败走降级路径。这样相当于给流程引擎和模型服务之间加了一个缓冲。第二给不同业务节点配置不同的 API Key 或单独的模型实例。核心流程用一个高配额 Key非核心流程用低配额 Key互不挤兑。5.5 模型结果不可解释审计困难企业流程最后一定会被审计。传统规则引擎你还能跟审计解释“金额大于 5000 自动转总监”但 LLM 的判断是个黑盒很难讲清楚“为什么判定为高风控”。我在实际项目中做了一个很土但有效的办法把每次调用 LLM 节点的输入 Prompt、输出原始结果、解析后的结构化结果、模型版本、响应耗时全部落一张流水表。审批界面只展示结构化结果审计后台可以翻原始流水。这样至少做到了“有据可查”虽然不能完全解释模型推理过程但比什么都没有强得多。6. 一些我做这个改造后的体会回到最初那个问题Flowable 里接入 LLM 节点到底是技术问题还是工程问题我现在的回答是代码只占了很小一部分真正花时间的全是在设计“节点边界”哪些输入能出库、失败怎么降级、成本怎么控制、结果怎么被信任。我自己调试这个功能的时候最深的感受是不要试图让大模型替你做流程决策而是让它帮你做“信息提取和预判”。Flowable 的价值在于流程的确定性和可追踪性LLM 的价值在于非结构化理解。两者加起来不是替代关系而是分工关系。如果你现在正准备在现有流程里接大模型节点我建议从小场景做起先挑一个“判断简单、人工兜底容易”的节点做验证跑通之后再逐步扩展到自动路由和自动审批。别一上来就做全自动审批不是模型能力不够而是流程治理体系和模型的协同机制还没建好。最后分享一个小技巧在 Flowable 的流程设计器里给 LLM 节点加一个自定义属性比如ai-sceneexpense_preaudit然后在 Delegate 里通过execution.getCurrentActivityId()和扩展属性去动态选择 Prompt 模板。这样以后新增场景只需要画一个新节点和配置一个模板完全不用改 Delegate 代码。我实测下来这个设计的扩展成本是最低的强烈建议你也在项目里用起来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑