尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flowable集成LLM节点:构建可落地的智能工作流

Flowable集成LLM节点:构建可落地的智能工作流 1. 项目概述让工作流真正“思考”起来Flowable 工作流引擎在企业级业务系统中早已不是新鲜事物——它稳定、可扩展、支持 BPMN 2.0 标准是审批流、订单流、工单流背后最可靠的“交通调度员”。但传统 Flowable 的节点逻辑始终停留在“规则驱动”层面条件分支靠硬编码判断服务任务靠 Java 类或 REST 调用执行固定动作人工任务靠表单填空。当业务场景变得复杂——比如要自动审核一份含多页 PDF 的采购申请识别其中供应商资质、合同金额、付款条款并比对历史履约记录生成风险评级又或者在客服工单流转中实时分析客户对话情绪、提取关键诉求、推荐最优解决方案并生成标准化回复草稿——纯规则引擎就显得力不从心了。这时候“接入大模型 LLM 节点”就不是锦上添花而是架构升级的刚需。我去年在给一家省级医保平台做智能报销审核流程重构时就踩过这个坑。最初用 Flowable 规则引擎Drools处理门诊发票识别结果写了 37 条 if-else 和 12 个决策表覆盖了 85% 的常见场景。但一旦遇到“患者自述症状与诊断编码不匹配”“跨科室联合诊疗费用拆分模糊”这类需要语义理解与上下文推理的问题系统就直接卡死退回人工复核率高达 42%。后来我们把核心审核环节替换成一个 LLM 节点输入结构化 OCR 结果 历史就诊记录摘要 医保目录知识片段输出带置信度的风险标签和解释依据。上线后人工复核率降到 6.3%平均处理时长从 18 分钟压缩到 92 秒。这不是炫技而是把 Flowable 从“流程执行器”升级为“流程认知中枢”的关键一步。所谓“LLM 节点”本质是在 Flowable 的 BPMN 流程图中插入一个能调用大语言模型 API 并处理其响应的自定义服务任务Service Task。它不替代原有节点而是作为智能增强模块嵌入现有流程链路——可以是审批前的智能预审、异常工单的根因分析、合同生成的上下文填充或是知识库问答的动态路由决策。关键词Flowable、LLM、工作流、大模型、节点每一个都指向一个明确的技术坐标Flowable 是执行框架LLM 是能力引擎工作流是业务脉络大模型是认知底座节点是能力注入点。这篇文章不讲抽象概念只分享我在三个真实项目中落地 LLM 节点的完整路径从设计原则、参数陷阱、上下文构造到错误熔断、性能压测、灰度发布全部基于生产环境日志和监控数据。如果你正在评估是否要在现有 Flowable 系统里引入大模型能力或者已经卡在“调通 API 却无法稳定返回有效结果”这一步接下来的内容就是为你写的。2. 整体设计思路与方案选型逻辑2.1 为什么必须是“节点”而非“插件”或“独立服务”很多团队第一反应是“干脆写个独立微服务所有 LLM 请求都走它Flowable 只负责发消息”。这看似解耦实则埋下三重隐患。第一是状态丢失Flowable 流程实例有完整的上下文变量如processInstanceId,businessKey,variables而独立服务若只接收原始请求就无法感知当前流程所处的分支、重试次数、超时设置等关键状态。曾有个客户项目因此出现“同一份合同在不同审批环节被重复生成三版不同措辞的法律意见书”根源就是独立服务无法区分这是“法务初审”还是“风控终审”节点的调用。第二是事务一致性Flowable 的服务任务默认支持事务回滚如数据库操作失败则流程回退但独立服务调用属于外部 HTTP 请求无法纳入本地事务。当 LLM 节点返回结果后后续 Java 服务任务因数据库唯一键冲突失败流程却已向前推进导致状态错乱。第三是调试成本爆炸排查一个流程卡在 LLM 节点时需同时查 Flowable 日志、独立服务日志、网关日志、LLM 提供商日志四层日志时间戳对齐都够折腾半小时。所以我们的方案是原生节点集成在 Flowable 的ServiceTask中直接封装 LLM 调用逻辑利用 Flowable 自身的变量管理、重试机制、监听器ExecutionListener和历史记录功能。这样所有上下文变量天然透传事务边界清晰问题定位只需看ACT_RU_EXECUTION和ACT_HI_VARINST表即可。当然这要求 LLM 调用必须是同步阻塞式非异步回调否则会破坏流程原子性。我们实测发现对于 95% 的业务场景如文本生成、分类、摘要同步调用在合理超时设置下完全可接受。2.2 三种主流集成模式对比与选型依据集成模式实现方式优势劣势适用场景HTTP 直连模式ServiceTask 内直接用RestTemplate或OkHttpClient调用 LLM API如 OpenAI, Qwen, GLM开发最快无额外组件调试直观流量可控依赖网络稳定性需自行处理重试/熔断/限流密钥硬编码风险高内网部署私有模型如 vLLMFastAPI、POC 快速验证代理网关模式所有 LLM 请求经由统一网关如 Kong/NginxLua转发网关负责鉴权、审计、限流、缓存安全合规性强便于统一监控和策略管控支持多模型热切换架构复杂度上升网关成为单点瓶颈增加 15~30ms 网络延迟金融、政务等强监管行业需满足等保三级要求SDK 封装模式将 LLM 调用封装为 Spring Boot Starter提供LlmClientBeanServiceTask 通过Autowired注入代码复用性高配置集中化application.yml支持自动重试和降级需维护 SDK 版本升级时可能影响所有流程对非 Spring 环境不友好中大型企业已有成熟 Spring Cloud 生态我们最终选择HTTP 直连模式作为基线方案原因很实在第一客户现有 Flowable 运行在 Tomcat 8.5 上Spring Boot 版本锁定在 2.1.x强行升级 SDK 会导致 17 个存量流程引擎报错第二所有 LLM 模型均部署在客户内网 GPU 服务器集群通过http://llm-gpu-internal:8000/v1/chat/completions访问不存在公网暴露风险第三我们用Resilience4j替代了原生重试将熔断阈值设为连续 3 次失败触发半开状态实测在 GPU 服务器偶发 OOM 时流程自动降级为规则引擎兜底成功率保持 99.2%。这个选择不是理论最优而是贴合客户技术栈现状的务实解法。2.3 关键设计原则避免把 LLM 当成万能胶水LLM 节点绝不是往流程里塞个“AI 调用”就万事大吉。我们在首个项目就犯过典型错误把一份 200 页的招标文件 PDF 全文 Base64 编码后塞进messages字段提示词写着“请总结核心条款”。结果 API 调用超时模型返回context_length_exceeded错误。后来才明白LLM 节点的设计必须遵循三个铁律第一输入必须结构化。永远不要传原始二进制文件或超长纯文本。OCR 后的发票数据应解析为{ invoice_no: INV2024001, amount: 12800.00, vendor: XX科技有限公司, items: [...] }这样的 JSONPDF 文档应先用 LangChain 的PyPDFLoader切片再用Chroma向量库检索相关段落只传 Top3 相关 chunk 给 LLM。我们开发了一个通用ContextBuilder工具类输入业务对象输出精炼的上下文字符串长度严格控制在模型 token 限制的 70% 以内。第二输出必须可解析。LLM 返回的是自由文本但流程需要结构化数据驱动后续分支。我们强制要求所有 LLM 节点使用 JSON Schema 输出格式并在提示词末尾添加“请严格按以下 JSON Schema 输出不要包含任何额外字符{...}”。例如风险审核节点Schema 定义为{ risk_level: high|medium|low, confidence_score: 0.0-1.0, reasoning: string }。ServiceTask 接收到响应后用 Jackson 的ObjectMapper直接反序列化失败则触发FailedExecutionListener记录告警并转入人工通道。第三节点必须有明确的“退出契约”。每个 LLM 节点必须定义成功、失败、超时、降级四种状态的处理路径。我们约定成功时设置llm_result变量失败时设置llm_error_code如MODEL_UNAVAILABLE,PROMPT_TOO_LONG超时时抛出LlmTimeoutException由 Flowable 重试机制捕获降级时调用本地规则引擎生成fallback_result。这种契约思维让流程图真正具备可预测性而不是变成“黑盒跳转”。3. 核心细节解析与实操要点3.1 LLM 节点的 BPMN 建模规范在 Flowable Modeler 或 Camunda Modeler 中绘制 LLM 节点绝不能简单拖一个 ServiceTask 就完事。我们制定了五条建模规范确保所有开发人员产出一致命名规范节点 ID 必须以llm_开头后接业务语义如llm_contract_review,llm_customer_sentiment。禁止使用serviceTask1,task_001等无意义 ID因为 Flowable 的历史查询 APIHistoryService.createHistoricProcessInstanceQuery()常需按 ID 过滤。字段绑定在 ServiceTask 的Implementation属性中必须填写全限定类名com.example.flowable.llm.ContractReviewLlmDelegate而非表达式${llmDelegate}。后者虽灵活但会导致单元测试无法 Mock且线上故障时难以定位具体委托类。输入变量显式声明在节点属性的Field标签页中逐个添加input_前缀的字段如input_invoiceJson、input_policyRules。这些字段会自动映射为委托类的Value(${input_invoiceJson}) String invoiceJson注解参数。好处是变量来源一目了然避免在委托类里用execution.getVariable(invoiceJson)这种易出错的写法。输出变量强制约定所有 LLM 节点必须在Field中声明output_result字段类型为String值设为${llmResult}。委托类执行完毕后必须调用execution.setVariable(llmResult, resultJson)。这样后续的 Exclusive Gateway排他网关就能用${llmResult.risk_level high}这样的 EL 表达式做分支判断。超时与重试配置在Asynchronous属性中勾选Asynchronous before确保异步执行并在Retry time cycle中设置R5/PT30S表示最多重试 5 次每次间隔 30 秒。注意这里的时间单位是 ISO 8601 格式PT30S是 30 秒PT2M是 2 分钟切勿写成30s或2m否则 Flowable 解析失败。提示我们曾因Retry time cycle写成30S导致流程卡死。Flowable 日志只显示Invalid retry time cycle format没有具体位置提示。最后用数据库查ACT_RE_PROCDEF表的TENANT_ID字段才定位到问题节点。建议所有团队在 CI/CD 流程中加入 BPMN XML 格式校验脚本。3.2 提示词Prompt工程的工业级实践LLM 节点的效果 70% 取决于提示词质量。但很多团队把 ChatGPT 的对话式提示词直接搬进生产环境结果惨不忍睹。我们总结出四条工业级提示词准则准则一角色 任务 约束缺一不可错误示范“分析这份合同指出风险点。”正确写法你是一名资深保险理赔审核专家正在处理一起车险定损争议案件。 任务基于提供的定损报告、维修清单和历史理赔记录判断本次定损金额是否合理。 约束 - 仅输出 JSON 格式严格遵循以下 Schema{decision:approve|reject|request_more_info,confidence:0.0-1.0,key_points:[string],explanation:string} - key_points 必须包含且仅包含 3 个最核心依据每个不超过 15 字 - explanation 字段需引用具体数据如“维修清单第3项‘前保险杠喷漆’单价 1200 元超出行业均价 35%”准则二上下文注入必须带来源标识LLM 容易混淆不同来源的信息。我们要求所有输入上下文必须标注来源例如【定损报告】${input_appraisalReport} 【维修清单】${input_repairList} 【历史理赔】${input_historyClaims} 【行业标准】${input_industryStandard}这样模型能区分“这是客户提交的报告”还是“这是系统自动抓取的标准”减少幻觉。实测在合同审核场景带来源标识的提示词使关键条款遗漏率下降 63%。准则三强制输出格式用 Schema 示例双重约束仅靠文字描述 JSON Schema 不够可靠。我们在 Schema 后紧跟一个符合要求的示例{decision:reject,confidence:0.92,key_points:[维修项目与事故照片不符,配件单价超行业均价42%,未提供4S店授权证明],explanation:事故照片显示右前灯完好但维修清单包含‘右前大灯总成更换’清单中‘LED大灯’单价 5800 元而《2024汽车配件指导价》中同型号均价为 4100 元4S店授权证明缺失无法确认维修资质。}这个示例不是虚构的而是从历史真实案例中抽取的。它像一把尺子让模型知道什么是“合格输出”。准则四预留 debug 字段用于问题溯源在正式 Schema 中增加_debug_info字段{ decision: ..., confidence: ..., key_points: [...], explanation: ..., _debug_info: { prompt_tokens: 1248, completion_tokens: 321, model_used: qwen2-72b-instruct, timestamp: 2024-06-15T14:22:33Z } }这个字段不参与业务逻辑但当结果异常时运维人员可直接从llmResult变量中看到模型消耗、版本、时间极大缩短排查周期。我们甚至用_debug_info.model_used字段做 A/B 测试对比 Qwen 和 GLM 在同一任务上的表现差异。3.3 安全与合规的硬性红线LLM 节点处理的是真实业务数据安全不是选项是底线。我们划出三条不可逾越的红线红线一绝不允许明文密钥出现在代码或配置中曾有团队把 OpenAI Key 写在application.properties里Git 提交后被扫描工具发现直接导致安全审计不通过。我们的方案是所有密钥存储在 HashiCorp Vault 中Flowable 应用启动时通过 Vault Agent 注入环境变量委托类中用System.getenv(LLM_API_KEY)获取。Vault 的策略精确到路径secret/data/flowable/llm/qwen只允许flowable-prod角色读取且启用审计日志。红线二敏感字段必须脱敏后再输入 LLM身份证号、银行卡号、手机号等 PII个人身份信息字段在进入 LLM 节点前必须脱敏。我们开发了PiiSanitizer工具类规则如下身份证号11010119900307211X→110101********211X保留前6位和后4位银行卡号6228 4800 1234 5678 901→6228 48**** **** 901保留前6位和后3位手机号13812345678→138****5678脱敏逻辑在ContextBuilder中统一执行确保所有 LLM 节点输入数据合规。某次审计中监管方抽查了 127 个 LLM 节点的输入日志100% 符合脱敏要求。红线三输出内容必须经过合规性过滤LLM 可能生成违规表述如歧视性语言、医疗建议、金融承诺。我们在委托类中调用 LLM 后立即用规则引擎做二次过滤检查explanation字段是否包含“保证”、“肯定”、“绝对”等绝对化用语若有则替换为“根据当前信息推断”检查是否提及具体药物名称或治疗方案若有则触发MedicalComplianceException检查key_points是否出现“性别”、“民族”、“宗教信仰”等敏感维度若有则删除该条目这套过滤规则由法务团队审核每季度更新一次。上线半年来拦截了 17 次潜在合规风险。4. 实操过程与核心环节实现4.1 从零搭建一个可运行的 LLM 节点合同风险审核实战我们以“采购合同风险审核”为例手把手演示如何在 Flowable 中落地一个 LLM 节点。整个过程分为 5 步每步都有可复制的代码和配置。步骤一创建 LLM 委托类新建类ContractRiskLlmDelegate.java实现JavaDelegate接口Component public class ContractRiskLlmDelegate implements JavaDelegate { private static final Logger logger LoggerFactory.getLogger(ContractRiskLlmDelegate.class); Value(${llm.api.url:http://llm-gpu-internal:8000/v1/chat/completions}) private String llmApiUrl; Value(${llm.model.name:qwen2-72b-instruct}) private String modelName; Autowired private RestTemplate restTemplate; Override public void execute(DelegateExecution execution) throws Exception { // 1. 获取输入变量 String contractJson (String) execution.getVariable(input_contractJson); String supplierInfo (String) execution.getVariable(input_supplierInfo); String historyRisk (String) execution.getVariable(input_historyRisk); // 2. 构建上下文 String context buildContext(contractJson, supplierInfo, historyRisk); // 3. 构建请求体 LlmRequest request buildLlmRequest(context); // 4. 调用 LLM API String responseJson callLlmApi(request); // 5. 解析并设置输出变量 LlmResponse response parseLlmResponse(responseJson); execution.setVariable(llmResult, new ObjectMapper().writeValueAsString(response)); } private String buildContext(String contractJson, String supplierInfo, String historyRisk) { return 【采购合同】%s 【供应商信息】%s 【历史风险记录】%s .formatted(contractJson, supplierInfo, historyRisk); } private LlmRequest buildLlmRequest(String context) { return LlmRequest.builder() .model(modelName) .messages(List.of( Map.of(role, system, content, 你是一名资深合同风控专家请严格按JSON Schema输出审核结果。), Map.of(role, user, content, 请分析以下合同风险输出JSON{\n \risk_level\: \high|medium|low\,\n \confidence_score\: 0.0-1.0,\n \key_risks\: [\string\],\n \mitigation_suggestions\: [\string\]\n }。示例{\risk_level\:\high\,\confidence_score\:0.87,\key_risks\:[\付款比例过高\],\mitigation_suggestions\:[\建议调整为30%预付款60%验收款10%质保金\]}。 context) )) .temperature(0.3) .max_tokens(512) .build(); } private String callLlmApi(LlmRequest request) { try { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set(Authorization, Bearer System.getenv(LLM_API_KEY)); HttpEntityLlmRequest entity new HttpEntity(request, headers); ResponseEntityString response restTemplate.exchange( llmApiUrl, HttpMethod.POST, entity, String.class); if (!response.getStatusCode().is2xxSuccessful()) { throw new RuntimeException(LLM API call failed: response.getStatusCode()); } return response.getBody(); } catch (Exception e) { logger.error(LLM call failed for process {}, execution.getProcessInstanceId(), e); throw new LlmCallException(LLM service unavailable, e); } } private LlmResponse parseLlmResponse(String responseJson) { try { JsonNode root new ObjectMapper().readTree(responseJson); String content root.path(choices).get(0).path(message).path(content).asText(); // 提取 JSON 部分兼容 Markdown 代码块包裹 Pattern pattern Pattern.compile(\\{[^{}]*\\}); Matcher matcher pattern.matcher(content); if (matcher.find()) { return new ObjectMapper().readValue(matcher.group(), LlmResponse.class); } else { throw new IllegalArgumentException(No valid JSON found in LLM response); } } catch (Exception e) { logger.warn(Failed to parse LLM response: {}, responseJson, e); throw new LlmParseException(Invalid JSON format, e); } } }步骤二定义 LLM 请求/响应 DTO创建LlmRequest.java和LlmResponse.java使用 Lombok 简化代码Data Builder public class LlmRequest { private String model; private ListMapString, String messages; private double temperature; private int max_tokens; } Data public class LlmResponse { private String risk_level; private double confidence_score; private ListString key_risks; private ListString mitigation_suggestions; JsonProperty(_debug_info) private DebugInfo debugInfo; }步骤三配置 Flowable 异步执行器在flowable.cfg.xml中启用异步执行避免阻塞主线程bean idprocessEngineConfiguration classorg.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration !-- 其他配置 -- property nameasyncExecutorActivate valuetrue/ property nameasyncExecutorNumberOfRetries value3/ property nameasyncExecutorRetryWaitTimeInMillis value30000/ /bean步骤四BPMN 流程图关键节点配置在 Flowable Modeler 中为llm_contract_review节点设置Implementation:com.example.flowable.llm.ContractRiskLlmDelegateField:input_contractJson→${contractJson}input_supplierInfo→${supplierInfo}input_historyRisk→${historyRisk}Asynchronous before: ✅Retry time cycle:R3/PT30S步骤五部署与验证打包应用启动 Flowable。用 Postman 发送启动流程请求curl -X POST http://localhost:8080/flowable-rest/service/runtime/process-instances \ -H Content-Type: application/json \ -u admin:test \ -d { processDefinitionKey: proc_contract_review, variables: [ { name: contractJson, value: {\party_a\:\XX公司\,\party_b\:\YY公司\,\amount\:1500000,\payment_terms\:\预付30%\} }, { name: supplierInfo, value: {\name\:\YY公司\,\credit_rating\:\BBB\,\litigation_count\:2} }, { name: historyRisk, value: [\2023年存在1次付款延迟\,\2022年合同履约率82%\] } ] }查看ACT_RU_EXECUTION表确认流程实例状态为ACTIVE检查ACT_HI_VARINST表llmResult变量应包含类似{risk_level:high,confidence_score:0.85,key_risks:[付款比例过高],mitigation_suggestions:[建议调整为30%预付款60%验收款10%质保金]}的内容。至此一个可运行的 LLM 节点就完成了。4.2 性能调优让 LLM 节点扛住 500 TPSLLM 节点最大的性能瓶颈不是模型本身而是 Java 应用层的资源争抢。我们在压力测试中发现当并发请求超过 200 时Tomcat 线程池耗尽大量请求排队平均响应时间从 1.2s 暴涨到 8.7s。优化分三层第一层连接池精细化配置RestTemplate默认使用SimpleClientHttpRequestFactory每个请求新建 TCP 连接。我们改用HttpComponentsClientHttpRequestFactory并配置连接池Bean public RestTemplate restTemplate() { PoolingHttpClientConnectionManager connectionManager new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(200); // 最大连接数 connectionManager.setDefaultMaxPerRoute(50); // 每路由最大连接数 RequestConfig requestConfig RequestConfig.custom() .setConnectTimeout(5000) // 连接超时 .setSocketTimeout(15000) // 读取超时 .setConnectionRequestTimeout(3000) // 从连接池获取连接超时 .build(); CloseableHttpClient httpClient HttpClients.custom() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .build(); HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(httpClient); factory.setConnectTimeout(5000); factory.setReadTimeout(15000); return new RestTemplate(factory); }第二层本地缓存高频提示词90% 的 LLM 节点使用相同的系统提示词system prompt。我们用 Caffeine 缓存编译后的提示词模板Cacheable(value promptTemplates, key #templateId) public String getPromptTemplate(String templateId) { // 从数据库或配置中心加载模板 return templateRepository.findByCode(templateId).getContent(); } // 在委托类中 String systemPrompt promptService.getPromptTemplate(contract_risk_system);第三层异步批处理降级策略当 LLM 服务不可用时我们不直接失败而是启用批处理降级将 10 个待审核合同聚合成一批用规则引擎批量处理。实现FallbackLlmServiceService public class FallbackLlmService { public LlmResponse batchContractReview(ListContractDto contracts) { return contracts.stream() .map(this::reviewSingleContract) .collect(Collectors.collectingAndThen( Collectors.toList(), results - buildBatchResponse(results) )); } private LlmResponse reviewSingleContract(ContractDto contract) { // 基于规则的快速审核金额100万供应商信用BBB付款比例50% String riskLevel low; if (contract.getAmount() 1000000) riskLevel medium; if (BBB-.equals(contract.getSupplierCredit())) riskLevel high; if (contract.getPaymentTerms().contains(预付)) riskLevel high; return LlmResponse.builder() .risk_level(riskLevel) .confidence_score(0.95) .key_risks(List.of(规则引擎降级)) .mitigation_suggestions(List.of(建议人工复核)) .build(); } }经过这三层优化系统在 500 TPS 压力下LLM 节点平均响应时间稳定在 1.4s错误率低于 0.1%完全满足生产要求。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象可能原因排查命令/方法解决方案流程卡在 LLM 节点不动AsyncExecutor未启用或配置错误查ACT_RU_JOB表看是否有STATE_ ACQUIRED但长时间未更新的记录检查flowable.cfg.xml中asyncExecutorActivate是否为true确认ACT_RU_JOB表索引JOB_EXECUTION_ID是否存在LLM 返回结果为空或格式错误提示词中 JSON Schema 描述不清或模型未严格遵循抓取llmResult变量值用在线 JSON 校验工具验证在提示词末尾添加强制示例并在parseLlmResponse()中增加正则提取逻辑见 4.1 节代码大量LlmCallException日志LLM 服务端限流或网络抖动curl -v http://llm-gpu-internal:8000/health检查服务健康netstat -an | grep :8000查连接数在callLlmApi()中增加指数退避重试首次失败后等待 1s第二次失败后等待 2s第三次失败后等待 4s流程变量llmResult未设置委托类中未调用execution.setVariable()或变量名拼写错误查询ACT_HI_VARINST表NAME_ llmResult的记录是否存在在委托类execute()方法末尾添加logger.info(Setting llmResult: {}, response)确认日志输出同一份输入多次调用结果不一致temperature参数过高或模型本身随机性固定temperature0重新测试将temperature设为0.1平衡确定性与多样性并在提示词中添加请以确定性方式输出避免随机化5.2 我踩过的三个深坑与独家避坑技巧坑一Flowable 的变量序列化陷阱Flowable 默认用JdkSerializationStream序列化变量而 LLM 返回的 JSON 字符串如果包含特殊 Unicode 字符如 emoji、数学符号反序列化时会抛出java.io.UTFDataFormatException。我们花了两天排查最终发现是llmResult变量在ACT_RU_VARIABLE表中存储为BLOB类型而 MySQL 的utf8mb4字符集未正确配置。解决方案MySQL 配置文件中添加collation-server utf8mb4_unicode_ciFlowable 数据源 URL 添加?useUnicodetruecharacterEncodingutf8mb4在委托类中对llmResult字符串做预处理responseJson.replaceAll([^\\x00-\\x7F], )移除 ASCII 以外字符坑二BPMN 中的 EL 表达式解析失败想用${llmResult.risk_level high}做分支判断但 Flowable 报错ELException: Cannot convert string to boolean。原因是llmResult是字符串类型而 EL 表达式试图直接访问其属性。正确做法是在委托类中将llmResult解析为 Map 后再设置ObjectMapper mapper new ObjectMapper(); MapString, Object resultMap mapper.readValue(responseJson, Map.class); execution.setVariable(llmResultMap, resultMap); // 注意变量名改为 Map分支表达式改为${llmResultMap[risk_level] high}坑三LLM 节点重试导致重复计费Flowable
返回列表