news 2026/10/1 13:44:56

Flowable工作流集成大模型:Service Task实现智能审批节点全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flowable工作流集成大模型:Service Task实现智能审批节点全指南

前阵子手上有个内部的费用报销审批流程要做智能化改造,业务方提了个需求:系统要在审批阶段自动判断一笔报销单的风险等级,并顺手生成一段处理建议。难点在于,判断依据不只是金额和类别这些结构化字段,还包括报销说明这种没法用硬规则处理的非结构化文本。

当时第一反应是写一堆 if/else 规则,或者搞一张决策表放到 Flowable 的条件表达式里。但很快就发现这条路行不通——“外出参加行业会议,对方开具的发票金额与会议日程不符”这种描述,规则引擎根本没法给出合理结论。真正能处理这种内容的东西只有大模型。

于是问题变成了一个非常具体的工程问题:如何在 Flowable 工作流里接入一个能自动执行的大模型节点。两个多星期折腾下来,从 BPMN 建模、JavaDelegate 编写,到异步执行、异常处理,踩了不少坑,也把一套可复用的接入模式跑通了。这篇就把完整思路、代码和排错过程整理出来,给同样在 Flowable 项目里做 AI 改造的同学做个参考。

1. 为什么要在 Flowable 里塞一个“AI 智能审核”节点:场景与路线选型

先明确一点:Flowable 本身是一个状态机 + 任务调度的引擎,它没有任何“智能”成分。我们说的“LLM 节点”,本质上是在流程定义的某个位置,挂一个由代码实现的自动步骤,这个步骤负责调用大模型接口,再把模型输出转化成后续路由需要的数据。

1.1 这类节点在真实项目里能干什么

从我接触过的项目来看,LLM 节点最常见的落地场景有这么几类:

  • 审批建议生成:读取单据内容,让模型输出风险等级、通过建议、需补充的材料清单。这是最常用的场景,改造成本低、效果最直观。
  • 智能分单:根据工单描述自动确定负责人团队,替代原来靠人肉选择或者简单关键词匹配的逻辑。
  • 简历初筛:把候选人简历文本和岗位要求一起喂给模型,让模型输出匹配度评分和不匹配点。
  • 条款风险提取:读取合同段落,提取可能的风险条款,给法务人员附上原文定位。
  • 动态条件判断:表单里没有现成字段支撑网关分支条件时,用模型输出一个枚举值,作为排他网关的路由依据。

这些场景有一个共同特点:输入是文本,输出是结构化或半结构化结果,而且结果直接参与流程走向。也就是说,这不是锦上添花的“智能问答”,而是流程内部的一个真实环节,它的稳定性会直接影响流程能否正确跑完。

1.2 三条可选技术路线,为什么我选了 Service Task

聊方案的时候,团队里出现过三种声音,这里把对比列出来:

路线做法优点缺点
外部回调AI 服务在 Flowable 外部运行,把结果通过 REST API 回写流程变量流程引擎改动最小需要额外开发回调接口,AI 执行节点在流程图上不可见,业务没法直观看到“这里有个智能判断”
事件监听器用 ExecutionListener 或 TaskListener 在节点触发时调用 LLM可以复用 Flowable 生命周期事件监听器逻辑分散在多个节点事件的回调里,不方便做成“一个粒度的 AI 动作”,测试和排查都比较绕
Service Task + JavaDelegate把 LLM 调用封装成 Flowable 的服务任务,在 BPMN 里作为一个独立节点节点可视化、可复用、可实现异步、可用流程变量天然传递数据需要写 Java 代码,对建模工具有一点要求

我最后选的是第三条路线。原因很实际:Service Task 是 BPMN 规范里标准的自动执行节点,Flowable 对它支持得最好,既能同步执行也能启用异步,出错后引擎会负责重试;而且节点画在流程图上是可见的,业务方评审流程图时能直观看到“这里有一个 AI 审核动作”,而不是去代码里猜哪条监听器做了什么事。

后来项目里又加了其他流程,这个 LLM Service Task 被直接复用,类似功能在多个流程里只要拖一个节点进来就行。如果当时用了监听器,每个流程都得单独维护一套监听逻辑,想想就头疼。

2. 扩展点梳理:Service Task 为什么是接入 LLM 的正解

