数据截至 (上游 commit 385faf0d6754)
Mastra — 观察式记忆(Observer/Reflector)
进阶章。前一章的记忆解决"找回相关历史";这一章解决另一个问题:对话太长,历史本身塞不进上下文窗口了怎么办? 答案是用后台 agent 把历史压缩成"观察"。读完你能讲清这个三 agent 系统的分工。
1. 这一章解决的问题
语义召回能找回"相关"的旧消息,但有个前提:历史得存得下、找得到。当一个会话累计到几十万 token,问题变了:
- 全塞进上下文 → 爆窗口、又贵又慢。
- 只塞最近 N 条 → 丢掉早期重要信息。
- 语义召回 → 召回的是零散片段,缺乏连贯的"我对这个用户的整体认识"。
观察式记忆(Observational Memory,OM) 的思路:像人一样,不记住每句话,而是记住"观察"——"这个用户是做 TS 的、对花生过敏、上周在调试一个内存泄漏"。这些观察由一个后台 agent 持续从对话里提炼,远比原始消息紧凑。
2. 三个 agent 的分工
OM 是个"三 agent 系统"(observational-memory.ts:234 的类注释说 "three-agent memory system")。直觉上的三方:
| 角色 | 是谁 | 干什么 |
|---|---|---|
| Actor | 你的主 agent | 正常对话;它看到的是"观察 + 建议续接 + 最近未观察的消息" |
| Observer | 后台 agent | 把累积的原始消息压成观察(第一层压缩) |
| Reflector | 后台 agent | 当观察本身也变多时,把观察再压缩(第二层压缩) |
这是两级压缩:消息 →(Observer)→ 观察 →(Reflector)→ 更精炼的观察。
Actor 看到的上下文长这样(observational-memory.ts:241-245 的注释):
- 观察(压缩后的历史)
- 一条"建议续接"消息
- 最近的未观察消息
3. 怎么触发:token 阈值
OM 不是每条消息都跑——那太贵。它按 token 阈值触发:
- 观察触发: 未观察消息的 token 累积超过
observation.messageTokens(默认 30000,types.ts:443与constants.ts:7)时,调 Observer。 - 反思触发: 观察的 token 累积超过
reflection.observationTokens(默认 40000,constants.ts:26)时,调 Reflector。
看 OM 作为处理器的两侧(observational-memory.ts:237-239 的注释):
- 输入侧: 把观察注入上下文,并过滤掉已被观察的原始消息(它们已经被压进观察里,不必再占上下文)。
- 输出侧: 跟踪新消息,token 命中阈值就触发 Observer / Reflector。
这也是为什么 OM 启用时 MessageHistory 被跳过(见上一章 §9)——OM 自己接管消息的载入与保存。装配逻辑里有这条注释:memory.ts:773 写道 "Check if ObservationalMemory is present ... it handles its own message loading and saving",随后 memory.ts:778 起的 if (!hasMessageHistory && !hasObservationalMemory) 守卫据此跳过 MessageHistory。
4. 一张图:OM 怎么转
对话持续进行,消息不断累积
│
▼
未观察消息 token 累积
│ 超过 messageTokens(默认 30k)?
┌──────┴───────┐
│ 否 │ 是
▼ ▼
照常对话 ┌──────────────┐
│ Observer │ 把这批消息压成"观察"
└──────┬───────┘
│ 观察入库,原始消息标记为已观察
▼
观察 token 累积
│ 超过 observationTokens(默认 40k)?
┌──────┴───────┐
│ 否 │ 是
▼ ▼
保留观察 ┌──────────────┐
│ Reflector │ 把观察再压缩
└──────────────┘
下一轮对话:Actor 上下文 = 观察 + 建议续接 + 最近未观察消息
怎么读: 两个独立的阈值门;消息满了触发 Observer,观察满了触发 Reflector。两级压缩让"历史总量"始终被压在可控范围。
5. 后台缓冲:让激活"瞬时"
一个工程难点:如果到了阈值才同步调 Observer,用户会卡住等一次 LLM 调用。OM 的解法是异步后台缓冲(types.ts:476 起的 bufferTokens 文档):
- 观察其实在后台提前、分批跑(每累积
bufferTokens就跑一次,默认是messageTokens的 20%,types.ts:489/constants.ts:21),结果存进缓冲区。 - 等真正命中
messageTokens阈值时,直接激活缓冲好的观察——不用现场阻塞调 LLM。
还有几个调旋钮(都在 types.ts):
bufferActivation(默认 0.8,types.ts:517):命中阈值时只激活 80% 的缓冲观察,留 20% 在手,维持对话连续性、给下个周期留缓冲。activateAfterIdle(字段在types.ts:537,JSDoc 自types.ts:530起):静默一段时间后强制激活缓冲(支持"5m"/"1hr"这种时间串,或"auto"按模型的 prompt-cache 行为自动选)。maxTokensPerBatch(字段在types.ts:474,@default 10000在types.ts:472):多线程观察时按这个大小分批并行处理——值越低并行越多但 API 调用越多。
直觉: 像"预加载"——趁你打字的间隙在后台慢慢压缩,真到了要用的时候直接拿现成的。
6. 入口方法
两个核心方法的签名值得一看:
observe(opts)(observational-memory.ts:3359):入参{ threadId, resourceId?, messages?, hooks?, requestContext?, writer?, observabilityContext? };返回{ observed, reflected, record }——一次调用可能既观察又反思,返回最新的ObservationalMemoryRecord。它内部用锁键(getLockKey,observational-memory.ts:3373)避免同一 thread 并发重复观察。reflect(...)(observational-memory.ts:3443):把观察再压缩。
注入上下文用的 prompt 模板(constants.ts:61):
The following observations block contains your memory of past conversations with this user.
配合一段指令(constants.ts:67 OBSERVATION_CONTEXT_INSTRUCTIONS)要求模型"引用观察里的具体细节、个性化回应、别给泛泛建议"——这就是 Actor 怎么"用上"压缩记忆的。
7. 巧妙之处
- 两级压缩。 消息→观察→反思,把"上下文无限增长"压成"近乎常量"。这是 OM 区别于普通召回的本质:它主动遗忘细节、保留要点,而非被动检索。
- 后台缓冲 + 瞬时激活。 把昂贵的压缩调用从"用户等待路径"挪到"后台空闲时"(
types.ts:476起),命中阈值时零阻塞激活。这是把延迟藏起来的经典手法。 - 留一手(bufferActivation 0.8)。 不一次性用光缓冲(
types.ts:517),保留储备维持连续性——很细的体验考量。 - 接管消息生命周期。 OM 启用时直接绕过 MessageHistory(
memory.ts:778的跳过守卫,注释见memory.ts:773),自己管载入与保存,避免两套机制打架。
8. 边界与局限
- 压缩有损。 观察是"提炼",必然丢细节;OM 适合"长期关系型"对话(记住用户画像),不适合"必须逐字精确"的场景。
- 多依赖、多旋钮。 阈值、缓冲比例、激活比例、TTL、批大小……配置面很大,默认值(
constants.ts)是起点但生产需调。 - 需要后台执行能力。 异步缓冲依赖后台任务机制;Observer/Reflector 是真实的 LLM 调用,有成本。
- 本文未深入的部分(诚实):
observe/reflect的内部分批、缓冲协调(BufferingCoordinator)、reflector 的具体压缩算法,本章只读了入口签名、类型注释(types.ts)和默认常量(constants.ts),未逐行追reflector-runner.ts/observer-runner.ts的实现细节。
9. 代码地图
| 主题 | 文件 | 关键符号 |
|---|---|---|
| OM 主处理器 | packages/memory/src/processors/observational-memory/observational-memory.ts | ObservationalMemory、observe、reflect、getLatestStepParts |
| 配置类型 | packages/core/src/memory/types.ts | ObservationalMemoryObservationConfig、messageTokens、bufferTokens、bufferActivation、activateAfterIdle |
| 默认值与 prompt | packages/memory/src/processors/observational-memory/constants.ts | OBSERVATION_CONTEXT_PROMPT、OBSERVATION_CONTEXT_INSTRUCTIONS、OBSERVATIONAL_MEMORY_DEFAULTS |
| 后台缓冲 | packages/memory/src/processors/observational-memory/buffering-coordinator.ts | BufferingCoordinator |
| Observer / Reflector | .../observer-runner.ts、.../reflector-runner.ts、.../observer-agent.ts | ObserverRunner、ReflectorRunner |
| 与 MessageHistory 互斥 | packages/core/src/memory/memory.ts | hasObservationalMemory 一带逻辑(memory.ts:773-779) |