news 2026/10/22 4:20:46

前端开发者如何用 Mastra 从零搭建自动摘要 Agent 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端开发者如何用 Mastra 从零搭建自动摘要 Agent 实战

很长一段时间里,我其实没觉得自己会跟“Agent 开发”沾边。做了好几年前端,日常打交道最多的是组件、状态管理、接口联调,偶尔写写脚本,成就感也大多来自页面跑得够快、交互够顺,跟那种“让模型自己决定下一步干什么”的玩法完全是两个世界。直到我接手一个内部信息整理工具,发现每天都要人工去十几个页面搬运资料、按模板汇总、再丢给大模型做摘要,反复折腾到开始怀疑人生,才认真考虑:与其靠一堆胶水脚本缝缝补补,不如系统学一下 Agent 的开发方式,让整个“收集—判断—整理—输出”的链路自动转起来。

那段时间我看了不少方案,最后把 Mastra 定为学习和落地的主框架。作为一个 TypeScript 背景的开发者,我对它的接受速度明显快于那些从 Python 生态过来的重型框架。这篇笔记会把我从“只会调接口”到“跑通一个带工具调用、记忆和多步骤工作流的 Agent”这段过程完整拆开来讲,包括为什么我没从 SDK 裸写,为什么对比一圈后还是选了 Mastra,以及这中间踩掉的真坑。想从前端转向全栈、又对 Agent 有点好奇的朋友,可以直接把它当成一份参考路线。

1. 让我真正下决心学 Agent 的不是概念,而是一场持续很久的重复劳动

1.1 前端的工作里,其实到处藏着 Agent 的切入点

很多前端都会有一个不太起眼但占时间很大的日常:信息规整。我在维护一个内部站点时,每天要订阅团队讨论、技术博客更新、线上问题记录,再把这些散落内容整理成固定格式的日报给同事。最初我用 Node 脚本把各个源拉下来,拼成一段固定 prompt,调一次大模型接口,输出摘要就完事。问题在于来源的格式各不相同,有的要绕过登录、有的要解析动态内容、有的正文里有一堆广告噪声,不得不给脚本不断加“如果来源是 X,就做什么”的分支判断。到后面分支越来越多,一周改三次,常改常错。

那时候我才意识到,我需要的其实不是更多 if else,而是一个能“看着目标,自己判断该调用哪一步”的系统。比如给定一批 URL 和整理目标,它能自行决定先去抓内容,哪些内容相关性不足就不进摘要,摘要生成了以后还知道把来源列表附上。这不正是 Agent 做的事吗?Agent 的核心循环说起来也很简单:模型拿到指令和可用工具,自己决定调用哪个工具,看到工具返回结果以后再做下一轮决策,直到任务完成。

1.2 裸调大模型接口看起来很自由,做成一整条链路就是另一回事

开始我没急着上框架,直接在自己的脚本里调大模型接口。单轮提问确实很爽,输入一堆文字,输出一段总结,问题不大。一旦任务变成“从多个来源采集并保存,再基于这些内容回答”,三个问题立刻冒出来。

第一,模型输出有很强的“脑补倾向”。你让它总结 5 个链接,它可能为了格式工整自己编出第 6 个来源;第二,缺结构化的工具调用能力。没有框架帮你把外部函数变成模型可选择的“工具清单”,你就只能靠 prompt 约定“如果提到某个关键词,就去解析某个格式”,脆弱得很;第三,状态维护全得靠自己。中间某一步失败了,怎么办?要不要重试?重试会不会重复调用外部服务?这些边界问题几乎可以让任何“写着玩”的脚本一夜之间变成维护梦魇。

等我自己动手把这些逻辑都写了一遍,才明白框架的抽象不是在增加学习负担,而是在帮你把“模型决策、工具执行、失败重试、上下文记录”这些通用步骤从业务代码里解放出来。想通这一点,我才开始认真做选型。

1.3 学 Agent 之前,我更在意的是它在全栈链路里的位置

