Mastra Observational Memory 实战:给你的 AI Agent 装一个"会遗忘"的大脑

Mastra Observational Memory 实战:给你的 AI Agent 装一个"会遗忘"的大脑

上周在一个客服 Agent 项目里踩了个坑。用户跟 Agent 聊了 200 多轮之后,Agent 开始胡说八道------把用户上午说的需求和下午的完全搞混了。查了下原因,context window 塞满了 15 万个 token 的历史消息。GPT-4o 的 128k 窗口,纸面上够大,实际上塞满之后回答质量断崖式下跌。

这不是个例。长对话场景下 AI Agent 的记忆管理,是个工程问题,不是模型能力问题。

今年 2 月,Mastra(Gatsby.js 原班人马做的 TypeScript Agent 框架,GitHub 23k 星)发布了 Observational Memory(OM),用一种模仿人类记忆的方式来解决这个问题。我花了两周时间把它集成进项目,替换掉了之前自己糊的滑动窗口方案。这篇文章记录具体的实现原理、代码和踩坑过程。

先说问题:为什么"塞更多历史消息"是个死胡同

大部分 Agent 框架处理长对话的方式很粗暴:保留最近 N 条消息,或者用 token 计数做截断。这两种方案我都用过,各有各的烂法。

滑动窗口(保留最近 N 条):用户第 3 轮说了"我要做的是一个电商后台",到第 50 轮这条消息被丢掉了,Agent 开始问"你要做什么项目?"------用户骂人。

Token 截断:从头部开始删消息,直到总 token 数降到阈值以下。问题跟滑动窗口一样,只是截断位置不同。

RAG 语义召回:把历史消息存到向量数据库,每轮用当前问题检索相关消息。听起来完美,实际有个隐蔽 bug------向量检索对"用户在第 10 轮改了需求"这种顺序相关的信息,召回率很差。因为 embedding 模型不编码时序关系。

我之前的方案是混合型:最近 20 条完整保留 + 更早的消息做 RAG 召回。效果比纯截断好,但代码量上去了,而且需求变更类的信息仍然容易丢。

Observational Memory 的设计思路

Mastra 的 OM 换了个思路:不保留原始消息,改成让两个后台 Agent 把对话"看一遍",然后写笔记。

架构上分三层:

  1. 最近几轮的原始消息------完整保留,因为当前任务需要精确上下文
  2. Observer 生成的观察笔记------把更早的对话压缩成简短记录
  3. Reflector 生成的反思------当观察笔记本身也太长时,进一步压缩

这跟人类记忆确实有点像。你不记得上周一中午跟同事说了哪些原话,但你记得"上周讨论过数据库选型,最后决定用 PostgreSQL"。

实际压缩比在 5 到 40 倍之间。一个 Playwright MCP Agent,每次页面快照 5 万 token,Observer 处理后变成几百 token 的笔记------页面上有什么、做了什么操作、结果是什么。

最小可运行的代码

先跑起来,再讲原理。

1. 初始化项目

bash 复制代码
mkdir mastra-om-demo && cd mastra-om-demo
npm init -y
npm install @mastra/core@latest @mastra/memory@latest @mastra/libsql@latest
npm install @ai-sdk/openai typescript tsx -D

2. 配置 Agent

typescript 复制代码
// src/agent.ts
import { Mastra } from '@mastra/core';
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';

// 存储层------开发阶段用内存数据库,生产换 PostgreSQL
const storage = new LibSQLStore({
  id: 'demo-storage',
  url: ':memory:',
});

const mastra = new Mastra({ storage });

// 创建带 OM 的 Agent
const agent = new Agent({
  name: 'support-agent',
  instructions: '你是一个技术支持助手。记住用户提到的所有技术栈和需求细节。',
  model: 'openai/gpt-4o',
  memory: new Memory({
    options: {
      lastMessages: 20,  // 保留最近 20 条原始消息
      observationalMemory: {
        model: 'google/gemini-2.5-flash',  // Observer/Reflector 用的模型
        temporalMarkers: true,  // 标记时间间隔
      },
    },
  }),
});

export { agent };

核心就一行:observationalMemory: true。如果不指定 model,默认用 google/gemini-2.5-flash,这个模型便宜够快,做压缩任务绰绰有余。

3. 运行对话

typescript 复制代码
// src/main.ts
import { agent } from './agent';

