Mastra 开发(12):Next.js + Mastra

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

上一篇把 Mastra 打成 Docker 镜像送进了生产。但很多场景里,Agent 只是 Web 产品的一个能力位——智能搜索、客服入口、摘要生成——为它单独立服务、立域名、立 CI,成本全在胶水上。这一篇讲 Mastra 的杀手级场景:Agent 直接长在 Next.js 应用里,一个仓库、一次部署、类型从数据库穿到浏览器——Python 栈给不了的体验。

这是《TypeScript Agent 开发(Mastra)》系列第 12 篇。接线方式按 2026 年 10 月的 Mastra v1 与 AI SDK v7 官方文档逐一核实,全文按"价值 → 工程结构 → API 路由 → 前端接线 → 会话贯穿 → 工具渲染 → 部署决策"组织,最后照例留练习。


1、全栈一体的价值

Mastra 核心团队出自 Gatsby,CEO 就是这个 React 头部框架的联合创始人,注定与前端工程亲缘:Agent 不是要单独供奉的模型服务,而是应用图层的一等公民。落到 Next.js 上,整条链路收进一个进程边界内:

flowchart TD subgraph S["Next.js 应用(一个仓库)"] A["app/page.tsx<br>React 页面"] -->|"fetch /api/chat"| B["api/chat/route.ts<br>同仓路由"] B -->|"同进程 import"| C["mastra/<br>Agent、Memory、工具"] C --> D["Prisma<br>业务数据库"] end

解读:页面、路由、Agent、数据库四层同仓同进程。与 Python 栈的典型拆法对照,差异集中在工程成本上:

维度 Python 栈拆分 Next.js + Mastra
仓库与部署 前端、Agent 服务两套 一个仓库一次部署
接口契约 OpenAPI 加 codegen 手工对齐 TS 类型直穿 DB 到 UI
联调 跨服务抓日志 同进程断点
语言栈 TypeScript 加 Python 双语言 全 TypeScript

公道话说在前面:Python 栈在重型计算与生态广度上仍是优势(第 1 篇横评)。但把 Agent 嵌进既有 Web 产品,一体化的迭代速度压倒一切——第 3 篇的 Zod schema 既约束工具返回,也被 React 组件直接 import 推导 props,第 6 节见分晓。

2、工程结构

先建 Next.js 工程,再装三件套。桥接包 @mastra/ai-sdk 负责两边流的互转,是接线的核心依赖:

npm install @mastra/ai-sdk@latest @ai-sdk/react ai

装完跑 npx mastra init,它会在 src 下生成 mastra 目录,与 app 平级共存。目录组织如下:

src/
├─ app/
│  ├─ page.tsx                 # 首页挂聊天组件
│  └─ api/chat/route.ts        # 对话路由(后端)
├─ mastra/
│  ├─ index.ts                 # new Mastra({...}) 汇总注册
│  ├─ agents/weather-agent.ts  # Agent 定义
│  └─ tools/weather-tool.ts    # 工具与 outputSchema
└─ components/Chat.tsx         # 前端聊天组件

解读:mastra/ 就是普通的服务端模块,被 route handler import 时才实例化;Server/Client 边界天然保证 Agent 代码不进浏览器 bundle。Agent 沿用第 2 篇与第 5 篇的组合,挂上工具与记忆:

import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/core/memory'
import { weatherTool } from '../tools/weather-tool'

export const weatherAgent = new Agent({
  id: 'weather-agent',
  name: 'Weather Agent',
  model: 'anthropic/claude-sonnet-5-5',
  instructions: '你是天气助手,先调用 weatherTool 查询,再用中文回答。',
  tools: { weatherTool },
  memory: new Memory({ options: { lastMessages: 20 } }),
})

再在 mastra/index.ts 里 new Mastra({ agents: { weatherAgent } }) 注册导出,后端就绪;开发期另跑 mastra dev 可起 Studio 看 trace,与 next dev 互不干扰。

3、API 路由挂载

后端只需一个 route handler(写法见 Next.js 集成指南):app/api/chat/route.ts 的 POST 用 handleChatStream 把流式响应转成 AI SDK 的 UIMessage 流,以 SSE(Server-Sent Events)返回:

import { handleChatStream } from '@mastra/ai-sdk'
import { createUIMessageStreamResponse } from 'ai'
import { mastra } from '@/mastra'

export async function POST(req: Request) {
  // 前端 body 里的 messages 与 memory 原样透传
  const params = await req.json()
  const stream = await handleChatStream({
    mastra,
    agentId: 'weather-agent',
    version: 'v7', // 桥接层默认按 v5 兼容,AI SDK v7 必须显式声明
    params,
  })
  return createUIMessageStreamResponse({ stream })
}

解读:handleChatStream 一行顶三件事——按 agentId 取 Agent 并带着 params.memory 调用、把 Mastra 流转成 UIMessage 流、包成标准流式 Response。最容易漏的是 version: 'v7':桥接函数默认按 AI SDK v5 兼容行为转换,不声明会静默错乱。

历史回放走同一个路由,同文件再补一个 GET,从 Memory 召回线程历史:

import { toAISdkMessages } from '@mastra/ai-sdk/ui'
import { NextResponse } from 'next/server'

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url)
  const memory = await mastra.getAgentById('weather-agent').getMemory()
  const res = await memory?.recall({
    threadId: searchParams.get('threadId') ?? '',
    resourceId: 'web-user',
  })
  return NextResponse.json(toAISdkMessages(res?.messages ?? [], { version: 'v7' }))
}

解读:recall 是第 5 篇的召回接口,toAISdkMessages 把存储格式转成 useChat 的消息结构,首屏历史就有了。

4、前端 useChat 接线

