上一篇把 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 上,整条链路收进一个进程边界内:
解读:页面、路由、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 篇的自托管方案,业务代码一行不用改。