当时我给自己两个问题:第一,这个框架能不能让我用 TypeScript 继续写所有逻辑,而不是为了 Agent 另起炉灶;第二,把这个 Agent 做好以后,我能不能很顺地把服务暴露给前端调用,比如做成一个接口或一条流式响应。后来 Mastra 出现在我视野里,恰好这两个问题都能给到让人满意的答案。于是接下来的对比,我就不再只看功能列表数量了,而是看它的概念方式跟我这个前端脑子合不合。

2. 为什么最后没有走“SDK 裸写”或“老牌重型框架”那两条路

2.1 不用把整条工人链上给前端再去打字不易,我列了一张需求表

我会把选型拆成五类需求:上手曲线、工具调用循环、可调试性、与前端项目衔接、可替换性。下表是我当时的判断方式:

需求在自己写的封装上解决更老牌的编排框架Mastra 这类 TypeScript 原生框架
上手曲线低,但后面逻辑越来越散中高,概念很多低,贴合前端心智
工具调用循环手工实现 while 循环框架内置,但思考方式偏其他生态内置,且 async 风格与前端一致
调试体验日志靠打印回调链路复杂自带链路追踪,可观察性友好
与前端项目整合要自己封装接口层依赖偏重普通 Node 服务即可跑,也能挂到全栈框架
可替换性越高越散绑定程度高核心对象边界清晰,拆了也不难迁移

这张表不是想说所有场景都该选同一项,而是帮我认清一个事实:对前端转过来的人,最大的成本不是模型怎么调,而是框架知不知道你习惯怎么组织代码。若一个框架让我天天查非 TypeScript 的抽象概念才能写业务,那等于每天都在交“转译税”。

2.2 为什么我没继续写自己的工具调用循环

自己写工具调用循环,本质上是一个状态管理问题:把“模型说该调用 toolA,参数是 xxx”这个意图解析出来,执行真实函数,把结果再送回给模型。听起来不复杂,真写起来会遇到几个经不起细看的地方——模型偶尔会输出格式不完全合法的参数,你要做容错;工具执行可能抛异常,你要决定是否终止整个循环;上下文超过模型窗口时,要怎么截断才能不丢关键信息。这些细节单独处理都不算难,但凑在一起,再叠加多工具、多步骤、记忆持久化,就已经是一个框架的量级了。

我不愿为了一个内部工具再造半套框架,成本上完全不划算。这也是我后来能坦坦荡荡接受“用框架”的原因:框架帮你把旅程架起来,你只需要专注业务逻辑。

2.3 Mastra 最打动我的是它的对象模型跟前端组件几乎同构

如果只是 TypeScript 原生,市面上也不是没有替代。真正让我下决定的是它在对象建模上带来的熟悉感:Agent 像一个封装好的组件,Tool 像声明 propTypes 并导出方法,Workflow 像把多个步骤组合成一条流水线,Memory 则天然让人联想到持久化缓存。这种“迁移成本低”的优势很难用一个参数写进表格,但一旦上手就会非常明显。

3. 为什么是 Mastra:四个基础概念,恰好对应前端早就熟悉的东西

3.1 Agent 像一个组件,指令是它的 props 文案

第一次写 Agent 时,我心里是把“组件”这个印象搬过去的。一个 React 组件接收 props,内部封装状态和回调,输出一段 UI;慢节奏来看也不太违和:Mastra 里的 Agent 接收模型与身份指令,内部挂工具和记忆,对外提供生成文本的能力。给它定义“角色说明”完全不像是写配置文件,更像在写组件的常量文案:

import { Agent } from '@mastra/core'; import { createProvider } from './provider'; // 按你实际接入的模型能力初始化 import { z } from 'zod'; export const digestAgent = new Agent({ name: 'daily-digest-agent', instructions: [ '你是一个信息整理助手,负责把多渠道收集来的资料整理成结构化摘要。', '规则:只能使用工具返回的真实内容作答,禁止补脑。', '规则:如果某个来源抓取失败,在结果中明确标注,不要跳过。', '规则:最终输出顺序为结论、要点、来源列表。', ].join('\n'), model: createProvider('当前选择的模型名称'), tools: { fetchSource: fetchSourceTool }, });