async function run() {
  const memoryCtx = {
    memory: {
      resource: 'user-001',
      thread: 'thread-001',
    },
  };

  // 第 1 轮:用户描述需求
  const r1 = await agent.generate(
    '我在做一个 Next.js 14 的电商后台,用 Supabase 做认证,deadline 下周五',
    memoryCtx,
  );
  console.log('Agent:', r1.text);

  // 第 2 轮:追问技术细节
  const r2 = await agent.generate(
    '数据库用 PostgreSQL,ORM 是 Drizzle,部署到 Vercel',
    memoryCtx,
  );
  console.log('Agent:', r2.text);

  // 模拟 100 轮对话后...
  // Observer 会在 token 超过 30000 时自动触发压缩
  // 生成类似这样的观察笔记:
  // - 🔴 用户在做 Next.js 14 电商后台,deadline 下周五
  //   - 认证:Supabase
  //   - 数据库:PostgreSQL + Drizzle ORM
  //   - 部署:Vercel
  //   - 🟡 用户询问了中间件配置的问题

  // 第 101 轮:Agent 仍然记得第 1 轮的信息
  const r3 = await agent.generate('我的项目 deadline 是什么时候?', memoryCtx);
  console.log('Agent:', r3.text);
  // 输出:你的 deadline 是下周五
}

run().catch(console.error);

resource 是用户 ID,thread 是会话 ID。同一个 resource 下可以有多个 thread,Observer 的观察笔记按 thread 隔离。

Observer 和 Reflector 的工作机制

说完用法,聊下内部机制,因为理解了机制才能调好参数。

Observer 什么时候触发

不是每轮都触发。Observer 监控当前 thread 的历史消息 token 数,用 tokenx 库做本地估算(不调 API),超过阈值(默认 30000 token)时才跑。

触发后,Observer 读取所有未处理的消息,生成一份观察笔记。笔记格式是带时间戳的条目,每条标注优先级:

  • 🔴 红色:关键事实(项目名称、技术栈、deadline)
  • 🟡 黄色:普通交互(问了个问题、确认了个方案)

这些笔记替换掉对应的原始消息。下次 Agent 回复时,context window 里看到的是笔记,不是原始对话。

Reflector 什么时候触发

当 Observer 的笔记本身超过 40000 token 时(对话特别长的场景),Reflector 启动。它把多份观察笔记合并、去重、归纳模式。

比如 Observer 记了 50 条关于"用户反复修改首页设计"的笔记,Reflector 可能压缩成一条:"用户对首页设计经历了 5 次迭代,从卡片布局改到瀑布流,最终选定双栏网格,核心诉求是移动端首屏加载速度。"

三层结构的最终形态

Agent 的 context window 大概长这样:

css 复制代码
[系统 prompt]
[Reflections - 最顶层的长期总结]
[Observations - Observer 的近期笔记]
[最近 20 条原始消息 - 当前任务的精确上下文]

从底往上,信息颗粒度递减,时间范围递增。最近的对话是逐字保留的,更早的被压缩成笔记,最早的被进一步归纳。

生产环境的配置建议

在实际项目里用了两周,总结几个经验。

存储选 PostgreSQL

开发用 LibSQL 没问题,生产建议 PostgreSQL。原因很实际------观察笔记和反思记录需要持久化,SQLite 在并发写入时性能不行。

typescript 复制代码
import { PostgresStore } from '@mastra/pg';

const storage = new PostgresStore({
  connectionString: process.env.DATABASE_URL,
});

Observer 模型选便宜的

Observer 和 Reflector 的工作是压缩文本,不需要最强的推理能力。我测试过几个组合:

Observer 模型 压缩质量 单次成本(10 万 token 输入)
gpt-4o 优秀 ~$0.25
gemini-2.5-flash 够用 ~$0.02
deepseek-reasoner 优秀 ~$0.14

gemini-2.5-flash 的性价比最高。压缩质量跟 gpt-4o 差距不大,成本低一个数量级。deepseek-reasoner 的压缩质量确实好,但推理 token 消耗多,成本介于两者之间。

调整触发阈值

默认 30000 token 触发 Observer,对于大多数场景合理。但如果你的 Agent 用了 MCP 工具(比如浏览器自动化),每次工具返回结果可能就有几万 token,可能需要调低阈值。

typescript 复制代码
const memory = new Memory({
  options: {
    observationalMemory: {
      model: 'google/gemini-2.5-flash',
      // 这里的配置用默认值就行,框架目前没有暴露阈值参数
      // 但可以通过 lastMessages 控制保留的原始消息数量
    },
    lastMessages: 10,  // MCP 工具场景建议减少原始消息保留数
  },
});

时间标记的实际用途

temporalMarkers: true 我强烈建议打开。它在消息间隔超过 10 分钟时插入一个时间标记。好处有两个:

  1. Agent 知道用户隔了多久回来,可以调整回复语气("好久不见"vs 接着聊)
  2. Observer 写笔记时能标注时间锚点:"用户在间隔 2 天后询问了部署问题"

踩坑记录

坑 1:客户端发了完整历史

这是官方文档特意警告的。如果你的前端用 AI SDK UI 之类的库,每次请求可能会把完整对话历史发过来。OM 自己也维护了一份存储的历史,两份历史的时间戳冲突会导致消息排序 bug。

解决方案:前端只发当前这条新消息,不发历史。

typescript 复制代码
// 错误写法------前端发了整个 messages 数组
const response = await fetch('/api/chat', {
  body: JSON.stringify({ messages: allMessages }),
});

