第 9 章 · Suspend & Resume 人机协同
本章目标:掌握 workflow 的挂起与恢复机制,实现"等待人工审批后继续执行"的 human-in-the-loop 流程。
9.1 为什么需要挂起
真实业务中大量关键动作不能全自动执行:
- 转账前需要人工复核;
- 群发邮件前需要主管审批;
- 大额退款需要风控签字。
这些环节的共同点是:流程要停下来,等外部输入后再继续——可能等几分钟,也可能等几天。Mastra 的 Suspend/Resume 机制把"暂停点"内建进 Workflow:挂起时执行状态持久化到 Storage,恢复时从断点精确续跑。
9.2 在步骤内触发挂起
execute 上下文中的 suspend() 可以随时中断当前步骤:
typescript
// src/mastra/workflows/refund-workflow.ts
import { createStep, createWorkflow } from '@mastra/core/workflows';
import { z } from 'zod';
const reviewStep = createStep({
id: 'review',
inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
outputSchema: z.object({
approved: z.boolean(),
reviewer: z.string(),
orderId: z.string(),
amount: z.number(),
}),
execute: async ({ inputData, suspend }) => {
// 大额订单必须人工审批 → 挂起流程
if (inputData.amount > 1000) {
// suspend 携带的数据会展示给审批人
await suspend({
message: `订单 ${inputData.orderId} 退款 ${inputData.amount} 元待审批`,
});
// suspend 抛出后,后续代码不会执行
}
// 小额订单自动通过(只有未挂起才会走到这里)
return {
approved: true,
reviewer: 'auto',
orderId: inputData.orderId,
amount: inputData.amount,
};
},
});suspend 是软中断
suspend() 通过抛出特定控制流信号实现中断,不要试图在它之后写"兜底逻辑"——那行代码只会在未挂起时执行。
9.3 resume:从断点恢复
typescript
// 触发工作流 → 运行至 review 步骤挂起
const run = await mastra.getWorkflow('refund').createRun();
await run.start({ inputData: { orderId: 'A1024', amount: 2500 } });
console.log(run.status); // 'suspended'
// ……几小时后,审批人在管理界面点了同意……
// 用同一个 run 恢复执行:提供审批结果作为注入数据
const result = await run.resume({
step: 'review',
resumeData: { approved: true, reviewer: '张经理' },
});typescript
// 恢复后步骤会重新执行 execute:通过 resumeData 区分两次进入
execute: async ({ inputData, resumeData, suspend }) => {
if (inputData.amount > 1000) {
if (!resumeData) {
// 第一次进入:没有审批数据 → 挂起等待
await suspend({ message: '等待审批' });
}
// 第二次进入:resumeData 携带审批结果
return {
approved: resumeData!.approved,
reviewer: resumeData!.reviewer,
orderId: inputData.orderId,
amount: inputData.amount,
};
}
return { approved: true, reviewer: 'auto', orderId: inputData.orderId, amount: inputData.amount };
}resumeData 是恢复时注入的载荷,与 inputData 分开传递,让"原始输入"和"人工补充"互不污染。
9.4 持久化:挂起为什么能跨进程存活
挂起的运行状态(已完成步骤、变量快照、挂起载荷)由 Storage 持久化保存:
typescript
// src/mastra/index.ts —— 生产环境配置持久化存储
import { Mastra } from '@mastra/core';
import { LibSQLStore } from '@mastra/libsql';
export const mastra = new Mastra({
storage: new LibSQLStore({ url: process.env.LIBSQL_URL! }),
// 有了 storage,服务重启后 run.resume() 依然可用
});这正是 Mastra 官方强调的能力:"pause indefinitely and resume where you left off"——审批拖了一周也没关系,状态躺在数据库里。
本章小结
- Suspend/Resume 让流程在任意步骤暂停等待外部输入,是实现 human-in-the-loop 的标准机制;
await suspend(payload)中断执行并携带说明数据;run.resume({ step, resumeData })注入结果续跑;- 挂起状态经 Storage 持久化,可跨越任意时长与服务重启;
- execute 可能被进入多次(挂起前 + 恢复后),用
resumeData是否存在区分分支。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. workflow 执行到 await suspend() 后会发生什么?
2. run.resume() 时传入的 resumeData 与最初 inputData 的关系是?
3. 挂起三天后才 resume,前提条件是什么?
4. 下列哪个场景最适合用 Suspend/Resume?
🛠️ 动手实践
- 给 refundWorkflow 增加拒绝路径:resumeData.approved 为 false 时走通知步骤而非打款步骤。
- 配置 LibSQLStorage 后,发起挂起 → 重启 dev 服务 → 再 resume,验证状态确实持久化了。
- 设计一个"内容发布需两级审批"的流程(编辑审核 → 法务审核),实现连续两次 suspend/resume。