ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flowable工作流集成LLM节点:Service Task与JavaDelegate实战指南

Flowable工作流集成LLM节点:Service Task与JavaDelegate实战指南 最近在做一个审批流的智能化改造正好碰到一个刚需在 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 调用 LLM3.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 容器里取 BeanAutowired就正常了。另一种做法是让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(); MapString, Object context parseJson(contextJson); context.put(processDefinitionKey, processDefinitionKey); String prompt buildPrompt(context); MapString, 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:asynctrue。开启后引擎会把该节点作为 job 放入异步执行器由线程池去执行execute方法原流程实例的调用线程先返回到 API 调用方。这意味着你可以先快速返回流程实例 ID然后在后台线程中慢慢等待大模型返回完成后继续执行后续节点。这个方案特别适合那些对“发起流程”这个动作的响应时间有要求的场景毕竟用户提交一个申请不希望浏览器一直转圈等大模型。配置方法很简单serviceTask idllmTask nameLLM 节点 flowable:asynctrue flowable:delegateExpression${llmDelegate} flowable:exclusivefalse /flowable:asynctrue启用异步执行flowable:exclusivefalse表示不与其他 job 互斥可以并发执行。但要注意异步执行需要 Flowable 的 Job Executor 处于激活状态。Spring Boot 集成 Flowable 时默认会自动激活。另外异步 job 如果执行失败会不断重试默认重试次数和时间间隔需要额外配置否则失败 job 会堆积在ACT_RU_JOB表里。4.2 使用 Spring Async 做异步调用避免捆绑引擎线程池除了 Flowable 自身的异步 job还有一种做法是让JavaDelegate内部通过 SpringAsync并发调用大模型同时立即返回。但这种做法要小心JavaDelegate.execute已经返回引擎就会认为节点执行完毕会把流程推进到下一步这时你的大模型结果可能还没回来。所以这种方案只适合“发了就不管”的通知类场景不适合需要结果参与后续网关的情况。如果真的需要异步拿到结果再继续那就不要用 SpringAsync而是考虑工作流引擎自带的“异步续跑”机制。Flowable 提供Asynchronous Continuation就是为了这个。或者干脆设计成人工触发在 LLM 节点前先进入一个用户任务人工点击“执行 AI 分析”后触发 Service Task服务完成后回到人工任务展示结果。这样把“等待大模型”的时间交给用户自己掌握也是实践中很常见的交互方式。4.3 重试策略与异常处理让流程自己有韧性大模型接口本质上是外部依赖可能出现网络抖动、模型过载、限流等异常。在JavaDelegate里直接抛异常会让流程进入异常状态所以要么能自动重试要么给该节点配置边界事件失败时走人工兜底分支。我比较推荐三级策略第一级LlmClient内置指数退避重试最多重试 2 次比如第 1 次失败等 1 秒第 2 次失败等 2 秒。第二级Flowable 节点配置flowable:asynctrueflowable:failedJobRetryTimeCycle失败 job 自动重试。第三级在流程图上给 Service Task 添加Boundary Error Event重试仍失败后将流程转向人工兜底节点比如生成一条待办让管理员处理。failedJobRetryTimeCycle可以配置为一个 ISO 8601 间隔例如serviceTask idllmTask nameLLM 节点 flowable:asynctrue flowable:failedJobRetryTimeCycleR3/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: trueasync-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:asynctrue一定要留意失败重试后的行为。默认情况下失败 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 文件核心部分大致如下只节选 serviceTaskprocess idaiApproval nameAI 智能审批 isExecutabletrue startEvent idstart / sequenceFlow idflow1 sourceRefstart targetRefllmTask / serviceTask idllmTask name生成 AI 摘要 flowable:asynctrue flowable:delegateExpression${llmDelegate} flowable:failedJobRetryTimeCycleR2/PT5S extensionElements flowable:field namepromptTemplate flowable:string![CDATA[请根据以下申请内容生成一段审批摘要并给出建议意见 ${llmContext}]]/flowable:string /flowable:field /extensionElements /serviceTask sequenceFlow idflow2 sourceRefllmTask targetRefapprovalTask / userTask idapprovalTask name人工审批 flowable:candidateGroupmanager / sequenceFlow idflow3 sourceRefapprovalTask targetRefend / endEvent idend / /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输入最终拼装好的 promptaiSummary输出模型返回的摘要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 流程的严谨性。
返回列表