这种体验和以前我写组件时给 props 写 default 文案、给某个模块写使用说明非常像。你不需要理解太深的“提示词工程”,就能先把边界立住。

3.2 Tool 是 propTypes 与接口封装的合体

Mastra 定义工具时,最显眼的是输入输出都用 zod 描述。这对前端来说舒服极了,因为我们本来就习惯用类型约束数据格式。zod schema 在这里不仅用于运行时校验,更关键的是给模型提供了“该不该调这个工具、参数该怎么填”的依据:

import { createTool } from '@mastra/core'; import { z } from 'zod'; export const fetchSourceTool = createTool({ id: 'fetch-source', inputSchema: z.object({ url: z.string().url(), take: z.number().int().positive().default(1600), }), description: '抓取指定链接的正文并做简单裁切,返回纯文本。需要获取外部资料时使用。', execute: async ({ context }) => { const text = await fetchPlainText(context.url); return { source: context.url, excerpt: text.slice(0, context.take) }; }, });

模型读到 description 和 inputSchema,就相当于看到一份接口文档。以前我们给前端组件写“调用方只需要传这些参数,会返回这些结构”,现在面向的调用方变成了模型。写 Tool 的过程对我来说,很快就过渡成了“设计一个严格入参、有明确返回结构的函数”,毫无门槛。

3.3 Workflow 像一条把步骤串起来的流水线,Memory 像持久化存储

只靠 Agent 本身,适合“给我一段文本,你自己看着办”的交互;但真实业务通常是有固定流程的,比如“定时拉取所有源,过滤重复,摘要,入库”。Mastra 的 Workflow 把这类逻辑组织成可追踪的多步链路:

import { Workflow } from '@mastra/core'; import { z } from 'zod'; const dailyWorkflow = new Workflow({ name: 'daily-digest-workflow', triggerSchema: z.object({ urls: z.array(z.string().url()), goal: z.string().default('日常技术阅读'), }), }); dailyWorkflow .step('fetch-all', async ({ trigger }) => { const items = await Promise.all( trigger.urls.map(async (url) => { const result = await fetchSourceTool.execute({ context: { url } }); return result; }) ); return { items }; }) .then('generate-report', async ({ data, agent }) => { const input = `整理目标:${data.goal}\n\n` + data.items .map((item) => `[${item.source}]\n${item.excerpt}`) .join('\n\n'); const result = await digestAgent.generate({ input }); return { report: result.text }; }) .then('save-report', async ({ data }) => { await saveReport(data.report); return { ok: true }; }) .commit();

一步对应一个大阶段,每步之间数据怎么流转写得明明白白。这种感觉就像把以前散落在异步回调里的多次调用,统一整理成了一个有顺序、可读、可单独修的业务流。

至于 Memory,一开始我以为是简单的聊天记录保存,后来发现它做的远比这多:它构建的是可查询的历史记忆结构,让 Agent 在多次调用之间保持一致的偏好。比如,告诉它“用户偏好简洁摘要”比较容易实现;困难的是让它理解“昨天已经整理过这两个来源,今天不要重复推荐”。这类长效记忆如果自己用关系表维护,会写得异常痛苦,而框架的记忆层天然帮你处理了大部分。

3.4 还有一点:调试体感对前端非常友好

以前别人描述 Agent 项目时,总带点“玄学”色彩,因为一不确定输出格式,二看不到中间决策。但 Mastra 的链路追踪能把每次工具调用、每段输入输出摊开给你看,配合本地开发服务,把一次生成过程拆得像浏览器 Network 面板一样清楚。这是很多骨架即使功能再强的轮子给不了的安全感。

4. 用 Mastra 写一个“每日摘要 Agent”:从空目录到真正跑通

4.1 先搭骨架再填业务,依赖不需要多

