ARTICLE DETAIL

资讯详情

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

Flowable 接入大模型:用 Service Task 封装 LLM 调用的完整指南

Flowable 接入大模型:用 Service Task 封装 LLM 调用的完整指南 Flowable 的老项目多数是在跑审批、跑单据流转、跑订单处理只要不涉及“阅读理解”类的工作它都挺省心。可一旦需求变成“合同进来以后先自动分析一下哪些条款有风险”“工单描述特别长的先自动分个类”“客户情绪差的先转人工”靠传统规则就非常痛苦。于是所有人不约而同想到了同一件事把大模型LLM作为工作流里的一个特殊节点接进去。这个想法听起来像画了个新功能但真正落地时争论最多的往往不是模型选哪个而是“LLM 节点到底算什么东西”。它既不是人工任务也不是数据库操作本质上就是一个外部服务调用。而 Flowable 的 BPMN 原生就支持 Service Task所以最稳的姿势很简单把 LLM 调用封装成一个 Service Task和流程变量、网关、人工审批配合起来。这篇我就直接从实操讲怎么把 LLM 包装成可复用的 Flowable 服务节点怎么处理失败和重试怎么让它的输出驱动流程分支以及那些 demo 里永远见不到的生产环境坑。1. 为什么是 Service TaskLLM 在 BPMN 里的正确身份1.1 传统流程引擎处理“非结构化信息”时的无力感客户合同、工单、简历这些文本进系统时以前能做的自动处理非常有限。最简单的做法是用关键词匹配或者写一堆 if-else 规则。中级的可以训分类模型但要样本、要持续迭代。一旦判断维度变多规则就会变成一团乱麻。举个例子工单分类“设备故障 / 网络问题 / 账户问题 / 其他”。关键词规则很容易误判“我的网很慢但是能开网页”会被分到“设备故障”而不是“网络问题”。规则写得再细也防不住用户换着花样表达。这种场景本质上是非结构化文本的语义理解问题传统工作流引擎根本不擅长。1.2 接入 LLM 后的第一原则BPMN 语义不变很多人一开始把 LLM 想象成一种全新的节点类型觉得要像“人工任务”那样给引擎加一个“AI 任务”。其实完全不用。LLM 调用在 BPMN 里就是一个 Service Task跟调用财务系统的接口没有本质区别输入流程变量执行外部计算或模型调用把结果写回流程变量。一旦认清这一点后面的设计就顺畅了。Flowable 的持久化、审计、超时控制、重试机制这些能力全部保留LLM 只是让服务节点拥有了“语义理解”能力。这也是工作流接入大模型最稳定的姿势引擎不改造逻辑不侵入收益却很大。1.3 什么样的业务才值得接接之前先判断业务是否真的需要。适合 LLM 节点的场景一般有三个特征输入是非结构化文本靠规则做不稳靠人做太贵。判断结果要流向后续流程节点而不是一次性问答。判断出错可以接受但必须有人工兜底或复核环节。典型的比如合同风险提示、客户投诉分类、简历初筛摘要、工单自动分派、审批意见生成。如果只是“聊天机器人”式的问答那没必要硬塞进 Flowable直接挂在门户或客服系统里更合适。2. 接入路线对比内置 HTTP 任务与自定义 Service Task 的取舍2.1 HTTP 任务零 Java 代码的捷径但只适合简单调用Flowable 支持在 Service Task 里直接发起 HTTP 调用配置 URL、请求方法、请求头、请求体再把响应解析出来存成流程变量。初看对 LLM 这种 REST API 很契合也确实是一条能跑通的捷径。适合用 HTTP 任务的情况是模型 endpoint 固定提示词基本写死返回只要从 JSON 里取一个字段。比如“把用户输入翻译成英文”这种场景在流程模型里埋几个配置项就够。但实际用起来会遇到三道坎提示词如果依赖上一步流程变量HTTP 任务的请求体就要在 XML 里拼出一大段表达式维护难度直接拉满。响应解析如果只取一层 JSON 字段还算简单一旦要处理错误格式、超时、多候选结果、上下文追加没有任何编程逻辑可以承接。统一日志、埋点、重试策略都做不了每个流程都要单独配置散落各处。2.2 自定义 Service Task把复杂度收敛在一个地方我更推荐第二种写一个 JavaDelegate 统一负责提示词渲染、模型调用、结果解析和失败兜底然后通过delegateExpression挂到任意流程节点上。这么做优势很明显逻辑可以单元测试不像 XML 配置一多就难以验证。多个流程复同一套调用逻辑换模型不用改流程模型。能按流程变量切换模型、Prompt 模板灵活性高。可观测性容易做日志和链路追踪集中在唯一入口。有一个要避开的反面模式每个流程单独建一个 delegate bean。那样不出三个月就会出现几十个 delegate每个只是改了 prompt代码重复严重。正确做法是做一个通用 LLM 节点把“Prompt 模板”和“输入数据”作为流程变量交给它节点本身保持可复用。2.3 怎么选先跑通还是做平台我现在的判断标准很简单如果只是某个项目里一次性的简单调用用 HTTP 任务快如果公司要把 AI 能力变成流程平台的一部分未来多个流程都会用直接上自定义 Service Task。绝大多数业务走着走着都会落到第二种方案因为 LLM 调用根本不是“一次请求”的事它涉及提示词管理、结果校验、成本统计和风险兜底。3. 核心实现把 LLM 包装成可复用的流程节点3.1 先用变量协议把接口定清楚任何涉及多节点协作的功能第一步就是规定变量协议。我习惯定义下面这套变量变量方向说明llmPromptTemplate入提示词模板可以用{var}占位llmInput入要分析的业务文本或 JSON 对象llmModel入模型名如gpt-4o、qwen-plus空则走默认llmOutput出LLM 返回的文本或 JSON 字符串llmError出失败原因为空表示成功llmStatus出success或failed这里的设计思路借鉴了函数接口调用方约定好输入节点返回状态和结果。后续流程不关心具体调的是哪家模型只看llmOutput和llmStatus。这种解耦在将来切换模型时尤其重要。3.2 JavaDelegate 骨架和 XML 绑定Java 端核心代码非常简单重点是 renderPrompt 和结果写回的约定Component(llmProcessor) public class LlmProcessorDelegate implements JavaDelegate { private final LlmGateway llmGateway; public LlmProcessorDelegate(LlmGateway llmGateway) { this.llmGateway llmGateway; } Override public void execute(DelegateExecution execution) { String prompt renderPrompt(execution); LlmConfig config resolveConfig(execution); try { LlmResult result llmGateway.complete(prompt, config); execution.setVariable(llmStatus, success); execution.setVariable(llmOutput, result.getContent()); execution.setVariable(llmError, null); } catch (Exception e) { execution.setVariable(llmStatus, failed); execution.setVariable(llmError, e.getMessage()); } } }renderPrompt内部从执行环境读llmPromptTemplate和llmInput替换占位符后返回完整提示词。resolveConfig负责模型名、温度、超时等参数。BPMN 侧绑定也很直接serviceTask idllmTask name调用LLM分析 extensionElements flowable:delegateExpression expression${llmProcessor}/ /extensionElements /serviceTask这里注意提示词不是强行塞进 XML 里的而是通过流程模型上游设置的变量传入。这样流程设计器里只体现“调用 LLM 分析”具体提示词归提示词管理模块管职责清晰。3.3 失败策略不要让异常直接卡死流程工作流里最让人紧张的状态就是“挂了就挂”。LLM 服务不是 100% 可用超时、限流、返回格式非法都常见。所以 LLM 节点的失败必须被显式设计而不是让异常直接炸掉整个流程实例。我一般用两层兜底第一层在 delegate 内部捕获异常后写入llmStatus和llmError流程继续往下走。第二层在 BPMN 模型上用一个排他网关判断llmStatus成功走自动分支失败走人工兜底分支。如果想用 BPMN 边界错误事件也可以但实战下来变量加网关的方式更直观也更容易在测试里断言。注意不要设置无限重试。LLM 调用失败大多不是“多试几次就能成功”的尤其限流时无限重试只会把故障时间拉长。指数退避重试一两次是合理上限。3.4 结果要写全局变量别写局部变量有一个非常常见的坑就是图省事用setVariableLocal写结果。局部变量只存在于当前执行分支的 scope一旦流程走到排他网关或者并行网关的另一个分支读取时很容易拿到 null流程就莫名其妙走到了默认分支。我在项目里真实见过一次并行分支里的服务任务用setVariableLocal写结果后面排他网关读llmStatus时一直为空明明日志里 delegate 执行成功了流程却绕到人工兜底去了。排查了半天才发现是局部变量作用域的问题。所以凡是需要跨越节点或网关使用的变量统一用execution.setVariable写入。如果确实想保留局部信息那就通过命名区分比如aiResult_local并确保后续真的不会跨作用域读取。4. 引入人工复核后“AI 自动判断”才真正可上生产4.1 LLM 结果本身不能全对必须有“人工兜底”我不太建议直接把 LLM 的输出当成事实直接驱动后续业务动作。模型幻觉、格式不对、理解偏差都是概率事件生产环境必须有一个人工复核关口。在 Flowable 里这很自然LLM 节点之后接一个用户任务。用户任务里展示三块信息原始输入、LLM 的分析结果、模型给出的理由。然后让用户提交一个审批结论比如“同意”“改为人工处理”“重新生成”。这个结论存成流程变量humanDecision后面排他网关根据它往下走。这套设计的好处是AI 只负责提效最终的业务责任还是落在人身上。这在审批类、法务类、财务类场景里尤其重要。4.2 “重新生成”其实就是一个小循环用户对 LLM 结果不满意时最常见诉求是让它重新分析。实现上不复杂排他网关里判断humanDecision regenerate让流程回到之前的 LLM 节点再跑一次。但这里有一个优化点重跑之前应当把用户的反馈或上次的结果拼进新的提示词里。比如“用户认为上次分类不准确请重点检查是否有退款相关的语义”。如果只是原样重跑大概率拿到相同的结果既浪费 token 又让用户觉得 AI 没用。4.3 重复执行的副作用要防正因为有循环分支也有引擎异步重试机制LLM 节点很可能对同一份输入执行多次。模型本身不具备幂等性同一个提示词在不同时间可能给出不同结果。处理经验是如果 LLM 节点只是做只读分析那重复执行问题不大最多费点 token如果节点后面要触发外部副作用比如发短信、创建工单、改外部系统状态那就必须在业务参数里带上唯一请求 ID或者在流程变量里标记“该输入已处理过”重跑时直接复用已有结果。5. 用 LLM 结果驱动流程分支结构化输出与网关的组合5.1 不要让模型自由发挥要求结构化 JSON很多人第一步只让 LLM “给个判断”结果输出一大段散文。后续要驱动流程分支时解析散文非常痛苦。正确做法是要求模型按 JSON 格式返回。以客户投诉分类为例提示词明确要求模型输出如下结构{ category: payment, priority: high, summary: 用户无法完成支付 }模型返回后delegate 里用 Jackson 解析把category和priority抽出来存成独立变量JsonNode node objectMapper.readTree(llmOutput); execution.setVariable(aiCategory, node.get(category).asText()); execution.setVariable(aiPriority, node.get(priority).asText());这一步很关键在 Java 侧完成字段结构化后续 BPMN 网关表达式直接引用普通 String 变量简洁又可靠。5.2 网关条件直接引用流程变量有了aiCategory变量排他网关的条件就非常直白${aiCategory payment} ${aiCategory network} ${aiCategory account}这比在网关里直接解析 JSON 字符串要可维护得多。Flowable 的 SpEL 表达式对普通字符串变量的兼容性也最好调试时一眼就能看出问题。5.3 多分支必须留 default 兜底LLM 最让人头疼的就是不守规矩。你明明限制了 enum它还是可能返回一个你从没见过的值。所以排他网关只要用了 LLM 结果驱动就必须写 default 分支default 落到人工处理或通用兜底节点。千万不要觉得“只要提示词写得好模型就一定听话”。实测下来提示词约束能降低错误率但永远到不了 100%。default 分支不是多余而是保险丝。6. 生产环境最容易翻车的四个细节以及我的处理方式6.1 delegate bean 是单例保存实例字段状态会炸LlmProcessorDelegate注册成 Spring bean 后默认是单例所有流程实例共享同一个对象。这意味着 delegate 里不能放“当前流程的临时状态”。我见过有人把流程数据缓存在 delegate 的实例 Map 里结果流程 A 和流程 B 的数据串了线而且是偶发性的特别难排查。正确的做法是所有状态都通过DelegateExecution的变量读写delegate 本身保持无状态。6.2 调用 LLM 的线程池要认真设计Flowable 执行服务任务时如果节点没有配置异步执行LLM 调用会阻塞当前请求的处理线程。并发量一上来处理线程全被模型请求占住其他流程实例也一起卡住。合理的处理方式有几个方向给 LLM 服务任务设置flowable:asynctrue让模型调用放到异步 JobExecutor 里执行避免阻塞前端流程线程。异步执行时要关注 Job 的锁超时时间。LLM 调用如果超过锁时间另一个执行器可能把任务抢走导致重复执行。给每次模型调用设置严格的超时上限比如 30 秒或 60 秒宁可失败走人工兜底也不能无限等。6.3 记录 Prompt 版本、Token 和耗时LLM 是不可控的无法预测所以在生产环境里更需要可观测性。每次调用至少记录流程实例 ID、模型名、Prompt 模板版本、输入长度、输出 Token 数、耗时、是否重试。这些数据不只是用于排查问题更能用来做成本核算和模型效果对比。换提示词后效果有没有变好不能靠感觉要看线上统计。我一般把日志直接打到结构化日志里带上processInstanceId和llmRequestId两个贯穿字段事后查询非常方便。6.4 模型调用层要做适配隔离虽然标题讲的是 Flowable但我要多说一句不要在主流程逻辑里直接写死某一家模型的 SDK。封装一个LlmGateway接口内部按模型名路由到不同供应商的实现这样做有两个实际好处一是公司以后换模型时业务代码不用动二是可以方便地在网关层统一加缓存、重试和限流。如果团队里资源紧张也可以先用 n8n、Dify 这类工具把模型调用包装成标准 APIFlowable 里只做 HTTP 调用。这种方式能快速上线但后续要做到精细的提示词和成本管理时还是逃不掉要维护一层自建网关。最后分享一个小技巧我做了几个项目后的最大体会是LLM 节点的变量命名越固定越好。把所有模型的输入输出都统一成llmPromptTemplate、llmInput、llmOutput、llmStatus这一套“约定大于配置”团队里任何人接手新流程时都能马上看懂。再补充一个细节renderPrompt里拼提示词时一定把业务文本截断到合理长度比如 8000 字以内超出的部分先做摘要再传给模型。别让用户输入一个几十万字的大文件直接把模型接口打爆这种事在真实环境里一定会发生。
返回列表