Skip to content

第 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?

🛠️ 动手实践

  1. 给 refundWorkflow 增加拒绝路径:resumeData.approved 为 false 时走通知步骤而非打款步骤。
  2. 配置 LibSQLStorage 后,发起挂起 → 重启 dev 服务 → 再 resume,验证状态确实持久化了。
  3. 设计一个"内容发布需两级审批"的流程(编辑审核 → 法务审核),实现连续两次 suspend/resume。

下一章:第 10 章 · Memory 对话历史