学习Agent开发8 高级Agent框架Mastra的基本用法

Mastra是什么

Mastra是一个由ts编写的Agent开发框架 ai-sdk langgraph 是低级的框架 因为它们关注的单次toolcall的自动响应以及ai会话本身 而Mastra则封装了ai-sdk的能力 提供了上下文管理、会话持久化、rag、skill、长期记忆等能力

这个库的协议是 Apache License 2.0 商业使用宽松 仅在使用云服务时付费

Mastra的上下文管理方法

Mastra提出了一种新的上下文管理方法 Observational Memory

指定一个token上限(默认30k) 每当主会话到达这个上限时 调用另一个小模型对其进行日志总结(gpt-5-mini) 在日志到达上限时 对日志进行再次总结

同时提供一个名为recall的tool 可以对当前会话、过往的其他会话进行精确的消息擦护照 确保模型的上下文始终充裕

构造Agent

ts 复制代码
import { createOpenAI } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { createSkill } from "@mastra/core/skills";
import { Memory } from "@mastra/memory";
import { PostgresStore } from "@mastra/pg";
import { z } from "zod";

const API_KEY = process.env.API_KEY!;
const BASE_URL = process.env.BASE_URL!;
/** 主会话模型 */
const CHAT_MODEL_ID = process.env.CHAT_MODEL_ID!;
/** OM模型 */
const OM_MODEL_ID = process.env.OM_MODEL_ID!;
const POSTGRES_URL = process.env.POSTGRES_URL!;

// 模型provider
const provider = createOpenAI({
  apiKey: API_KEY,
  baseURL: BASE_URL,
});

// 数据存储层 当前采用postgres
export const store = new PostgresStore({
  connectionString: POSTGRES_URL,
  id: "mastra-demo",
});

// 消息持久化和上下文管理层
const memory = new Memory({
  storage: store,
  options: {
    // om配置
    observationalMemory: {
      model: provider.languageModel(OM_MODEL_ID),
      // om模型的总结范围 仅限当前会话
      scope: "thread",
      // 主会话的检索范围 仅限当前会话
      retrieval: { scope: "thread" },
    },
    // 生成标题
    generateTitle: {
      model: provider.languageModel(OM_MODEL_ID),
      instructions: "根据用户第一条消息生成一个简短的会话标题",
    }
  },
});

// 需要审批的tool
export const sendEmail = createTool({
  id: "sendEmail",
  description: "发邮件",
  inputSchema: z.object({
    body: z.string(),
    subject: z.string(),
    to: z.string(),
  }),
  outputSchema: z.object({
    status: z.union([z.literal("success"), z.literal("failed")]),
    reason: z.string().optional(),
  }),
  // 预审批 可以传函数
  // 拒绝时无法传入理由
  requireApproval: true,
  // 执行过程中可以中断 进行二次确认
  suspendSchema: z.object({
    body: z.string(),
    subject: z.string(),
    to: z.string(),
  }),
  // 最后一次恢复得到的值
  resumeSchema: z.object({
    approved: z.boolean(),
    reason: z.string().optional(),
  }),
  // 如果抛出异常 其消息会被作为tool-call-error
  execute: async (input, context) => {
    // input可以信任 但resumeData不可信任
    const resumeData = context.agent?.resumeData;
    if (resumeData) {
      if (!resumeData.approved) {
        return { status: "failed" as const, reason: resumeData.reason };
      }
    } else if (input.to === "a-dangerous-email@host.com") {
      return await context.agent?.suspend(input);
    }
    /** 发送邮件 */
    return { status: "success" as const };
  },
});

const testSkill = createSkill({
  name: "xx",
  description: "xx",
  instructions: "skill-body",
})

export const agent = new Agent({
  id: "demo",
  name: "demo",
  // 系统提示词 允许异步函数返回string
  instructions: "系统提示词",
  memory,
  model: provider.languageModel(CHAT_MODEL_ID),
  tools: { sendEmail },
  skills: [testSkill],
});

关于requireApproval和suspend

不要同时使用两种机制

它们的底层共用同一套行为逻辑 通过resumeData恢复挂起的流

二者同时开启时 会先经过requireApproval的校验 检查resumeData的approved属性 不为true就直接以Tool call was denied by user作为toolcall的结果 即使之前已经approval过了

这似乎是mastra的一个bug 不过两者同时使用的情况本来也比较少见

使用Agent

Agent需要先在Mastra实例里注册才能进行多轮对话

ts 复制代码
export const mastra = new Mastra({
  agents: { demo: agent },
  storage: store,
})

agent对话涉及三个id resourceId threadId runId

目前可以简单地理解为resoueceId=userId threadId=sessionId runId=每轮会话的id

发起流式消息(开始会话或多轮对话)

ts 复制代码
// stream.id就是runId
const stream = await agent.stream('xx', {
  memory: { thread: threadId, resource: resourceId },
})
for await (const c of stream.fullStream){
  /** */
}

