Flowable工作流引擎集成LLM大模型节点:三种方式与实战避坑
发布时间:2026/10/1 13:45:35来源:尧图网络
上一家公司做的是政务类审批系统每天上千条工单要流转业务方提了个需求能不能让AI先看一遍工单内容自动给出预审意见、风险等级和初步处理建议人工只需要做最终确认。这个需求落到技术侧翻译过来其实就是一个很具体的问题——Flowable 工作流引擎里怎么接入一个大模型 LLM 节点当时我在这块踩了不少坑从 BPMN 建模到 JavaDelegate 实现从超时控制到上下文传递前前后后折腾了小半个月。今天把这些东西完整整理出来分享给正在做同类改造的人。文章会偏实战基于 Flowable 6.x 版本代码都是可以直接复制到项目里跑通的水平。1. 先把问题说透LLM 节点在 Flowable 里到底该长什么样1.1 需求从哪来那些看着简单、做起来头疼的场景先说几个最常见的诉求你多半也遇到过智能预审用户提交申请后AI 自动读一遍材料判断是否齐全、有没有明显问题给出通过、补充材料、转人工的建议。自动分单工单进来之后按文本内容自动匹配到对应部门或处理人替代原先复杂的条件网关配置。审批意见生成系统根据历史操作记录和当前表单数据自动生成一段审批意见草稿待办人打开后可以直接修改提交。风险识别在贷款申请、合同审核等流程中用 LLM 识别文本里的风险点输出风险标签和摘要。这类需求的共同点是什么它们本质上都是把判断和生成交给模型来做Flowable 只负责流程编排和状态流转。所以千万要想清楚一件事LLM 节点不是一个 BPMN 标准节点你需要选择一个合适的载体去承载它。1.2 BPMN 里没有大模型这种节点你得自己选载体熟悉 BPMN 的人都知道标准规范里有 Start Event、User Task、Service Task、Gateway、Boundary Event 这些元素但没有AI Task。那 LLM 能力应该挂在哪实践中主要有三种载体载体方式灵活度实现成本Service Task JavaDelegate写 Java 类实现调用逻辑高中Service Task HTTP TaskBPMN 里直接配 HTTP 请求低低节点监听器 Listener在节点的执行监听器里调用中中我个人的建议非常简单粗暴后端是 Java 技术栈的项目优先用 Service Task JavaDelegate不想写代码、只想快速联调验证的项目用 HTTP Task。监听器方式尽量少用原因后面会单独讲。为什么要这样选先说 JavaDelegate。它把调用逻辑封装在 Spring Bean 里依赖注入、事务管理、日志链路都是现成的出错也好排查。流程引擎层面只是一个 Service Task干干净净。而 HTTP Task 看起来美实际上有个问题——响应结果解析能力很弱你定义好 URL 和请求体发出去返回的 JSON 会落到一个变量里后面要取字段还得写脚本或者再加解析节点复杂度反而上去了。2. 三种接入方式怎么选JavaDelegate、HTTP Task、监听器2.1 JavaDelegate最灵活适合复杂业务JavaDelegate 的方式核心代码结构是这样的Service(llmReviewDelegate) public class LlmReviewDelegate implements JavaDelegate { Override public void execute(DelegateExecution execution) { // 1. 取输入 String content execution.getVariable(bizContent, String.class); // 2. 调用 LLM String result llmClient.call(content); // 3. 写回结果 execution.setVariable(llmReviewResult, result); } }BPMN 里对应的 Service Task 配置serviceTask idaiReviewTask nameAI预审 flowable:delegateExpression${llmReviewDelegate}/用delegateExpression绑定 Spring 中的 Bean 名称这是最推荐的做法。相比flowable:class直接写包名类名delegateExpression 的好处是Bean 本身由 Spring 容器管理构造函数注入其他服务非常方便单测时也容易替换 Mock。JavaDelegate 适合的场景包括需要前置处理数据、需要拼接复杂提示词、需要做多轮模型调用、需要根据结果分支流转、需要把结果回写到多个流程变量。说白了凡是有一点业务逻辑的 LLM 集成都建议走这条路。2.2 HTTP Task不写代码快速实现Flowable 从 6.5.0 开始提供了内置的 HTTP Task 类型你可以在 BPMN 编辑器里直接配一个调用外部接口的任务不用写任何 Java 代码。配置方式是这样的serviceTask idhttpLlmTask nameCall LLM API flowable:typehttp extensionElements flowable:httpRequest flowable:httpRequestUrlhttps://your-llm-api.example.com/v1/chat/completions/flowable:httpRequestUrl flowable:httpRequestMethodPOST/flowable:httpRequestMethod flowable:httpRequestTimeout60000/flowable:httpRequestTimeout flowable:httpRequestBody${llmRequestPayload}/flowable:httpRequestBody flowable:httpRequestHeaders flowable:httpRequestHeader nameContent-Type valueapplication/json/ flowable:httpRequestHeader nameAuthorization valueBearer ${llmApiKey}/ /flowable:httpRequestHeaders /flowable:httpRequest /extensionElements /serviceTaskllmRequestPayload要求你在前面的节点里已经拼好一个合法的 JSON 字符串响应内容会存到response变量里后面要用 Groovy 脚本或表达式解析。HTTP Task 最大的价值是快速验证。我有个朋友在项目里用 HTTP Task 接大模型做演示 Demo从画流程到跑通只花了一个下午。它的短板也恰恰在这里没有代码兜底异常处理只能靠流程本身的设计比如加边界事件做降级请求里的鉴权信息会出现在流程引擎配置中安全管控上要多加注意。2.3 监听器方式能用但别滥用有些人习惯直接在节点上挂 executionListener在start事件里调用大模型。比如在 User Task 上配置userTask idapprovalTask name审批 extensionElements flowable:executionListener eventstart expression${llmListener}/ /extensionElements /userTask这种方式能跑但我后来在实际项目中吃过亏。举个例子在用户任务的 start 监听器里发起了一个异步线程去调大模型代码看起来是不阻塞流程但流程引擎根本不会等这个线程完成。监听器返回后任务直接进入待办状态而 AI 结果还在路上你是写在流程变量里还是专门查表写在流程变量里用户可能已经在页面上看到了旧数据查表就需要额外维护一套数据同步逻辑。所以要我说监听器最适合的场景是无状态的旁路操作比如记录日志、发送简单通知。真正需要 LLM 结果参与流程判定的别把宝押在监听器上。用 Service Task流程引擎自然地等待服务任务执行完成后续网关判断、用户任务展示都能拿到完整数据。3. 实操落地从 BPMN 建模到 Java 代码完整走一遍3.1 流程图设计服务任务 边界事件兜底我画一张简单但完整的流程图给你这是一个工单智能预审流程开始 - 用户提交User Task- AI 预审Service Task- 是否转人工排他网关 - 是 - 人工复审User Task- 结束 - 否 - 自动通过End EventAI 预审这个 Service Task 需要挂一个边界事件Boundary Error Event用来处理模型服务不可用的情况。这是实战里特别重要的一环——大模型服务不稳定是常态接口超时、限流、返回格式异常你总得留条退路。BPMN XML 简化后大概是这个样子process idticketReviewProcess name工单智能预审流程 isExecutabletrue error idllmError errorCodeLLM_ERROR/ startEvent idstart name开始/ userTask idsubmitTask name提交工单 flowable:formKeyticketForm/ serviceTask idaiReviewTask nameAI预审 flowable:delegateExpression${llmReviewDelegate}/ exclusiveGateway idneedManualGateway name是否需要人工/ userTask idmanualReviewTask name人工复审/ endEvent idend name结束/ sequenceFlow idflow1 sourceRefstart targetRefsubmitTask/ sequenceFlow idflow2 sourceRefsubmitTask targetRefaiReviewTask/ sequenceFlow idflow3 sourceRefaiReviewTask targetRefneedManualGateway/ sequenceFlow idflow4 sourceRefneedManualGateway targetRefmanualReviewTask conditionExpression xsi:typetFormalExpression ![CDATA[${needManual true}]] /conditionExpression /sequenceFlow sequenceFlow idflow5 sourceRefneedManualGateway targetRefend conditionExpression xsi:typetFormalExpression ![CDATA[${needManual false}]] /conditionExpression /sequenceFlow boundaryEvent idaiErrorBoundary nameLLM异常兜底 attachedToRefaiReviewTask errorEventDefinition errorRefllmError/ /boundaryEvent sequenceFlow idflow6 sourceRefaiErrorBoundary targetRefmanualReviewTask/ /process边界事件的意义在于当 LLM 节点抛出BpmnError(LLM_ERROR)时流程不会跑到网关去做判断而是直接流转到人工复审任务。这保证了AI 挂了流程不能挂最终还是有人兜底。3.2 写一个靠谱的 JavaDelegateJavaDelegate 是最核心的部分我直接给你一个可以参考的完整实现Component(llmReviewDelegate) public class LlmReviewDelegate implements JavaDelegate { private static final Logger log LoggerFactory.getLogger(LlmReviewDelegate.class); private final LlmClient llmClient; private final PromptTemplateService promptTemplateService; public LlmReviewDelegate(LlmClient llmClient, PromptTemplateService promptTemplateService) { this.llmClient llmClient; this.promptTemplateService promptTemplateService; } Override public void execute(DelegateExecution execution) { // 1. 从流程变量取业务数据 String ticketContent (String) execution.getVariable(ticketContent); String creator (String) execution.getVariable(creator); // 2. 拼装提示词 String prompt promptTemplateService.render(ticket-review, Map.of(content, ticketContent, creator, creator)); // 3. 调用大模型 LlmRequest request LlmRequest.builder() .model(your-model-name) .prompt(prompt) .maxTokens(512) .temperature(0.3) .build(); try { LlmResponse response llmClient.call(request); // 4. 解析结果写回流程变量 LmmReviewResult reviewResult parseResponse(response.getContent()); execution.setVariable(llmSuggestion, reviewResult.getSuggestion()); execution.setVariable(llmScore, reviewResult.getScore()); execution.setVariable(needManual, reviewResult.isNeedManualReview()); log.info(AI预审完成, 建议{}, 分数{}, reviewResult.getSuggestion(), reviewResult.getScore()); } catch (LlmServiceException e) { log.error(LLM调用失败, 触发兜底转人工, e); throw new BpmnError(LLM_ERROR, 大模型调用失败自动转人工处理, e); } } }这里有几个关键点一定要重视第一temperature 参数建议调低一点。审批、预审这类业务场景我们需要的是稳定、可解释的输出而不是发散的话术。我通常设置 0.2~0.4过低会显得机械化过高则结果难以预测。第二返回结果必须做结构化解析。你可以让模型输出 JSON然后反序列化也可以输出固定格式文本再正则提取。但我强烈建议用 JSON 格式并在提示词里写清楚字段。机器生成的东西如果不做结构校验就拿来用迟早要出事故。第三抛异常要区分类型。业务逻辑上的参数缺失和模型服务本身的网络超时是两回事。前者说明流程编排有坑后者才是正常的兜底场景。BpmnError 只应该在真正需要流程走兜底路径时抛。3.3 结果回写与流程变量处理LLM 节点执行完之后结果必须写回流程变量后续的网关判断、用户任务展示才能用到。但这里有个值得展开说的细节——Flowable 流程变量是有存储限制的。默认情况下流程变量存在ACT_RU_VARIABLE表里Text 类型字段上限是 4000 字符。超过这个长度需要把变量类型设置为longText或者serializable数据会被存放到单独的字节流表中。而 LLM 的输出往往非常长比如一份几百字的审批意见摘要、一长串的结构化分析结果很容易就超限。处理方式有两种。推荐的做法是execution.setVariable(llmRawResult, response.getContent(), VariableValueType.LONG_STRING);通过指定变量类型为LONG_STRINGFlowable 会把你这个变量存到单独的表里避免超长文本把主表撑爆。很多人在这一步踩坑结果后面任务详情查询时发现数据莫名被截断排查半天才发现是变量存储类型的问题。另外流程变量的命名我建议有个约定。比如 LLM 相关的统一前缀llm_llm_suggestion、llm_score、llm_status。一个流程里如果接了多个模型节点后续检查历史记录ACT_HI_VARINST时你会感谢这个命名习惯的。3.4 超时、并发与降级策略LLM 这个外部依赖跟数据库、Redis 这些内部组件不一样的地方在于它的响应时间方差极大。平时 200ms 能返回遇到模型繁忙可能 30 秒还在排队。如果你在 Flowable 里用同步方式调用这个时间会直接算进流程引擎事务的持有时间后果就是数据库连接被长时间占用甚至拖垮整个引擎。我见过一个实际案例某个生产环境在高峰期突然大量流程堆积后来一查是某个 LLM 节点平均响应 15 秒导致 Flowable 的命令执行线程池全部阻塞。所以这块必须做防护超时设置。HTTP 客户端的 connectTimeout 设短一点5 秒足够readTimeout 一般 60 秒给模型留够生成时间。不要以为模型输出 token 多就一定慢关键看排队情况。并发控制。给 LLM 调用加上信号量限制比如Semaphore最多允许 20 个并发请求超过直接进入等待或降级路径避免流量突增时打爆上游。降级策略。也就是前面说的边界事件。调用失败降级到人工结果置信度低于阈值降级到人工响应 JSON 解析失败降级到人工。核心原则就一条AI 永远只是辅助人工永远有最终决定权。4. 提示词模板与上下文管理4.1 提示词别硬编码在代码里提示词这个东西看起来只是字符串但工程化之后水很深。最原始的做法是直接在 Java 代码里拼接String prompt 你是工单审批助手请对以下内容进行预审…… ticketContent;这样写的问题不是能不能跑而是后续迭代非常痛苦——业务方隔三差五让你调整预审口径你要么改代码重新发版要么在代码里塞一大坨条件判断。提示词就是业务策略策略应该跟代码分开管理。我的做法是把提示词模板放到数据库或者配置中心用模板引擎渲染。下面是一个简单示意Service public class PromptTemplateService { private final StringTemplateLoader loader new StringTemplateLoader(); public String render(String templateKey, MapString, Object variables) { // 从缓存/数据库加载模板内容 String template getTemplateFromDb(templateKey); // 使用 Freemarker 渲染 Configuration cfg new Configuration(Configuration.VERSION_2_3_32); cfg.setTemplateLoader(loader); Template tpl new Template(inline, new StringReader(template), cfg); try { Writer out new StringWriter(); tpl.process(variables, out); return out.toString(); } catch (Exception e) { throw new RuntimeException(提示词渲染失败, e); } } }模板内容类似你是工单智能预审助手请根据以下工单信息进行预审。 工单内容${content} 提交人${creator} 请以 JSON 格式输出以下字段 { suggestion: 建议类型通过/补充材料/转人工, score: 风险评分 1-10, needManualReview: 是否需要人工复核true/false, reason: 判断理由不超过50字 }把提示词模板化管理的好处是显而易见的运营同学改提示词不用等研发排期上线后不同流程、不同模型可以用同一套代码不同模板A/B 测试也方便。4.2 多节点共享上下文的变量设计一个复杂的业务流程往往不止一个 LLM 节点。比如流程先是 AI 做意图识别再让 AI 做资料完整性检查最后 AI 生成处理意见。这种情况下前一个节点的输出往往要作为后一个节点的输入这就涉及到上下文共享。Flowable 的流程变量天然就是流程级的所以你不需要额外搞什么上下文对象用好流程变量就行。但要注意变量之间的耦合关系。比如意图识别节点输出intent资料检查节点需要用到这个intent同时还需要原始文本ticketContent。流程变量设计成这个样子// 节点1输出 execution.setVariable(intent, 退款申请); execution.setVariable(confidence, 0.92); // 节点2输入直接引用 String intent execution.getVariable(intent, String.class); String content execution.getVariable(ticketContent, String.class);这里有个细节不同节点的提示词最好都能拿到原始输入 前序结果 当前节点职责三段信息。所以我在每个节点的提示词模板里都会预留三个区块context上游节点输出的关键结论、current_data本次要处理的业务数据、task当前节点要完成的任务。这样模型每次调用都能理解自己是流程中的哪一环上下文不丢。5. 真实项目中的坑和排查实录5.1 常见问题速查表把我在实战中遇到过的问题整理成一个速查表遇到同样问题的人可以直接对照排查现象原因处理方式流程实例卡在服务任务不再往下走LLM 调用无超时网络一直等待给 HTTP 客户端配 readTimeout并加边界事件兜底网关判断条件拿不到 AI 结果变量名拼写不一致或变量未写回统一变量命名规范调试时打印所有流程变量数据在详情页显示不完整流程变量超过 4000 字符被截断使用 LONG_STRING 类型存储大文本变量模型返回 JSON 解析失败提示词没有约束输出格式或温度过高温度调低到 0.3 以下提示词里给示例格式高峰期流程引擎卡顿同步 LLM 调用占用命令执行线程增加并发控制、异步化改造历史表数据暴涨查询变慢LLM 结果全文存为流程变量产生大量历史数据控制长文本变量存储定期归档同一个 Bean 被多线程执行出现状态错乱JavaDelegate 被设计成有状态Delegate 必须是无状态的共享数据放流程变量5.2 三个让我印象深刻的翻车现场第一个坑是在 JavaDelegate 里开异步线程。当时天真地以为把一个耗时的 LLM 调用丢到线程池里流程任务就能快速流转等结果回来再回写。实际跑起来才发现完全不是那么回事——Flowable 的流程实例在一个事务里流转异步线程根本不会去更新ACT_RU_VARIABLE。最后结果要么丢要么就得靠额外的持久化逻辑补救。后来我彻底想明白Flowable 的模型是同步引擎模型每一个节点的执行都意味着一个完整的事务提交。你想要的异步应该发生在流程引擎之外而不是引擎内部。第二个坑是模型输出的 JSON 字段和代码里解析的字段对不上。上线第一周运营就反馈有时候 AI 预审建议是通过代码却解析不出来。查了一天才发现是提示词里没约束字段枚举值模型自由发挥写了Approved、通过偶尔还写可以考虑通过。从那之后我所有的提示词都强制要求输出固定枚举值并且解析时做一次兜底映射识别不了统一走人工。第三个坑是生产环境变量存储。有一次 AI 生成了一个非常长的风险分析报告大概有三千多字直接在详情页展示时发现最后几百字没了。排查后发现是流程变量的默认存储类型问题。这在 5.3 里有详细说明但我想强调大模型接入后流程变量的体量会完全超出传统表单字段的量级这个变化会带来一整套存储和查询层面的连锁影响必须在设计阶段就想清楚。5.3 排查工具与调试技巧最后分享三个非常实用的调试技巧技巧一把流程实例的所有变量打印出来。Component(debugDelegate) public class DebugDelegate implements JavaDelegate { Override public void execute(DelegateExecution execution) { MapString, Object vars execution.getVariables(); vars.forEach((k, v) - System.out.println(variable: k v)); System.out.println(current activity: execution.getCurrentActivityId()); } }在怀疑问题出在哪的时候把这个节点临时插到流程里比看一堆日志高效得多。技巧二用 Spring Boot Actuator 和 Flowable 的引擎状态接口配合。当怀疑流程卡住时第一时间检查 Flowable 的 Job Executor 状态、Active 的流程实例数、以及数据库锁等待时间能快速定位是引擎问题还是外部服务问题。技巧三链路追踪。在 JavaDelegate 的调用入口和 LLM 返回出口各打一行带 traceId 的日志。后续接 APM 或者自研日志平台时这对排查耗时瓶颈和定位慢流程非常有帮助。大模型调用这种外部依赖跟普通 RPC 一样建议纳入全链路追踪体系。结尾一点个人经验如果只是想先跑通一个验证 Demo别想太多Service Task JavaDelegate 最直接。等真正要上生产了再回头看异步化、降级策略、上下文管理这些更深的设计。我个人踩过最大的坑就是一开始把所有逻辑都塞在监听器里流程图倒是漂亮了真出问题的时候两只手都填不完 one by one 的排查清单。另外真心建议所有接入 LLM 的流程在设计评审时多问一句模型挂了会怎样这个问题答不上来的流程上线就是要出事的。AI 在你流程里的角色应该是一个高效但需要被约束的辅助者而不是一个不可控的决定者。把这条想明白剩下的事情都是技术细节。
网站建设高端定制企业官网