☰
Flowable工作流集成LLM节点:Service Task与JavaDelegate实战指南
2026/10/1 19:00:34 网站建设 项目流程

最近在做一个审批流的智能化改造,正好碰到一个刚需:在 Flowable 工作流里接一个大模型节点,让某个审批环节自动生成意见、判断风险,或者把录入的自然语言自动转成结构化数据。折腾了几天,踩了几个坑,也总结出了一套比较稳的接入方式。今天把这套思路和代码实践完整写出来,给后面接类似需求的朋友一个参考。

先说结论:Flowable 接入 LLM 节点,最核心的载体就是Service Task(服务任务),配合自定义JavaDelegate,在流程执行到某个节点时调用大模型接口,再把模型返回结果写回流程变量。整个过程不复杂,但真正做起来需要注意的东西不少,比如线程阻塞、超时控制、变量序列化、异常对流程状态的影响等等。这篇文章适合已经基本了解 Flowable 建模,想给流程加 AI 能力,但还没完全想清楚怎么落地的开发者。

1. 为什么要在工作流里加 LLM 节点:需求与场景

1.1 传统工作流的决策瓶颈

工作流引擎的核心价值是把业务过程编排成一个个节点,让任务按规则流转。但传统工作流里的节点逻辑是写死的,哪怕用了规则引擎,也只能做“如果条件A则走分支B”这种确定性判断。遇到需要理解语义、生成文本、或者从一段非结构化描述里提取关键字段的场景,传统工作流就没法处理了。

举个例子,一个请假审批流程,审批人需要看申请理由是否合理。传统做法是申请人在表单里填一个请假类型(事假、病假、年假),再由条件表达式判断走哪个审批分支。但如果申请理由是一段长文本,比如“家里人突然生病需要照顾,想请三天假,计划用年假抵扣”,传统工作流根本没法自动理解这段话,更不可能自动帮你判断“是否涉及紧急情况”或者“是否应该转给部门负责人加签”。

这就限制了很多业务场景的自动化空间。过去遇到这类需求,只能靠人工节点,由真人来阅读判断。现在有了大模型,工作流节点完全可以承担一部分“阅读理解”和“生成建议”的工作。

1.2 LLM 节点能给工作流带来什么

把 LLM 接入工作流,本质上是把以前需要人脑完成的“判断、总结、提取、生成”操作,变成流程里的一个自动化节点。它在工作流里的价值主要体现在几类能力上:

  • 文本分类与意图识别:根据候选分支的语义,自动决定走哪个网关。比如把工单描述喂给模型,模型返回一个类别,流程再根据类别转给不同处理组。
  • 关键信息抽取:从用户填写的非结构化文本中提取姓名、金额、日期、问题类型等结构化字段,存入流程变量供后续节点使用。
  • 内容生成与建议:在审批节点前自动生成一份审批建议摘要,帮助审批人快速了解上下文;或者生成合同条款修改意见、风险提示等。
  • 自然语言转结构化:把用户用自然语言填写的需求转换成标准表单字段或 JSON,为后续数据流转做准备。

1.3 适用的业务场景举例

这里说几个我实际接触过或调研过的场景,方便你判断自己的需求是否适合用 LLM 节点。

第一个是智能审批摘要。比如在财务报销流程中,报销人上传发票照片和说明文字,LLM 节点自动提取“报销类别、金额、项目归属”,并生成一段摘要附在审批任务里。审批人打开任务时,不用再看一堆附件,直接看摘要就能判断。

第二个是工单自动分派。客户提交故障描述,工作流调用 LLM 判断故障属于“网络问题、硬件问题、软件问题”还是“账号权限问题”,然后自动路由到对应技术组。

第三个是简历筛选初筛。招聘流程里,候选人简历转成文本后,LLM 节点按照预设的岗位要求打分,输出“通过、待定、不合适”,并附带理由。流程根据结果自动进入下一轮或标记淘汰。