要把 LLM 节点做对,先得理解 Flowable 给自定义自动节点提供的几种实现方式,以及它们各自的适用边界。这块值得单独说清楚,因为选错实现方式,后面维护会很难受。

2.1 BPMN 里的 Task 类型乱花眼,自动执行其实只有一个标准入口

BPMN 2.0 里和“任务”相关的东西很多:User Task(人工任务)、Service Task(服务任务)、Script Task(脚本任务)、Send Task(发送任务)、Receive Task(接收任务)等。

对一个 LLM 节点来说,它要做的是:读取数据、调用外部 API、写回结果,这个模式就是锁定 Service Task。Script Task 虽然也能做这件事,但要在脚本里拼 HTTP 请求、处理 JSON、管理密钥,既不安全也不好调试;Send Task 更多用于消息发送模式,不适合承载业务逻辑。

所以结论很简单:Service Task 是接入 LLM 的正解,没有之一。

2.2 Service Task 的三种实现方式,要分清“绑定类”和“绑定表达式”

Flowable 的 Service Task 有几种绑定方式,官方文档里都列了,但实际项目里最容易混淆的是这三种:

  • flowable:class:直接指定一个实现JavaDelegate的类全限定名。引擎会自行实例化这个类,不经过 Spring 容器。
  • flowable:expression:写一个表达式,比如${myService.doWork()},引擎直接调用这个表达式返回结果。
  • flowable:delegateExpression:也是表达式,但表达式的值的需要是一个实现了JavaDelegate的对象。最常见写法是${myDelegateBean},其中myDelegateBean是 Spring 容器里的一个 Bean。

先看一个容易踩的坑:如果用flowable:class,引擎是通过反射创建类的,这个类不会注入 Spring 依赖,里面的@Autowired全是 null。很多刚接触 Flowable 的人在这里碰壁,包括我。

如果有 Spring Boot 环境,强烈建议用delegateExpression指向一个注册成 Bean 的 Delegate 实现类,这样可以正常使用依赖注入、读取配置、走 AOP 日志。

2.3 为什么我选 Delegate Expression 而不是直接写死在类上

除了依赖注入的问题,delegateExpression还有一个好处:可以在部署不同流程版本时,通过配置切换不同的节点实现。

举个实际例子:我们在做 LLM 节点时,先接了一个通用对话模型做验证,后来切换成更便宜的专用分类模型。如果类名硬编码在 BPMN 里,就得改流程图重新部署;但用delegateExpression的话,可以在 Spring 配置里控制哪个 Bean 生效,流程图不用动,切模型就像改了个配置项。

另外,配合 Spring 的@Primary、@Qualifier或者 Profile 机制,还可以实现测试环境用 Mock 实现、生产环境用真实模型,这对工作流自动化测试特别有用。后面讲测试的时候我会再展开。

3. 把 LLM 节点写成可用代码:从依赖到 BPMN 配置

下面进入正题,直接上一套可以在 Spring Boot + Flowable 环境里跑起来的代码。这套代码我按生产标准写过,不是 demo。

3.1 工程准备:Flowable 版本和依赖选择

项目用的是 Spring Boot 2.7,Flowable 7.1.0。这里有个经验:Flowable 7.x 对 Spring Boot 版本有对应关系,不要随意混搭。可以直接看官方文档的版本兼容矩阵,或者干脆用 Flowable 提供的 BOM 来管理版本,省心不少。

Maven 依赖核心就两个:

<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>7.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

HTTP 客户端我直接用 Spring 的RestTemplate,设置好超时时间即可。如果项目里已有 OkHttp 或者 WebClient,也没问题,只要封装在 LLM Client 里就行。这里不推荐 service task 里直接写裸的HttpClient代码,后面要加超时、重试、日志都会很零散。

3.2 核心实现:LlmServiceDelegate

Flowable 的自定义节点逻辑,核心是实现org.flowable.engine.delegate.JavaDelegate接口。我在一个报销审批流程里定义了一个 LLM 节点,它的做法是这样的:从流程变量中读取报销类别、金额、报销说明,组装提示词,调用大模型,解析返回的 JSON,最后把风险等级和处理建议写回流程变量。