前端不用手写 SSE 解析:useChat(官方文档)配 DefaultChatTransport 指向自家路由,细节见 AI SDK UI 集成文档:

'use client'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'

export function Chat({ threadId }: { threadId: string }) {
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/chat',
      // 只发最新一条消息,会话标识随请求带上
      prepareSendMessagesRequest({ messages }) {
        return {
          body: {
            messages: [messages[messages.length - 1]],
            memory: { thread: threadId, resource: 'web-user' },
          },
        }
      },
    }),
  })
  // 渲染逻辑见下
}

解读:prepareSendMessagesRequest 定制请求体,是接线的关键一环——历史消息不重发,只发增量;memory 里的 thread 与 resource 就是第 5 篇的双层标识,透传给后端的 handleChatStream。消息渲染按 AI SDK v5 之后的 parts 模型展开:

{status !== 'ready' && <p>思考中……</p>}
{messages.map((m, i) => (
  <div key={i} className={m.role}>
    {m.parts.map((part, j) => {
      if (part.type === 'text') return <p key={j}>{part.text}</p>
      if (part.type === 'tool-weatherTool') return <WeatherPart key={j} part={part} />
      return null
    })}
  </div>
))}

解读:文本 part 随 chunk 到达逐步更新,part.text 每帧变长,天然就是打字机效果。status 在 submitted、streaming、ready 间流转,加载态与按钮禁用都靠它。工具 part 的类型是 tool- 加注册名,下一节处理。

5、会话标识贯穿

threadId 从前端来。匿名用户用 localStorage 持久化一个 UUID 就够了:

function getThreadId(): string {
  const key = 'mastra-thread-id'
  let id = localStorage.getItem(key)
  if (!id) {
    id = crypto.randomUUID()
    localStorage.setItem(key, id)
  }
  return id
}

解读:挂载时取一次传进 Chat;进页面先 fetch 上节的 GET,返回值用 setMessages 灌回 useChat,历史即恢复。为什么只发最后一条?服务端 Memory 存着全量历史,前端再发全量,两份消息在时间戳与序号上打架——官方文档明确警告过这个乱序 bug。

注意:localStorage 方案要防"换账号串会话"。接了登录体系后,更稳的做法是在 route handler 里用服务端会话的 userId 覆盖 thread 与 resource。

6、工具结果驱动界面

全栈一体的高光在这节:第 3 篇的 outputSchema 不只约束工具返回,还直接成为前端组件的 props 来源——前后端共享同一份界面契约。工具侧声明:

import { z } from 'zod'

// 与前端共享的输出契约
export const weatherOutput = z.object({
  city: z.string(),
  temperature: z.number(),
  condition: z.string(),
})

前端用 z.infer 推出类型,配合第 4 节引用的 WeatherPart 做三态渲染:

import type { ToolUIPart } from 'ai'
import type { z } from 'zod'
import type { weatherOutput } from '@/mastra/tools/weather-tool'

type Weather = z.infer<typeof weatherOutput>

function WeatherPart({ part }: { part: ToolUIPart }) {
  switch (part.state) {
    case 'input-available':
      return <p>正在查询天气……</p>
    case 'output-available':
      return <WeatherCard weather={part.output as Weather} />
    case 'output-error':
      return <p>查询失败:{part.errorText}</p>
  }
}

解读:三态对应"参数已到、结果已到、执行出错",part.output 的结构就是 weatherOutput 描述的结构——WeatherCard 的 props 类型由后端 schema 推导,改一处前端编译期立刻报错。import type 编译期擦除,不会把服务端模块拖进浏览器包。Python 栈要实现同样效果得从 Pydantic 走 codegen,这里只有一份事实。

7、部署形态决策

一体化后部署只有一个产物,但选平台要过两道关。一是时长:Vercel 函数执行受档位与 maxDuration 约束(详见 Vercel 官方文档),免费档在分钟以内,长工具链容易触顶。二是无状态文件系统:Memory 默认的 LibSQL 落在本地 .db 文件,serverless 下冷启动即清零。

维度 Vercel Docker 自托管
函数时长 档位设限,长任务易触顶 无硬限制
记忆存储 必须外置(Turso、Postgres) 本地 LibSQL 或内网库均可
运维成本 近零 第 11 篇的 compose 一套
适配场景 轻聊天、短工具 重工具、长流程

结论不玄:轻量产品选 Vercel,把 Memory 的 storage 换成 Turso(LibSQL 的云版)或 Postgres 即可上线;一跑几分钟的重 Agent,用第 11 篇的 Docker 方案自托管——Next.js 配 output: 'standalone' 打镜像,与 Mastra 同容器运行。

8、总结

回顾全文:全栈一体的价值在一个仓库一次部署、类型从 Zod schema 直穿 React props;mastra/ 与 app/ 平级共存,Server/Client 边界保证 Agent 不进浏览器;后端 handleChatStream 配 createUIMessageStreamResponse 完成流式输出,version: 'v7' 别漏;前端 useChat 免手写 SSE 解析;会话靠 thread 与 resource 双标识贯穿;工具输出经 outputSchema 驱动三态卡片;部署按负载在 Vercel 与自托管间取舍。

留一个练习:把第 2 篇的 Agent 升级成完整聊天页——加一个带 outputSchema 的工具(查天气、查股票都行),实现流式打字机、三态工具卡片、localStorage 线程与刷新回放四个特性;进阶再把 threadId 换成登录用户的 userId 并改由服务端派生。对于想把 Agent 嵌进自己产品的团队,建议直接从这套一体结构起步:先用 Vercel 加 Turso 跑通闭环,负载上来再平移到第 11 篇的自托管方案,业务代码一行不用改。

相关推荐

获取系统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