// 正确写法------只发最新一条
const response = await fetch('/api/chat', {
  body: JSON.stringify({ message: latestMessage }),
});

坑 2:resourceId 和 threadId 的关系

每个 thread 创建时会绑定一个 resourceId(用户 ID),之后不能改。如果你复用了同一个 threadId 但换了 resourceId,会报错。

实际开发中的建议:threadId 用 UUID,不要用业务含义的 ID(比如订单号),避免不同用户的会话被路由到同一个 thread。

坑 3:存储适配器的兼容性

OM 目前只支持三种存储:@mastra/pg@mastra/libsql@mastra/mongodb。如果你用了 Pinecone 或者 Qdrant 做向量存储,那是 RAG 的存储,跟 OM 的存储不是一回事。OM 需要的是关系型存储来放观察笔记和反思记录。

跟其他方案的对比

我之前用过三种长对话记忆方案,做个真实对比:

自己写滑动窗口 + RAG 召回:灵活但维护成本高。需要自己处理 embedding、向量数据库、召回逻辑、时序关系。500 行代码起步,而且时序相关信息的召回率始终不理想。

LangChain 的 ConversationSummaryBufferMemory:用一个 LLM 做实时摘要。问题是每轮都触发摘要,API 成本高,而且摘要是全量重写而非增量更新。

Mastra OM:增量压缩,按需触发,三层结构。配置简单------一行开启,存储适配器换一下就行。限制是目前只能在 Mastra 框架内用,不能独立引入。

完整示例:带工具调用的客服 Agent

最后给一个接近真实场景的例子------一个能查询订单状态的客服 Agent:

typescript 复制代码
import { Mastra } from '@mastra/core';
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { PostgresStore } from '@mastra/pg';
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

const storage = new PostgresStore({
  connectionString: process.env.DATABASE_URL!,
});

const mastra = new Mastra({ storage });

// 定义工具
const queryOrder = createTool({
  id: 'query-order',
  description: '查询订单状态',
  inputSchema: z.object({
    orderId: z.string().describe('订单编号'),
  }),
  execute: async ({ context }) => {
    // 实际项目里这里查数据库
    return {
      orderId: context.orderId,
      status: 'shipped',
      trackingNumber: 'SF1234567890',
      estimatedDelivery: '2026-05-10',
    };
  },
});

const supportAgent = new Agent({
  name: 'customer-support',
  instructions: `你是电商平台的客服。回答用户关于订单、物流、退款的问题。
记住用户之前提到的订单号和问题,不要让用户重复说。`,
  model: 'openai/gpt-4o',
  tools: { queryOrder },
  memory: new Memory({
    options: {
      lastMessages: 15,
      observationalMemory: {
        model: 'google/gemini-2.5-flash',
        temporalMarkers: true,
      },
    },
  }),
});

这个 Agent 在 100 轮对话后,仍然记得用户第 3 轮提到的订单号,因为 Observer 会把"用户在 14:30 查询了订单 #20260501-003,状态是已发货"写进观察笔记。

总结

OM 不是万能方案,但它解决了长对话场景下最头疼的问题:怎么在有限的 context window 里保留足够的历史信息。

适合用 OM 的场景:多轮技术支持、项目管理助手、长期陪伴类 Agent------任何对话轮次可能超过 50 轮的场景。

不需要 OM 的场景:单轮问答、每次交互独立的工具调用。

框架本身还在快速迭代。从 1.1.0 加入 OM 到现在的版本,已经加了多模态附件支持、资源级跨线程记忆(实验性)等功能。代码在 GitHub 上,感兴趣可以直接翻 PR #12599 看实现细节。

相关推荐
周末程序猿1 小时前
技术总结|十分钟了解GEO
人工智能·aigc
周末程序猿1 小时前
技术总结|十分钟了解大模型投毒
人工智能·aigc
sunneo2 小时前
专栏E-产品品牌与叙事-00-专栏简介
人工智能·产品运营·aigc·产品经理·ai-native
SmartBrain2 小时前
Harness 工程建设与 AI 平台建设对比
大数据·人工智能·华为·aigc
发哥来了12 小时前
AI视频生成模型选型指南:五大核心维度对比评测
大数据·人工智能·机器学习·ai·aigc
发哥来了12 小时前
AI驱动生产线的实际落地:一个东莞厂商的技术选型实录
大数据·人工智能·机器学习·ai·aigc
Rubin智造社13 小时前
2026年热门AI工具汇总|8大类别全覆盖,办公/创作/编程一键解锁
人工智能·ai作画·aigc·ai工具
Aision_19 小时前
LangGraph 中 State、Node、Edge 是怎么协作的?
langchain·prompt·aigc·embedding·ai编程·ai写作·agi
Code_Artist1 天前
格力之🐯王自如48天做出AI App:这不是“技术神话”,而是工程能力的重构!
aigc·agent·ai编程