Mastra 开发(04):Mastra 的图编排

QuibblerAgentQuibblerAgent 2026-11-11 约 23 分钟 3 次阅读

前三篇把单个 Agent 和工具跑通了:模型拿着一套工具,自己决定下一步做什么。但生产里另一类任务恰恰相反——步骤顺序是人定的,每步何时执行、失败了怎么办、中间要不要停下来等真人审批,都得写死在代码里。这类确定性流程就是 Workflow 的主场。本篇是系列第 4 篇,对应 Agent 系列第 6~8 篇的 LangGraph StateGraph:同样的图编排问题,看 Mastra 用 TypeScript 原生的方式怎么回答。全文按"分工 → 步骤定义 → 数据流 → 编排原语 → 人机协同 → 错误处理 → 实战"组织,最后照例留一个练习。

1、Agent 与 Workflow 的分工

先回答"什么时候上 Workflow"。Agent 的工具循环本质是一个隐式循环:模型看到工具列表,自己决定调哪个、调几次、何时停。这种开放式决策适合探索型任务——调研一个话题、排查一个 bug,路径本身不可预知,交给模型是对的。

代价是可控性。你无法在编译期保证"生成之后必须经过审核才能发布",也无法让流程在第 3 步挂起八小时等审批再继续。而这些恰好是生产流水线的硬需求:

  • 顺序固定:先抓取、再清洗、再生成、最后入库
  • 要挂起:等审批、等人工复核、等外部事件到达
  • 要重试:外部 API 抖动,按策略退避重试
  • 要审计:每步输入输出可查,执行路径可复现

判断标准很朴素:下一步谁说了算。由模型说了算,用 Agent;由代码说了算,用 Workflow。两者是组合关系而非替代——Workflow 的一个 step 里完全可以跑一个 Agent,把确定性骨架和开放性关节拼在一起,骨架管"必须先审后发",关节管"这一稿怎么写"。本篇实战就是这么干的。

还有一层工程红利:workflow 注册进 new Mastra({ workflows: {...} }) 之后,mastra dev 的 Studio 会把整张图可视化——节点、连边、每次运行走到哪一步,全部摊开在界面上。Agent 的隐式循环要靠翻 trace 还原路径,Workflow 的路径天生就是一张看得见的图,这是"确定性"送的可观测性。

2、createStep 步骤定义

Workflow 的最小构件是 step,用 createStep 定义。最小示例如下:

import { createStep } from '@mastra/core/workflows';
import { z } from 'zod';

const fetchSourceStep = createStep({
  id: 'fetch-source',
  inputSchema: z.object({ url: z.string() }),
  outputSchema: z.object({ markdown: z.string(), chars: z.number() }),
  execute: async ({ inputData }) => {
    const res = await fetch(inputData.url);
    const markdown = await res.text();
    return { markdown, chars: markdown.length };
  },
});

四个字段各司其职:id 是步骤标识;inputSchema、outputSchema 用 Zod 声明进出的数据形状;execute 写业务逻辑,入参解构出 inputData——它已被 schema 校验并推导好类型,inputData.url 就是 string。和第 2 篇的 createTool 摆在一起看,形似而神不同:

维度 createTool createStep
服务对象 模型,模型决定何时调用 代码,编排关系写死
description 必须写清,模型靠它决策 不需要,没人看说明书
execute 入参 { context } { inputData }
触发时机 工具循环里模型说了算 上一步完成即触发

最大的差异是 createStep 没有 description:步骤不是被模型"选中"的,是被代码排定的。去掉模型这层不确定性,换来的是类型检查贯穿全流程。还有一点工程上的好处:step 是普通值,定义一次就能在多个 workflow 里复用,测试时也能单独调用,粒度比"塞进某个 Agent 的工具列表"友好得多。

3、显式数据流模型

步骤定义好了怎么连?答案是链式编排加类型咬合,完整示例如下:

import { createWorkflow } from '@mastra/core/workflows';

const summarizeStep = createStep({
  id: 'summarize',
  inputSchema: z.object({ markdown: z.string(), chars: z.number() }),
  outputSchema: z.object({ summary: z.string() }),
  execute: async ({ inputData }) => ({ summary: `共 ${inputData.chars} 字` }),
});