这些场景的共同点是:流程本身没有变化,还是原来的发起-审批-归档,但对某个环节的“决策逻辑”从规则判断升级成了“语义理解”。这就是 LLM 节点的价值。

2. 接入前的技术选型与架构设计

2.1 引擎内嵌调用 vs 外部工作流平台

提到“工作流 + AI”,现在市面上有两条技术路线。一条是切到 Dify、n8n、Coze 这类专门支持 AI 节点的工作流平台,它们自带大模型节点,配置起来确实方便。另一条是继续使用 Flowable、Camunda 这类传统 BPM 引擎,自己开发集成层,让流程节点能调用外部模型 API。

如果你的项目已经基于 Flowable 构建了完整的审批体系,比如接入了用户体系、权限模型、流程表单、历史归档,那迁移到 AI 工作流平台的成本极高。更好的做法是在现有 Flowable 上扩展一个专门的“LLM 节点”类型。这样原有流程资产不用推翻,还能在流程任意位置插入 AI 能力。

如果是新项目,业务又特别偏内容生成、智能体编排,那 Dify/n8n 这类平台会更合适。但如果你需要强事务、强审批角色、完整的历史审计,那我仍然建议 Flowable 这套,因为 BPM 流程的健壮性和管理能力是 AI 工作流平台短时间比不了的。

2.2 Service Task 为什么是最合适的载体

Flowable 提供多种任务类型。用户任务(User Task)用于人工处理,Java 服务任务(Service Task)用于自动执行一段逻辑,脚本任务(Script Task)用于执行表达式,调用活动(Call Activity)用于子流程调用。LLM 节点本质上是“调用外部 API 并处理结果”的自动化逻辑,所以Service Task是最自然的选择。

Service Task 支持三种实现方式:JavaDelegate类、DelegateExpression表达式、Expression表达式。其中JavaDelegate是最常见也最可控的方案。你在 BPMN 文件里给某个 serviceTask 节点设置flowable:class或者flowable:delegateExpression,引擎执行到该节点时会实例化对应的委托类,调用execute方法。

这里有个关键点:Service Task 是同步执行的。流程引擎的主线程会调用你的execute方法,一直等到方法返回,才会推进到下一个节点。这意味着如果你在execute里直接调用大模型接口,整个流程实例会阻塞在那里,直到 HTTP 请求超时或返回。所以选型阶段就要想清楚:流程对实时性要求多高?如果要求不高,同步调用最简单;如果要求高,就要考虑异步执行或异步回调。后面会详细讲。

2.3 大模型接口调用方式的选择

接大模型,现在主流有两种方式。一种是直接调用各家厂商的 HTTP API,比如 OpenAI 兼容接口、通义千问、文心一言等。另一种是使用 AI 平台的 SDK,打包好请求逻辑。不管哪种,到了 Java 后端都一样,本质上是一次 HTTP POST 请求,入参是messages或prompt,出参是choices或类似字段。

我建议在 Flowable 项目里自建一个LlmClient封装层,统一管理 API Base URL、API Key、模型名称、超时时间、重试策略。这样 BPMN 里的JavaDelegate只依赖这个封装,不直接和厂商 SDK 耦合。以后换模型商,只改LlmClient的配置,不用动流程定义。

如果你的模型服务是通过私有化部署的,比如部署了 vLLM、Ollama 这类推理服务,它们通常也提供 OpenAI 兼容的/v1/chat/completions接口。那你的LlmClient可以直接指向内网地址,搞一个统一的开源模型网关,这样流程配置里只需要维护一个 base-url,后面换模型都不需要重新发布流程。

3. 核心实现:自定义 JavaDelegate 调用 LLM

3.1 先写一个 LlmDelegate 骨架

下面进入实操。先创建一个简单的JavaDelegate,让它能从流程变量里读取 prompt,调用大模型接口,把结果写回流程变量。

