1. 项目概述:一个前端工程师的真实转型切口
“前端转全栈笔记:前端开发者入门 Agent,为什么我选择 Mastra”——这个标题里藏着三重真实需求:第一层是职业路径的主动迁移,不是被裁后的被动求生,而是前端工程师在 React/Vue 已成肌肉记忆、工程化工具链烂熟于心之后,对“系统级影响力”的自然渴求;第二层是技术选型的理性判断,不是跟风炒作“Agent 热”,而是面对 LangChain、LlamaIndex、AutoGen、CrewAI 等十余个主流框架时,一个有完整 CI/CD 经验、写过 BFF 层、部署过 SSR 应用的人,如何用前端工程师的直觉去评估“可维护性”“调试友好度”“与现有基建兼容性”;第三层是落地节奏的务实控制,拒绝“三个月从零造 AGI”的幻觉,而是把 Agent 当作一个可拆解、可测试、可灰度发布的模块来设计。
我试过用 Next.js + Vercel AI SDK 搭建一个带记忆的客服对话流,也用 Express + OpenAI SDK 写过带工具调用的订单查询服务,但真正让我停下手头所有 demo、花三天通读 Mastra 文档并重构整个 POC 的,是它把“Agent 生命周期”这件事,用前端工程师最熟悉的方式重新定义了:状态管理像 Zustand,流程编排像 React Query 的依赖链,错误重试像 SWR 的 fallback 机制,甚至日志追踪都默认集成 OpenTelemetry 并自动关联 trace ID。这不是“用 JS 写 Agent”,而是“用前端工程思维重构 Agent 开发范式”。
适合谁看?如果你已经能独立完成 Ant Design Pro 项目、能手写 Webpack 插件、能看懂 Chrome DevTools 的 Performance 面板,但每次看到 LangChain 的 Chain 类继承树就头皮发紧;如果你的团队已有成熟的 Node.js BFF 层,不想为一个智能体功能单独起一套 Python 微服务;如果你需要向产品同事演示“这个按钮点下去,背后是 Agent 在调度三个 API、校验两次数据、最后生成 PDF 报告”,而不是解释“我们用了 RAG 和 ReAct 框架”——那这篇笔记就是为你写的。它不教你怎么调大模型 API,而是告诉你:当你的 React 组件树已经长到 200 行、你的前端监控平台每天上报 50 万条性能数据、你的 Git 提交记录里有 37 次 “fix: handle edge case in form validation” 时,Agent 不该是另一个黑盒,而应是你工程能力的自然延伸。
2. 核心思路拆解:为什么不是 LangChain,也不是 AutoGen?
2.1 前端工程师的“心智负担”到底卡在哪?
先说结论:LangChain 的核心抽象是Chain → LLM → Tool → OutputParser,这是一套典型的命令式、面向过程的编程范式。而前端工程师的日常是Component → State → Effect → UI Update,这是声明式、响应式、以状态为中心的思维模式。两者不是技术优劣问题,而是认知摩擦问题。
举个具体例子:实现“用户输入订单号,Agent 查询物流并返回预计送达时间”。在 LangChain 中,你需要:
- 定义一个
OrderQueryTool类,继承BaseTool,实现run()方法; - 在
run()里手动处理 HTTP 错误、超时、重试逻辑; - 编写
OutputParser解析 JSON 响应,还要处理字段缺失、类型错误等边界情况; - 最后把 Tool 注入到
LLMChain或AgentExecutor,再配置AgentType.ZERO_SHOT_REACT_DESCRIPTION—— 这个枚举值名字本身就在暗示:你得先理解 ReAct 论文。
而 Mastra 的写法是:
// mastra.config.ts export const config = { tools: [ { id: "get-order-status", name: "查询订单状态", description: "根据订单号获取物流信息和预计送达时间", schema: z.object({ orderNo: z.string().describe("16位纯数字订单号"), }), execute: async ({ orderNo }) => { const res = await fetch(`/api/orders/${orderNo}/status`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }, } ], // 其他配置... };注意三点:第一,schema直接用 Zod 定义,前端人秒懂;第二,execute是个 async 函数,和你在useEffect里写的逻辑完全一致;第三,没有Chain、没有Executor、没有AgentType,只有“工具注册”和“执行逻辑”。这不是简化,而是把抽象层级拉回到前端工程师熟悉的“函数即服务”层面。
2.2 为什么 AutoGen 的多 Agent 协作反而增加复杂度?
AutoGen 的亮点是GroupChatManager,支持多个 Agent 角色(Coder、Reviewer、Executor)协作。但对前端转全栈者而言,这恰恰是陷阱。因为它的协作基于消息队列和角色状态机,调试时你得同时打开 4 个终端看不同 Agent 的日志,而每个 Agent 的generate_reply()方法里又嵌套着 LLM 调用、工具调用、条件分支——这比调试一个有 12 个 useEffect 的复杂表单还痛苦。
Mastra 的解法很“前端”:它不预设多 Agent 架构,而是提供createWorkflow()函数,让你用 Promise 链或 async/await 显式编排步骤。比如实现“先查订单,再查库存,最后生成发货单”的流程:
export const shippingWorkflow = createWorkflow({ id: "generate-shipping-doc", steps: [ { id: "fetch-order", tool: "get-order-status", input: (ctx) => ({ orderNo: ctx.input.orderNo }), }, { id: "check-stock", tool: "check-inventory", input: (ctx) => ({ sku: ctx.steps["fetch-order"].output.sku }), if: (ctx) => ctx.steps["fetch-order"].output.status === "paid", }, { id: "generate-pdf", tool: "render-pdf", input: (ctx) => ({ order: ctx.steps["fetch-order"].output, stock: ctx.steps["check-stock"]?.output, }), if: (ctx) => !!ctx.steps["check-stock"]?.output, }, ], });这个 workflow 的每个step都是独立可测试的单元:你可以 mockctx.steps["fetch-order"].output直接测试check-stock步骤;可以给if条件写单元测试;甚至可以把整个 workflow 导出为 OpenAPI Schema 供前端调用。这才是前端工程师理解的“可组合、可测试、可文档化”。
2.3 Mastra 的底层架构:为什么它天然适配前端基建?
Mastra 的核心设计哲学是“Agent 即函数,Workflow 即组件”。它的运行时(runtime)本质是一个轻量级的 Node.js 执行引擎,但关键在于它把所有基础设施能力都封装成了“可插拔模块”,且每个模块的接口都刻意模仿前端生态:
- 状态管理:默认使用
@mastra/state,底层是基于 IndexedDB 的持久化 Zustand store,支持useStore()Hook; - 日志追踪:内置 OpenTelemetry,但暴露
useTracer()Hook,和@opentelemetry/web的用法一致; - 错误处理:提供
retryWithExponentialBackoff()工具函数,参数签名和SWRConfig.retry完全相同; - 环境配置:用
mastra.config.ts文件,支持defineConfig({}),和 Vite 的defineConfig一模一样。
这意味着什么?意味着你现有的前端工程能力可以直接复用:
- 你写过自定义 Hook 封装 localStorage?那
useStore()对你毫无学习成本; - 你配置过 Sentry 的
beforeSend过滤日志?那useTracer().startSpan()的用法你闭着眼都会; - 你优化过 Webpack 的 splitChunks?那
mastra.config.ts里的tools: []数组,就是你的代码分割点。
这不是“让前端学后端”,而是“让后端能力以前端方式交付”。当你发现mastra.config.ts里加一行tools: [myCustomTool]就能让整个 Agent 系统获得新能力时,那种掌控感,和你在vite.config.ts里加plugins: [vitePluginSvgr]后 SVG 图标直接变成 React 组件是一样的。
3. 核心细节解析:从零搭建一个可上线的 Agent 服务
3.1 环境准备:避开 Node.js 版本和包管理器的坑
Mastra 官方要求 Node.js ≥ 18.17.0,但实际踩坑点在于pnpm 的 workspace 协议。如果你用pnpm create mastra@latest初始化项目,它会生成一个 monorepo 结构,包含apps/api(服务端)、packages/core(核心库)、packages/ui(可选管理界面)。但很多前端工程师习惯用npm init -y创建项目,然后npm install @mastra/core—— 这会导致@mastra/core无法正确解析其 peer dependencies(如zod,openai,@opentelemetry/api),报错Cannot find module 'zod'。
正确做法是:
- 全局安装 pnpm:
npm install -g pnpm(别用 npm install pnpm,会有权限问题); - 创建空目录,进入后执行:
pnpm create mastra@latest --use-pnpm; - 选择
api模板(不要选 fullstack,前端转全栈先聚焦服务端); - 初始化完成后,立即执行
pnpm build,而不是先pnpm dev—— 因为 Mastra 的 dev server 依赖构建产物,未构建直接启动会报Cannot find module './dist/index.js'。
提示:如果公司内网限制 pnpm registry,需提前配置
.pnpmrc:registry=https://registry.npmjs.org/ strict-ssl=false注意:
strict-ssl=false仅限内网测试环境,生产必须用 HTTPS。
3.2 工具开发:如何把一个前端 API 调用封装成 Agent 工具
假设你已有一个前端项目,里面有个useOrderApi()自定义 Hook,用于调用/api/orders/:id。现在要把它变成 Mastra 工具。关键不是“怎么写”,而是“怎么写得像前端人写的”。
第一步:创建工具文件packages/core/src/tools/order-tool.ts:
import { z } from 'zod'; import { createTool } from '@mastra/core'; // 复用前端已有的类型定义 export const OrderSchema = z.object({ id: z.string().regex(/^\d{16}$/, '订单号必须是16位数字'), status: z.enum(['pending', 'paid', 'shipped', 'delivered']), }); // 复用前端已有的 API client(重点!) import { apiClient } from '../api/client'; // 这个 client 和前端用的是同一份代码 export const getOrderTool = createTool({ id: 'get-order', name: '查询订单详情', description: '根据订单ID获取完整订单信息,包括商品、地址、物流状态', schema: z.object({ orderId: z.string().describe('16位数字订单ID'), }), // execute 函数里直接调用前端 API client execute: async ({ orderId }) => { try { // 注意:这里用的是和前端完全相同的 apiClient const data = await apiClient.get(`/orders/${orderId}`); // 前端已有的数据校验逻辑,直接复用 return OrderSchema.parse(data); } catch (error) { if (error instanceof Response && error.status === 404) { throw new Error(`订单 ${orderId} 不存在`); } throw new Error(`查询订单失败:${error.message}`); } }, });第二步:在mastra.config.ts中注册:
import { getOrderTool } from './tools/order-tool'; export const config = defineConfig({ tools: [getOrderTool], // 就这一行,没有其他配置 // 其他配置... });为什么这样设计是“前端友好”的?
- 类型复用:
OrderSchema和前端组件里useOrderApi()返回的类型完全一致,改一处,前后端同时生效; - 逻辑复用:
apiClient是前端项目里已封装好的 Axios 实例,带统一拦截器、错误处理、token 注入; - 错误语义复用:
throw new Error('订单不存在')和前端toast.error('订单不存在')的文案完全一致,产品同学验收时不会困惑。
3.3 Workflow 编排:用 React 思维写业务流程
继续上面的例子,现在要实现“用户提交售后申请 → Agent 自动判断是否可退 → 若可退则生成退货单 → 发送短信通知”。这不是一个 LLM 调用就能解决的,而是典型的多步骤、有条件分支、需人工审核介入的业务流。
Mastra 的createWorkflow()就是为此设计的。在packages/core/src/workflows/return-workflow.ts中:
import { createWorkflow, z } from '@mastra/core'; import { getOrderTool } from '../tools/order-tool'; import { checkRefundEligibilityTool } from '../tools/refund-tool'; import { generateReturnLabelTool } from '../tools/label-tool'; import { sendSmsTool } from '../tools/sms-tool'; export const returnWorkflow = createWorkflow({ id: 'process-return-request', // 输入 schema 必须严格定义,这是前端工程师的底线 input: z.object({ orderId: z.string().regex(/^\d{16}$/, '订单号格式错误'), reason: z.enum(['quality', 'wrong-item', 'no-need']).describe('退货原因'), }), steps: [ // 步骤1:查订单 { id: 'fetch-order', tool: getOrderTool, input: (ctx) => ({ orderId: ctx.input.orderId }), }, // 步骤2:判断是否可退(带条件) { id: 'check-eligibility', tool: checkRefundEligibilityTool, input: (ctx) => ({ order: ctx.steps['fetch-order'].output, reason: ctx.input.reason, }), // 关键:if 字段决定是否执行此步骤 if: (ctx) => ctx.steps['fetch-order'].output.status === 'delivered' && Date.now() - new Date(ctx.steps['fetch-order'].output.shippedAt).getTime() < 7 * 24 * 60 * 60 * 1000, }, // 步骤3:生成退货单(仅当 eligibility 通过才执行) { id: 'generate-label', tool: generateReturnLabelTool, input: (ctx) => ({ orderId: ctx.input.orderId, address: ctx.steps['fetch-order'].output.shippingAddress, }), if: (ctx) => !!ctx.steps['check-eligibility']?.output?.eligible, }, // 步骤4:发送短信(最终步骤,无 if 条件) { id: 'send-notification', tool: sendSmsTool, input: (ctx) => ({ phone: ctx.steps['fetch-order'].output.customerPhone, message: `您的退货申请已受理,退货单号:${ctx.steps['generate-label']?.output?.labelNo || '待生成'}`, }), }, ], });这个 workflow 的每一个step都可以独立测试。比如测试check-eligibility步骤:
// packages/core/src/workflows/return-workflow.test.ts import { returnWorkflow } from './return-workflow'; import { mockTool } from '@mastra/test-utils'; describe('returnWorkflow', () => { it('should skip eligibility check for undelivered orders', async () => { const workflow = returnWorkflow.withTools({ 'get-order': mockTool({ output: { status: 'paid', shippedAt: '2024-01-01' }, }), // 其他工具 mock... }); const result = await workflow.execute({ orderId: '1234567890123456', reason: 'quality', }); // 断言:check-eligibility 步骤未执行 expect(result.steps['check-eligibility']).toBeUndefined(); }); });这种测试方式,和你在 Jest 里测试useEffect的依赖更新逻辑,或者测试React.memo的 shouldComponentUpdate,是完全同源的思维。
3.4 服务暴露:如何让前端安全调用 Agent
Mastra 默认生成apps/api/src/routes/workflows.ts,里面是标准的 Express 路由。但直接暴露POST /workflows/process-return-request给前端存在两个风险:一是跨域问题(前端在https://app.example.com,API 在https://api.example.com),二是认证缺失(任何拿到 URL 的人都能调用)。
解决方案是:用前端已有的认证体系接管 Agent 调用。假设你的前端用 JWT,登录后存到localStorage.token,并在请求头加Authorization: Bearer xxx。
第一步:修改apps/api/src/middleware/auth.ts:
import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; export const requireAuth = (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Unauthorized' }); } const token = authHeader.split(' ')[1]; try { // 复用前端 JWT secret(必须前后端一致) const payload = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }; (req as any).userId = payload.userId; next(); } catch (err) { res.status(401).json({ error: 'Invalid token' }); } };第二步:在apps/api/src/routes/workflows.ts中应用中间件:
import { requireAuth } from '../middleware/auth'; // 所有 workflow 路由都加认证 router.post('/:workflowId', requireAuth, async (req, res) => { // ...原有逻辑 });第三步:前端调用时,复用现有 axios 实例:
// frontend/src/lib/agent-client.ts import axios from 'axios'; const agentClient = axios.create({ baseURL: 'https://api.example.com/workflows', headers: { 'Content-Type': 'application/json', }, }); // 自动注入 token agentClient.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); export const callReturnWorkflow = (data: { orderId: string; reason: string }) => agentClient.post('/process-return-request', data);这样做的好处是:
- 前端不用学新认证方式,
localStorage.token和axios.interceptors都是现成的; - 后端不用维护两套用户体系,JWT secret 一份配置;
- 安全审计时,所有 Agent 调用都走统一的
requireAuth中间件,日志格式和普通 API 完全一致。
4. 实操过程详解:从本地开发到生产部署的全流程
4.1 本地开发:如何高效调试 Agent 执行流
Mastra 的本地调试体验,是我选择它的最关键理由。它不像 LangChain 那样需要console.log塞满代码,也不像 AutoGen 那样要在终端里tail -f日志文件。它的调试方式,就是前端工程师最熟悉的——Chrome DevTools。
当你运行pnpm dev启动服务后,访问http://localhost:3000/debug,会看到一个类似 React DevTools 的可视化界面:
- 左侧是 workflow 的 DAG 图,每个节点显示
id、status(pending/running/success/error)、duration; - 点击节点,右侧显示该步骤的
input(JSON 格式,可折叠)、output(JSON 格式,可折叠)、logs(结构化日志,带时间戳和 level); - 最关键的是
trace标签页:它展示完整的 OpenTelemetry trace,包括 LLM 调用耗时、工具执行耗时、网络延迟,甚至能点击跳转到对应的代码行(需 source map)。
实操心得:
- 不要在
execute函数里写console.log,Mastra 的日志系统会自动捕获console.*,但会丢失上下文。正确做法是用getLogger().info():
这样日志会自动带上import { getLogger } from '@mastra/core'; const logger = getLogger('order-tool'); logger.info('Fetching order', { orderId });workflow_id、step_id、trace_id,在 debug 页面里可精准过滤。 - 调试条件分支时,用
if字段的返回值做断点:在if函数里加debugger,Chrome 会自动在满足条件时暂停,比console.log看布尔值直观十倍。 - Mock 工具时,用
mockTool({ output: {...} })而不是jest.mock:前者能保持完整的 trace 链路,后者会断开 OpenTelemetry 上下文。
4.2 测试策略:为什么单元测试比 E2E 更重要
Agent 系统的 E2E 测试(比如用 Cypress 模拟用户点击“申请退货”按钮)价值极低,因为:
- LLM 调用不稳定,同一 prompt 可能返回不同 JSON;
- 外部 API(如物流查询)不可控,测试环境常返回 404;
- workflow 执行路径多,覆盖所有分支成本极高。
Mastra 的测试哲学是:只测试确定性部分,放行不确定性部分。确定性部分包括:
- 工具的
inputschema 校验(Zod); if条件函数的布尔逻辑;- workflow 的
steps依赖关系(A 步骤的输出是否被 B 步骤的input正确引用); - 错误处理逻辑(如网络失败时是否抛出预期错误)。
一个典型的测试文件packages/core/src/tools/refund-tool.test.ts:
import { refundEligibilityTool } from './refund-tool'; import { mockTool } from '@mastra/test-utils'; describe('refundEligibilityTool', () => { it('should return eligible for delivered orders within 7 days', () => { const now = Date.now(); const order = { status: 'delivered' as const, shippedAt: new Date(now - 3 * 24 * 60 * 60 * 1000).toISOString(), }; const result = refundEligibilityTool.execute({ order, reason: 'quality', }); expect(result.eligible).toBe(true); }); it('should return ineligible for orders shipped 10 days ago', () => { const now = Date.now(); const order = { status: 'delivered' as const, shippedAt: new Date(now - 10 * 24 * 60 * 60 * 1000).toISOString(), }; const result = refundEligibilityTool.execute({ order, reason: 'quality', }); expect(result.eligible).toBe(false); }); });这些测试运行速度是毫秒级的,且 100% 确定。而 LLM 调用部分,Mastra 提供@mastra/llm-mock包,可以全局 mock:
// vitest.setup.ts import { mockLLM } from '@mastra/llm-mock'; mockLLM({ provider: 'openai', model: 'gpt-4-turbo', response: JSON.stringify({ action: 'APPROVE', reason: '符合7天无理由政策' }), });这样,所有涉及 LLM 的 workflow 测试,都走 mock 响应,既保证覆盖率,又不依赖网络。
4.3 生产部署:如何用现有 CI/CD 流程发布 Agent
假设你的公司已有一套成熟的 CI/CD:GitLab CI 触发,构建 Docker 镜像,推送到私有 Harbor,K8s Helm Chart 部署。Mastra 的部署没有任何特殊要求,因为它就是一个标准的 Node.js Express 应用。
关键配置点:
- Dockerfile:必须用
node:18-alpine基础镜像,因为 Mastra 依赖sharp(图片处理库),而sharp在 Alpine 上需预编译:FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY dist ./dist COPY node_modules ./node_modules EXPOSE 3000 CMD ["node", "dist/index.js"] - 环境变量:Mastra 通过
process.env读取配置,所以JWT_SECRET、OPENAI_API_KEY等必须作为 K8s Secret 挂载,绝不能写进代码或 config 文件。 - 健康检查:Mastra 默认提供
/health端点,返回{ status: 'ok', timestamp: Date.now() },K8s livenessProbe 可直接用。
部署后验证 checklist:
- 访问
https://api.example.com/health,确认返回 200; - 访问
https://api.example.com/debug,确认能看到 workflow 列表(需配置MASTRA_DEBUG=true环境变量,生产环境默认关闭); - 用 curl 测试一个简单 workflow:
预期返回curl -X POST https://api.example.com/workflows/get-order \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"orderId": "1234567890123456"}'{"id":"1234567890123456","status":"delivered",...}。
注意:生产环境务必设置
MASTRA_DEBUG=false,否则/debug页面会暴露所有 workflow 的输入输出,存在严重安全风险。
4.4 监控告警:复用前端监控体系
Mastra 的 OpenTelemetry 支持导出到多种后端:Jaeger、Zipkin、Datadog、New Relic。但对前端团队来说,最省事的是对接已有的前端监控平台(如 Sentry)。
配置方法(apps/api/src/otel.ts):
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { Resource } from '@opentelemetry/resources'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; const provider = new NodeTracerProvider({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'mastra-api', }), }); provider.addSpanProcessor( new BatchSpanProcessor( new OTLPTraceExporter({ url: 'https://o123456.ingest.sentry.io/v1/traces', // Sentry 的 traces endpoint headers: { 'Authorization': `DSN ${process.env.SENTRY_DSN}`, }, }) ) ); registerInstrumentations({ instrumentations: [ getNodeAutoInstrumentations(), ], tracerProvider: provider, });这样配置后,所有 workflow 的 trace 会自动出现在 Sentry 的 Performance 页面,和前端的useEffect耗时、API 请求耗时在同一视图里对比。当发现某个 workflow 平均耗时突增,你可以直接点击 trace,看到是 LLM 调用慢了,还是check-inventory工具的数据库查询慢了,还是网络延迟高了——这和你在 Sentry 里分析一个慢加载的 React 组件,思路完全一致。
5. 常见问题与排查技巧实录
5.1 工具执行失败:90% 的问题出在 Zod Schema
现象:调用 workflow 时返回400 Bad Request,错误信息是Validation failed: ...,但input数据看起来完全正确。
原因:Zod 的regex()、email()等校验器在不同环境下行为不一致。比如z.string().email()在 Node.js 里会校验 DNS,而你的测试环境可能没配 DNS,导致校验失败。
排查步骤:
- 在
execute函数开头加日志:console.log('Raw input:', JSON.stringify(input, null, 2)); - 在
schema定义后加.parse()的显式调用,并捕获错误:try { const parsed = MySchema.parse(input); console.log('Parsed input:', JSON.stringify(parsed, null, 2)); return parsed; } catch (e) { console.error('Schema parse error:', e); throw e; } - 检查 Zod 版本:Mastra 依赖
zod@3.22.4,如果你的package.json里锁了zod@3.23.0,可能出现不兼容。解决方案是pnpm dedupe zod强制统一版本。
实操心得:永远用
z.preprocess()处理前端传来的“脏数据”。比如前端可能传""(空字符串)给orderId字段,而 schema 要求非空:schema: z.preprocess((val) => val === '' ? undefined : val, z.string().regex(/^\d{16}$/) )
5.2 Workflow 不执行:检查if条件的返回值类型
现象:某个step明明if条件为true,但steps['step-id']在结果里是undefined。
原因:if字段必须返回boolean,但 JavaScript 里0、''、null、undefined都是 falsy。如果你的if函数写了return ctx.steps['prev'].output.count;,而count是0,那整个步骤就被跳过了。
排查技巧:
- 在
if函数里加console.log:if: (ctx) => { const condition = ctx.steps['prev'].output.count > 0; console.log('if condition for step-x:', condition, 'count=', ctx.steps['prev'].output.count); return condition; } - 使用
Boolean()显式转换:if: (ctx) => Boolean(ctx.steps['prev'].output.items?.length) - Mastra 提供
isDefined()工具函数:import { isDefined } from '@mastra/core'; if: (ctx) => isDefined(ctx.steps['prev'].output.items)
5.3 日志不显示:OpenTelemetry 配置的三个致命陷阱
现象:/debug页面看不到日志,console.log也不输出。
原因和解决方案:
| 陷阱 | 表现 | 解决方案 |
|---|---|---|
未调用provider.register() | getLogger()返回空对象 | 在apps/api/src/index.ts顶部加provider.register() |
环境变量OTEL_EXPORTER_OTLP_ENDPOINT未设置 | trace 不上报,但console.log正常 | 设置OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces |
@opentelemetry/sdk-trace-node版本冲突 | 启动时报TypeError: tracer.startSpan is not a function | 运行pnpm why @opentelemetry/sdk-trace-node,确保只有一个版本,用pnpm dedupe修复 |
提示:开发时用
OTEL_LOG_LEVEL=debug环境变量,会输出 OpenTelemetry 内部日志,帮你定位配置问题。
5.4 性能瓶颈:LLM 调用耗时过长的优化方案
现象:workflow 平均耗时 8 秒,其中 7 秒花在 LLM 调用上。
优化不是换模型(gpt-4-turbo 比 gpt-3.5-turbo 贵 3 倍),而是减少不必要的 LLM 调用:
- 缓存 LLM 响应:Mastra 支持
@mastra/cache,用 Redis 存储 prompt → response 映射:import { RedisCache } from '@mastra/cache'; const cache = new RedisCache({ url: 'redis://localhost:6379' }); // 在 workflow 配置中启用 export const config = defineConfig({ cache, // ... }); - 预计算确定性逻辑:比如“订单是否可退”完全由规则决定,根本不需要 LLM。把这部分写成纯函数工具,只在真正需要语义理解时(如分析用户留言情感)才调 LLM。
- 流式响应:Mastra 的
execute()返回AsyncIterable,前端可以用for await接收流式输出,不必等整个 workflow 结束:const stream = await workflow.executeStream({ orderId: '...' }); for await (const chunk of stream) { console.log('Received chunk:', chunk); // 可能是 { type: 'step-start', stepId: 'check-eligibility' } }