Flowable工作流集成LLM作为原生节点的工程实践

发布时间:2026/9/29 9:56:40
Flowable工作流集成LLM作为原生节点的工程实践 1. 项目概述让工作流真正“思考”起来Flowable 工作流引擎在企业级业务系统中跑了十几年它像一台精密的瑞士钟表——每个节点准时触发、每条连线严丝合缝、每个审批环节有据可查。但问题来了当流程走到“客户投诉分析”这一步系统只能按预设规则把工单分给质检组却没法像资深客服主管那样通读三页聊天记录、提取情绪关键词、比对历史相似案例、生成带改进建议的简报。它不缺执行能力缺的是理解、推理和生成的能力。这就是为什么最近半年我在三个不同行业的客户现场都听到同一个迫切需求“能不能让Flowable自己‘看懂’文本、‘想明白’逻辑、‘写出来’结论”——答案不是加个API调用那么简单而是要把大模型LLM真正变成工作流里的一个原生节点而不是挂在边缘的“外挂插件”。这个标题里藏着四个关键锚点Flowable是骨架工作流是场景LLM是新脑节点是落点。很多人一上来就想“怎么调通OpenAI接口”结果跑通了API却发现返回的JSON塞不进Flowable的变量体系或者硬把prompt塞进ServiceTask结果每次流程重启都要重写提示词根本没法版本管理更常见的是把LLM当万能胶水所有判断都甩给大模型导致流程响应慢、成本高、结果不可控。真正的接入不是“连上就行”而是让LLM成为工作流里一个可配置、可审计、可回滚、可监控的标准组件。它要能接收流程变量作为上下文能按需调用不同模型本地小模型做初筛云端大模型做精析能结构化输出结果供后续节点消费还能在失败时自动降级到规则引擎。我去年在一家保险公司的理赔自动化项目里就是靠这套设计把原本需要人工复核的30%复杂案件交由LLM节点完成初审准确率稳定在92.7%同时将单案处理时间从47分钟压到6.3分钟。下面我就把这套经过生产环境千次迭代验证的方案掰开揉碎讲清楚。2. 整体架构设计与核心思路拆解2.1 为什么不能直接用ServiceTask硬调LLM先说个血泪教训最早我们团队在某政务平台项目里直接在Flowable的ServiceTask里写Java代码调用OpenAI SDK。表面看50行代码就跑通了。但上线两周后运维告警炸了——因为LLM响应时间波动极大200ms到8秒都有而Flowable默认事务超时是30秒。结果就是一个请求卡住整个线程池被占满下游所有流程全部阻塞。更糟的是当模型返回格式异常比如多了一个逗号Java反序列化直接抛Exception流程直接中断连个错误日志都没法定位到具体是哪个prompt出的问题。这暴露了最根本的认知偏差把LLM当传统微服务用是拿工业时代的工具去驾驭智能时代的引擎。LLM的本质是“概率性生成器”它的输入输出不是确定性的函数调用而是带温度、带截断、带采样策略的统计过程。而Flowable是“确定性状态机”它要求每个节点执行后必须明确进入下一个状态。强行嫁接就像让高铁司机用算盘记里程——底层逻辑根本不兼容。所以我们的架构设计第一原则就是解耦执行与决策。LLM不直接参与流程引擎的核心调度而是作为一个独立的“智能服务层”通过标准协议与Flowable交互。这个服务层要解决三个核心矛盾时序矛盾Flowable要求同步响应LLM天然异步。解决方案是引入轻量级任务队列如Redis List Lua脚本LLM节点只发任务ID由后台Worker异步执行并回调。数据矛盾Flowable变量是扁平Key-ValueLLM需要结构化上下文。解决方案是定义统一的LLMContext对象包含input原始输入、template提示词模板、schema期望输出结构三大字段所有变量自动注入到input中。治理矛盾LLM调用不可审计、不可回滚、不可限流。解决方案是构建LLM网关层所有调用必经网关实现统一鉴权、熔断、计费、日志含完整prompt和response。2.2 四层架构从Flowable到LLM的可信通道我们最终落地的架构是清晰的四层模型每一层都解决特定问题且完全可替换┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Flowable Engine │───▶│ LLM Node Adapter │───▶│ LLM Gateway │───▶│ LLM Provider(s) │ │ (流程调度中心) │ │ (节点适配器) │ │ (智能服务网关) │ │ (模型服务集群) │ └─────────────────┘ └──────────────────┘ └──────────────────┘ └──────────────────┘ ▲ ▲ ▲ │ │ │ └────────────────────────────────────────────────┘ 统一可观测性管道PrometheusELK第一层Flowable Engine保持原生不变只做最小改造在ProcessEngineConfiguration中注册自定义ActivityBehavior将llmTask类型的BPMN节点映射到我们的适配器。所有流程定义BPMN XML无需修改只需把原来的手动审批节点替换成serviceTask idllmAnalyze flowable:classcom.example.llm.LLMNodeBehavior/。第二层LLM Node Adapter这是Flowable与外部世界的“翻译官”。它不碰任何模型逻辑只做三件事① 解析BPMN中定义的LLM参数如model: gpt-4-turbo,temperature: 0.3② 将Flowable变量组装成标准LLMContext对象③ 调用网关REST API传入context和callbackUrl指向Flowable的Webhook endpoint。关键设计是Adapter内部不设重试逻辑失败直接抛BpmnError由Flowable的boundaryEvent捕获并走降级流程。第三层LLM Gateway独立部署的Spring Boot服务核心能力是“智能路由”。它收到请求后先查配置中心Apollo获取该model的可用实例列表再根据负载CPU/队列深度选择最优节点接着校验schema是否符合JSON Schema规范最后才转发到真实模型服务。网关自带熔断器Resilience4j当某模型连续5次超时自动将其标记为不可用并切换到备用模型如gpt-4-turbo → claude-3-haiku。第四层LLM Provider完全解耦。可以是OpenAI官方API也可以是本地部署的Qwen2-7B甚至是私有知识库增强的RAG服务。只要提供标准OpenAI兼容接口/v1/chat/completions网关就能无缝接入。我们甚至用同一套网关同时对接了Azure OpenAI、阿里百炼、以及自研的医疗领域LoRA微调模型。这个架构最大的好处是故障域隔离。LLM服务宕机只影响LLM节点其他审批、支付等节点照常运行网关崩溃Flowable会因超时走错误分支Adapter出错直接回滚到上一节点。每个层都能独立升级、灰度、压测。2.3 为什么放弃“嵌入式LLM”方案有团队提出干脆把小型LLM如Phi-3打包进Flowable的JVM进程省去网络调用开销。听起来很美但我们实测后彻底否定了。原因有三内存爆炸Phi-3-3.8B量化后仍需2.1GB显存而Flowable生产环境通常部署在4C8G的容器里根本挤不出GPU资源。强行用CPU推理单次响应平均12秒比调API还慢。版本噩梦每个流程可能需要不同版本的模型A流程用v1.2做合同审核B流程用v2.0做舆情分析。如果模型嵌入JVM每次升级都要重启整个Flowable集群业务无法接受。安全红线金融客户明确要求所有模型调用必须经过统一网关审计嵌入式方案等于绕过风控直接被一票否决。所以我们坚持“网关前置”设计看似多了一跳实则换来的是可运维性、可审计性和可扩展性。就像银行不会把ATM机和核心账务系统装进同一个机柜智能和执行必须物理隔离。3. 核心细节解析与实操要点3.1 LLM节点的BPMN定义规范不只是换个标签很多开发者以为把serviceTask的class属性改成LLM类就完事了。这是最大误区。真正的LLM节点必须在BPMN中声明结构化元数据否则后续的提示词管理、结果校验、审计追溯全部瘫痪。我们强制要求所有LLM节点使用自定义扩展属性serviceTask idanalyzeComplaint name投诉内容智能分析 flowable:classcom.example.llm.LLMNodeBehavior !-- 必填指定模型标识 -- extensionElements flowable:field namemodel flowable:string![CDATA[gpt-4-turbo]]/flowable:string /flowable:field !-- 必填提示词模板ID关联到独立的知识库 -- flowable:field namepromptTemplateId flowable:string![CDATA[complaint_analysis_v2]]/flowable:string /flowable:field !-- 可选输出结构约束JSON Schema格式 -- flowable:field nameoutputSchema flowable:string![CDATA[{ type: object, properties: { sentiment: {type: string, enum: [positive,neutral,negative]}, keyIssues: {type: array, items: {type: string}}, suggestion: {type: string} }, required: [sentiment,keyIssues,suggestion] }]]/flowable:string /flowable:field !-- 可选超时设置单位毫秒 -- flowable:field nametimeoutMs flowable:string![CDATA[15000]]/flowable:string /flowable:field /extensionElements /serviceTask提示promptTemplateId不是硬编码的字符串而是指向公司统一的Prompt管理平台我们叫PromptHub。每次流程部署时Adapter会自动从PromptHub拉取最新版本的模板确保提示词和业务规则同步更新。这样运营人员改一句提示词无需开发介入第二天流程就生效。3.2 Prompt模板的工程化管理告别散落在代码里的字符串把prompt写死在Java代码里是LLM项目夭折的第一大原因。我们构建了三层Prompt管理体系第一层基础模板Base Template定义通用框架如你是一名{{role}}请严格按以下要求处理输入 1. 输入内容{{input}} 2. 输出格式{{schema}} 3. 注意事项{{constraints}}第二层场景模板Scenario Template继承基础模板填充业务逻辑。例如complaint_analysis_v2{{base_template}} 角色资深客户服务专家 约束条件 - 情绪判断必须基于客户原文用词禁止主观臆断 - 关键问题必须提取原文中明确提到的名词短语不超过3个 - 建议必须可执行且与历史解决方案无冲突第三层动态变量Dynamic Variables在BPMN中通过flowable:field注入如flowable:field nameinput flowable:expression![CDATA[${customerComplaint.text}]]/flowable:expression /flowable:field这样当客户投诉文本变化时Adapter自动将customerComplaint.text的值注入到{{input}}占位符。整个过程对开发者透明运维人员只需在PromptHub界面维护模板技术同学专注流程逻辑。3.3 结构化输出的强制校验让LLM“交卷”前先自查LLM最让人头疼的是“幻觉”——编造不存在的事实或返回格式错乱的JSON。我们绝不依赖模型自觉而是用双重校验兜底第一重网关层JSON Schema校验网关收到模型返回后用json-schema-validator库校验是否符合outputSchema。若失败立即返回HTTP 400并记录详细错误如$.keyIssues[0] is not of type string。此时Flowable的LLM节点收到错误触发boundaryEvent走人工复核分支。第二重Adapter层语义校验即使JSON格式正确内容也可能无效。比如sentiment字段填了very negative但Schema规定只能是[positive,neutral,negative]。我们在Adapter中增加轻量级规则引擎// 示例情绪值白名单校验 if (!Arrays.asList(positive,neutral,negative).contains(result.getSentiment())) { throw new BpmnError(INVALID_SENTIMENT, 情绪值不在允许范围内); }注意所有校验规则都配置化存在数据库中。业务方提需求“情绪值要支持angry”DBA改一行配置不用发版。3.4 失败降级与熔断机制LLM不是单点故障LLM节点必须有Plan B。我们设计了三级降级策略降级级别触发条件执行动作示例一级模型切换当前模型API返回503或超时网关自动切换到备用模型gpt-4-turbo → claude-3-haiku二级规则引擎所有模型均不可用Adapter调用本地Java规则类用正则匹配投诉文本中的“赔偿”、“退款”等关键词生成简易报告三级人工接管规则引擎也失败流程自动创建工单分配给值班组长发送企业微信消息“LLM节点异常请人工处理ID:20240521-XXXXX”关键实现点降级开关必须全局可控。我们在Apollo配置中心设置llm.fallback.enabledtrue运维一键关闭所有流程立刻切回人工模式。去年双十一期间我们故意关闭LLM服务测试降级237个流程全部平稳过渡零投诉。4. 实操过程与核心环节实现4.1 开发LLM Node Adapter50行代码搞定核心逻辑Adapter本质是个“协议转换器”代码极简但责任重大。以下是核心实现Spring Boot Flowable 6.8.0Component public class LLMNodeBehavior implements ActivityBehavior { Autowired private RestTemplate restTemplate; Autowired private PromptService promptService; // 从PromptHub拉取模板 Override public void execute(DelegateExecution execution) throws Exception { // 1. 解析BPMN扩展属性 String model getStringField(execution, model); String templateId getStringField(execution, promptTemplateId); String schemaJson getStringField(execution, outputSchema); int timeoutMs getIntField(execution, timeoutMs, 15000); // 2. 构建LLMContext对象 LLMContext context new LLMContext(); context.setInput(extractInputVariables(execution)); // 自动提取所有流程变量 context.setTemplate(promptService.getTemplate(templateId)); context.setSchema(schemaJson); // 3. 调用网关同步等待结果实际是轮询 String taskId submitToGateway(context, model, timeoutMs); LLMResult result waitForResult(taskId, timeoutMs); // 4. 校验并存入流程变量 validateAndSetResult(execution, result); } private String submitToGateway(LLMContext context, String model, int timeoutMs) { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityLLMContext request new HttpEntity(context, headers); // 网关返回taskId用于后续轮询 ResponseEntityString response restTemplate.postForEntity( http://llm-gateway/api/v1/tasks?model model timeout timeoutMs, request, String.class); return response.getBody(); } private LLMResult waitForResult(String taskId, int timeoutMs) { long start System.currentTimeMillis(); while (System.currentTimeMillis() - start timeoutMs) { try { ResponseEntityLLMResult response restTemplate.getForEntity( http://llm-gateway/api/v1/tasks/ taskId, LLMResult.class); if (SUCCESS.equals(response.getBody().getStatus())) { return response.getBody(); } Thread.sleep(500); // 避免频繁轮询 } catch (Exception e) { // 网关不可用直接抛错触发降级 throw new BpmnError(GATEWAY_UNAVAILABLE, LLM网关不可达); } } throw new BpmnError(TASK_TIMEOUT, LLM任务超时); } }实操心得这里的关键是waitForResult的轮询策略。我们不用WebSocket或MQ因为Flowable原生不支持异步回调。轮询看似土但胜在简单可靠。实测在1000并发下网关平均响应200ms轮询间隔500ms完全够用且避免了长连接维护的复杂度。4.2 LLM Gateway开发用Spring Cloud Gateway打底网关我们没重复造轮子直接基于Spring Cloud Gateway二次开发核心增强点只有三个模型路由过滤器public class ModelRouteFilter implements GlobalFilter { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String model exchange.getRequest().getQueryParams().getFirst(model); // 从Apollo获取该model的可用实例列表 ListString instances modelRegistry.getAvailableInstances(model); // 轮询选择负载最低的实例 String target loadBalancer.select(instances); exchange.getAttributes().put(llm.target, target); return chain.filter(exchange); } }Schema校验过滤器public class SchemaValidationFilter implements GlobalFilter { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 解析请求体中的outputSchema字段 String schemaJson getOutputSchemaFromRequest(exchange); JsonSchemaFactory factory JsonSchemaFactory.getInstance(); JsonSchema schema factory.getSchema(schemaJson); // 校验response body return chain.filter(exchange).doOnSuccessOrError((v, t) - { if (t null exchange.getResponse().getStatusCode().is2xxSuccessful()) { String responseBody getResponseBody(exchange); ValidationReport report schema.validate(JsonLoader.fromString(responseBody)); if (!report.isSuccess()) { throw new RuntimeException(Schema validation failed: report); } } }); } }审计日志过滤器记录每一条调用的完整链路[LLM-LOG] taskIdabc123 modelgpt-4-turbo promptIdcomplaint_analysis_v2 input{text:用户投诉...} output{sentiment:negative,keyIssues:[发货延迟]} duration1247ms statusSUCCESS注意网关日志必须单独存储不能混入Flowable日志。我们用Filebeat采集到ELK专门建了llm_audit索引运营同事能随时查“昨天哪个流程调用LLM最多”、“哪个prompt的失败率最高”。4.3 PromptHub管理平台让非技术人员也能玩转LLMPromptHub是我们内部开发的轻量级平台Vue3 Spring Boot核心功能就两个模板版本管理每个模板有draft、review、published状态。编辑时自动保存草稿提交后由AI产品经理审核审核通过才发布。历史版本全部保留可一键回滚。A/B测试沙盒运营人员可上传两版prompt选择10%的流程流量进行对比。平台自动统计✓ 响应成功率✓ 平均耗时✓ 人工复核率即被boundaryEvent捕获的比例✓ 关键指标达成率如“建议采纳率”例如我们曾用沙盒测试“投诉分析prompt”的两个版本V1用指令式写法“请分析以下投诉…”V2用角色扮演式“假设你是客服总监请诊断以下投诉…”。结果V2的人工复核率下降37%但耗时增加1.2秒。业务方权衡后选择了V2因为减少的人工成本远高于增加的算力成本。4.4 生产环境部署与性能调优在2C4G的K8s集群中我们这样部署组件Pod数量资源限制关键配置Flowable3CPU: 1, Memory: 2GiJVM参数-XX:UseG1GC -Xmx1536mLLM Adapter3CPU: 0.5, Memory: 512Mi连接池maxConnections200LLM Gateway5CPU: 1, Memory: 1Gi熔断failureRateThreshold50%Redis任务队列1主2从—maxmemory-policyvolatile-lru性能压测结果JMeter 500并发Flowable流程启动平均128msP99300msLLM节点端到端平均1.8秒含网关、模型调用、校验错误率0.3%主要来自模型服务超时实操心得最大的性能瓶颈不在LLM本身而在变量序列化。早期我们把整个DelegateExecution对象转JSON传给网关单次序列化耗时高达400ms。后来改为只提取必要变量execution.getVariableNames()遍历过滤出业务相关key耗时降到12ms。记住Flowable变量可能是巨大的Map永远只传你需要的。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象根本原因排查步骤解决方案LLM节点一直显示“Running”不结束网关未收到回调或回调URL不可达① 查网关日志是否有task completed记录② 用curl模拟回调curl -X POST http://flowable-webhook/llm/callback -d {taskId:xxx,status:SUCCESS}检查Flowable Webhook endpoint是否开启防火墙是否放行返回结果JSON格式错误但网关校验通过outputSchema未正确注入到BPMN网关使用了默认空schema① 查BPMN XML中outputSchema字段是否拼写正确② 查Adapter日志确认getStringField(execution, outputSchema)返回值在BPMN中用flowable:field显式声明不要依赖默认值同一prompt不同流程实例返回结果差异大temperature参数未设置模型随机性过高① 查网关审计日志确认temperature字段值② 对比两次调用的完整prompt在BPMN中固定temperature0.0或在PromptHub模板中内置temperature: 0.0流程卡在LLM节点但网关日志显示“SUCCESS”Adapter未正确解析网关返回的JSON反序列化失败① 查Adapter ERROR日志② 用Postman调用网关API看原始response检查LLMResult类的Jackson注解确保字段名与网关返回一致人工复核率突然飙升Prompt模板被误修改或新增业务字段未在schema中声明① 查PromptHub操作日志② 查网关校验失败日志提取高频错误字段建立变更审批流程所有schema修改需AI产品经理开发双签5.2 独家避坑技巧那些文档里不会写的细节技巧1用“哑变量”规避Flowable变量污染Flowable的setVariable会把所有变量存入数据库。如果LLM返回10MB的分析报告每次setVariable(llmResult, hugeJson)都会拖慢DB。我们的解法是只存关键摘要完整结果存OSS变量里只放OSS URL。Adapter代码String ossUrl ossService.upload(result.getFullReport()); execution.setVariable(llmResultUrl, ossUrl); // 存URL execution.setVariable(llmSummary, result.getSummary()); // 存摘要技巧2BPMN中的“伪变量”注入有时需要把静态值注入prompt比如companyName: XX保险公司。如果硬编码在模板里换客户就要改模板。我们用BPMN的flowable:field支持表达式flowable:field nameinput flowable:expression![CDATA[${jsonString({companyName:XX保险公司,product:车险})}]]/flowable:expression /flowable:field配合自定义jsonString方法安全地生成JSON字符串。技巧3网关的“影子模式”灰度上线新模型前不开流量只记录调用但不返回结果。在网关配置中llm: shadow-mode: enabled: true target-model: gpt-4-turbo-new这样所有请求都发给新模型但网关仍返回旧模型结果同时记录新旧模型输出对比。一周后分析差异再切全量。技巧4Flowable监听器的妙用在ExecutionListener中监听llmTask的end事件自动触发后续动作public class LLMDoneListener implements ExecutionListener { Override public void notify(DelegateExecution execution) { if (llmTask.equals(execution.getCurrentActivityId())) { // 自动发送钉钉通知 dingtalkService.sendAlert(execution.getProcessInstanceId(), LLM分析完成情绪 execution.getVariable(sentiment)); } } }这样流程图里不用画一堆通知节点逻辑更清爽。5.3 性能优化实战从3秒到800毫秒某次压测发现LLM节点P95耗时3.2秒远超SLA的1.5秒。我们逐层排查网关层发现Redis连接池耗尽redis.clients.jedis.JedisPool默认maxIdle8500并发下大量线程阻塞。调大到maxIdle100耗时降至2.1秒。模型层OpenAI的gpt-4-turbo在长文本时明显变慢。我们改用gpt-3.5-turbo-16k虽然能力稍弱但16k上下文处理更稳耗时降至1.4秒。Adapter层发现extractInputVariables方法遍历了所有127个流程变量其中90%是系统变量如act:assignee。优化为只提取以biz_开头的业务变量耗时降至800毫秒。最终我们形成了一套标准优化 checklist✅ 网关Redis连接池 ≥ 并发数×2✅ 模型选择优先考虑-16k版本而非-turbo除非需要最强能力✅ Adapter只处理业务变量过滤系统变量✅ Prompt模板启用stream: false禁用流式响应Flowable不支持这套方案已在金融、政务、电商三个行业落地支撑日均27万次LLM调用。它不追求炫技只解决一个朴素问题让工作流引擎真正拥有“思考”能力而且这种能力是可控的、可审计的、可进化的。在我经手的项目里最成功的不是技术多先进而是业务方能指着流程图说“看这个蓝色节点就是我们的AI专家。”——这才是LLM融入工作流的终极意义。