实际动手时,我建了一个普通 Node 项目,装好@mastra/core、zod,以及一个模型接入适配包。如果你愿意,也可以直接用官方脚手架,我这里更喜欢“最小依赖”起步,避免脚手架生成一堆暂时看不懂的文件。

初始化一个 provider 时,记住两件事:一是密钥优先走环境变量,不要写进代码;二是很多模型服务都兼容通用调用协议,你可以把路径指向自己可用的兼容接口。核心目标只有一个:让模型实例先能返回文本。

4.2 工具数量宁少勿多,先满足关键能力

我第一次做反了,一上来定义了抓详情、查时效、判相关性、转格式四个工具,结果模型频繁在工具之间摇摆,输入 schema 稍微模棱两可,它就选错。后来我把工具收敛到两个:一个负责抓取正文,一个负责按标题去重。schema 也尽量写严格,避免“一切都行”的宽松类型:

import { createTool } from '@mastra/core'; import { z } from 'zod'; export const dedupeTool = createTool({ id: 'dedupe-by-title', inputSchema: z.object({ articles: z.array(z.object({ title: z.string().min(1) })), }), description: '根据标题去掉重复文章,保留最先出现的一条。适合在汇总前清理重复资料。', execute: async ({ context }) => { const seen = new Set<string>(); const unique = context.articles.filter((item) => { const key = item.title.trim(); if (seen.has(key)) return false; seen.add(key); return true; }); return { unique }; }, });

给工具的 schema 越严格,模型对“什么时候不该调用”的理解就越清楚,这是一个非常重要的实战经验。

4.3 Agent 的指令要短,边界要明确

很多人在 instructions 里塞大段资料,结果模型读完就忘,或者把资料当成自己的知识去回答。我更倾向让指令只限定行为规则,具体内容让工具去拿。上面我写的 Agent 里只给了四条约束,不包含任何领域知识,效果反而稳定得多。你可以在实战中对比一下:把指令当成“工作手册”而不是“资料库”,整个系统的可维护性会提高不少。

4.4 连通整个流程,第一次能跑通即可

在跑通阶段,我直接用digestAgent.generate传入最小输入,看它能不能调用工具、输出文本。真正稳定以后才引入 Workflow。注意,这一步刻意不分两步,是为了让你先建立“最小结果可验证”的闭环。如果你直接上 Workflow,出问题时很难判断到底是模型的问题,还是某一步的数据传递出了问题。先小后大,是这套链路里最有效的调试顺序。

4.5 第一次运行日志长什么样,哪些信息值得关注

我第一次跑通时,日志里能看到模型先请求了fetch-source工具,传入 URL 参数,工具返回一个截断后的文本对象,然后模型基于返回文本开始写作。那一瞬间我明确了 Agent 运行不是“一次大模型调用”,而是“几次工具调用与大模型生成交替出现”的循环。遇到结果不对时,看这个循环在哪里断了,通常就能定位问题,而不是像裸调 SDK 一样只能盲猜。

5. 从“跑通”到“好用”之间,我踩过至少五道坎

5.1 schema 写得太宽,模型开始乱调工具

我踩的第一个坑是把某个工具的输入参数声明成z.any(),因为当时觉得“反正模型懂我想要什么”。结果模型真的“懂”到乱来,把本应传给 A 工具的个人信息塞给 B 工具,输出结构也乱七八糟。后来把 schema 收窄成具体字段,加上默认值与枚举约束,工具调用准确率立刻上了一个台阶。给机器的 schema,越精确越好,这不是形式主义。

5.2 外部请求没有超时,整个流程可以卡在第一步

Node 环境下的fetch默认没有超时时间。如果某个被抓取的站点在几十秒内不响应,工作流就会一直挂着,后面所有步骤连带被卡死。我的教训是给所有工具的执行函数都包一层超时控制:

const res = await fetch(url, { signal: AbortSignal.timeout(10000) });

