Mastra 开发(05):Mastra memory

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

Agent 没有记忆是什么体验,写过两个 demo 的人都领教过:上一句刚说完"我喜欢蓝色",下一轮问"我喜欢什么颜色",它一脸真诚地反问你。会话状态是 Agent 从玩具走向产品的第一道坎——《AI Agent 开发》系列第 23 篇在 Python 栈用 checkpointer 加 Store 解决过一遍,《上下文工程》系列第 7 篇的结构化笔记与第 11 篇的记忆分层又是另一路手工方案。这一篇轮到 TypeScript 栈给答案:Mastra 把记忆做成了一等公民,一个 Memory 类盖住短期窗口、语义召回、工作记忆三层,配置即得。

这是《TypeScript Agent 开发(Mastra)》系列第 5 篇。全文按"双层标识 → 开箱即用 → 配置三件套 → 工作记忆深挖 → 存储后端 → 检索注入 → 内置与手写的取舍"组织,所有参数名与默认值已按 2026 年 10 月的 Mastra v1 官方文档逐一核实,文末留一个跨会话记忆的练习。


1、双层标识模型

Mastra 记忆的钥匙是两个 ID:resourceId 标识"谁",threadId 标识"哪次对话"。resource 是用户或实体的稳定标识,thread 是一次会话的隔离边界——同一个用户开三个会话,就是三个 thread 共享一个 resource。这层关系在存储层是硬约束:thread 一经创建就绑定了属主,拿另一个 resource 复用同一个 thread 会直接报错。

对照 LangGraph 就很清楚了,两边分层思想一致、叫法不同:

概念 LangGraph Mastra
会话隔离 thread_id + checkpointer threadId
跨会话实体 Store 的 namespace resourceId
长期记忆 Store 的 put/search 语义召回 + 工作记忆

LangGraph 的 记忆模型里,thread 内的短期记忆和跨 thread 的长期记忆是两套 API;Mastra 把它们收进同一个 Memory,靠 scope 决定共享范围。还有一个顺手的细节:Agent 委派子 Agent 时,Mastra 自动用"父 resourceId 加子 Agent 名"生成确定性的 resource 标识,thread 则每次新开——子 Agent 的记忆天然与父隔离,又可追溯到归属,这在 LangGraph 里要自己拼 namespace。

2、开箱即用的多轮记忆

先跑通最小闭环。沿用系列第 2 篇的工程,给 Agent 挂一个 Memory 实例,代码如下:

import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/core/memory'

export const memoryAgent = new Agent({
  id: 'memory-agent',
  name: 'Memory Agent',
  model: 'anthropic/claude-sonnet-5-5',
  instructions: '你是一个带记忆的助手。',
  memory: new Memory({ options: { lastMessages: 20 } }),
})

注意构造器里没写任何 storage——storage 和 vector 省略时默认走 LibSQL(本地文件),这就是"开箱即用"的底气:不装数据库,记忆先落进项目目录的一个 .db 文件。再用两轮对话验证记忆生效:

await memoryAgent.generate('记住:我最喜欢的颜色是蓝色', {
  memory: { resource: 'user-123', thread: 'conv-1' },
})
const res = await memoryAgent.generate('我最喜欢的颜色是什么?', {
  memory: { resource: 'user-123', thread: 'conv-1' },
})

解读:v1 里标识在调用侧以 memory: { resource, thread } 传入,generate 会自动完成"召回历史、注入 prompt、落库新消息"三件事,第二轮的回答里就有蓝色了。把 thread 换成 'conv-2',Agent 立刻失忆——对话历史只按 thread 隔离;但 resource 没变,后面配置的工作记忆和 resource 作用域的语义召回还能把信息捞回来。这正是双层标识的意义:thread 管对话内延续,resource 管用户级沉淀。

顺手再做一步验证:重启 dev 进程,用同一组 ID 问第三遍,答案依旧在——记忆落在 LibSQL 文件里而不是进程内存里。这也是它和"把历史手动塞回 prompt"的本质区别:状态持久化交给了框架和存储层,业务代码只管传 ID。

3、记忆配置三件套

Memory 的 options 里有三个核心键,正好对应三层记忆,完整配置示例如下:

new Memory({
  options: {
    lastMessages: 20,                                    // 短期:最近消息窗口
    semanticRecall: {                                    // 长期:向量检索旧对话
      topK: 4,
      messageRange: 2,                                   // 每个命中前后附带的消息数
      scope: 'resource',                                 // 检索范围:用户全部会话
    },
    workingMemory: { enabled: true },                    // 工作记忆:运行时笔记
  },
})