从挂起的流恢复

ts 复制代码
// 通过予审批(拒绝也可以)
// approved是一个新的流
const approved = agent.approveToolCall({ runId, toolCallId })

// resume一个挂起的tool
const resumed = agent.resumeStream(resumeData, { runId, toolCallId })

停止生成

ts 复制代码
// 停止所有指定thread的run
agent.abortThreadStream({
  resourceId,
  threadId
})

// 停止指定run
agent.abortRunStream(runId)

agent.sendMessage用于向thread中加入用户消息 可以基于当前会话fork新thread 可以等待当前run生成完毕 适用于一个thread多人参与的场合(比如群聊Agent)

agent.sendSignal和sendMessage类似 不过它插入的是"系统消息"不是"用户消息" 展示给模型的方式有差别

AgentController

AgentController是mastra预设的Agent运行时 包含了自动会话管理、消息队列、会话fork等 封装了ui更新 工具审批等一系列api

这个api还处于beta阶段 bug很多 文档不准 体验极差 未来可期

resource,thread和run

resource和thread是记忆层 thread必定归属一个resouce

run属于执行层 每次调用stream都会从thread继承过往消息 并开启新的对话 本轮完全生成完毕后 run删除

在tool 审批/suspense时 run挂起 runid不变

thread不是session

thread内存储的是完整的消息 即完整的toolcall+result/text(一次step的完整输出) 流式消息片段不会加入thread

mastra并不默认限制从thread产生run 可以同时从thread发起对话 然后按照时间顺序将新消息加入thread 这就可能导致会话的混乱(例如两个tab页同时对话)

需要在数据库操作上加锁 且在业务上加限制 即存在挂起/流式传输的run时不允许创建新run

函数为agent.listSuspendedRuns agent.listActiveRuns

前端展示

mastra是基于ai-sdk构建的 所以next+ai-sdk/react+ai-elements就非常适合展示mastra

但mastra并不是以peerDependencies方式引入ai-sdk的 所以不能直接用

参考下官方文档 搭建基础设施 并做如下优化

解决类型错误

api/chat/route.ts POST函数

ts 复制代码
  const stream = await handleChatStream({
    // 增加version
    version: "v6",
  });

不要改GET的内容 v5的sdk messages是正确的版本

增加approval和suspend能力

目标:无论流式传输还是历史消息 都可以以相同的方式向用户展示挂起情况并恢复会话

两种情况的消息结构是不同的

approval:在流式传输期间 toolcall和data-tool-call-approval之间有一条tool-approval-request消息 而历史消息中缺少这条消息

suspense则始终在toolcall后跟随data-tool-call-suspended消息 流式传输和历史消息是一致的

这里恢复会话的方案是

ts 复制代码
  const { sendMessage } = useChat({/* xx */})
  const handleResume = (runId: string, resumeData: Record<string, unknown>) =>
    sendMessage(undefined, { body: { runId, resumeData } })

body会被传给后端并用于恢复会话。前文已经说过 approval和suspended本质是一套机制 可以这样写 这种写法还抹平了流式传输和历史会话消息结构的差异

前端做如下修改

tsx 复制代码
messages.map(message=>{
    message.parts?.map((part, i)=>{
        if (part.type === 'data-tool-call-approval') {
            const data = part.data as ToolCallApprovalData
            return (
                  <>
                    <Button onClick={() => handleResume(data.runId, { approved: false })}>
                      拒绝
                    </Button>
                    <Button onClick={() => handleResume(data.runId, { approved: true })}>
                      同意
                    </Button>
                  </>
                )
        }
        if (part.type === 'data-tool-call-suspended') {
            const data = part.data as ToolCallSuspendedData
            // 同上 resumeData按照tool要求的格式即可
        }
    })
})

以上代码仅作示例 实际使用时还需要考虑这个挂起是否已经处理过

相关推荐
百工蜂Agent1 小时前
按下回车之后,Claude Code 在 100ms 里干了什么?
agent
颜进强1 小时前
从零搭建私人 RAG 知识库:让项目决策真正“可检索、可追溯”
前端·后端·ai编程
钱六两1 小时前
#6、Spring AI RAG 深度技术解析 — 从原理到生产实践
ai编程
leeyi1 小时前
ReAct Agent 源码拆解:Eino 如何把 Graph 变成 Agent(第64篇-E50)
llm·aigc·agent
颜进强1 小时前
Embedding 模型介绍:热门模型对比与应用场景
前端·后端·ai编程
颜进强1 小时前
LanceDB 基础使用:用 TypeScript 完成第一次向量检索
前端·后端·ai编程
颜进强1 小时前
Ollama 从入门到实践:本地模型运行、API 调用
前端·后端·ai编程
颜进强1 小时前
Embedding 基础使用:用 Ollama 和 LangChain.js 生成文本向量
前端·后端·ai编程
颜进强2 小时前
Vector Store 入门:什么是向量数据库,主流产品如何选择
前端·后端·ai编程