Java 8 老系统如何接入 AI:第 9 讲 - 企业 AI 应用不是一次聊天,而是一条可恢复工作流
很多 AI 应用刚开始都是一个聊天框。
用户输入需求,模型输出结果。
这适合原型验证,但进入企业流程后,很快会遇到问题:
text
中途失败怎么办?
关键节点谁确认?
上一次输出在哪里?
能不能从失败节点恢复?
每一步输入输出能不能审计?
所以第 9 讲要把一次性 AI 对话升级成工作流。
最终效果
代码目录:
text
code/spring-ai-enterprise-lab/labs/chapter09-ai-workflow
运行:
powershell
.\compile-and-run.ps1
Demo 会演示一个"需求到测试用例"的工作流:
text
需求提取
↓
接口设计
↓
人工审批
↓
测试用例生成
↓
发布检查清单
启动后,流程会停在 api-design 节点。
审批后,流程继续执行到 COMPLETED。
代码结构
核心代码分三块:
text
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 方法做了四件事:
text
创建 WorkflowRun
↓
运行 RequirementExtractNode,写入第一个快照
↓
把 lastOutput 交给 ApiDesignNode,写入第二个快照
↓
把流程状态改成 WAITING_APPROVAL,waitingNodeId=api-design
这里的 lastOutput() 很关键。
它表示节点之间不是靠一大段聊天上下文传递信息,而是靠上一个节点的结构化输出继续往下走:
text
需求原文
↓
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 节点。
审批前,流程停住:
text
status=WAITING_APPROVAL
waitingNodeId=api-design
审批后,approve 方法会:
text
找到 api-design 快照
↓
写入 approvedBy,并把节点状态改成 APPROVED
↓
把流程状态恢复为 RUNNING
↓
运行 TestcaseGenerateNode
↓
运行 ReleaseChecklistNode
↓
把流程状态改成 COMPLETED
所以这个 Demo 真正想表达的是:企业 AI 工作流要能暂停、能继续、能看到每一步的输入输出,而不是把所有事情塞进一次模型调用。
当前 Stub 还没有实现失败节点回退,但它已经把恢复所需的最小结构留出来了:每个节点都有 input、output 和 status。真实落地时,失败节点应该记录 FAILED、错误原因和重试次数,Runtime 从最近一个 SUCCESS 或 APPROVED 快照继续,而不是从头重跑。
每个节点都要有快照
启动工作流后,输出里会看到:
json
{
"status": "WAITING_APPROVAL",
"waitingNodeId": "api-design",
"snapshots": [
{
"nodeId": "requirement-extract",
"status": "SUCCESS"
},
{
"nodeId": "api-design",
"status": "WAITING_APPROVAL"
}
]
}
这就是企业工作流的基本形态。
不是模型一次性吐出全部内容,而是每一步都有记录:
text
节点 ID
节点输入
节点输出
节点状态
审批人
对应到当前 WorkflowNodeSnapshot,快照字段是:
json
{
"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 的启动输入是:
text
tenantId=demo
operatorId=u1001
requirementText=新增订单延迟预警功能:当订单超过 48 小时未发货时,系统需要提醒客服介入。
第一步,RequirementExtractNode 接收原始需求,输出需求摘要:
text
input=新增订单延迟预警功能:当订单超过 48 小时未发货时,系统需要提醒客服介入。
output=需求点:订单超过 48 小时未发货时触发延迟预警,并提醒客服介入。
status=SUCCESS
第二步,WorkflowRuntime 调用 run.lastOutput(),把这段需求摘要交给 ApiDesignNode:
text
input=需求点:订单超过 48 小时未发货时触发延迟预警,并提醒客服介入。
output=POST /api/orders/delay-alerts,输入 thresholdHours=48,输出待介入订单列表。
status=WAITING_APPROVAL
然后 Runtime 把整个流程停住:
text
WorkflowRun.status=WAITING_APPROVAL
WorkflowRun.waitingNodeId=api-design
这时还不会生成测试用例,也不会生成发布清单。
只有当审批调用发生:
text
runtime.approve(run, "api-design", true, "tech-lead")
Runtime 才继续执行后两个节点。
第三步,TestcaseGenerateNode 接收已经审批过的接口设计:
text
input=POST /api/orders/delay-alerts,输入 thresholdHours=48,输出待介入订单列表。
output=测试用例:48 小时边界、未发货订单、已发货订单、重复提醒幂等、客服可见性。
status=SUCCESS
第四步,ReleaseChecklistNode 接收测试用例输出:
text
input=测试用例:48 小时边界、未发货订单、已发货订单、重复提醒幂等、客服可见性。
output=发布检查:灰度租户、SQL 索引、告警阈值、客服通知模板、回滚开关。
status=SUCCESS
最后流程完成:
text
WorkflowRun.status=COMPLETED
WorkflowRun.waitingNodeId=""
这条时间线说明了一件事:节点之间传递的是上一个节点的 output,不是把所有原始上下文无限追加给模型。
失败恢复应该怎么落地
当前 Stub 只演示了成功和审批暂停,没有真正模拟 FAILED。但它的快照结构已经能说明恢复设计。
真实项目里,节点快照至少应该扩展成:
json
{
"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"
}
恢复时不是从需求提取重新开始,而是:
text
找到最后一个 SUCCESS / APPROVED 快照
↓
读取它的 output
↓
作为失败节点的 input
↓
重跑失败节点
↓
写入新的快照版本
比如 TestcaseGenerateNode 失败,最近的稳定节点是 api-design:
text
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:
text
ApiDesignNode 执行
↓
输出接口草案(请求/响应/错误码)
↓
HumanApprovalNode 暂停
↓
审批人看到草案 + 上一节点输出
↓
审批(通过 / 驳回)
↓
APPROVED → 继续下一个节点
REJECTED → 打回修改
审批人看到的是结构化输出,不是原始提示词界面。这是企业协作的基础------让人看到该看的,不需要理解 AI 内部逻辑。
不确定项要显式输出
本讲 Demo 会输出不确定项:
text
客服提醒渠道是短信、企微还是站内信,需要业务确认。
这类信息不能藏在正文里。
工作流报告应该显式列出:
text
哪些事情已确定
哪些事情待确认
哪些节点需要人工审批
哪些输出不能直接执行
企业 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 应用的成熟形态不是:
text
用户提问
↓
模型回答
而是:
text
任务输入
↓
节点执行(快照持久化)
↓
人工审批(关键节点)
↓
失败恢复(从上一个成功节点)
↓
最终报告
把 AI 放进工作流里,它才更像企业系统的一部分。