跳到主要内容

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:443constants.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 就跑一次,默认是 messageTokens20%,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 10000types.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.tsObservationalMemoryobservereflectgetLatestStepParts
配置类型packages/core/src/memory/types.tsObservationalMemoryObservationConfigmessageTokensbufferTokensbufferActivationactivateAfterIdle
默认值与 promptpackages/memory/src/processors/observational-memory/constants.tsOBSERVATION_CONTEXT_PROMPTOBSERVATION_CONTEXT_INSTRUCTIONSOBSERVATIONAL_MEMORY_DEFAULTS
后台缓冲packages/memory/src/processors/observational-memory/buffering-coordinator.tsBufferingCoordinator
Observer / Reflector.../observer-runner.ts.../reflector-runner.ts.../observer-agent.tsObserverRunnerReflectorRunner
与 MessageHistory 互斥packages/core/src/memory/memory.tshasObservationalMemory 一带逻辑(memory.ts:773-779)