const pipeline = createWorkflow({
  id: 'fetch-pipeline',
  inputSchema: z.object({ url: z.string() }),
  outputSchema: z.object({ summary: z.string() }),
})
  .then(fetchSourceStep)
  .then(summarizeStep)
  .commit();

const run = await pipeline.createRun();
const result = await run.start({ inputData: { url: 'https://example.com' } });

这套代码有三层语义。其一,上一步的输出就是下一步的输入:fetchSourceStep 的 outputSchema 与 summarizeStep 的 inputSchema 逐字段咬合,不匹配编辑器直接标红——schema 即契约,且在编译期检查。其二,.commit() 把链定型成一张固定的图,之后可以无数次 createRun(),每次运行互不干扰。其三,run.start() 的返回值是按 status 区分的联合类型:success 时取 result.result,failed 时取 result.error,suspended 的处理见第 5 节。

相邻两步形状不一致时不硬凑 schema,而是用 .map() 显式变换、.unmap() 取回变换前的数据——变换本身也要过类型检查。最典型的场景在并行之后:parallel 的下游收到的是数组,想回到单个对象,就在中间垫一层 .map() 把数组归并成下一步要的形状。这是与 LangGraph 哲学分野的地方:

维度 LangGraph Mastra
状态载体 全图共享 State 对象 步骤间显式输入输出
并发写 需声明 reducer 合并策略 parallel 自动收进数组
契约检查 主要靠运行时校验 编译期类型推导
改字段影响 全图排查谁读了它 定位相邻两步

LangGraph 是黑板模型:节点读写同一个 State,并发写同一字段要靠 reducer 声明合并策略,字段一多心智负担在"谁动了我的 state"。Mastra 是管道模型:数据穿隧道,每步只声明吃什么吐什么。管道的短板是不适合处处要读的全局变量——1.x 为此补了可选的 workflow state:createWorkflow 里加 stateSchema,execute 里解构 state 与 setState,挂起恢复后保留,嵌套子流程也能继承。默认走管道、确有全局诉求再开 state,这是官方Workflow 文档的顺序,也是我的建议。

注意:Workflow API 在 1.x 线上仍在快速演进,本文以 @mastra/core 1.79 为基准,动手前建议对照官方文档。

4、四大编排原语

then 串行已见,剩下三个原语各给最小代码。

// 条件分支:条件是普通函数,返回 true 的分支执行
reviewFlow.branch([
  [({ approved }) => approved, publishStep],
  [({ approved }) => !approved, rejectStep],
]);

// 并行:所有分支拿到同一份输入,结果按声明顺序收进数组
pipeline.parallel(summarizeStep, translateStep, extractTagsStep);

// 循环:先执行后判断,条件成立即停
pipeline.dountil(reviseStep, (output) => output.approved);

branch 的条件函数收到上一步输出,实战里习惯写成互补对,兜底就不会漏;若所有条件都不命中,这一层不派发任何分支,所以互补条件值得当成纪律来写。parallel 是 fan-out:三个分支同时开跑,下游步骤的 inputSchema 把结果声明为数组即可——不需要 reducer,合并逻辑显式写在下游。dountil 是循环原语,注意它和 dowhile 都是先执行再判断、至少跑一轮;条件函数第二个参数是已循环轮数 loopCount,熔断就靠它,练习会用到。另有两个轻量原语提一句:sleep(ms) 与 sleepUntil(date) 是等待步骤,适合"轮询前先歇五分钟"这类定时场景;.foreach() 对数组输入逐项处理,concurrency 默认 1、调大即并发。完整清单见控制流文档。

5、人机协同中断恢复

审批是 Workflow 相对 Agent 最硬的优势:流程可以挂起,等人操作后原位继续。挂起步骤的定义如下:

const approveStep = createStep({
  id: 'human-approve',
  inputSchema: z.object({ draft: z.string() }),
  outputSchema: z.object({ approved: z.boolean() }),
  execute: async ({ inputData, resumeData, suspend, bail }) => {
    if (!resumeData) {
      // 首轮:挂起,resumeData 为 undefined
      await suspend({ draft: inputData.draft, note: '请审批' });
      return { approved: false }; // 满足类型检查,实际不会执行到
    }
    if (resumeData.reject) {
      await bail(); // 打回:整个 workflow 以 failed 终止
    }
    return { approved: resumeData.approved };
  },
});

suspend 从 execute 上下文解构而来:调用后流程在此冻结,run.start() 正常返回,result.status 为 'suspended',result.suspendPayload 带着你给审批人看的数据。审批结果用 resume 灌回:

const result = await run.start({ inputData: { draft: '……' } });

if (result.status === 'suspended') {
  // 真实场景:把 suspendPayload 推给审批人,隔天再恢复
  const final = await run.resume({
    resumeAs: 'human-approve', // 只有一个挂起步骤时可省略
    resumeData: { approved: true },
  });
}

恢复后该步骤从头重跑 execute,这次 resumeData 有值,走下半段逻辑。关键认知是:挂起的是运行记录,不是进程——runId 持久化在 storage 里,跨进程、隔天、服务器重启后恢复都行,配好 LibSQL 或 Postgres 存储后端即可长期挂着等。调试也方便:Studio 的 Playground 已支持直接触发挂起并手动恢复,不用先写审批界面就能把人机协同跑通。对照 LangGraph 的 interrupt(Agent 系列第 11 篇讲过):那边靠 checkpointer 存档加 Command(resume=...) 恢复,Mastra 把同一件事收进 suspend 与 resume 两个原语,语义更窄也更直白。bail() 是拒绝通道:审批人打回,整条流程立刻以 failed 收场。更多模式(多轮交互、按 URL 远程恢复)见人机协同文档。

6、错误处理与重试

step 抛出异常,流程立刻中断:result.status === 'failed',result.error 带堆栈,默认不重试。瞬时故障想自动扛过去,用 try 配置,写在链上即可:

pipeline
  .then(fetchSourceStep.try({ attempts: 3, delayMs: 500 }))
  .then(summarizeStep)

attempts 是总尝试次数,delayMs 是重试间隔,默认 1 次、不等待;也支持函数形式,按错误对象动态决定次数。等价写法是在 createStep 配置里直接放 tryConfig。要点是重试是编排能力,不侵入业务代码:execute 里不写 while 和退避,换一个流程复用同一个 step 时,重试策略跟着编排走而不是跟着步骤走。除自动重试外还有两类兜底:给 createWorkflow 配 onStepStatusChange 之类的生命周期回调,可以在步骤状态翻转时发告警;Studio 的运行视图会直接标出失败步骤与重试轨迹,排障不用翻日志。另一类"再来一次"是业务级的——生成质量不达标重写——那不是异常,是循环,用 dountil 表达。两类机制别混用,细节见错误处理文档。

7、写作审稿修订实战

Agent 系列第 8 篇用 StateGraph 做过"写作-审稿-修订"循环,同款任务换 Mastra 重写,正好检验前六节。两个 Agent(写手 writer、审稿人 reviewer,定义沿用第 2 篇写法),三个 step,修订加复审打包成子流程循环。先看前两个步骤:

const draftSchema = z.object({ draft: z.string() });
const reviewSchema = z.object({ approved: z.boolean(), comments: z.string() });

const draftStep = createStep({
  id: 'draft',
  inputSchema: z.object({ topic: z.string() }),
  outputSchema: draftSchema,
  execute: async ({ inputData }) => {
    const { object } = await writer.generate(
      `就主题「${inputData.topic}」写一段 300 字技术导语`,
      { output: draftSchema },
    );
    return object;
  },
});

const reviewStep = createStep({
  id: 'review',
  inputSchema: draftSchema,
  outputSchema: reviewSchema,
  execute: async ({ inputData }) => {
    const { object } = await reviewer.generate(
      `审阅这段导语能否发布:\n${inputData.draft}`,
      { output: reviewSchema },
    );
    return object;
  },
});

