去年年底,我接到一个任务:给后台管理系统加一个智能客服入口。当时我还没从"纯前端"的舒适区里出来,第一反应是——这不就是接个大模型 API,把用户问题丢过去,再把返回文本显示出来吗?后来真正动手才发现,事情远没有这么简单。当我开始研究工具调用、对话记忆、动态路由这些东西时,一个做后端的朋友推荐我看看 Mastra,说这是 TypeScript 原生的 Agent 编排框架,对前端出身的人特别友好。我花了两个周末翻文档,又用两周把一个带记忆的技术客服 Agent 跑通并上了测试环境。这篇笔记就把我从前端视角转向全栈 Agent 开发的完整过程写出来,包括选型对比、上手实操、踩坑记录和给同行的路线建议,希望能让想转型又不想一头扎进 Python 生态的前端同事少走几步弯路。
1. 前端转全栈的第一个认知转变:Agent 不是"调一次大模型接口"
1.1 先弄清 Agent 的决策循环和普通程序的分界
我最初犯的错误,是拿"接受请求、调用大模型、返回文本"这种线性思路去理解 Agent。实际做下来才明白,Agent 的核心在于循环决策:大模型先理解用户意图,决定调用哪个工具,拿到工具返回结果后继续判断,是直接回复用户还是再调用下一个工具。这个循环往往要重复好几轮。
举个例子。用户问"我上周买的东西到哪儿了",如果只调一次大模型接口,模型最多给你一段笼统的瞎猜。但 Agent 的场景是:模型先判断需要查订单号,于是调用"查询订单"工具,工具返回订单信息和物流单号;模型发现还需要查物流轨迹,再调用"物流查询"工具;拿到轨迹后,模型才组织语言回答用户。整个过程里,模型是一个"调度员",业务数据的获取完全靠工具。
这个差异对前端开发者来说特别重要。因为很多人觉得"全栈化就是多会写几个 Node 接口",但 Agent 开发更像是搭一套系统:你需要设计工具(对应后端的服务),需要管理上下文(对应前端的状态管理),需要处理异步流(对应事件流处理),甚至要考虑成本与限流(对应性能优化)。如果不先建立这套认知,后面用任何框架都会觉得别扭。
1.2 前端经验在 Agent 里的复用点比你想的更多
我一开始也怀疑:一个天天写页面的人,凭什么转型去做 Agent?实际做了之后发现,前端经验里有一大半可以直接迁移。
首先是状态管理。对话历史本质上就是一个全局状态,Agent 的记忆机制无非是在给这个状态加读写策略、摘要策略和过期策略。你如果熟悉前端状态管理库的设计思路,理解 Agent 的上下文窗口管理会非常快。
其次是异步与流式交互。前端处理过 websocket、EventSource、超时重连的人,对 Agent 的流式输出会有天然的亲切感。流式响应、中断、缓冲、增量渲染,这些概念前端每天都在做。
再次是组件化思维。在 Mastra 里,一个工具、一段提示词、一个工作流节点,都可以理解为一个"组件"。前端开发者习惯把一个页面拆成多个组件,到了 Agent 里就是把能力拆成多个工具和多个步骤。这个拆分的直觉,反而比很多后端同事更敏捷。
1.3 全栈 Agent 开发的工作边界
当我真正走完一个完整需求后,我才清楚"全栈 Agent 开发"到底覆盖哪些范围:
- 服务端接口:把 Agent 暴露成 HTTP/SSE 接口,供前端调用;
- 工具开发:把订单系统、商品库、权限系统的能力封装成工具;
- 提示词与指令设计:约束模型的行为边界,防止乱说话;
- 记忆管理:短期记忆、长期记忆、摘要压缩;
- 工作流编排:把人工审核、条件分支、定时任务串起来;
- 可观测与成本:记录每次调用的 token 消耗、成功率、延迟。
也就是说,以前我只需要考虑"页面上怎么展示",现在要盯着"模型怎么思考、工具怎么执行、数据怎么流转"。这个转变并不轻松,但一旦跨过去,你对整个业务系统的理解会完全不一样。
2. 我对比的几条路:LangChain、Vercel AI SDK、自研封装,为什么留下 Mastra
2.1 我的选型标准:TypeScript 原生、模块化、能交接给团队
做技术选型之前,我先列了自己的硬性条件。第一,必须 TypeScript 原生,因为我会 JavaScript,但不想被迫去啃 Python 生态;第二,组件要模块化,能按需引入,别一个框架把所有东西都绑死;第三,团队其他人要能看懂,代码是可维护的,不能只有我一个人会玩。
在确认看 Mastra 之前,我实际上花了不少时间看 LangChain 和 Vercel AI SDK。这三者代表了三种完全不同的设计思路。
2.2 Mastra 与 LangChain、Vercel AI SDK 的实际对比
先说我感受最深的 LangChain。它在 Agent 领域无疑是最成熟的一档,文档示例极多,社区资源丰富。但对前端开发者来说,它的准入成本是存在的。其设计更偏重数据管线和复杂的链式编排,很多东西需要先理解它的抽象概念才能用起来。虽然它也提供 JS/TS 版本,但你能明显感觉到核心生态、教程密度、工具适配都是以 Python 为第一优先级的。对于一个想快速上手的前端,啃你的学习曲线会显得过陡。
然后是 Vercel AI SDK,它在前端开发圈子里很火,流式交互也做得非常顺手,尤其是它把流式 UI 的体验做得很好。但它解决的问题偏向"大模型会话框架",对于 Agent 的核心机制——比如复杂的工具循环、多轮工具决策、长期记忆、工作流编排——它需要你自己去拼接很多模块。如果你只是做一个"聊天机器人",它完全够用;但如果你想做真正会干活、能操作系统的 Agent,你就得在它之上再盖一层脚手架。
再看我自己写封装的方案。动手之前我认真评估过"不用框架,自己写 Agent 循环"这条路。结论是:做 Demo 可以,做项目不行。因为一轮完整的 Agent 循环,至少涉及模型调用、工具注册解析、结构化输出、错误重试、上下文压缩、调用链追踪这些事情。你可能花两周能写出一个"看起来能跑"的版本,但要想在并发、异常、记忆这些层面经得起使用,成本其实非常高。而且这类代码一旦写得抽象程度不够,后期每个新场景都会逼你重写。
Mastra 给我最直接的感觉是,它把"Agent 应用开发"这件事重新拆成了几个我熟悉的概念:Agent、Tool、Workflow、Memory、RAG、Evaluator。这些东西在编程模型上非常接近前端框架的设计——根实例、子模块、hook,它们有清晰的分层,可以按需引入,不强迫你一下子把整个框架全部学会。
为了方便对照,我把当时做表格对比的信息整理在下表:
| 对比维度 | Mastra | LangChain(TS版) | Vercel AI SDK | 自研封装 |
|---|---|---|---|---|
| 语言优先度 | TypeScript 原生 | Python 优先,TS 后置 | TypeScript | 取决于你自己 |
| Agent 决策循环 | 内置封装 | 有,但概念层较厚 | 需要手动拼 | 完全自己写 |
| 记忆管理 | 内置短期/长期记忆 | 需要额外组合记忆模块 | 不提供 | 完全自己写 |
| 工作流编排 | 内置 Workflow | 搭配 LangGraph 使用 | 不提供 | 完全自己写 |
| 工具封装 | createTool,schema 清晰 | 工具定义链路较长 | 支持简单工具 | 完全自己写 |
| 上手速度 | 较快,贴近前端心智 | 有学习门槛 | 快,但功能薄 | 前期很快后期很痛 |
从这个表可以明显看到,Mastra 的定位正好切在了"功能完整度"和"前端友好度"的平衡点上。它不像 LangChain 那样给你一整片森林,也不像 Vercel AI SDK 那样只给你一株树苗,而是给了你一套足够用的"预制组件"。
2.3 为什么不建议一上来就自己封装 Agent 库
这里多说一句自研的诱惑。我当时差点被"自己写一个 Agent 框架很酷"这个念头带跑,后来是被一个真实需求劝退的。我们客服模块需要支持:多轮对话、查订单、查物流、自动转人工、消息摘要。这些需求如果自研,我需要先解决"如何让模型稳定输出工具调用参数"的问题,又要解决"工具调用结果太长如何压缩"的问题。而这些恰恰是 Agent 框架最成熟、最值得复用的部分。
当然,我不是说每个项目都必须上框架。如果只是展示型 Demo,或者模型只负责生成文本、不调用任何外部工具,那么自己封装一层未必不行。但一旦进入"有工具、有记忆、有状态"的真实业务场景,框架的价值会迅速释放。用框架不是偷懒,是为了把精力集中在业务逻辑上。
3. 上手实战:用 Mastra 写一个带记忆的客服 Agent
3.1 把需求拆开:工具、记忆、回复策略三件事
我在规划阶段没有直接写代码,而是先把需求拆成了三块。
工具层:客服需要查订单状态、查物流轨迹、查退换货政策。每个查询都是一次工具调用,工具返回的数据格式要足够结构化,方便模型二次组织语言。
记忆层:用户在对话里先后问了订单、又问了物流,Agent 应该记住前文,而不是每次都当成新会话。这里需要一个工作记忆来保留最近几轮的对话内容,同时需要一个长期记忆来沉淀"这个用户喜欢问什么"之类的用户画像。
回复策略:不是所有问题都要调工具。比如用户问"你们几点下班",直接根据提示词回复就行,没必要浪费一轮工具调用。这个策略在 Mastra 里可以通过指令和工具筛选来控制,给模型配置 toolChoice 参数,让它按需选择工具。
3.2 初始化项目并搭好最小目录
我用了 Mastra 官方提供的脚手架,直接生成一个带基础结构的最小工程。命令大致如下:
npm create mastra@latest customer-agent生成之后的目录结构会包含 agents、tools、workflows 这些顶层目录,你可以根据自己的习惯调整。我当时的目录结构大概是这样的:
customer-agent/ ├── src/ │ ├── agents/ │ │ └── support-agent.ts │ ├── tools/ │ │ ├── order-lookup.ts │ │ └── logistics-lookup.ts │ ├── workflows/ │ │ └── handoff-workflow.ts │ ├── server.ts │ └── index.ts ├── .env └── package.json这种"工具归工具、Agent 归 Agent、流程归流程"的划分方式,对前端开发者来说非常友好——就像你在管理 components、hooks、services 目录一样自然。
3.3 定义两个工具:查订单和查物流
工具是 Agent 和外部世界打交道的唯一通道。在 Mastra 里,一个工具需要给模型说清楚三件事:什么时候调用、需要什么参数、会返回什么结构。我自己偏向用 zod 来定义参数和输出的 schema,这样既能做运行时校验,也能让 TypeScript 推导出完整类型。
下面是我写订单查询工具的简化版本:
import { createTool } from '@mastra/core'; import { z } from 'zod'; export const orderLookup = createTool({ id: 'order-lookup', description: '根据订单号查询订单状态、商品信息和预计送达时间。', inputSchema: z.object({ orderId: z.string().describe('用户的订单号'), }), outputSchema: z.object({ orderId: z.string(), status: z.enum(['pending', 'shipped', 'completed', 'cancelled']), items: z.array(z.string()), eta: z.string().nullable(), }), execute: async ({ context }) => { // 真实项目里这里会联数据库,或者调用订单中心 HTTP 接口 return { orderId: context.orderId, status: 'shipped', items: ['订单号 #20250301 包含商品 A、商品 B'], eta: '预计 3 月 5 日前送达', }; }, });一个容易忽略的点:description 比 schema 更重要。模型决定"要不要调用这个工具"的决策依据,主要就看 description 写得够不够清楚,而不只是看参数定义。你写"根据订单号查询订单状态、商品信息和预计送达时间",模型就能判断什么时候该调;如果你只写"查询订单",模型很可能在不需要的时候也去乱调。
物流查询工具同理,接收物流单号,返回轨迹列表。我在这里刻意让工具返回"结构化数据"而不是一句自然语言文本,因为结构化数据更利于模型润色和组织最终回复,也不容易产生幻觉。
3.4 创建带记忆的 Agent 并接入 Express 路由
工具定义好之后,Agent 的创建就变得很简洁。配置一个模型、注册工具、设置记忆规则即可。我当时用的配置大概是这个样子:
import { Agent } from '@mastra/core'; import { orderLookup } from '../tools/order-lookup'; import { logisticsLookup } from '../tools/logistics-lookup'; export const supportAgent = new Agent({ name: 'technical-support', instructions: ` 你是电商平台的技术客服助手,负责解答订单、物流、退款相关的问题。 规则: 1. 涉及订单、物流信息时,只能依据工具返回的数据回答,不得编造。 2. 工具返回“无结果”时,要如实告知用户暂时查不到,并引导联系人工客服。 3. 回答保持简洁,不要超过 100 字。 4. 如果用户的问题与业务无关,请礼貌引导回正题。 `, tools: { orderLookup, logisticsLookup, }, memory: { workingMemory: { enabled: true, maxMessages: 10, }, summaries: { enabled: true, maxMessages: 20, strategy: 'last-message-summary', }, }, model: { provider: 'OPEN_AI', name: 'gpt-4o-mini', toolChoice: 'auto', }, });这里有三个细节我想额外说明。第一,instructions不只是"提示词",它是 Agent 的边界约束,一个项目上线后出的大部分问题都在这一层没写清楚。第二,toolChoice: 'auto'并不是让模型完全自由,而是让它在"调用工具"和"直接回答"之间自己权衡,对客服场景来说是最省 token 的选择。第三,workingMemory我限制在 10 条消息,是为了避免把早期无关内容留在上下文里浪费上下文窗口。
Agent 建好之后,需要把它暴露给前端调用。我直接在 Express 里加了一条路由,接收前端的消息数组,转成 Mastra 的消息格式后再调 Agent 的流式接口,最后以 SSE 格式推回前端:
import express from 'express'; import { supportAgent } from './agents/support-agent'; const app = express(); app.use(express.json()); app.post('/api/chat', async (req, res) => { const userId = req.body.userId; const userMessages = req.body.messages; try { const stream = await supportAgent.stream({ messages: userMessages, threadId: `user-${userId}`, }); res.setHeader('Content-Type', 'text/event-stream'); const reader = stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; res.write(`data: ${JSON.stringify(value)}\n\n`); } res.end(); } catch (error) { res.status(500).json({ error: 'Agent 调用失败' }); } });首版代码拿到测试环境跑起来后,我发现了一个关键问题:threadId必须稳定传入。如果你每次请求都随机生成一个 threadId,记忆形同虚设;但如果你是按 userId 来传,又要注意用户数据隔离,别让 A 用户的上下文串到 B 用户那边。
3.5 接入前端:从 EventSource 到 UI 展示
服务端用 SSE 返回后,前端接入其实是我最熟悉的部分。我直接在 Vue 页面里用fetch配合ReadableStream去读取,或者用EventSource简化推送。重点是把增量内容边读边追加到消息列表,而不是等全部生成完再一次性渲染,不然用户会等得很焦虑。
一个我踩过的细节:EventSource只支持 GET 请求,而我们的对话接口是 POST,所以我最后用的是fetch+stream解析方案,服务端保持 SSE 格式,但前端用流式解析去消费。这样既保留了 POST 的灵活性,又实现了逐字展示的效果。
4. 实测里踩过的四个坑:SSE 转发、工具循环、记忆膨胀和幻觉
4.1 SSE 转发:后端不规范,前端接收到一堆乱码
第一次联调时,我天真地以为 Agent 的 stream 接口返回的就是一个可直接给前端的 SSE 流。结果发现,直接返回的内容格式和前端预期的格式不一致,前端解析器直接罢工。原因在于 Mastra 流式接口吐出的数据块有自身的结构,我不能原样转给前端,必须自己在服务端组装一层标准 SSE 格式。
这个问题的教训是:流式转发不是简单的管道透传。如果你做后端转发,一定要在服务端格式化为约定好的 SSE 事件结构,比如事件名、数据字段、结束标识,前端才能稳定消费。我在服务端加了一小段归一化逻辑,把模型增量数据统一包装成:
event: delta data: {"content":"你好"} \n同时把工具调用的状态作为独立事件推给前端,这样前端能展示"正在查询订单..."的中间状态,对用户来说更有"真在做事情"的感觉。
4.2 工具无限循环:一次查询失败让 Agent 停不下来
我最头疼的一个 bug,是 Agent 在工具调用失败后陷入死循环。场景是这样的:用户报了一个不存在的订单号,工具按约定返回了"无结果"。按理说模型应该回复"查不到订单,请核对订单号",但实际它反复调用查询工具,一连调用三四次,白白烧掉大量 token。
我后来定位到原因有两个。第一个是工具错误信息写得太模糊,模型不知道这个错误是不可重试的,还是暂时失败的。第二个是缺少调用次数上限,框架默认对工具循环次数的限制偏宽松,模型在某些配置下会不断尝试。
解决办法分两层。工具层,我把异常情况拆成"业务无结果"和"系统异常"两类,"业务无结果"返回明确的中性消息,"系统异常"才抛给重试逻辑。配置层,我给 Agent 设置了一个工具调用的最大轮数,达到上限就强制让模型收尾并给出兜底话术。同时我在指令里加了一句:"如果查询不到,请直接如实告知用户,不要重复尝试。"
4.3 记忆膨胀:上下文越来越贵,对话越来越慢
客服对话如果一直不结束,记忆会把上下文撑爆。短期对话还好,但真实用户往往会持续追问,导致每一轮的 prompt 里历史消息越来越多,模型响应越来越慢,费用越来越高。
Mastra 自带的 summary 策略帮了我不少忙。我把maxMessages设为 20,超过这个范围后,框架会把更早的对话压缩成摘要,只保留关键结论。但我也和你说实话,自动摘要并不总能完美保留细节,尤其是用户之前提到的订单号,一旦被摘要模糊掉,后面再问就很麻烦。
我自己的补充策略是:在工具调用层面对关键参数做持久化提取。也就是说,用户在第一轮里报了订单号,我就把订单号存到会话级别的记录里;后续如果模型需要查订单,工具内部优先使用这个会话记录里的订单号,而不是依赖模型从记忆里翻。这个做法本质上是把"靠模型记"变成"靠系统记",可靠性高很多。
4.4 幻觉治理:让工具结果成为唯一事实来源
大模型生成"看起来像真的"的信息太容易了。客服场景里最怕的就是商品信息、赔付金额这些东西被模型随口编出来。我控制幻觉的方法主要是两道防线。
第一道是工具返回结构化数据时附带数据来源。比如查询订单返回不只给状态,还要带上"订单商品清单"的原文快照,模型只能基于这份快照改写回答。第二道是在 instructions 里写明"涉及订单状态、价格、物流轨迹时,只允许引用工具返回的数据,禁止根据常识推测"。如果模型确实拿不到数据,它应该直接说不知道,或者建议用户转人工。
实际操作下来,这两道防线能把客服场景的明显幻觉降到很低的水平,不是零,但至少不会再出现让用户以为收到货但其实还没发货这种严重事故。
4.5 部署时的一个隐藏坑:内存与超时
本地跑得很顺畅的流式接口,一旦部署到线上容器,会遇到两个隐藏问题。一是 Node 服务的默认超时时间可能很短,而大模型流式响应可能长达十几秒,如果你的网关层没有把超时调大到 60 秒以上,前端会在消息还没生成完时就收到 504。二是长连接带来的内存增长,尤其在并发对话多的时候,每个连接都在缓存消息,如果没有及时释放,内存占用会慢慢爬上去。
我当时的处理是:网关层单独配置更长的读取超时;服务端对每个会话设置空闲超时,超时就关闭流,避免连接长期挂起;同时把日志和 token 消耗都加上监控,方便出问题时回溯。
5. 给前端同行的 Agent 入门路线:从练手项目到团队落地
5.1 补 LLM 基础:框架只是骨架,模型能力决定上限
学 Agent 之前,我建议先补一点大模型的基础知识,不一定要啃论文,但至少要弄清楚几个概念:上下文窗口、temperature 的含义、system prompt 的作用、结构化输出、函数调用。这里面的很多概念都直接影响你在框架里的选择。
比如你要理解"模型不是数据库"。它不知道你的订单数据长什么样,只有通过工具才能拿到。你还要理解"上下文窗口不是无限大的"。任何框架都帮你管理记忆,但如果你自己设计业务流程时不控长度,再好的框架也救不了你的延迟和成本。
这些基础不需要花太久,几天的时间,动手写几个调用 API 的小例子就能建立起来。有了这个底子,你再打开 Mastra 文档,理解的速度会快很多。
5.2 练手项目怎么选:尽可能是高频、有工具、有状态的场景
我给同行的建议是,不要去写一个纯聊天 Demo,因为纯聊天机器人和 Agent 的区别,恰好在工具和状态这两个维度上。挑一个你自己的真实业务场景,最好满足三个条件:高频(这样你有大量反馈可以迭代)、有工具调用(至少要接一个查询类接口)、有状态(用户多次交互之间需要依赖历史)。
我当时做客服就是一个典型例子。你也可以做:工单助理(接工单查询和创建)、内容助手(接搜索服务和文档库)、运营看板问答(接数据查询接口)。这类项目做下来,你会把工具注册、记忆管理、错误处理、成本监控这些关键环节都走一遍,收获会比跟着教程抄十遍都要多。
5.3 团队落地时的小建议:把 Agent 当成微服务来对待
当你想把 Agent 引入团队时,我的第一个建议是不要把它塞进现有的单体应用里,而是独立成一个服务。这不是 Mandra 特有的限制,而是 Agent 的调用模式、并发特征、超时策略都和普通 REST 接口不一样,独立部署会更可控。
第二个建议是提前定义输入输出契约。前端的请求格式、服务端的事件协议、工具返回的数据模型,要在动手前先定好。我在测试环境里吃过亏,就因为前端以为收到的是完整 JSON,后端推的是 SSE 流,两边吵了半天,最后发现是契约没对齐。
第三个建议是从一个低风险场景开始。不要一上来就让 Agent 操作数据库、发邮件、扣款,先让它做只读类的信息查询,跑稳了再加写操作。这样既能验证可行性,也不会在早期出现安全事故。
5.4 什么时候不要选 Mastra:两个被我放弃的场景
选型这事没有银弹,我确认 Mastra 适合大多数业务场景,但也遇到了两个我选择不用它的场景。
第一个是超复杂多智能体编排。如果项目里需要十几个不同角色的 Agent 互相协作、动态创建、复杂拓扑切换,我会考虑用更成熟的编排系统,而不是硬套单一框架。Mastra 的 Workflow 已经能覆盖大部分常用流程,但超出它舒适区的复杂拓扑,还是另外想办法比较稳妥。
第二个是重度依赖 Python 生态的场景。如果你想做的 Agent 要大量使用数据分析、科学计算、专用机器学习库,那么 TS 生态反而不方便,这种情况不如直接用 Python 的方案。前端开发者确实可以从 TS 切入,但也不要因为"怕学 Python"而把项目带进不合适的工具里。
做这个客服 Agent 的那段时间,我最大的感受是:前端转全栈,转的不是"再学一门后端语言",而是"换一种组织业务的思维方式"。Mastra 对我来说像一个翻译器,它把我已经熟悉的前端心智模型,顺畅地平移到了 Agent 开发里。当然框架本身也在快速迭代,你看到这篇文章时 API 可能又有变化,但核心的几条经验不会变:工具描述写清楚、记忆策略分层次、指令边界设明白、流式协议前后端对齐。如果你也想尝试 Agent 开发,别急着学一堆新东西,先找一个小场景,把一个带工具的 Agent 完整跑通,你会立刻理解我说的所有内容。