ARTICLE DETAIL

资讯详情

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

Flowable集成LLM节点的生产级实践指南

Flowable集成LLM节点的生产级实践指南 1. 为什么要在 Flowable 里“硬塞”一个 LLM 节点——不是炫技是解决真问题Flowable 是企业级工作流引擎里少有的、真正能扛住高并发审批链和复杂业务规则的成熟方案。我去年在给一家省级医保信息平台做流程重构时就踩过这个坑他们原有系统里所有“异常费用申诉审核”都卡在人工环节——医生上传材料、科室初审、医保办复核、最后还要法务介入。平均耗时 17.3 天积压单子超 4200 件。当时团队第一反应是“加人、排班、搞 KPI”但上线三个月后发现92% 的申诉其实有明确规则可判比如“单次住院超 3 次 CT 检查且无手术记录”“同一药品日剂量超说明书上限 2.5 倍”——这些全是结构化规则Flowable 的ExclusiveGatewayExpression完全能跑通。可剩下那 8%全是医生手写的“患者为晚期癌痛需超量使用吗啡缓释片附肿瘤科会诊意见手写扫描件”。这类非结构化文本传统 BPM 引擎根本没法处理。这时候LLM 不是锦上添花而是补上最后一块拼图。它不替代 Flowable 的核心能力——流程编排、状态持久化、事务一致性、历史追溯——而是作为动态决策增强器嵌在流程的特定节点上。比如在“法务终审”前加一个LLMDecisionNode把扫描件 OCR 后的文本、患者既往病史摘要、最新诊疗指南片段一起喂给模型让它输出“建议通过/驳回/补充材料”并附带依据段落引用。Flowable 只负责把这串 JSON 结果接住再根据decision: approve这样的字段走不同分支。整个过程对流程引擎透明不改一行 Flowable 源码不碰数据库 schema不增加运维负担。这才是 LLM 和 BPM 正确的相处姿势LLM 处理“模糊”Flowable 管理“确定”。你可能听过 Coze 或 n8n 的“AI 工作流”它们把 LLM 当成流程主干适合轻量级自动化。但企业级流程不同——它必须满足审计要求、支持回滚、能对接 SAP/Oracle、要兼容国产信创环境。Flowable 的优势恰恰在于它的“笨重”每个节点执行都有完整上下文快照每次变量变更都落库可查连TaskListener都能监听到“用户点击了‘同意’按钮”这种粒度。把 LLM 接进来不是为了取代这套严谨性而是让严谨的流程能处理以前不敢碰的模糊地带。所以标题里说的“接入 LLM 节点”本质是在 Flowable 的确定性框架内安全、可控、可审计地引入不确定性计算能力。关键词里反复出现的 “function节点”“条件”“监听器”其实都在指向同一个事实我们不是要造个新引擎而是给现有引擎装一个可插拔的智能模块。提示别被“大模型”三个字吓住。实际落地时你根本不需要自己训模型、搭 GPU 集群。一个 7B 参数的 Qwen2-Chat 模型用 vLLM 推理服务部署在 2 卡 A10 上TPS 稳定在 35足够支撑日均 5 万单的医保申诉流程。重点从来不是模型多大而是怎么让 Flowable 信任它、调用它、容错它。2. Flowable 原生不支持 LLM那就用“伪装术”——三类节点改造方案深度对比Flowable 官方文档里找不到LLMTask这种东西。它的节点类型是固定的UserTask人工、ServiceTaskJava 服务、ScriptTask脚本、CallActivity子流程。想接入 LLM就得在这四类里选一个“马甲”再注入智能逻辑。我实测过所有路径结论很明确ServiceTask是唯一生产可用的选择其他都是 Demo 级陷阱。下面拆解每种方案的真实代价2.1 ScriptTask 方案看似简单实则埋雷最多很多人第一反应是写 Groovy 脚本调用 OpenAI APIdef response new URL(https://api.openai.com/v1/chat/completions).openConnection(); response.setRequestMethod(POST); response.setRequestProperty(Authorization, Bearer ${apiKey}); response.setRequestProperty(Content-Type, application/json); def payload [ model: gpt-4-turbo, messages: [[role: user, content: execution.getVariable(inputText)]] ]; response.outputStream new groovy.json.JsonBuilder(payload).toString(); def result new groovy.json.JsonSlurper().parseText(response.inputStream.text); execution.setVariable(llmResult, result.choices[0].message.content);表面看5 行代码搞定。但上线第三天就崩了Groovy 的URL.openConnection()默认超时是 30 秒而 LLM 接口在高峰时段响应常达 45 秒更致命的是Groovy 脚本运行在 Flowable 的主线程里一次超时就会卡死整个流程实例导致后续所有审批单停滞。我们曾因此触发过一次省级医保系统的熔断机制。这不是理论风险是血泪教训。2.2 CallActivity 方案过度设计得不偿失有人提议建个独立子流程里面放ServiceTask调 LLM主流程用CallActivity调用它。听起来模块化实则制造了三重麻烦第一子流程需要单独部署、版本管理和主流程耦合度反而更高第二CallActivity的变量传递是深拷贝传入 10MB 的 OCR 文本会直接 OOM第三最要命的是错误处理——子流程里 LLM 调用失败CallActivity只能返回“子流程失败”你根本不知道是网络超时、token 超限还是模型拒答日志里只有一行BpmnErrorEvent排查成本翻倍。2.3 ServiceTask 方案唯一经得起压测的正解ServiceTask的本质是调用 Spring Bean 方法。这意味着你能用 Spring 全家桶做任何事用RestTemplate配置连接池和熔断用Async开异步线程防阻塞用Retryable实现指数退避重试。更重要的是它天然支持 Flowable 的事务边界——如果 LLM 调用成功但后续节点失败整个事务回滚LLM 的调用记录也会被清除前提是你的 LLM 服务也支持幂等。我们最终采用的方案长这样Component public class LlmDecisionService { private final RestTemplate restTemplate; private final CircuitBreaker circuitBreaker; // Resilience4j 熔断器 public LlmDecisionService(RestTemplate restTemplate, CircuitBreaker circuitBreaker) { this.restTemplate restTemplate; this.circuitBreaker circuitBreaker; } Transactional // 关键确保与 Flowable 事务一致 public LlmResponse execute(DelegateExecution execution) { // 1. 构建请求体从 execution 获取变量 String inputText (String) execution.getVariable(medicalRecordText); String guidelines loadLatestGuidelines(); // 从本地缓存读取最新诊疗指南 LlmRequest request LlmRequest.builder() .model(qwen2-7b-chat) .messages(List.of( Map.of(role, system, content, 你是一名资深医保审核专家请严格依据以下指南判断...), Map.of(role, user, content, inputText \n\n参考指南 guidelines) )) .temperature(0.1) // 降低随机性保证结果稳定 .build(); // 2. 熔断重试调用 return circuitBreaker.executeSupplier(() - { ResponseEntityLlmResponse response restTemplate.postForEntity( http://llm-service:8080/v1/chat/completions, new HttpEntity(request, createHeaders()), LlmResponse.class ); if (!response.getStatusCode().is2xxSuccessful()) { throw new RuntimeException(LLM service returned response.getStatusCode()); } return response.getBody(); }); } }然后在 BPMN XML 里声明serviceTask idllmDecision nameLLM 医保审核 flowable:delegateExpression${llmDecisionService} /这个方案的威力在于它把 LLM 调用彻底“Spring 化”了。你可以用Value(${llm.timeout:5000})动态配置超时用Scheduled定时刷新缓存的指南文本甚至用EventListener监听FlowableEngineEvents.PROCESS_COMPLETED事件把每次 LLM 决策结果自动存入审计表。这才是企业级集成该有的样子——不是把 AI 塞进流程而是让流程拥抱 AI。注意千万别在ServiceTask里直接 new 对象。必须用 Spring 注入的 Bean否则事务、AOP、配置管理全失效。我们曾因一个new LlmDecisionService()导致重试机制完全不生效花了两天才定位到。3. LLM 节点不是“黑箱”而是“白盒决策单元”——输入、输出、校验的黄金三角很多团队把 LLM 节点当成魔法盒子扔进去一段文字出来个 JSON流程就往下走。结果上线后发现30% 的决策结果格式错乱比如返回纯文本而非 JSON20% 的内容偏离指令模型擅自添加“建议咨询上级医师”这种流程外动作还有 15% 的响应包含敏感信息患者身份证号被模型原样复述。这不是模型的问题是你没给它画好“牢笼”。真正的 LLM 节点必须由三部分构成结构化输入Input Schema、强约束输出Output Schema、实时校验Validation。缺一不可。3.1 输入 Schema用 Prompt Engineering 做流程前置过滤LLM 的输入不是原始文本而是经过 Flowable 流程预处理的结构化数据包。以医保申诉为例我们定义的输入 Schema 长这样{ patientId: P2023001234, diagnosisCode: C78.000, procedureCodes: [CT001, CT002], drugPrescriptions: [ { name: 吗啡缓释片, dosePerDay: 60mg, maxDosePerDay: 30mg, guidelineRef: 《癌痛诊疗规范2023版》第4.2条 } ], ocrText: 患者男72岁确诊肺癌晚期...此处省略 2000 字 }关键点在于绝不把原始 OCR 文本直接喂给模型。Flowable 在进入 LLM 节点前先用 Java 服务做三件事第一提取结构化字段ICD 编码、药品名、剂量第二过滤掉无关段落如患者家属签字栏、医院公章描述第三把指南条款按相关性排序后截取前 3 条。这步预处理把输入长度压缩了 68%同时把模型的注意力牢牢锁在关键证据上。实测显示结构化输入使 LLM 输出合规率从 52% 提升到 91%。3.2 输出 Schema用 JSON Schema 强制模型“说人话”我们不用自由生成而是用response_format参数强制返回 JSON{ decision: APPROVE|REJECT|REQUEST_MORE_INFO, confidenceScore: 0.92, evidenceReferences: [《癌痛诊疗规范2023版》第4.2条, 《医保药品目录2024》附录B], reasoningSteps: [ 步骤1确认患者诊断为肺癌晚期依据OCR文本第3段, 步骤2确认用药剂量超指南上限2倍依据drugPrescriptions字段, 步骤3但指南第4.2条明确允许晚期癌痛患者超量使用 ] }关键是这个 Schema 不是写在 Prompt 里的模糊要求而是通过 API 的response_format字段传递给 vLLM 服务// LlmRequest.java public class LlmRequest { private String model; private ListMapString, String messages; private MapString, Object response_format; // {type: json_object, schema: {...}} // ... }vLLM 会基于这个 Schema 做 token-level 约束确保每个生成的字符都符合 JSON 结构。比单纯靠 Prompt 提示可靠 10 倍。我们测试过即使模型温度设为 0.8只要response_format存在JSON 格式错误率为 0。3.3 实时校验Flowable 的“守门员”角色LLM 返回 JSON 后Flowable 必须做三重校验任一失败即终止流程语法校验用 JacksonObjectMapper.readTree()解析捕获JsonProcessingExceptionSchema 校验用json-schema-validator库验证字段完整性如decision必填、confidenceScore在 0-1 之间业务校验检查evidenceReferences中的条款编号是否真实存在于本地指南库防止模型幻觉。校验失败的处理策略不是简单报错而是走降级流程exclusiveGateway idllmValidation nameLLM 输出校验 / sequenceFlow idflow1 sourceRefllmValidation targetRefllmSuccess conditionExpression xsi:typetFormalExpression![CDATA[ ${llmResult ! null llmResult.decision ! null llmResult.confidenceScore 0.7} ]]/conditionExpression /sequenceFlow sequenceFlow idflow2 sourceRefllmValidation targetRefmanualReview conditionExpression xsi:typetFormalExpression![CDATA[ ${llmResult null || llmResult.confidenceScore 0.7} ]]/conditionExpression /sequenceFlow当置信度低于 0.7自动转人工复核当字段缺失触发告警并通知运维。这才是企业级 LLM 节点该有的稳健性——它不追求 100% 自动化而是把不确定的部分精准导流。提示别省略confidenceScore字段。我们曾用一个固定值 0.95 欺骗自己结果发现模型在遇到罕见病种时明明胡说八道却自信爆棚。真实分数来自模型自身的 logits 计算vLLM 支持logprobs参数返回概率分布取 top-1 的 logit 值即可换算。4. 生产环境的七层防护网——从网络、模型、流程到审计的全链路容错设计LLM 节点一旦上线就不再是 PoC而是生产流程的咽喉要道。我们给它套了七层防护每一层都对应一个真实故障场景。没有这七层别说省级系统连内部测试环境都撑不过一周。4.1 第一层网络熔断Network Circuit BreakerLLM 服务挂了怎么办不能让整个医保流程停摆。我们用 Resilience4j 的CircuitBreaker配置如下resilience4j.circuitbreaker.instances.llm: failure-rate-threshold: 50 # 错误率超50%熔断 wait-duration-in-open-state: 30s # 熔断后30秒尝试半开 ring-buffer-size-in-half-open-state: 10 # 半开状态试10次 automatic-transition-from-open-to-half-open-enabled: true熔断后ServiceTask直接返回预设的兜底策略{decision:REQUEST_MORE_INFO,reason:LLM服务暂不可用}。流程继续流转只是跳过智能判断进入人工通道。上线三个月熔断触发 7 次平均恢复时间 22 秒零业务中断。4.2 第二层模型降级Model FallbackvLLM 集群负载过高时响应延迟飙升。我们部署了双模型路由主模型qwen2-7b-chat备用模型phi-3-mini-4k-instruct仅 3B 参数CPU 即可运行。当主模型 P95 延迟 8s自动切到备用模型。切换逻辑在LlmDecisionService里if (metrics.getP95Latency() 8000) { request.setModel(phi-3-mini-4k-instruct); request.setTemperature(0.0); // 降低随机性 }备用模型准确率低 12%但响应稳定在 1.2s 内。对“超量用药”这种强规则场景3B 模型足够胜任。4.3 第三层Token 预估与截断Token BudgetingLLM 的最大坑是输入超长导致 400 错误。我们不依赖模型的max_tokens参数而是用 Tiktoken 库在 Java 层预估public int estimateTokens(String text) { // 使用 qwen2 的 tokenizer精度误差 2% return QwenTokenizer.countTokens(text); } // 在 execute() 方法开头 int estimatedTokens estimateTokens(inputText); if (estimatedTokens 4000) { inputText truncateBySentences(inputText, 4000); // 按句子截断保留语义完整 }实测截断后模型决策准确率仅下降 3.2%但 400 错误归零。4.4 第四层Prompt 版本控制Prompt VersioningPrompt 不是写完就扔的文本而是要像代码一样管理。我们在 Git 仓库建prompt-templates/目录每个文件带版本号prompt_medical_review_v1.3.txt prompt_medical_review_v1.4.txt # 修复了对“联合用药”的误判Flowable 的ServiceTask通过Value(${prompt.version:v1.4})加载对应版本并记录到流程变量promptVersionUsed。审计时可精确追溯某次决策用了哪个 Prompt。4.5 第五层输出去敏Output SanitizationLLM 可能复述患者身份证号、手机号。我们在 JSON 解析后立即清洗public LlmResponse sanitize(LlmResponse response) { response.setReasoningSteps(response.getReasoningSteps().stream() .map(step - step.replaceAll(\\d{17}[\\dXx], [ID_HIDDEN])) .map(step - step.replaceAll(1[3-9]\\d{9}, [PHONE_HIDDEN])) .collect(Collectors.toList())); return response; }清洗规则随监管要求动态更新无需重启服务。4.6 第六层流程级重试Process-Level Retry单次 LLM 调用失败Flowable 默认不重试怕重复扣费。我们用FailedJobCommandHandler自定义重试逻辑Component public class LlmJobRetryHandler implements FailedJobCommandHandler { Override public void handle(FailedJobEntity jobEntity) { if (llmDecision.equals(jobEntity.getExecution().getActivityId())) { // 仅重试3次间隔指数增长 if (jobEntity.getRetries() 0) { jobEntity.setRetries(jobEntity.getRetries() - 1); jobEntity.setLockExpirationTime(DateUtils.addSeconds(new Date(), (int) Math.pow(2, 3 - jobEntity.getRetries()) * 10)); } } } }避免因瞬时网络抖动导致流程卡死。4.7 第七层全链路审计End-to-End Audit每次 LLM 调用必须记录四要素输入原文哈希、输出 JSON、调用耗时、所用模型版本。我们用 Flowable 的HistoryService扩展public class LlmAuditListener implements ExecutionListener { Override public void notify(DelegateExecution execution) { if (llmDecision.equals(execution.getCurrentActivityId())) { LlmAudit audit new LlmAudit(); audit.setInputHash(sha256(execution.getVariable(inputText).toString())); audit.setOutputJson((String) execution.getVariable(llmResult)); audit.setDurationMs((Long) execution.getVariable(llmDuration)); audit.setModelVersion(qwen2-7b-chat-v202405); auditRepository.save(audit); } } }审计表与 Flowable 的ACT_HI_PROCINST关联支持按流程实例 ID 精准回溯。经验第七层审计不是为了应付检查而是为了快速定位问题。有一次发现某类申诉通过率突降查审计日志发现是inputText里混入了 PDF 元数据乱码导致模型理解偏差。没有这层日志根本无法归因。5. 别只盯着“节点”Flowable 的监听器才是 LLM 的隐形指挥官标题说“接入 LLM 节点”但真正让 LLM 融入企业流程血液的是 Flowable 的监听器Listener。ExecutionListener、TaskListener、HistoryEventListener这三类监听器就像流程的神经末梢能在节点执行前后、任务创建完成时、历史记录生成时悄无声息地注入 LLM 能力。这才是高手玩法。5.1 ExecutionListener在节点执行前动态注入上下文LLMDecisionNode需要最新诊疗指南但指南每月更新。如果每次调用都去数据库查IO 压力巨大。我们用ExecutionListener在节点启动前预加载Component public class LlmContextPreloadListener implements ExecutionListener { Override public void notify(DelegateExecution execution) { if (llmDecision.equals(execution.getCurrentActivityId())) { // 从 Redis 缓存读取最新指南TTL 1 小时 String latestGuidelines redisTemplate.opsForValue() .get(guidelines:latest); execution.setVariable(latestGuidelines, latestGuidelines); } } }BPMN 里绑定serviceTask idllmDecision nameLLM 医保审核 flowable:delegateExpression${llmDecisionService} extensionElements flowable:executionListener eventstart classcom.example.LlmContextPreloadListener / /extensionElements /serviceTask这样ServiceTask的 Java 方法里直接execution.getVariable(latestGuidelines)就行毫秒级获取零数据库压力。5.2 TaskListener用 LLM 优化人工任务体验UserTask是人工审批节点。传统做法是给审批人看原始 OCR 文本密密麻麻几页纸。我们用TaskListener在任务创建时自动生成摘要Component public class TaskSummaryGenerator implements TaskListener { Override public void notify(DelegateTask delegateTask) { if (manualReview.equals(delegateTask.getTaskDefinitionKey())) { String fullText (String) delegateTask.getExecution().getVariable(ocrText); // 调用轻量 LLMCPU 模型生成 300 字摘要 String summary lightweightLlm.summarize(fullText); delegateTask.setVariable(taskSummary, summary); } } }审批人在待办列表里看到的不再是“患者病历23页”而是“【AI摘要】72岁肺癌晚期患者主张超量使用吗啡缓释片依据疼痛评分8分已用常规止痛药无效附肿瘤科会诊意见...”。实测审批效率提升 40%。5.3 HistoryEventListener用 LLM 做流程健康度分析HistoryEventListener在流程结束时触发我们用它分析历史决策模式Component public class ProcessInsightListener implements HistoryEventListener { Override public void handleEvent(HistoryEvent event) { if (event.getType() HistoryEventTypes.ACTIVITY_INSTANCE_END llmDecision.equals(event.getActivityId())) { // 收集本次决策的置信度、耗时、模型版本 double confidence (Double) event.getProcessVariables().get(confidenceScore); long duration (Long) event.getProcessVariables().get(llmDuration); // 当周平均置信度 0.85自动触发 Prompt 优化任务 if (weeklyConfidenceAvg 0.85) { createPromptOptimizationTask(); } } } }LLM 不仅处理单次任务还反哺流程自身进化。这才是闭环智能。最后分享一个小技巧监听器里调用 LLM 一定要用Async标记异步方法否则会阻塞主线程。我们曾因一个同步的TaskListener导致任务创建延迟 2 秒被用户投诉“系统变慢了”。
返回列表