如果超时,就返回一个明确的结果对象,让 Agent 在后续生成时能引用“某来源抓取失败”这个事实,而不是默默吞掉异常。

5.3 流式输出看着爽,前端断开连接后端却还在跑

我后来把 Agent 接入了一个带流式输出的页面,模型一个字一个字吐出来,体验很好。但漏掉了一个关键细节:用户一旦关闭页面,前端断开了连接,Node 后端的生成任务并不会自动停止,会一直跑到结束,白白消耗 tokens。解决办法是在服务端监听中止信号,及时对异步操作做中断处理。你如果不做这一步,线上的 token 账单会有很多看起来莫名其妙的增长。

5.4 记忆越攒越多,token 悄悄膨胀

启动 Memory 之后,我发现每次调用都会把历史记忆拼进上下文,长对话跑几十轮之后,输入 token 开始肉眼可见地飙升。后来把记忆的短期缓冲区容量调小,让 Agent 只保留最近几条消息,并启用语义回忆而不是“全部回忆”,成本才压下来。记住一点:Memory 是策略问题,不是容量问题,并不是记得越多越好。

5.5 在服务端用 for 循环疯狂调 Agent,最容易把一切打崩

写测试时经常能看到这种写法:拿到一个数组,循环调用 Agent 处理每一段。功能上没错,但并发一高就很危险——模型接口可能限流,账单也可能瞬间失控。更重要的是并发高时出错,排查会变得非常困难,因为不知道是哪一轮哪一步出了问题。建议把批量任务拆到 Workflow 里,加并发限制和失败重试,而不是把 Node 服务的线程当无底洞去用。

6. 转全栈后真正受益的地方:把 Agent 变成服务的一部分

6.1 用一条接口把 Agent 暴露出去,但别在请求里反复 new

把 Agent 放进全栈项目最直接的方式,就是通过服务端接口把生成过程包一层。前端 POST 过来一批 URL,服务端调用 Agent 生成日报,然后返回 JSON。一个需要注意的细节是:不要在请求回调里每次都new Agent(),那样会反复初始化模型配置和记忆对象,浪费资源又慢。正确的做法是在模块加载阶段初始化,之后每个请求复用同一个实例。

如果要做流式,你可以把 Agent 的 stream 变成可读流,直接作为 HTTP 响应返回。对前端来说,就像消费一个普通的流式接口,区别只在于内容的产生方多了模型调度。

6.2 把 Workflow 挂上定时调度,实现“每天早晨自动跑”

跑通接口之后,我把整个日报流程挂到了定时任务上,每天早晨固定时间触发一次。定时触发的形式很多,可以是服务进程内的调度器,也可以写成一条只有自己能调用的内部接口,再由运维侧的定时任务来请求。关键是 Workflow 的触发 schema 设计得足够独立,任何入口都可以拉起同一套链路。

6.3 记忆落库,才真正跨过“临时 Demo”的边界

默认的 Memory 如果不落库,重启进程以后就全部丢失。要让 Agent“越用越懂你的偏好”,得把记忆的存储端接到持久化数据库上,常见选型是支持向量检索的数据库。这一步会引出一个新功课:向量化用什么模型、存储怎么分表、检索阈值怎么设。对前端来说,这些概念不算难,但确实需要花一两个晚上系统理解一下。好在这个框架没有把存储实现写死,接存储层时自由度比较大。

6.4 会算账,Agent 才敢往生产里放

成本是会跟着上下文长度跑的。给你一个放之四海皆准的估算公式:

月成本 ≈ 每日调用次数 × 单次输入 token × 输入单价 × 30 + 每日调用次数 × 单次输出 token × 输出单价 × 30

这里单次输入 token 往往比你想的大,因为指令、工具说明、历史记忆都算进去了。我习惯在日志里记录每次请求的计费用量,定期拉出来看曲线。别等账单出来才意识到某天一次异常流程跑出了几十万 token。

写在最后:一次未必正确,但确实让我少走了很多弯路的尝试