@Component("llmReviewDelegate") public class LlmReviewDelegate implements JavaDelegate { private final LlmClient llmClient; public LlmReviewDelegate(LlmClient llmClient) { this.llmClient = llmClient; } @Override public void execute(DelegateExecution execution) { String category = execution.getVariable("category", String.class); BigDecimal amount = execution.getVariable("amount", BigDecimal.class); String description = execution.getVariable("description", String.class); String prompt = """ 你是财务报销审核助手。根据以下信息判断该报销单的风险等级。 类别:%s 金额:%s 报销说明:%s 只输出如下 JSON 格式,不要输出多余文字: {"riskLevel":"LOW|MEDIUM|HIGH","suggestion":"处理建议","reason":"判断理由"} """.formatted(category, amount, description); LlmResponse response = llmClient.chat(prompt); // 解析模型输出,注意要做容错处理 RiskResult riskResult = RiskResultParser.parse(response.getContent()); // 把结果写回流程变量,后续网关和人工任务都能读取 execution.setVariable("riskLevel", riskResult.getRiskLevel()); execution.setVariable("riskSuggestion", riskResult.getSuggestion()); execution.setVariable("riskReason", riskResult.getReason()); } }

这段代码有几个容易忽略的点,提一下:

  • 变量命名规范:写回的变量如果后续要在网关条件里使用,建议保持纯小写驼峰,避免在表达式里和 JavaBean 属性解析混淆。
  • 模型输出解析不能假设一定成功:大模型偶尔会返回多出前缀的文本,建议在解析前做一次清理,后面我会单独讲这个坑。
  • DelegateExecution 是线程安全的吗?每个流程实例有独立的 execution 实例,但 Delegate Bean 是单例的,里面不要存跟具体请求相关的状态,所有数据都放 execution 变量。

3.3 LLM Client:多模型适配的关键封装

代码里的LlmClient是一个接口,我没有直接把某个厂商的 SDK 硬编码到 Delegate 里。理由很简单:大模型领域变化太快,今天用这个模型,明天可能要换成别的,接口隔离一下,Delegate 代码就不用动了。

public interface LlmClient { LlmResponse chat(String prompt); }

对于大模型服务,我建议优先找支持 OpenAI 兼容接口的服务。因为现在私有化部署方案、国内外主流云厂商,绝大部分都提供 OpenAI 兼容的/chat/completions接口。这样我们只需要写一个实现:

@Component public class OpenAiCompatibleClient implements LlmClient { @Value("${llm.api-url}") private String apiUrl; @Value("${llm.api-key}") private String apiKey; @Value("${llm.model}") private String model; private final RestTemplate restTemplate; public OpenAiCompatibleClient(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @Override public LlmResponse chat(String prompt) { Map<String, Object> body = new HashMap<>(); body.put("model", model); body.put("messages", List.of( Map.of("role", "system", "content", "你是企业流程自动化助手,总是输出严格要求的格式。"), Map.of("role", "user", "content", prompt) )); body.put("temperature", 0.1); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(apiKey); HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers); ResponseEntity<JsonNode> resp = restTemplate.exchange(apiUrl, HttpMethod.POST, entity, JsonNode.class); String content = resp.getBody().path("choices").path(0).path("message").path("content").asText(); return new LlmResponse(content); } }

这里有两个细节值得注意:

  • temperature 要调低:工作流里的模型输出直接影响流程走向,我不希望它“太有创造力”,通常设 0.1 或者 0,尽量稳定输出。
  • 超时设置必须在 RestTemplate 层面做,否则默认是不超时的,一旦模型接口卡住,整个服务可能被拖垮。超时配置后面单独说。

3.4 BPMN 里怎么配这个节点

代码写好了,接下来要把节点画到流程图上。这里有个经验:Flowable 官方自带的 Modeler 也能画,但很多生产项目其实是拿 Camunda Modeler 画图,再导出 BPMN 文件给 Flowable 部署,因为 Camunda Modeler 是独立桌面端,画起来顺手,版本兼容性也好。两个工具生成的 BPMN XML 都是标准格式,Flowable 能直接识别。

在 Camunda Modeler 里拖一个 Service Task,然后在右侧属性面板找到Implementation相关设置,填入flowable:delegateExpression="${llmReviewDelegate}"。对应生成的 XML 片段长这样:

<serviceTask id="llmReviewTask" name="AI 智能审核" flowable:delegateExpression="${llmReviewDelegate}" flowable:async="true"> </serviceTask>

注意flowable:async="true"这个属性,这是异步执行开关,我后面专门讲为什么建议打开。

部署流程还是在代码里最直观:

@SpringBootTest class ProcessDeployTest { @Autowired private RepositoryService repositoryService; @Test void deployProcess() { repositoryService.createDeployment() .name("报销审批流程") .addClasspathResource("processes/expense-approval.bpmn20.xml") .deploy(); } }

部署完成后,去 Flowable 的 ACT_RE_PROCDEF 表里查一下流程定义是否正常注册,只要状态正常,节点就挂上去了。

4. 数据流转和异步保护:别让一次 API 调用拖垮整个流程实例

LLM 节点和其他 Service Task 最大的不同,在于它的执行时间可能很长。普通服务任务几十毫秒就结束了,LLM 调用动辄三五秒,高峰期甚至二三十秒。如果不做异步和超时保护,整个流程引擎都会被拖垮。这一章把变量传递和异步执行的实操讲透。

4.1 流程变量作用域:getVariable 和 setVariable 到底写在哪儿

先解释一个容易懵的点:DelegateExecution.getVariable()和setVariable()操作的是当前执行实例可见的变量,但 Flowable 的变量不是全平铺在一个大 Map 里。

一个流程实例启动时,会创建一个根执行实例(Process Instance Execution)。排他网关、并行网关会从它分支形成子执行实例。在子执行实例里调用getVariable(),会沿着执行树的父级向上查找;而setVariable()默认会写到当前执行实例的父级,也就是流程实例级别,除非你在多实例节点里。

这个机制绝大多数时候是够用的,但有一个场景必须注意:并行网关。

如果流程里有并行分支,两条分支同时跑,其中一条分支的 LLM 节点setVariable("riskLevel", ...),另一条分支如果也操作同名变量,就会出现互相覆盖的竞态问题。解决办法是:尽量让写变量发生在合并后的节点上,或者在分支里用带前缀的变量名区分,比如leftRiskLevel和rightRiskLevel。

多实例节点(会签、或签)里更要注意:每个实例有独立的 execution,getVariable能取到共享变量,但如果你setVariable想改共享变量,默认行为可能会造成局部变量和全局变量不一致。建议多实例场景下用execution.getParent().setVariable()显式指定作用域,或者用execution.setVariable()后针对多实例结果做getVariable("nrOfCompletedInstances")汇总判断。

4.2 同步执行会让流程引擎线程池寸步难行

Flowable 默认情况下,Engine 会在发起流程的线程里直接执行 Service Task。如果你在接口线程里启动流程实例,而这个流程里有一个同步的 LLM 节点,这个线程就会在restTemplate.exchange()上阻塞好几秒。

我实测过一个场景:压测并发 30 个报销单启动流程,每个 LLM 调用平均 4 秒,后面的流程全部排队,看起来就像线程池被“卡死”了。试想生产环境高峰期几十个工单同时在跑,系统响应时间直接上天。

解决办法就是给 Service Task 开异步执行:

<serviceTask id="llmReviewTask" ... flowable:async="true">

开了这个属性,Flowable 会把该节点的执行封装成一个 Job,放入异步执行器的队列中,由 AsyncExecutor 的后台线程去消费。这样启动流程的接口线程立刻返回,流程实例在那个节点处“暂停”,等异步线程把 LLM 调用完成后,流程再从该节点继续往下走。

Flowable 在 Spring Boot 环境下默认会启动 AsyncExecutor,可以在配置里调整线程池大小和队列长度:

flowable: async-executor-activate: true async: async-executor-core-pool-size: 8 async-executor-max-pool-size: 16 async-executor-queue-capacity: 100

这里注意一点:并发量特别大、模型接口又慢的时候,队列容量填太大会导致流程实例长期停留在“未完成”状态,业务上要注意超时提醒。容量太小又会触发RejectedExecutionException,Job 会按重试策略反复补偿。

4.3 超时和重试:一张表说清楚配置思路

LLM 调用失败是常态,网络抖动、模型服务过载、返回格式不对,都可能导致节点执行失败。Flowable 对异步 Job 有默认重试机制,但重试次数和间隔要按场景调。

RestTemplate 超时配置:

@Bean public RestTemplate llmRestTemplate() { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setConnectTimeout(5_000); factory.setReadTimeout(30_000); return new RestTemplate(factory); }
场景建议配置原因
连接超时5 秒模型服务地址不可达时要快速失败
读取超时30 秒大模型生成可能较慢,但超过 30 秒基本就是出问题了
Flowable Job 重试次数3 次次数太少抖动时误失败,太多会积压消息队列
重试间隔指数递增,从 2 秒开始给模型服务恢复时间

Flowable 异步 Job 重试配置在process-engine-config:

flowable: async: async-executor-activate: true default-timer-job-acquire-wait-time: 10000 async-job-retry-wait-time: 2000 async-job-retry-max-retries: 3

重试有一个隐藏问题:LLM 调用如果已经成功写入了流程变量,只是因为解析或网络异常抛了错,重试会把同一个请求再发一次。所以要在节点里做幂等处理。我在代码里加了一个简单判断:

if (execution.getVariable("riskLevel") != null) { return; }

这是在流程变量中写入标记,重试时发现已有结果就直接跳过,避免重复调模型浪费成本。

5. 真实踩坑记录:变量丢失、线程阻塞、返回解析的排查全过程

这章把我在接入过程中遇到的最典型的四个问题按排查链路写出来。看答案没意思,看排查思路才有价值,以后换个坑也能自己定位。

5.1 坑一:Service Task 里 getVariable 拿到的全是 null

现象:流程流转到 LLM 节点,日志打印出来 category、description 全是 null。

排查过程:

  1. 先怀疑流程变量没传对,去 ACT_RU_VARIABLE 表查,发现变量明明存在。
  2. 再怀疑节点 ID 写错,BPMN 图看了一遍,没错。
  3. 最后进调试模式,把execution.getVariables()全部打印,发现能拿到变量,但getVariable("category")返回 null,因为变量名在数据库里存的是category和Category之类的差异,大小写不一致。

定位到原因:流程启动时用Map.of("Category", ...)传入,而 Delegate 里读取的是"category"。Flowable 变量名是大小写敏感的,而且我踩过的是Map.of构造出来的 key 是Category,我以为是category。

解决办法:统一变量名。我在项目里定了一条规范,所有流程变量统一小驼峰命名,启动流程和 Delegate 读取必须用同一个常量类引用。后来再没出过这类问题。

5.2 坑二:同步调用把流程服务线程池整个占满

现象:压测时从第 20 个请求开始接口响应从 200ms 涨到 15 秒,日志里出现Acquire async job和大量阻塞线程。

排查过程:

  1. 先看数据库 ACT_RU_EXECUTION,发现大量流程实例停在 LLM 节点。
  2. 再用jstack抓线程,发现线程都停留在RestTemplate的 socketRead0。
  3. 确认是同步 Service Task 耗死线程。

这个坑我之前已经讲了,解决办法就是开flowable:async="true"。还有一个隐蔽点:即使开了异步,如果 AsyncExecutor 线程池太小或者队列容量太高,同样会出现隐性问题。我最后把核心线程数设为 8,最大 16,队列 100,压测下来吞吐没问题。

5.3 坑三:模型返回的 JSON 里带了 markdown 代码块标记,直接解析失败

现象:生产环境偶发报JsonParseException,有时候同一个输入这次成功下次失败。

排查过程:

  1. 打点日志记录模型原始返回。果然,模型偶尔会在 JSON 外面包上 ```json 代码块标记。
  2. 这确实是很多模型的“种族天赋”:你让它输出 JSON,它非要给你加装饰。

解决办法是写一个健壮的解析函数:

public class RiskResultParser { public static RiskResult parse(String raw) { String content = raw.trim(); // 去掉可能的 markdown 代码块标记 content = content.replaceAll("^```json\\s*", "").replaceAll("\\s*```$", ""); JsonNode node = new ObjectMapper().readTree(content); String level = node.path("riskLevel").asText(); if (!Set.of("LOW", "MEDIUM", "HIGH").contains(level)) { throw new IllegalStateException("riskLevel 不在合法枚举内: " + level); } return new RiskResult(level, node.path("suggestion").asText(), node.path("reason").asText()); } }

5.4 坑四:Flowable 和项目里 Jackson 版本冲突,启动直接报错

现象:接入 Flowable 后,Spring Boot 启动时报JsonMappingException,或者NoSuchMethodError,指向 Jackson 的某个类。

排查过程:

  1. mvn dependency:tree查 Jackson 依赖,发现 Flowable 7.x 传递依赖 Jackon 2.15,而项目里被一个旧依赖压成了 2.13。
  2. 典型错误是InvalidDefinitionException,Jackson 2.13 和 2.15 在时间日期序列化上行为不同。

解决办法:在 pom 里统一 Jackson 版本到 Flowable 认可的版本。如果不想动全局版本,也可以在 flowable-spring-boot-starter 里把 jackson 排除,显式声明和项目一致的版本。这个问题的通用排查思路就一句话:报NoSuchMethodError先查依赖树,别急着定位业务代码。

6. 可以从这三点继续扩展:动态路由、结果校验、成本缓存

LLM 节点跑通只是第一步,实际生产要考虑的还有不少。但把这三个方向做好,体系就比较完整了。

6.1 动态路由:用模型输出当排他网关的路由条件

LLM 节点输出的riskLevel可以直接作为排他网关的判断依据,在 BPMN XML 里就是这样:

<exclusiveGateway id="riskGateway" name="风险等级网关" /> <sequenceFlow id="flowLow" sourceRef="riskGateway" targetRef="autoApproveTask"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${riskLevel == 'LOW'}]]> </conditionExpression> </sequenceFlow> <sequenceFlow id="flowHigh" sourceRef="riskGateway" targetRef="manualAuditTask"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${riskLevel != 'LOW'}]]> </conditionExpression> </sequenceFlow>

这里有一个我在表达式中踩过的细节:Flowable 的 EL 表达式里,字符串比较要用==而不是equals,它用的是 Spring EL 的语法。如果你写成${riskLevel.equals('LOW')}也能跑,但格式风格不统一会容易出错。另外,条件表达式里不能直接依赖riskSuggestion这种中文字符串做精确匹配,常有换行和空格问题,建议以枚举值为准。

6.2 结果校验:给模型输出上保险

模型输出不像接口文档那么可靠。这里说的校验有两个层面:

  • 格式校验:必须能被解析成合法 JSON,且字段完整。解析失败就走重试或降级。
  • 业务语义校验:riskLevel必须在合法枚举内,金额大但风险等级为 LOW 这种结果,建议校验通过后再放行。

我在自定义的RiskResultParser里做了第一道校验,在 Delegate 里还可以加入规则联动:

if ("LOW".equals(riskResult.getRiskLevel()) && riskResult.getReason() == null || riskResult.getReason().isBlank()) { riskResult = RiskResult.fallbackHigh("模型输出缺少理由,按高风险人工处理"); }

这个降级策略很重要:宁可让模型保守一点走人工,也不要因为一个失误把不该通过的单子直接放行了。LLM 节点先做好异常兜底,才敢真正驱动业务决策。

6.3 成本缓存:同样的输入别反复调模型

LLM API 按 token 计费,工作流里的节点调用频率还蛮高的,同一个工单因为流程回退、重试可能被反复触达。我给 LLM 节点加了简单缓存,以“提示词内容的 SHA-256 Hash”为 key,带上流程的业务 ID 一起存:

@Component public class LlmCacheService { private final Cache<String, LlmResponse> cache = Caffeine.newBuilder() .maximumSize(500) .expireAfterWrite(30, TimeUnit.MINUTES) .build(); public LlmResponse get(String prompt) { String key = DigestUtils.sha256Hex(prompt); return cache.getIfPresent(key); } public void put(String prompt, LlmResponse response) { String key = DigestUtils.sha256Hex(prompt); cache.put(key, response); } }

注意缓存 key 只包含提示词,不包含流程实例 ID,这样两个审批单如果描述完全一样就能命中。但如果你的业务场景对时效性要求高,缓存时间要调短;如果每次都希望模型重新判断,就不要加缓存。这个看业务取舍。

6.4 日志与观测:记录 token 消耗和模型耗时

LLM 节点进生产以后,运维上最实用的就是记录每次调用的模型、耗时、token 消耗、原始输出、流程实例 ID。建议在LlmServiceDelegate里用 MDC 塞上流程实例 ID:

MDC.put("processInstanceId", execution.getProcessInstanceId()); try { LlmResponse response = llmClient.chat(prompt); log.info("LLM 调用成功, 耗时={}ms, token={}, 输出={}", costMs, response.getUsage(), response.getContent()); } finally { MDC.remove("processInstanceId"); }

这套日志做好了,以后出问题可以按流程实例 ID 直接拉出这个节点的完整调用链路,排查效率提升不是一点半点。

最后分享个我个人的习惯:在做 Flowable 和 LLM 集成这类偏探索性的工作时,先把主链路用通用模型跑通,再慢慢加缓存、校验、降级这些“防倒”机制。这里面的顺序很重要。另外,真正让这个方案稳定的是把“模型输出”当成一个不信任的输入源来对待,所有下游逻辑都要默认它可能是错的、慢的、格式不对的。把这一点想明白了,踩坑的速度会慢一半。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 13:43:56

YOLOv8人员轨迹跟踪实战:从检测到轨迹的完整方案

简介&#xff1a;这份资源围绕YOLOv8目标检测模型构建了一套完整的人员轨迹跟踪算法实现&#xff0c;面向计算机视觉入门与进阶开发者、需要做行人追踪项目的学生及工程人员。它解决的是从检测到多目标跟踪的落地问题&#xff0c;适合安防监控、客流统计、视频分析等场景。压缩…

作者头像 李华
网站建设 2026/10/1 13:42:33

GPU加速效率优化实战:从内存布局到多卡调度的全链路指南

1. 从“显卡跑不满”说起&#xff1a;GPU 加速的真实瓶颈在哪很多人第一次接触 GPU 计算&#xff0c;脑子里想的都是“把任务丢给显卡&#xff0c;速度直接起飞”。结果代码跑起来一看&#xff0c;GPU 利用率常年趴在 20% 以下&#xff0c;风扇都不怎么转&#xff0c;训练一个 …

作者头像 李华
网站建设 2026/10/1 13:42:31

RTX 40 解锁 DLSS 5:OpenDLSS-NR 开源方案实战

1. 这件事到底在聊什么 DLSS 5 刚有点风声的时候&#xff0c;圈子里普遍觉得这又是一次“新卡独占”的常规操作——老黄刀法精准&#xff0c;RTX 40 系用户大概率只能看着 50 系吃满新特性。结果没想到&#xff0c;民间开发者的动作比官方驱动更新还快。最近在几个技术社区里&a…

作者头像 李华
网站建设 2026/10/1 13:42:26

GPU加速实战:从数据管道到计算图优化的全链路指南

1. GPU 加速的真相&#xff1a;为什么你的显卡跑不满 1.1 从一次压测说起&#xff1a;算力利用率的残酷现实 很多人拿到一张 RTX 4060 Laptop 或者更高级别的卡&#xff0c;第一反应是跑个 PyTorch 训练脚本&#xff0c;然后盯着 nvidia-smi 看利用率。结果发现 GPU 利用率在…

作者头像 李华
网站建设 2026/10/1 13:41:19

AI Agent全栈开发速成:从调API到独立交付Agent的四周路径

1. 从“会用API”到“能交付Agent”&#xff1a;这个速成计划到底在解决什么问题这两年AI Agent这个词被炒得火热&#xff0c;招聘网站上挂着“AI Agent工程师”的岗位薪资也确实诱人&#xff0c;但我见过太多人卡在一个尴尬的位置上&#xff1a;能调通大模型的API&#xff0c;…

作者头像 李华
网站建设 2026/10/1 13:41:00

C++ 构建 AI Agent:整体架构设计与学习路线

1. 为什么想不开要用 C 写 AI Agent 先把结论摆在最前面&#xff1a;用 C 写 AI Agent&#xff0c;不是因为 C 时髦&#xff0c;恰恰相反&#xff0c;是因为它在某些场景下“没得选”。我最初动这个念头&#xff0c;是在做一个需要本地推理、低延迟响应、还要嵌到现有桌面端程序…

作者头像 李华