三层各自的职责与 v1 默认值整理成表:

配置项 作用 v1 默认值
lastMessages 每轮注入的最近消息条数 10(v0 为 40)
semanticRecall 对历史消息做向量检索召回 关闭,需显式开启
workingMemory 一块 Agent 可自主更新的笔记 关闭,需显式开启

从 v0 升级的话有三处默认值变化值得盯紧:lastMessages 从 40 砍到 10,省 token 但更容易"失忆",按对话密度显式设置;semanticRecall 从默认开启改为默认关闭;工作记忆与语义召回的 scope 默认从 thread 改成 resource——同样的配置,v1 下会跨会话共享。官方提供了迁移指南与配套 codemod,Memory.query() 已改名 recall(),老代码照着清单过一遍即可。

三层怎么叠进同一轮请求?工作记忆作为 Markdown 块拼进 system message,lastMessages 圈定的最近消息按时间序跟在后面,语义召回命中的旧消息再作为附加上下文注入。记住这个注入顺序,排查"Agent 为什么想起或想不起某件事"就有了抓手——先确认信息到底进了哪一层、有没有被窗口裁掉。

4、工作记忆深挖

三层里最值得单独讲的是工作记忆——上下文工程第 7 篇手写过"结构化笔记",Mastra 把同一套东西做成了框架内置。

4.1、模板与更新机制

工作记忆是一块 Markdown 便签,随每轮对话注入 system message。不配 template 时用默认模板,自定义模板写法如下:

workingMemory: {
  enabled: true,
  template: `# 用户画像
## 基本信息
- 姓名:
- 城市:
- 时区:
## 偏好
- 沟通风格:[正式/随意]
- 项目目标:
- 关键截止日:
  - [事项]:[日期]`,
}

解读:你只定义格式,Mastra 负责注册内置的 updateWorkingMemory 工具、把便签注入 system message、持久化到存储——Agent 对话中发现"用户叫张三、在上海",会自己调工具把便签填上。这正对应上下文工程第 7 篇的"读笔记、干活、更新笔记"循环:手写版要自己实现 note_write 工具和笔记纪律,框架版一行配置。注意模板是全量替换语义:Agent 每次提交完整的新便签内容,框架不做增量 diff,所以模板别太长。

代码侧也能直接写入,绕过 Agent 自主更新:

const memory = await memoryAgent.getMemory()
await memory.updateWorkingMemory({
  threadId: 'conv-1',
  resourceId: 'user-123',
  workingMemory: '# 用户画像\n- 姓名:张三',
})

这段适合在业务侧做初始化或纠偏——用户登录时把已有画像预填进去,比让 Agent 从零问一遍效率高。

4.2、作用域与只读

workingMemory 的 scope 默认 resource:同一用户的便签跨所有 thread 共享,新会话开场 Agent 就"认识"用户;改成 thread 则退化为单会话的草稿纸。v1 还加了 schema 模式:用 Zod 声明结构,Agent 以 JSON 读写,对象深合并、数组整体替换——@mastra/pg 等原子合并适配器上还能用 mergeWorkingMemory 做并发安全的部分更新。

只读模式是路由与子 Agent 场景的刚需:便签看得到、改不了,按请求粒度开启即可:

await routerAgent.generate('帮我转给技术支持', {
  memory: {
    resource: 'user-123', thread: 'conv-1',
    options: { readOnly: true },
  },
})

路由 Agent 只需要读用户画像做分发,绝不该污染数据——readOnly 把写权限在请求层锁死。

5、存储后端切换

5.1、从 LibSQL 到 Postgres

LibSQL 是本地开发的默认选项,生产环境换 Postgres 只动 storage 一处:

import { PostgresStore } from '@mastra/pg'

new Memory({
  storage: new PostgresStore({
    id: 'prod-storage',
    connectionString: process.env.DATABASE_URL,
  }),
  options: { lastMessages: 20, workingMemory: { enabled: true } },
})

解读:换库不换 API,recall、listThreads 等方法在所有适配器上行为一致。官方存储适配器有二十余种——MongoDB、Upstash、Cloudflare D1、DynamoDB、LanceDB、ClickHouse 都在列。选型时查一眼能力矩阵:resource 作用域的工作记忆要求适配器支持 mastra_resources 表,目前是 libSQL、PostgreSQL、MongoDB、Upstash、OracleDB 五家。

什么时候必须切?判断标准就一条:是否有多个进程实例。单实例本地开发,文件版 LibSQL 足够;一旦服务部署多实例、或多个 Agent 服务要共享同一份用户记忆,各自为政的本地文件就失效了,必须换集中式数据库。