现在回过头看,从“只会调大模型接口”到“能把一个带工具调用、记忆和多步工作流的 Agent 放进全栈服务”,前后不过几周。我的体会是:对前端开发者来说,进入 Agent 开发的关键不在学会某个秘技,而是找到了一套跟原有工程习惯兼容的抽象。Mastra 给我最大的价值就是把 Agent 拆成了几个我早就熟悉的形状——组件、propTypes、流水线、缓存——让我把精力省下来真正处理业务,而不是对付框架。

最后分享一个小到不起眼的建议:如果你也刚准备尝试,别一次性把所有功能都开满。先做一个只有一个工具、一次generate调用、手动触发的最小闭环;跑通以后再逐步加记忆、加工作流、加流式。每加一个环节都停下来观察它带来的成本与复杂度,你会走得比那些一上来就想复制完整生产架构的人快得多。这套路我用下来,写代码的底气来自“每一步都能被验证”,而不是来自“把文档背得多熟”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 4:20:07

影视剧片段细粒度描述:从镜头切分到微调的全流程实践

做影视剧片段理解这个方向&#xff0c;我最早犯过一个特别朴素的错误&#xff1a;觉得视频描述无非就是图像描述的加长版。这个想法在我搭建Qwen-VL-Narrator时被彻底推翻——把模型输出和真实片段一对照&#xff0c;画面在切、人物在动、台词在走&#xff0c;生成的描述却停留…

作者头像 李华
网站建设 2026/10/22 4:19:02

联邦学习如何赋能AI原生应用:从架构选型到工程落地

联邦学习这个词&#xff0c;前几年更多还停留在隐私计算圈的讨论里&#xff0c;但真正把它塞进AI原生应用跑出业务价值&#xff0c;说实话并不多见。我最近带的一个跨平台智能推荐系统&#xff0c;就是在联邦学习框架下重新设计了整条数据链路——端侧本地训练、参数上传、服务…

作者头像 李华
网站建设 2026/10/22 4:17:13

AI业务流架构师:打通AI落地最后一公里的关键角色

干AI落地这几年&#xff0c;我最大的感触是&#xff1a;大部分项目不是死在模型精度上&#xff0c;而是死在业务流没接上。老板想看到降本增效&#xff0c;技术团队交了一堆算法指标&#xff0c;业务方又说不符合使用习惯&#xff0c;最后方案只能躺在PPT里。市面上因此冒出一批…

作者头像 李华
网站建设 2026/10/22 4:15:59

Docker容器化Java开发环境搭建实战:从JDK镜像选型到Maven私服配置

1. 为什么我放弃了在物理机上裸装JDK刚入行那会儿&#xff0c;每换一台开发机&#xff0c;我都要重复一套固定动作&#xff1a;去官网找JDK安装包、配JAVA_HOME、改PATH、装Maven、调settings.xml、再装个Tomcat或者Spring Boot的运行时。一套流程走下来&#xff0c;顺利的话半…

作者头像 李华
网站建设 2026/10/22 4:15:57

ShardingSphere与@DS注解共存方案:数据源切换与分片路由的职责分离

最近在做一个订单类的演示项目&#xff0c;技术选型时同时用到了 ShardingSphere 和DS注解。本来觉得这两者各管各的没什么问题&#xff0c;结果一跑起来就出事&#xff1a;要么 ShardingSphere 的分片规则完全不生效&#xff0c;要么DS切数据源像没写一样&#xff0c;日志里 S…

作者头像 李华
网站建设 2026/10/22 4:15:37

Windows相对路径快捷方式制作:BAT脚本实现便携应用路径自适应

1. 为什么需要相对路径快捷方式1.1 从一次真实的翻车经历说起去年帮一个做影视后期的朋友处理过一台工作机&#xff0c;他习惯把项目素材放在移动硬盘里&#xff0c;桌面放了一堆快捷方式。结果硬盘盘符从E:变成F:之后&#xff0c;所有快捷方式全部失效&#xff0c;双击弹窗提示…

作者头像 李华