package com.example.flowable.delegate; import com.example.flowable.llm.LlmClient; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.JavaDelegate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component("llmDelegate") public class LlmDelegate implements JavaDelegate { @Autowired private LlmClient llmClient; @Override public void execute(DelegateExecution execution) { // 从流程变量中读取输入 String prompt = (String) execution.getVariable("prompt"); String model = (String) execution.getVariableOrDefault("model", "default-model"); // 调用大模型 String result = llmClient.chat(prompt, model); // 结果写回流程变量 execution.setVariable("llmResult", result); } }

这里用 Spring 的@Component注册 Bean,然后在 BPMN 的 serviceTask 上通过flowable:delegateExpression="${llmDelegate}"引用。不要用flowable:class直接写全类名,因为那样 Flowable 会用无参构造器实例化,依赖不能注入。

3.2 为什么用 delegateExpression 而不是 class

很多 Flowable 初学者容易在这里踩坑。用flowable:class时,Flowable 引擎自己通过反射创建实例,这个实例不在 Spring 容器里,Spring 的@Autowired无法生效,你的LlmClient会是 null。

解决办法是使用delegateExpression指定 Spring Bean 名称。这样 Flowable 从 Spring 容器里取 Bean,@Autowired就正常了。另一种做法是让JavaDelegate实现org.flowable.engine.impl.el.Expression接口,或者通过SpringProcessEngineConfiguration配置自定义 Bean 解析,但最简洁的还是delegateExpression。

如果你的项目没有用 Spring Boot,而是原生的 Flowable 引擎,那可以在 bpmn 里配置一个类似${llmDelegate}的表达式,引擎执行时会从ProcessEngineConfiguration的 Bean 列表里查找。总之,要让 Flowable 和 Spring 共享 Bean,就必须走delegateExpression。

3.3 让流程变量兼容更多输入输出形式

上面那个最简版本只支持一个 prompt 变量,实际业务往往需要更灵活的结构。例如输入不是单一字符串,而是一个 JSON 对象,包含多个字段;又或者需要透传一些上下文,比如流程发起人、当前任务节点名称、历史审批意见等。

推荐的做法是把所有上下文封装成一个 JSON 字符串存入变量llmContext,在 delegate 里解析,按需拼装 prompt;返回结果也封装成 JSON,写回llmResult,后面节点再用表达式解析其中的某个字段。

这里给一个稍复杂但更实用的写法:

@Override public void execute(DelegateExecution execution) { // 获取上下文 JSON,可能是发起人填写的表单数据 String contextJson = (String) execution.getVariable("llmContext"); // 也可以额外拼接流程信息 String processDefinitionKey = execution.getProcessDefinitionId(); Map<String, Object> context = parseJson(contextJson); context.put("processDefinitionKey", processDefinitionKey); String prompt = buildPrompt(context); Map<String, Object> response = llmClient.chatWithJson(prompt); execution.setVariable("llmResult", response.get("strategy")); execution.setVariable("llmRaw", response.get("raw")); }

返回结果不要直接存一个大字符串就算完,尽量结构化。比如让模型强制返回 JSON,然后在 delegate 里把 JSON 解析成一个 Map,按业务键存入不同流程变量,这样后面的排他网关可以直接用$ {llmApproved == 'PASS'}这类表达式判断。

3.4 同步调用与超时控制:避免流程实例卡死

如果直接使用同步调用,最怕的是大模型接口持续不返回。默认 HTTP 客户端如果不设置超时,可能会一直等下去。这在工作流里是致命的,因为流程实例会一直停留在 RUNNING 状态,任务表里可能看不到任何待办,看起来就像流程“丢了”。

所以LlmClient里必须设置连接超时和读取超时。我一般设置为连接超时 3 秒,读取超时 30 秒。另外还要让外部异常向上抛,Flowable 在JavaDelegate抛出异常时会标记该流程实例为错误状态(或触发边界事件),而不是默默吞掉。吞掉异常会导致流程继续走,但结果为空,后面节点拿到 null,更难排查。

下面是一个基于 Spring 的RestTemplate或WebClient配置超时的示例:

@Configuration public class LlmClientConfig { @Bean public RestTemplate llmRestTemplate() { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setConnectionRequestTimeout(3000); factory.setConnectTimeout(3000); factory.setReadTimeout(30000); return new RestTemplate(factory); } }

注意,如果是高并发场景,建议用WebClient并配置响应超时,或者使用虚拟线程。Spring Boot 3.2+ 可以比较容易地启用虚拟线程,配合HttpClient的异步调用,避免同步阻塞。

4. 高级设计:异步化、重试与 Spring Boot 集成

4.1 同步 Call 改成异步:Flowable 的异步执行机制

前面提到,同步调用会阻塞流程实例。Flowable 本身就支持Asynchronous Continuation,也就是在流程定义里给节点设置flowable:async="true"。开启后,引擎会把该节点作为 job 放入异步执行器,由线程池去执行execute方法,原流程实例的调用线程先返回到 API 调用方。

这意味着你可以先快速返回流程实例 ID,然后在后台线程中慢慢等待大模型返回,完成后继续执行后续节点。这个方案特别适合那些对“发起流程”这个动作的响应时间有要求的场景,毕竟用户提交一个申请,不希望浏览器一直转圈等大模型。

配置方法很简单:

<serviceTask id="llmTask" name="LLM 节点" flowable:async="true" flowable:delegateExpression="${llmDelegate}" flowable:exclusive="false" />

flowable:async="true"启用异步执行,flowable:exclusive="false"表示不与其他 job 互斥,可以并发执行。但要注意,异步执行需要 Flowable 的 Job Executor 处于激活状态。Spring Boot 集成 Flowable 时默认会自动激活。另外,异步 job 如果执行失败会不断重试,默认重试次数和时间间隔需要额外配置,否则失败 job 会堆积在ACT_RU_JOB表里。

4.2 使用 Spring @Async 做异步调用,避免捆绑引擎线程池

除了 Flowable 自身的异步 job,还有一种做法是让JavaDelegate内部通过 Spring@Async并发调用大模型,同时立即返回。但这种做法要小心:JavaDelegate.execute已经返回,引擎就会认为节点执行完毕,会把流程推进到下一步,这时你的大模型结果可能还没回来。所以这种方案只适合“发了就不管”的通知类场景,不适合需要结果参与后续网关的情况。

如果真的需要异步拿到结果再继续,那就不要用 Spring@Async,而是考虑工作流引擎自带的“异步续跑”机制。Flowable 提供Asynchronous Continuation就是为了这个。或者干脆设计成人工触发:在 LLM 节点前先进入一个用户任务,人工点击“执行 AI 分析”后触发 Service Task,服务完成后回到人工任务展示结果。这样把“等待大模型”的时间交给用户自己掌握,也是实践中很常见的交互方式。

4.3 重试策略与异常处理:让流程自己有韧性

大模型接口本质上是外部依赖,可能出现网络抖动、模型过载、限流等异常。在JavaDelegate里直接抛异常会让流程进入异常状态,所以要么能自动重试,要么给该节点配置边界事件,失败时走人工兜底分支。

我比较推荐三级策略:

  • 第一级:LlmClient内置指数退避重试,最多重试 2 次,比如第 1 次失败等 1 秒,第 2 次失败等 2 秒。
  • 第二级:Flowable 节点配置flowable:async="true"+flowable:failedJobRetryTimeCycle,失败 job 自动重试。
  • 第三级:在流程图上给 Service Task 添加Boundary Error Event,重试仍失败后,将流程转向人工兜底节点,比如生成一条待办让管理员处理。

failedJobRetryTimeCycle可以配置为一个 ISO 8601 间隔,例如:

<serviceTask id="llmTask" name="LLM 节点" flowable:async="true" flowable:failedJobRetryTimeCycle="R3/PT5S" flowable:delegateExpression="${llmDelegate}" />

表示这个异步任务失败后最多重试 3 次,每次间隔 5 秒。如果 3 次都失败,job 进入死信队列。这时可以在流程里挂错误边界事件,也可以靠定时任务扫描死信 job 做补偿。

4.4 Spring Boot 集成 Flowable 的配置要点

如果你用的是flowable-spring-boot-starter,那么集成非常顺滑。只需要在application.yml里配置数据库连接和引擎参数,它会自动创建所需的ACT_表。这里有几个和 LLM 节点有关的配置值得注意:

flowable: async-executor-activate: true async: executor: core-pool-size: 4 max-pool-size: 16 queue-capacity: 100 database-schema-update: true

async-executor-activate必须为 true,异步节点才能执行。core-pool-size和max-pool-size决定了异步 job 执行的并发度。由于大模型接口通常耗时较长,建议把 core pool 调大一点,不然其他异步任务会被 LLM 节点阻塞。但也不要盲目调大,要看你的应用总体线程资源。

如果业务上只允许一部分节点异步,可以在流程定义里单独设置节点级的async属性,不一定全局开异步。

4.5 通过 Listener 增强 LLM 节点的上下文感知

有时候我们不光希望节点从流程变量取数据,还想拿到当前任务办理人的意见、流程发起原因、甚至上几个节点的输出。这个时候可以引入ExecutionListener或者TaskListener做前置处理。

例如在 Service Task 的 start 事件上挂一个 Listener,动态把当前流程的待办信息拼进 LLM 上下文:

@Component("llmContextListener") public class LlmContextListener implements ExecutionListener { @Override public void notify(DelegateExecution execution) { String businessKey = execution.getProcessBusinessKey(); FlowableEngine flowableEngine = ...; // 注入 // 根据 businessKey 查询业务数据,拼接上下文 execution.setVariable("llmContext", businessDataJson); } }

这种“初始化上下文”的监听器很有价值。它把业务数据准备和 LLM 调用解耦,让 delegate 只关心“拿着 context 调模型”,职责更干净。

5. 常见问题与排查技巧实录

5.1 流程实例一直处于运行中,但看不到任何待办

这个问题最容易发生。如果你用了同步 Service Task,且调用的模型接口超时时间很长,流程实例就会一直停留在 RUNNING 状态。因为没有任何用户任务,待办列表自然是空的,看起来就像流程“凭空消失”。

排查方法:连上数据库,看ACT_RU_EXECUTION表,找到ID对应的流程,再看ACT_RU_ACTINST表当前活动的节点,大概率停在了你的llmTask上。另外看ACT_RU_JOB表是否有 pending 的异步 job,这个表如果有记录,说明异步任务还没处理完。

解决思路很简单:给 HTTP 请求设 20~30 秒读取超时;如果需要长时间等待,把节点改成异步;再不行就加定时任务检测这种“卡住”的节点,超过一定时间就终止或补偿。

5.2 大模型返回的 JSON 写入流程变量后变成字符串/对象混淆

Flowable 的流程变量本质上存在ACT_RU_VARIABLE表中,有类型转换逻辑。你如果把一个Map对象直接setVariable,它默认会以二进制序列化存储(serializable类型)。后面在表达式里或者getVariable时虽然可以还原,但如果流程实例被持久化到别的环境、或者经过历史归档,可能会有反序列化问题。

更稳妥的做法是:在 delegate 里把模型返回的 JSON 转成String存入变量,需要使用时再解析。例如:

String rawJson = objectMapper.writeValueAsString(response); execution.setVariable("llmResultRaw", rawJson);

然后在网关条件里可以用表达式解析,比如$ {T(com.fasterxml.jackson.databind.ObjectMapper).readTree(llmResultRaw).get("approved").asText() == 'true'}。但这种写法可读性差,建议还是在 delegate 里解析好,再分别存入llmApproved、llmReason这样的原子变量。

5.3 异步 LLM 节点执行失败,流程悄悄“消失”

如果你启用了flowable:async="true",一定要留意失败重试后的行为。默认情况下失败 job 会重试,但如果超过最大重试次数,job 会移动到ACT_RU_DEADLETTER_JOB表,流程实例本身并不会自动结束,也不会抛给用户。

这导致现象是:流程没有任何异常提示,但节点就是没完成,后续节点也没走。你必须自己实现一个定时器,扫描 dead letter job 表,结合业务表判断是否需要完成后置动作,或者直接标记流程失败。或者,在流程图上给该异步节点挂一个Error boundary event,并配置一个捕获异常的中间事件,让流程走到“人工兜底”分支。

这里有一个我踩过的坑:异步节点如果抛异常,流程实例不会立刻回滚已执行的变量。也就是说,setVariable的某些值还是会被保留,后续重试时再次执行可能造成数据重复。所以你在 delegate 里写变量时应考虑幂等性,比如先判断是否存在,或者在重试前先清除上次写入的变量。

5.4 并发调用大模型导致线程池耗尽

大模型接口响应慢,平均 2 到 5 秒,如果流程并发量高,很可能把应用线程池占满。尤其是同步调用时,一个请求占用一个 worker 线程等待模型返回,并发 50 个请求,线程池就吃不消了。

我做性能评估后,有两个方向:第一是给 LLM 节点单独配置一个独立的ExecutorService,比如容量 20 的固定线程池,专门用来调模型,避免占用 Tomcat 容器线程。第二是改用 Flowable 的异步执行,让调用发生在 job executor 里,Tomcat 线程先释放。但无论哪种,都要设置合理的线程池队列和拒绝策略,否则流量突增一样会击穿。

更好的做法是给LlmClient增加一个本地信号量或断路器:当并发调用超过阈值时,直接快速失败并返回一个固定的 Error Code,由流程网关把该节点导向人工处理。这比无限排队更合理,因为大模型接口的吞吐就是有限的,宁可让人工兜底,也别让整个流程系统抱死。

5.5 测试 LLM 节点时不要真的去调模型接口

写单元测试的时候,一定要把LlmClientmock 掉,否则每次测试都要消耗真实 API 调用,慢不说,还容易触发限流。推荐用 Mockito 把 delegate 里的 llmClient 替换成 mock,固定返回一个伪 JSON,重点验证“变量读取、解析、写入、网关分支判断”是否正常。

集成测试时再起一个 WireMock 模拟 OpenAI 格式的接口,保证LlmClient的序列化和响应解析逻辑正确。我习惯先把整个 Flowable 流程引擎跑起来,插入一条测试流程,走完整个 BPMN,用断言检查最终的流程变量。

如果要在本地跑真实模型,建议用一个轻量级本地推理服务或者一个 Echo Server:接收任意输入,返回固定{"choices":[{"message":{"content":"PASS"}}]}。这样流程测试可以快速进行,也不会烧钱。

5.6 流程变量名称冲突导致结果被覆盖

LlmDelegate 里的输出变量名不要起得太普通,比如result、status,很容易和流程中其他节点的变量冲突。最好统一前缀,例如ai_opinion、ai_reason、ai_raw。同时可以在流程设计文档里维护一个变量字典,每个变量由哪个节点写入、哪个节点读取,都写明。

另外,流程变量在 Flowable 里没有强类型约束,读出来要自己转类型。比如getVariable("aiApproved")返回 Object,千万不要直接强转 Boolean,因为它在数据库中可能已变成 String。稳妥的做法是用Boolean.valueOf(String.valueOf(execution.getVariable("aiApproved")))这类转换,代码看着啰嗦,但能够避免很多隐形的隐式转换 Bug。

6. 一个端到端的最小化示例:智能审批摘要节点

6.1 流程定义与 BPMN 部署

把前面这些知识点串起来,我们设计一个最简单的“智能审批摘要”流程:表单提交后,先自动调用 LLM 生成摘要,然后进入人工审批,最终结束。

BPMN 文件核心部分大致如下(只节选 serviceTask):

<process id="aiApproval" name="AI 智能审批" isExecutable="true"> <startEvent id="start" /> <sequenceFlow id="flow1" sourceRef="start" targetRef="llmTask" /> <serviceTask id="llmTask" name="生成 AI 摘要" flowable:async="true" flowable:delegateExpression="${llmDelegate}" flowable:failedJobRetryTimeCycle="R2/PT5S"> <extensionElements> <flowable:field name="promptTemplate"> <flowable:string><![CDATA[请根据以下申请内容生成一段审批摘要,并给出建议意见: ${llmContext}]]></flowable:string> </flowable:field> </extensionElements> </serviceTask> <sequenceFlow id="flow2" sourceRef="llmTask" targetRef="approvalTask" /> <userTask id="approvalTask" name="人工审批" flowable:candidateGroup="manager" /> <sequenceFlow id="flow3" sourceRef="approvalTask" targetRef="end" /> <endEvent id="end" /> </process>

上面的flowable:field用来给 delegate 传入静态参数。如果你想让 prompt 模板可配置,就可以通过DelegateExecution.getCurrentActivityId()获取当前节点配置的参数,再结合流程变量渲染成最终的 prompt。

6.2 改进 Delegate 以支持模板渲染

@Component("llmDelegate") public class LlmDelegate implements JavaDelegate { @Autowired private LlmClient llmClient; @Value("${llm.default.prompt-template:请对申请内容进行摘要:${context}}") private String defaultPromptTemplate; @Override public void execute(DelegateExecution execution) { String context = (String) execution.getVariableIgnoreCase("llmContext"); String promptTemplate = null; Object promptTemplateObj = execution.getVariable("promptTemplate"); if (promptTemplateObj != null) { promptTemplate = promptTemplateObj.toString(); } if (promptTemplate == null) { promptTemplate = defaultPromptTemplate; } String prompt = promptTemplate.replace("${context}", context == null ? "" : context); String rawResult = llmClient.chat(prompt); // 假设模型返回 JSON: {"summary":"...", "suggestion":"同意"} JsonNode node = objectMapper.readTree(rawResult); execution.setVariable("aiSummary", node.get("summary").asText()); execution.setVariable("aiSuggestion", node.get("suggestion").asText()); } }

注意,${context}在 BPMN 里如果直接写在 XML 的 string 里,Flowable 可能会尝试当成表达式解析,所以要么用 CDATA 包裹,要么在 Java 模板引擎里处理。上面我用了replace方式,简单但容易出错。更推荐你用 Spring 的SpelExpressionParser或者直接拼接字符串,确保模板解析可控。

6.3 异步节点部署后的数据库表现

当流程走到llmTask并且节点配置为异步,你会观察到ACT_RU_JOB表多出一条 job 记录,类型为message。异步执行器拿到 job 后会执行 delegate,执行完成删除 job,流程实例继续前进。如果 job 失败,该记录会出现在ACT_RU_DEADLETTER_JOB表,直到重试耗尽或手动删除。这对运维排查非常关键,建议在监控面板里把这两张表的高水位线纳入告警。

我个人的经验是,给生产环境的ACT_RU_DEADLETTER_JOB建一个每 5 分钟跑一次的任务,查询当前死信 job,如果超过阈值就发邮件告警。并且写一个简单的补偿逻辑,记录job_id和execution_id,方便开发人员拿到这两个 ID 去代码里定位是哪次 LLM 调用失败。

7. 把 LLM 节点做成可复用的工作流能力

7.1 定义一套标准流程变量命名规范

如果你要在一个项目里规模化接入 LLM 节点,强烈建议定下这些约定:

变量名方向说明
llmContext输入传给模型的上下文 JSON 字符串
llmPrompt输入最终拼装好的 prompt
aiSummary输出模型返回的摘要
aiSuggestion输出模型给出的审批建议
aiRaw输出原始响应 JSON,用于审计
aiError输出最后重试仍然失败时的错误信息

统一命名之后,不同流程之间的重复节点可以共用同一个 delegate。以后就算换模型,也不需要重新部署流程,只要改LlmClient的实现,所有流程自动生效。

7.2 不要把所有逻辑都塞进 Delegate

很多新手容易把 delegate 写得很长,从流程变量解析到 prompt 拼接,再到调用模型、解析结果、写回变量,全部塞在一起。这样确实能跑,但后续维护很痛苦。

我推荐至少拆三层:

  • LlmClient:负责 HTTP 通信、超时、重试、反序列化,只暴露chat(prompt)和chatWithJson(prompt)两个方法。
  • PromptBuilder:负责把流程变量和业务上下文拼成 prompt,支持模板配置。
  • LlmDelegate:只做流程层的事情,读取变量、调用前两个类、写回变量。

这样测试时可以分别 mock 每一层。比如PromptBuilder做单测,不需要启动流程引擎;LlmClient做集成测试,用 WireMock 仿真接口;LlmDelegate做流程测试,mock 掉下面两层。

7.3 如何评估 LLM 节点对流程性能的影响

接入 LLM 节点前,一定要做容量估算。假设平均一次大模型调用耗时 3 秒,一台服务器最多同时处理 10 个调用,那单个 LLM 节点的吞吐上限就是每分钟 200 次。如果业务每日新增流程实例 5000 个,平均分布在 8 小时工作时间内,每秒大约 0.17 个实例,那完全没问题。但如果是促销、月末报销集中爆发,峰值可能到每秒 5 个实例,那就需要排队或扩容。

建议在网关层做限流,给 LLM 节点设置一个信号量,比如最大并发 10,超过后返回“模型繁忙”错误节点,走人工兜底。不要指望无限线程池解决问题,大模型服务的并发上限是硬约束。

8. 一些值得注意的工程细节

8.1 安全与敏感信息处理

大模型接口的 API Key 一定不要硬编码在 BPMN 文件或 Java 代码里。要用配置中心或者环境变量,并且配置脱敏。生产环境的流程定义要控制编辑权限,避免有人通过修改流程变量把 prompt 注入到模型里,导致信息泄露。用户输入的长文本不能直接无过滤地传给模型,至少要做一个脱敏和长度截断。限制 prompt 最大长度,比如 4000 字符,防止异常输入把模型调用成本打爆。

8.2 审计与追踪

工作流系统本身强调审计。LLM 节点的输入和输出都应该留痕,尤其当 AI 建议会影响审批结论时。除了把aiRaw写入流程变量,还建议落一张独立的LLM_AUDIT_LOG表,记录流程实例 ID、节点定义 ID、请求时间、响应时间、模型名称、输入摘要、输出摘要。这样后面如果审批结果有争议,可以把 AI 当时的判断依据调出来复核。

8.3 模型版本管理

大模型升级版本非常频繁,同一个 prompt 在不同模型版本下表现可能不一样。建议对每个流程节点配置一个模型版本字段,比如llmModelVersion,并且在审计日志里带上该字段。上线新模型前,先在测试流程里用同样的历史数据跑一遍,对比输出稳定性再切换。千万别在生产环境直接改模型名称,因为输出差异可能导致审批流走向完全不同。

最后说点个人实操感受

这几套方案我在真实项目里都试过,最开始的版本图省事,直接同步调用,结果月度报表跑批时用户反馈流程“卡死”,打开数据库一看,一堆流程实例阻塞在大模型节点上,有些甚至等了几十分钟。后来把所有 LLM 节点统一改成异步 + 死信告警 + 人工兜底,问题才彻底缓解。

再一个体会是,别把大模型当成 100% 可靠的服务,它就是个外部依赖,要有超时、重试、降级、降级后的人工处理路径。把这些工程化做好,LLM 节点才能真正成为工作流里一个稳定的自动化单元。Flowable 本身的设计哲学就是“流程必须有确定性状态”,而大模型天生带随机性,我们做的所有封装,本质上就是把这种随机性约束在可控的边界里。这样既拿到了 AI 的智能能力,又不破坏 BPM 流程的严谨性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询