5.2、向量与嵌入

semanticRecall 依赖向量检索,必须同时提供向量库和 embedder,缺一个直接报错。完整配置:

import { LibSQLStore, LibSQLVector } from '@mastra/libsql'
import { ModelRouterEmbeddingModel } from '@mastra/core/llm'

new Memory({
  storage: new LibSQLStore({ id: 'st', url: 'file:./local.db' }),
  vector: new LibSQLVector({ id: 'vec', url: 'file:./local.db' }),
  embedder: new ModelRouterEmbeddingModel('openai/text-embedding-3-small'),
  options: { semanticRecall: true },
})

解读:semanticRecall: true 是布尔简写,对象写法可细调 topK 与 messageRange。工作机理:每条消息落库时同步算嵌入存进向量库,每轮对话用当前输入做语义检索,命中的旧消息连同上下文一并注入——相当于对自己的历史做 RAG。Postgres 阵营用 pgvector,还可在 semanticRecall 里配 HNSW 索引参数。注意 scope: 'resource' 的跨会话检索只有 libSQL、PostgreSQL、MongoDB、Upstash 等部分适配器支持。

6、记忆检索与注入

记忆不只服务于 Agent 生成,也是业务数据:做"历史会话"列表、做审计、做数据分析,都要从应用侧直接读。Mastra 的 Memory API 如下:

const memory = await memoryAgent.getMemory()

// 读某个 thread 的全部消息
const { messages } = await memory.recall({
  threadId: 'conv-1',
  perPage: false,                                        // false = 不分页全量返回
})

// 列出某用户的全部会话
const { threads, hasMore } = await memory.listThreads({
  filter: { resourceId: 'user-123' },
  page: 0,
  perPage: 10,
})

解读:recall 是读消息的统一入口,支持分页、日期范围过滤,传 vectorSearchString 还能对历史做语义搜索;listThreads 按 resourceId 拉会话列表,返回的 hasMore 正好喂给列表页的加载更多。单取一个会话用 getThreadById。完整参数见消息历史文档。

注意:官方明确记忆系统不做访问控制——哪个用户能读哪个 resourceId 的记忆,鉴权必须在你自己的业务层把关,别把裸的 Memory API 暴露给前端。

7、内置与手写的取舍

把 Mastra 的三层放回《上下文工程》系列的坐标系,对应关系一目了然:

Mastra 内置 手工方案(上下文工程系列)
lastMessages 裁剪 第 2 篇窗口解剖、第 4 篇 token 预算
semanticRecall 第 10 篇检索注入:对自己历史做 RAG
workingMemory 第 7 篇结构化笔记

内置的收益很实际:几行配置换一条生产级管线,存储抽象、注入格式、Studio 里逐轮查看注入内容的可视化,全都白拿。代价是策略被框架定死:压缩比、笔记更新纪律、注入顺序都不归你管;v1 甚至把 Memory 上的 processors 整个移除,token 限制统一挪到 Agent 的 inputProcessors(如 TokenLimiter)——框架在持续收权,深度定制注入逻辑的人会越来越觉得束手束脚。顺带一提,v1 还在加第四层 observationalMemory:后台 Agent 自动压缩陈旧消息,方向仍是往框架里收。

我的判断:产品向的多轮对话 Agent 用内置,长任务与特殊上下文结构手写。内置版把精力省下来花在 prompt 与工具上;强审计、上下文结构特殊的场景,手写笔记与检索依然占优。两者也不互斥——内置记忆管对话层,关键任务状态照旧落到自己的笔记文件里。

8、总结

这篇把 Mastra 的记忆系统走了一遍:resourceId 与 threadId 的双层标识是地基,thread 管会话内延续、resource 管用户级沉淀;lastMessages、semanticRecall、workingMemory 三件套分别盖住短期窗口、长期召回和运行时笔记;存储从默认 LibSQL 一行切到 Postgres,检索侧 recall 与 listThreads 把记忆变成可编程的业务数据。与 LangGraph 相比,它把两套 API 收成一个类;与上下文工程的手工方案相比,它用默认值换走了灵活度。

练习:给 Agent 配上 semanticRecall(scope: 'resource')和工作记忆,在两个不同的 thread 里分别告诉它两条你的偏好(比如喜欢的颜色和时区),然后开第三个 thread 问"我告诉过你哪些偏好"。验证两件事:语义召回能否捞回两个旧会话里的信息;工作记忆的便签里是否已自动累积了这些画像。对于正要在 TS 栈做对话产品的团队,建议就从默认 LibSQL 加三件套起步,先跑通体验再谈换库。

相关推荐

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