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。两级压缩让"历史总量"始终被压在可控范围。