一句话看懂the pitch
你不记得每场对话的每一个字——你下意识地观察发生了什么,然后大脑反思,重组、合并、压缩成长期记忆。OM 就是把这套机制装进了 Agent。
开启后,Agent 的上下文窗口不再无限膨胀:原始消息在后台被持续压缩成观测日志(observation log),日志太长时再被压缩成反思(reflections)。Agent 始终只看到"值得看的那部分",而不是一坨越拖越长的聊天记录。
一句话:OM 同时接管了 message history 和 working memory 的职责,官方称其准确率更高、成本比 Semantic Recall 更低。
它解决什么问题two enemies
上下文窗口是有限的。即使模型支持超大 token 上限,窗口塞满时表现也会变差。这会带来两个问题:
历史越长,表现越差
Agent 携带的原始消息越多,越容易被无关内容干扰,偏离当前任务。工具调用、页面快照、中间思考……全是噪声。
大部分 token 已无用
历史里的大多数 token 对"保持 Agent 在线"早已没有贡献,却仍然每轮付费、每次拖慢响应。
OM 的解法:把旧上下文压缩成密集观测,同时解决问题。典型例子——一个用 Playwright MCP 的 Agent,每次页面快照就是 50,000+ tokens;有了 OM,Observer 把交互过程凝成几百 tokens 的观测,Agent 不再背着每一张原始快照。
三层记忆系统three tiers
无论对话跑多久,OM 的记忆始终围绕三个层级组织,内存有界,不无限增长:
当前任务的精确对话历史,原样保留,负责"现在在做什么"。
≈ 6k tokens 常驻Observer 写下的浓缩笔记:发生了什么、当前任务、建议的下一步。
≈ 40k tokens 有界Reflector 把观测合并、提炼模式后的更浓缩层,重写整个日志。
由 Reflector 持续压缩上下文如何随时间变化observe-and-shrink cycle
默认设置下,上下文窗口不会无界增长——它在一个"观察-收缩"的循环里振荡(右侧动画即模拟这一过程):
-
0 → 30k tokens:消息历史正常增长。后台 Observer 每 ~6k tokens 缓冲一批观测(
bufferTokens: 0.2)。 -
30k 触发:缓冲的观测立即激活。被观察的消息移出上下文窗口,只留 ~6k tokens 近期历史(
bufferActivation: 0.8保留阈值的 20%)。被移除的 ~24k tokens 按典型 5–40× 压缩比变成约 1–5k tokens 的观测。
-
重复:历史从 ~6k 再涨回 30k、再次收缩。每一轮向观测日志追加内容,而日志增长速度远慢于原始历史。
-
观测达 40k:Reflector 基于当前观测和更早的反思生成一份更小的日志。循环继续,但内存始终有界。
这些是"激活阈值"而非硬上限:当后台缓冲跟不上时历史可以越过阈值。高于 blockAfter(默认 1.2,≈36k)时允许激活越过保留目标;开启 shareTokenBudget 后两个预算池共享,观测日志还小时消息历史最多可借到 ~70k tokens。
Observer 观察者how observations are made
当消息历史 token 超过阈值(默认 30,000),Observer 创建观测——关于"发生了什么"的简明笔记。它用本地快速 token 估算做阈值判断:文本用 tokenx,图片部分用 provider 感知的启发式,多模态对话也能在正确时机触发观察。
观测长这样
一条条带优先级标记和时间戳的浓缩事实——Observer 还会顺带追踪当前任务和建议的响应,让 Agent 在窗口收缩后无缝接续。
🔴 12:10 User is building a Next.js app with Supabase auth, due in 1 week
- 🔴 12:10 App uses server components with client-side hydration
- 🟡 12:12 User asked about middleware configuration for protected routes
🔴 12:15 User stated the app name is "Acme Dashboard"
缓冲与提前激活
Open observation.threadTitle 后,话题明显转变时 Observer 还能建议一个短标题(Mastra Code 就是用它给线程列表更新标题)。
OM 支持在达到 token 阈值之前激活缓冲的观测——适合提示词缓存即将过期、或 Agent 切换模型供应商的场景:
activateAfterIdle空闲即激活
线程空闲超过设定时间就激活缓冲观测。接受毫秒数、"5m" 等时长字符串,或 "auto" ——按当前模型的提示词缓存 TTL 自动选择(OpenRouter/xAI 5 分钟、Gemini 24 小时、DeepSeek 1 小时等)。
activateOnProviderChange换模型即激活
下一步要使用不同 provider/model 时强制激活——换模型必然作废提示词缓存,与其把大段原始窗口发给无法复用缓存的新 provider,不如先压缩。反思的提前激活是 opt-in,不继承顶层设置。
空闲缓冲 & 附件
observation.bufferOnIdle 设为 true:Agent 回合结束进入空闲时立即做后台观测缓冲,适合"短回合很多、不想等下一次对话或阈值"的应用(默认关,与 bufferTokens 的步内异步缓冲相互独立)。
Observer 能看到历史中的附件:文本转录里保留 [Image #1: reference-board.png] 这类可读占位符,真实附件则随文本一并转发;图片类 file 部件会尽量升级为图片输入。若 Observer 模型是纯文本或拒绝多模态输入,设 observeAttachments: false 丢弃附件(占位符仍在),或用 'auto' 让 Mastra 按模型能力自动决定。
Reflector 反思者when observations get long
当观测超过阈值(默认 40,000),Reflector 登场:压缩观测、合并相关条目、并反思其中的模式。
关键设计:反思不会积累成一个独立且无限增长的层。每次反思都会重写整份观测日志——Reflector 的输出成为新日志,新观测在其后追加;下次达到阈值时,Reflector 重新处理一切(包括更早的反思),对旧信息压缩得更激进,同时保留近期细节。
因此无论对话跑多久,内存都围绕反思阈值保持有界。⚠️ 注意:gemini-2.5-flash 极擅长在长输出中保留细节,可能导致反思在最大压缩重试后仍高于阈值——此时它会返回重试中最小的非退化候选来终止循环。想要更激进的压缩,可给 Reflector 换 grok-4-1-fast、deepseek-v4-pro 等,Observer 继续用 gemini。
提取器 Extractorpersist structured values
想要 OM 在观测之外持久化特定值?用提取器。内置的当前任务、建议响应、线程标题和自定义值走同一条提取管线。
带 schema 的提取器会作为一次结构化的 follow-up 输出请求运行;不带 schema 的是内联字符串提取,直接写进 Observer/Reflector 的响应里。默认情况下,后续运行会把上次提取的值展示给提取器;设 includePreviousExtraction: false 可让 Observer 看不到旧值。
// 从观测中提取一份紧凑的用户画像 const memory = new Memory({ options: { observationalMemory: { model: 'openai/gpt-5-mini', observation: { extract: [ new Extractor({ name: 'User profile', instructions: 'Extract stable user profile facts.', schema: z.object({ preferredName: z.string().optional(), timezone: z.string().optional(), tools: z.array(z.string()).optional(), }), }), ], }, }, }, })
从流中读取提取结果——OM 完成一次观察或反思时,data-om-observation-end(同步完成)与 data-om-buffering-end(后台完成)事件会带上 extractedValues 和 extractionFailures;提取器失败不影响其他成功值。
另有 onExtracted 回调可在持久化前规范化或做出反应(如发信号给前端);observation.manageWorkingMemory 让 Observer 自动管理工作记忆——主 Agent 不再需要自己记得调用工作记忆工具,且自动把工作记忆移入 state signals 以保持提示词缓存友好。
检索模式 Retrievalget the exact words back
压缩的代价是"原话没了"。检索模式解决这个:每个观测组都保留指向其来源消息的 range(如 startId:endId),并注册一个 recall 工具,让 Agent 在需要精确措辞、工具输出或时间线时翻回原始消息。
retrieval: true纯浏览
启用 recall 工具翻阅原始消息。不需要向量库。默认可浏览当前 resource 的全部线程。
retrieval: { vector: true }语义搜索
复用 Memory 上配置的向量库与 embedder。新观测组在缓冲时与同步观察时自动索引(非阻塞),语义搜索返回"观测组 + 它对应的原始消息 ID 区间",边说结论边给出来源。
recall 工具能力:按任意观测组区间翻页原始消息(可设 detail 粒度)· mode: "search" 语义搜索(需 vector)· 列出/浏览全部线程(默认 scope: 'resource')· 设 scope: 'thread' 则只限当前线程。自定义指引用 retrieval.instructions 追加,内置指令永不被覆盖。
作用域 Scopethread vs resource
thread · 默认每线程独立观测
每个线程有自己的观测记录,久经考验,适合作通用记忆系统,尤其适合长时间 agentic 任务。⚠️ 必须提供有效 threadId,否则直接抛错——防止多个线程静默共享一份观测记录导致数据库死锁。
resource · 实验性跨会话记忆
观测在某个 resource(通常是用户)的所有线程间共享,实现真正的跨对话记忆。注意:每个线程都变成"该用户所有线程的一个视角",可能需调整 system prompt 防止一个线程接着做另一个线程没做完的活。
resource 作用域下,所有线程的未观察消息会一起被处理——历史线程很多的用户首次启用会非常慢;且 resource 作用域不支持异步缓冲(自动禁用)。现有应用请用 thread 作用域。
Token 预算与缓冲设置token budgets
OM 用 token 阈值决定何时观察、何时反思。核心默认值:
| 设置项 | 默认 | 作用 |
|---|---|---|
| observation.messageTokens | 30,000 | 何时运行 Observer(消息历史阈值) |
| reflection.observationTokens | 40,000 | 何时运行 Reflector(观测日志阈值) |
| observation.bufferTokens | 0.2 | 缓冲频率:每 20% × messageTokens 跑一次后台观察(≈6k);也可用绝对数如 5000;false 关闭异步缓冲(观察+反思都变同步) |
| observation.bufferActivation | 0.8 | 激活时清窗力度:保留 messageTokens 的 20%(≈6k);调小保留更多历史 |
| observation.blockAfter | 1.2 | 安全网:缓冲跟不上时允许激活越过保留目标(1–100 之间乘阈值,100+ 为绝对 token 数) |
| reflection.bufferActivation | 0.5 | 观测达到阈值 50% 时开始在后台反思 |
| reflection.blockAfter | 1.2 | 反思的安全阈值,超出时无缓冲反思可用则同步运行 |
| shareTokenBudget | false | 两个预算池共享:观测日志小时消息历史可借空间(上限约 70k);需 bufferTokens: false |
缓存友好的建议
依赖提示词缓存的话:把 activateAfterIdle 设为 "auto" 或具体 TTL——线程空闲够久、缓存即将过期时,下一次请求先激活缓冲观测、发送更小的压缩窗口,而不是把大段原始消息发给注定无法命中缓存的请求。模型中途切换时用 activateOnProviderChange: true 同样避免把大窗口发给无法复用缓存的新 provider。
Token 估算另有逐 part 缓存(存在 part.providerMetadata.mastra,缓存版本/tokenizer 源匹配时才复用);文件部件可调用方直接提供 tokenEstimate 覆盖估算(仅对 image/file 部件生效)。observation.previousObserverTokens 可限制每次发给 Observer 的旧观测量(默认 2k,0 完全不发,false 不截断)。
模型建议models
Observer 和 Reflector 在后台运行,任何支持 Mastra 模型路由的模型(provider/model)都可用。未设置时默认 google/gemini-2.5-flash。建议选上下文窗口大(128K+)且足够快的模型,别拖慢用户操作。
官方实测过的模型:gemini-2.5-flashgpt-5-miniclaude-haiku-4-5deepseek-reasonerdeepseek-v4-pro / flashgrok-4-1-fastqwen3glm-4.7
@mastra/memory@1.10.0 起支持 ModelByInputTokens:按输入 token 量分级选模型——小输入用便宜快速的模型,大上下文换更强的模型,运行时按实际输入量解析层级。
快速开始quickstart
// 1) 最简单的开启方式 const agent = new Agent({ id: 'my-agent', instructions: 'You are a helpful assistant.', model: 'openai/gpt-5-mini', memory: new Memory({ options: { observationalMemory: true }, }), }) // 2) 完整示例:本地 LibSQL + 指定观察模型 const memory = new Memory({ storage: new LibSQLStore({ id: 'memory-storage', url: 'file:./memory.db' }), options: { observationalMemory: { model: 'deepseek/deepseek-reasoner', // 默认 gemini-2.5-flash }, }, }) // 同一 resource + thread 复用 → 第二次回答记得第一次的对话 const opts = { resource: 'user-123', thread: 'conversation-123' } await agent.generate('Remember that my favorite color is blue.', { memory: opts }) await agent.generate('What is my favorite color?', { memory: opts }) // → blue
只发新消息,别发完整会话历史。OM 本身依赖存储的历史;客户端重发全量历史既冗余,且客户端时间戳与存储时间戳冲突时会造成消息排序 bug。
OM 需要存储 provider,且目前只支持 @mastra/pg、@mastra/libsql、@mastra/mysql、@mastra/mongodb、@mastra/convex、@mastra/oracledb。时间间隙标记(temporalMarkers)默认关闭,开启后消息间隔 ≥10 分钟会插入一条"时隔提醒",Observer 也能据此把记忆锚定到时间点。
与其他记忆功能对比vs other memory features
| 功能 | 定位 | 与 OM 的关系 |
|---|---|---|
| Message history | 当前对话的高保真记录 | OM 接管其生命周期,messageTokens 决定保留多少原始历史 |
| Working memory | 小型结构化状态(JSON/MD):偏好、名字、目标 | OM 更适合"会长大的摘要/状态";WM 留给小而结构化的数据 |
| Semantic Recall | 基于 RAG 检索相关旧消息 | OM 声称准确率更高、成本更低;retrieval 模式可兼得语义搜索 |
注意:如果用 working memory 存"越来越长的对话摘要或状态",OM 是更好的替代——它自动管理消息历史,长对话事件日志正是它的主场。旧线程无需迁移:OM 惰性读取,首次超阈值时自动处理存量消息。
调研小结takeaways
OM 的本质:把"无限增长的上下文"换成"有界的、不断自我压缩的记忆"。Observer 负责把消息变观测(5–40× 压缩),Reflector 负责把观测变反思(整卷重写),三层结构让 Agent 同时拥有精确的近期记忆和浓缩的长期记忆。
最值得抄的三个设计:
① 异步缓冲——观察在后台预计算,阈值触发瞬间激活、零停顿,绝不打断对话;② 缓存友好——稳定、追加式的上下文让提示词前缀始终可缓存,配合空闲/换模型提前激活省成本;③ 检索兜底——压缩丢了原话?recall 工具随时翻回原始消息,且带来源 range,可追溯。
适用场景:长时间 agentic 任务、Mastra Code 这类编码 Agent、跨会话个人助理(resource 作用域)。成本:每次观察/反思是一次后台 LLM 调用,模型与频率由阈值表完全可控。