Agent 进 step 就是普通函数调用:generate 配 output 选项拿到结构化的 object(写法见结构化输出文档),返回值恰好就是 step 的 outputSchema——同一份 Zod schema 从模型输出贯穿到流程编排。修订步骤同构,只是输入换成审稿意见:

const reviseStep = createStep({
  id: 'revise',
  inputSchema: reviewSchema,
  outputSchema: draftSchema,
  execute: async ({ inputData }) => {
    const { object } = await writer.generate(
      `按审稿意见重写导语:${inputData.comments}`,
      { output: draftSchema },
    );
    return object;
  },
});

reviseStep 吃 reviewSchema 吐 draftSchema,正好把审稿结论翻译回新的草稿。有了它,循环体和主流程就能组装起来:

// 子流程:按意见修订一轮并复审,输入输出同为 reviewSchema
const reviseOnce = createWorkflow({
  id: 'revise-once',
  inputSchema: reviewSchema,
  outputSchema: reviewSchema,
})
  .then(reviseStep)
  .then(reviewStep)
  .commit();

const writingWorkflow = createWorkflow({
  id: 'writing-review-loop',
  inputSchema: z.object({ topic: z.string() }),
  outputSchema: reviewSchema,
})
  .then(draftStep)
  .then(reviewStep)
  .dountil(reviseOnce, ({ approved }) => approved)
  .commit();

两个细节。其一,workflow 本身可以当 step 用:reviseOnce 嵌进 dountil,循环体是"修订加复审"两步——这正是 LangGraph 里"条件边指回上游节点"的等价写法,差别是 Mastra 要求循环体收敛成一个可复用单元。其二,dountil 先跑再判:首审已通过时也会多修订一轮,演示可接受,生产可在循环前用 branch 拦一道。运行与取结果:

const run = await writingWorkflow.createRun();
const result = await run.start({ inputData: { topic: 'Mastra Workflow' } });

console.log(result.status);  // success / failed / suspended
console.log(result.result);  // 最新一轮审稿结论

长流程不想干等,把 start 换成 run.stream() 即可逐事件拿进度:走到哪个 step、每轮审稿结论如何,边跑边出。对照 Agent 系列第 8 篇:那边是节点加条件边,状态在共享 State 里流转;这边是链加循环原语,数据在 schema 间咬合。行数相近,但这份代码从输入到输出全程有类型兜底——改一个字段名,编辑器立刻告诉你断在哪两步之间。

8、总结

回顾全文:Workflow 管确定性流程、Agent 管开放式决策,两者组合而非替代;createStep 用 Zod 声明输入输出,没有 description,因为不需要模型做选择;数据流是显式管道,上一步输出即下一步输入,map 补桥、stateSchema 兜全局;then、branch、parallel、dountil 覆盖串行、条件、并行、循环;suspend 与 resume 挂起的是运行记录,跨进程恢复不丢;try 让重试跟着编排走。实战把七件事串成了一根全程可类型检查的流水线。

练习:给实战加熔断——最多修订三轮,三轮仍不通过就带最后一轮意见终止。两个提示:dountil 的条件函数第二个参数是 loopCount;若希望终止时同时输出"结论加轮数",把主流程 outputSchema 换成能携带两者的形状。对于正在把 Agent 能力产品化的工程师,建议挑一段手头最像流水线的胶水代码用 Workflow 重写,再跑 mastra dev 看 Studio 的图视图——哪步慢、哪步挂、哪步在等人,一眼见底。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
Multi-Agent 详解:多智能体协作架构与实践
精选Agent

Multi-Agent 详解:多智能体协作架构与实践

一个智能体配上一套工具,已经能完成相当多任务。但让它同时“当产品经理、写后端、跑测试、做代码评审”,很快就会露馅:上下文里塞满了互相干扰的指令,十几件工具的清单来回切换,任务做到一半忘了开头。解法看起来很自然——像人类团队一样分工:让多个智能体各司其职,由一个协调者调度,这就是 Multi-Agent(多智能体)系统。它也是 2026 年智能体工程里讨论度最高、误用率也最高的架构。本文按“架构模式…

122