Java 8老系统AI Workflow实战:把一次性AI对话升级成可恢复工作流
Java 8老系统AI Workflow实战:把一次性AI对话升级成可恢复工作流
很多 AI 应用刚开始都是一个聊天框。
用户输入需求,模型输出结果。
这适合原型验证,但进入企业流程后,很快会遇到问题:
中途失败怎么办? 关键节点谁确认? 上一次输出在哪里? 能不能从失败节点恢复? 每一步输入输出能不能审计?所以第 9 讲要把一次性 AI 对话升级成工作流。
最终效果
代码目录:
code/spring-ai-enterprise-lab/labs/chapter09-ai-workflow运行:
.\compile-and-run.ps1Demo 会演示一个"需求到测试用例"的工作流:
需求提取 ↓ 接口设计 ↓ 人工审批 ↓ 测试用例生成 ↓ 发布检查清单启动后,流程会停在api-design节点。
审批后,流程继续执行到COMPLETED。
代码结构
核心代码分三块:
src/main/java/com/ynzz/lab/chapter09 ├── common │ ├── WorkflowStartRequest ← 工作流启动参数 │ ├── WorkflowRun ← 流程实例(运行时状态) │ └── WorkflowNodeSnapshot ← 节点快照 ├── nodes │ ├── RequirementExtractNode ← 需求提取节点 │ ├── ApiDesignNode ← 接口设计节点(含 HumanApproval) │ ├── TestcaseGenerateNode ← 测试用例生成节点 │ └── ReleaseChecklistNode ← 发布检查清单节点 └── runtime └── WorkflowRuntime ← 运行时引擎WorkflowRuntime负责控制流程何时继续,何时停住等待审批。
WorkflowRuntime 的控制流
WorkflowRuntime的核心不是"调用几个 AI 节点",而是控制状态怎么流动。
当前 Demo 的start方法做了四件事:
创建 WorkflowRun ↓ 运行 RequirementExtractNode,写入第一个快照 ↓ 把 lastOutput 交给 ApiDesignNode,写入第二个快照 ↓ 把流程状态改成 WAITING_APPROVAL,waitingNodeId=api-design这里的lastOutput()很关键。
它表示节点之间不是靠一大段聊天上下文传递信息,而是靠上一个节点的结构化输出继续往下走:
需求原文 ↓ RequirementExtractNode.output ↓ ApiDesignNode.input ↓ ApiDesignNode.output ↓ 审批通过后进入 TestcaseGenerateNode.input节点之间的数据传递靠lastOutput()完成:它返回最近一个SUCCESS或APPROVED快照的output字段,作为下一个节点的input。快照格式里包含nodeId、status、input、output、approvedBy、errorCode、retryCount——失败时 Runtime 靠这些字段定位恢复点:status=SUCCESS/APPROVED的快照是安全恢复点,output是重跑失败节点的输入,retryCount用于控制重试次数。这样设计是为了避免失败后从头重跑整个工作流。
审批发生在api-design节点。
审批前,流程停住:
status=WAITING_APPROVAL waitingNodeId=api-design审批后,approve方法会:
找到 api-design 快照 ↓ 写入 approvedBy,并把节点状态改成 APPROVED ↓ 把流程状态恢复为 RUNNING ↓ 运行 TestcaseGenerateNode ↓ 运行 ReleaseChecklistNode ↓ 把流程状态改成 COMPLETED所以这个 Demo 真正想表达的是:企业 AI 工作流要能暂停、能继续、能看到每一步的输入输出,而不是把所有事情塞进一次模型调用。
当前 Stub 还没有实现失败节点回退,但它已经把恢复所需的最小结构留出来了:每个节点都有input、output和status。真实落地时,失败节点应该记录FAILED、错误原因和重试次数,Runtime 从最近一个SUCCESS或APPROVED快照继续,而不是从头重跑。
每个节点都要有快照
启动工作流后,输出里会看到:
{"status":"WAITING_APPROVAL","waitingNodeId":"api-design","snapshots":[{"nodeId":"requirement-extract","status":"SUCCESS"},{"nodeId":"api-design","status":"WAITING_APPROVAL"}]}这就是企业工作流的基本形态。
不是模型一次性吐出全部内容,而是每一步都有记录:
节点 ID 节点输入 节点输出 节点状态 审批人对应到当前WorkflowNodeSnapshot,快照字段是:
{"nodeId":"api-design","status":"WAITING_APPROVAL","input":"需求点:订单超过 48 小时未发货时触发延迟预警,并提醒客服介入。","output":"POST /api/orders/delay-alerts,输入 thresholdHours=48,输出待介入订单列表。","approvedBy":""}这样才能审计、复盘、重试和恢复。
四个节点的职责边界也应该讲清楚:
| 节点 | 输入 | 输出 | 卡住或失败时的处理重点 |
|---|---|---|---|
RequirementExtractNode | 用户需求原文 | 结构化需求点 + 不确定项列表 | 需求含糊时输出不确定项,转人工确认;自身失败记录 errorCode,需求作为 input 重跑 |
ApiDesignNode | 需求摘要(lastOutput) | 接口草案(请求/响应/错误码) | 卡在 WAITING_APPROVAL;被驳回则带着修改意见重跑;自身失败从 requirement-extract 快照恢复 |
TestcaseGenerateNode | 审批后的接口设计(approved snapshot output) | 测试用例集合 | 前置节点未审批则拒绝执行;自身失败记录 errorCode,从 api-design 快照恢复重跑 |
ReleaseChecklistNode | 测试用例输出(lastOutput) | 发布检查清单 + 待办项 | 发布条件不完整时输出待办清单;自身失败从 testcase-generate 快照恢复;始终不输出"强制通过" |
这张表比类名列表更重要。
因为企业工作流里,每个节点都要回答三个问题:接收什么、产出什么、什么时候停下来。
当前 Demo 完整跑一遍
本讲 Demo 的启动输入是:
tenantId=demo operatorId=u1001 requirementText=新增订单延迟预警功能:当订单超过 48 小时未发货时,系统需要提醒客服介入。第一步,RequirementExtractNode接收原始需求,输出需求摘要:
input=新增订单延迟预警功能:当订单超过 48 小时未发货时,系统需要提醒客服介入。 output=需求点:订单超过 48 小时未发货时触发延迟预警,并提醒客服介入。 status=SUCCESS第二步,WorkflowRuntime调用run.lastOutput(),把这段需求摘要交给ApiDesignNode:
input=需求点:订单超过 48 小时未发货时触发延迟预警,并提醒客服介入。 output=POST /api/orders/delay-alerts,输入 thresholdHours=48,输出待介入订单列表。 status=WAITING_APPROVAL然后 Runtime 把整个流程停住:
WorkflowRun.status=WAITING_APPROVAL WorkflowRun.waitingNodeId=api-design这时还不会生成测试用例,也不会生成发布清单。
只有当审批调用发生:
runtime.approve(run, "api-design", true, "tech-lead")Runtime 才继续执行后两个节点。
第三步,TestcaseGenerateNode接收已经审批过的接口设计:
input=POST /api/orders/delay-alerts,输入 thresholdHours=48,输出待介入订单列表。 output=测试用例:48 小时边界、未发货订单、已发货订单、重复提醒幂等、客服可见性。 status=SUCCESS第四步,ReleaseChecklistNode接收测试用例输出:
input=测试用例:48 小时边界、未发货订单、已发货订单、重复提醒幂等、客服可见性。 output=发布检查:灰度租户、SQL 索引、告警阈值、客服通知模板、回滚开关。 status=SUCCESS最后流程完成:
WorkflowRun.status=COMPLETED WorkflowRun.waitingNodeId=""这条时间线说明了一件事:节点之间传递的是上一个节点的output,不是把所有原始上下文无限追加给模型。
失败恢复应该怎么落地
当前 Stub 只演示了成功和审批暂停,没有真正模拟FAILED。但它的快照结构已经能说明恢复设计。
真实项目里,节点快照至少应该扩展成:
{"workflowId":"wf-001","nodeId":"testcase-generate","status":"FAILED","input":"POST /api/orders/delay-alerts...","output":"","errorCode":"MODEL_TIMEOUT","retryCount":2,"createdAt":"2026-06-15T10:00:00","updatedAt":"2026-06-15T10:02:00"}恢复时不是从需求提取重新开始,而是:
找到最后一个 SUCCESS / APPROVED 快照 ↓ 读取它的 output ↓ 作为失败节点的 input ↓ 重跑失败节点 ↓ 写入新的快照版本比如TestcaseGenerateNode失败,最近的稳定节点是api-design:
api-design.status=APPROVED api-design.output=POST /api/orders/delay-alerts... ↓ 重试 testcase-generate这样才能做到"失败可恢复",而不是"失败后从头再问一遍模型"。
WorkflowStateRepository:为什么当前是 Stub,持久化后有什么不同
当前 Demo 的WorkflowStateRepository是内存 Stub,故意不依赖外部存储。原因有两个:
第一,降低上手成本。不需要装数据库、不需要配 Redis,跑compile-and-run.ps1就能看到完整工作流效果。
第二,先跑通流程,再落地存储。工作流引擎的正确性是第一步——节点拆分对不对、快照字段全不全、恢复逻辑合不合理。这些问题不依赖存储也能验证。
但内存 Stub 有明显局限:服务重启,所有快照丢失。
真实落地时,WorkflowStateRepository应该持久化到数据库或 Redis。持久化后的差异:
| 维度 | 内存 Stub | 持久化(DB / Redis) |
|---|---|---|
| 服务重启 | 快照全丢,工作流只能重新开始 | 快照不丢,工作流从断点恢复 |
| 多实例部署 | 不支持(每个实例内存独立) | 支持(共享存储) |
| 审计与复盘 | 无法实现 | 可以查询历史快照 |
| 长时间运行的工作流 | 不适合(可能 OOM) | 适合(存储外包) |
当前 Stub 的定位很明确:先把工作流跑通,验证节点拆分和快照设计;持久化是下一步,但快照字段设计从一开始就要为持久化做准备——这也是为什么WorkflowNodeSnapshot里有一组结构化字段,而不是随便写一个output就完事。
关键节点必须经过 HumanApprovalNode
接口设计是一个关键节点。
如果接口设计错了,后面的测试用例、发布检查清单都会跟着错。
ApiDesignNode内嵌了HumanApprovalNode:
ApiDesignNode 执行 ↓ 输出接口草案(请求/响应/错误码) ↓ HumanApprovalNode 暂停 ↓ 审批人看到草案 + 上一节点输出 ↓ 审批(通过 / 驳回) ↓ APPROVED → 继续下一个节点 REJECTED → 打回修改审批人看到的是结构化输出,不是原始提示词界面。这是企业协作的基础——让人看到该看的,不需要理解 AI 内部逻辑。
不确定项要显式输出
本讲 Demo 会输出不确定项:
客服提醒渠道是短信、企微还是站内信,需要业务确认。这类信息不能藏在正文里。
工作流报告应该显式列出:
哪些事情已确定 哪些事情待确认 哪些节点需要人工审批 哪些输出不能直接执行企业 AI 应用不是追求模型看起来很自信,而是追求流程可靠。
企业避坑
第一个坑:不要把复杂任务都做成一次聊天。
复杂任务应该拆节点。
第二个坑:不要让关键节点自动越过审批。
接口设计、发布计划、生产变更都应该停一下。
第三个坑:不要丢失中间过程。
没有节点快照,失败后就很难恢复——更不要在内存里存快照,服务重启就全丢了。
第四个坑:不要隐藏不确定项。
AI 不确定的地方,应该变成流程里的待办。
从 Demo 到落地,还差什么
本讲 Demo 验证了"节点拆分 + 快照记录 + 人工审批 + 失败可恢复"的工作流基础,但企业 AI Workflow 落地还差几步:
状态持久化:当前 Stub 用内存模拟WorkflowStateRepository,真实项目需要把快照落库(PostgreSQL / MySQL)或存 Redis,配合定时清理策略,避免快照膨胀。
可视化审批界面:当前审批是代码里调用 API 触发,真实项目需要一个审批控制台(参考approval-console子模块),让非技术人员在页面上做审批操作。
工作流定义 DSL:当前节点定义是 Java 类,真实项目需要把工作流定义抽成 YAML 或 JSON,让业务人员可以配置流程而不需要改代码。
节点失败自动告警:某个节点连续失败 N 次后,应该自动通知工作流发起人,而不是等到人工发现工作流卡住了。
并行节点支持:当前 Demo 是串行节点(一个接一个),真实项目经常有"接口设计和测试用例生成可以并行"的需求,这需要WorkflowRuntime支持 DAG 调度。
第 9 讲的工作流编排能力,也是第 10 讲多 Agent 编排的基础——多 Agent 的输出最终也需要进入工作流、由人类做审批决策。
小结
企业 AI 应用的成熟形态不是:
用户提问 ↓ 模型回答而是:
任务输入 ↓ 节点执行(快照持久化) ↓ 人工审批(关键节点) ↓ 失败恢复(从上一个成功节点) ↓ 最终报告把 AI 放进工作流里,它才更像企业系统的一部分。