注:文章由 codex 整理。

背景#

oh-my-pi 的 memory 系统不是一个单一模块,而是一层 MemoryBackend 抽象下面挂了几种不同的实现。它要解决的问题是:Agent 的上下文窗口是短期的,但用户偏好、项目约定、设计决策、踩坑经验应该跨 session 保留下来,并能在未来对话中重新进入模型上下文。

从实现上看,oh-my-pi 把 memory 拆成三个问题:

  1. 记忆写到哪里:本地 Markdown 摘要、本地 SQLite、还是远端 Hindsight 服务。
  2. 什么时候写入:工具显式 retain,还是每 N 轮自动保留 transcript。
  3. 什么时候读出:启动时注入摘要,首轮 prompt 前自动 recall,或者由模型主动调用 recall / reflect

总体架构#

memory backend 的公共接口定义在:

packages/coding-agent/src/memory-backend/types.ts
packages/coding-agent/src/memory-backend/resolve.ts

resolveMemoryBackend(settings) 根据 memory.backend 选择具体实现:

memory.backend = off        -> offBackend
memory.backend = local      -> localBackend
memory.backend = hindsight  -> hindsightBackend
memory.backend = mnemopi    -> mnemopiBackend

MemoryBackend 的关键 hook 是:

  • start():session 启动时建立运行时状态、订阅 session event。
  • buildDeveloperInstructions():构造要注入系统提示词的 memory 文本。
  • beforeAgentStartPrompt():在当前 turn 生成前追加一次性 recall 结果,保证首轮就能吃到记忆。
  • enqueue():强制触发 retain / consolidation。
  • clear():清理当前 backend 的持久化状态。
  • status() / search() / save() / stats() / diagnose():给 UI、slash command、extension 使用。

三种真实 backend 的定位不同:

Backend 定位 主要存储 查询能力
local 历史 session 摘要管线 memory_summary.mdMEMORY.mdskills/learned.md 无结构化 search,只做提示词注入
hindsight 远端托管 memory 服务 Hindsight server-side bank 服务端 retain / recall / reflect
mnemopi 本地 SQLite memory engine SQLite bank,含 working / episodic / FTS / embedding / facts 本地混合检索,可编辑、可诊断

用数据流表示:

flowchart TD
    A["AgentSession"] --> B["resolveMemoryBackend(settings)"]
    B --> C["local backend"]
    B --> D["hindsight backend"]
    B --> E["mnemopi backend"]

    C --> C1["rollout summaries"]
    C1 --> C2["memory_summary.md / MEMORY.md"]
    C2 --> F["developer instructions"]

    D --> D1["Hindsight HTTP API"]
    D1 --> D2["remote bank / mental models"]
    D2 --> F

    E --> E1["MnemopiSessionState"]
    E1 --> E2["Mnemopi facade"]
    E2 --> E3["BeamMemory"]
    E3 --> E4["SQLite: working, episodic, FTS, embeddings, facts"]
    E4 --> F

三种 backend 的差异#

local:把历史会话压缩成长期提示词#

localBackend 包装的是 packages/coding-agent/src/memories/ 目录下的旧管线。它启动时扫描过去的 session jsonl,做两阶段 LLM 处理:

  1. Stage 1:逐个 session 提取 durable signal,例如技术决策、约束、踩坑、工作流。
  2. Stage 2:把多个 session 的提取结果合并成 MEMORY.mdmemory_summary.md 和 generated skills。

local 的优点是简单、可读、可审计。最终注入的东西是一个稳定的摘要块,而不是每次都跑检索。缺点是它没有结构化 search,不能针对当前问题精确召回某条记忆,更像“启动时读一本项目笔记”。

hindsight:远端托管记忆#

hindsightBackend 通过 HindsightApi 调用远端 HTTP API,agent 侧只负责:

  • 计算 bank scope。
  • 首轮或压缩前 recall。
  • 每 N 轮 retain transcript。
  • 加载 mental models 并注入 <mental_models>
  • 暴露 retain / recall / reflect 工具。

它的优势是服务端可以持续演进,天然适合跨设备、跨团队共享。缺点是依赖远端服务,涉及 API token、隐私、网络可用性和服务端可观测性。

mnemopi:本地可检索记忆库#

mnemopi 是最完整的本地实现。它不是简单把文本 append 到文件,而是把 memory 存成 SQLite bank,并同时维护:

  • 短期原始记忆:working_memory
  • 长期情节记忆:episodic_memory
  • 全文检索索引:fts_working / fts_episodes
  • 向量索引:memory_embeddings,以及可选 sqlite-vec 相关表
  • 结构化事实:memoria_factsfacts
  • 关系和标注:triplesannotations
  • 临时便签:scratchpad

它的优势是本地、可搜索、可编辑、可诊断;即使 embedding 或 LLM fact extraction 不可用,也能靠 FTS / lexical fallback 降级。缺点是复杂度明显更高:要管理 SQLite schema、embedding model、后台 extraction、consolidation,以及不同 bank 的隔离策略。

mnemopi 的分层#

mnemopicoding-agentpi-mnemopi 两个 package 之间分层:

packages/coding-agent/src/mnemopi/backend.ts
packages/coding-agent/src/mnemopi/state.ts
packages/coding-agent/src/mnemopi/config.ts

packages/mnemopi/src/core/memory.ts
packages/mnemopi/src/core/beam/index.ts
packages/mnemopi/src/core/beam/schema.ts
packages/mnemopi/src/core/beam/store.ts
packages/mnemopi/src/core/beam/recall.ts
packages/mnemopi/src/core/beam/consolidate.ts

可以理解为三层:

  1. Agent 集成层mnemopiBackendMnemopiSessionState,负责 session 生命周期、提示词注入、工具接入、记忆库隔离策略。
  2. Facade 层Mnemopi class,提供 rememberrecallrecallEnhancedsleepgetStats 等稳定 API。
  3. Beam 存储/检索层BeamMemory,直接操作 SQLite、FTS、embedding、fact extraction、consolidation。

mnemopi 的生命周期#

启动#

mnemopiBackend.start() 在 session 启动时运行:

  1. 读取配置:loadMnemopiConfig(settings, agentDir)
  2. lazy load @oh-my-pi/pi-mnemopi@oh-my-pi/pi-mnemopi/core
  3. 构造 MnemopiSessionState
  4. 订阅 AgentSession event。

它特意 lazy load mnemopi,因为 mnemopi 会引入 embedding stack。源码里还把 fastembed 初始化路由到专门的 embedding subprocess,避免 onnxruntime-node 直接进入 agent 主进程地址空间。

首轮 recall#

MnemopiSessionState.beforeAgentStartPrompt()maybeRecallOnAgentStart() 都会在首轮生成前尝试 recall:

  1. 取最新用户输入。
  2. 加上最近几轮上下文,调用 composeRecallQuery()
  3. mnemopi.recallMaxQueryChars 截断。
  4. 调用 recallForContext()
  5. 把结果格式化为 <memories>...</memories>
  6. 刷新 base system prompt。

这里复用了 hindsight/content.ts 的工具函数。一个重要细节是:retention 之前会剥掉 <memories><mental_models>,避免 recall 出来的记忆再次被 retain,形成自我强化的污染循环。

自动 retain#

每次 agent_end 时,maybeRetainOnAgentEnd() 检查是否达到 mnemopi.retainEveryNTurns。默认 schema 里是 4,也就是每 4 个 user turn 自动保存一次 transcript。

保存时会:

  1. 从 session manager 提取 user/assistant 消息。
  2. 调用 prepareRetentionTranscript(messages, true) 生成带 role marker 的 transcript。
  3. 另外生成只包含 user-authored turns 的 extractText,用于事实/实体抽取。
  4. 调用 rememberInScope() 写入当前 retain bank。

自动 retain 写入的记忆参数大致是:

source: coding-agent-transcript
importance: 0.65
scope: bank
extract: true
extractEntities: true
veracity: unknown
memoryType: episode
metadata: session_id, source_id, message_count, cwd

显式 retain 工具写入的参数更像事实:

source: coding-agent-retain
importance: 0.75
scope: bank
extract: true
extractEntities: true
veracity: tool
memoryType: fact
metadata: session_id, cwd, context, tool

Session 收尾与记忆固化#

MnemopiSessionState.dispose() 默认会先 consolidate() 再关闭 SQLite handle。consolidate() 做两件事:

  1. forceRetainCurrentSession():把当前 session 再保存一次。
  2. 对所有 owned bank 执行:
    • flushExtractions():等待后台 fact extraction 完成。
    • sleepAllSessions(false):把 eligible working memory consolidate 到 episodic memory。

sleep 是 mnemopi 的长期化动作:把 working_memory 中尚未 consolidated 的内容聚合、摘要、写入 episodic_memory,并标记原始 working rows 的 consolidated_at

记忆库的隔离策略#

mnemopi 的记忆库隔离逻辑在 packages/coding-agent/src/mnemopi/config.tsstate.ts

支持三种 scoping:

模式 写入 bank 召回 bank 语义
global shared bank shared bank 所有项目共享
per-project project bank project bank 项目硬隔离
per-project-tagged project bank project bank + shared bank 项目记忆隔离写入,同时召回全局偏好

注意:hindsightper-project-tagged 是“同一个 bank + project tag”;但 mnemopi 没有 tag-filtered recall,所以它用“项目 bank + 共享 bank”的方式模拟:

retain -> project bank
recall -> project bank + global bank

project bank 的名字来自 cwd 的绝对路径和 Bun.hash(),而不是 git root。源码注释说这是为了避免 .git 出现/消失导致 bank id 变化,从而把同一个项目的记忆打散。

SQLite 数据模型#

BeamMemory 打开数据库时调用 initBeam(db) 初始化 schema。数据库打开逻辑在 packages/mnemopi/src/db.ts

  • 自动创建父目录。
  • 开启 PRAGMA foreign_keys=ON
  • 设置 busy_timeout=5000
  • 非内存 DB 开启 journal_mode=WAL

核心表如下:

working_memory#

working_memory 是新写入记忆的主表。字段包括:

  • id
  • content
  • source
  • timestamp
  • session_id
  • importance
  • metadata_json
  • veracity
  • memory_type
  • consolidated_at
  • recall_count
  • last_recalled
  • valid_until
  • superseded_by
  • scope
  • author_id / author_type / channel_id
  • trust_tier
  • temporal fields

它承担“近期、原始、还没被长期化”的角色。

episodic_memory#

episodic_memory 是长期化后的情节记忆。它和 working_memory 字段相似,但多了:

  • rowid
  • summary_of
  • tier
  • degraded_at
  • binary_vector

tierdegraded_at 支持后续的降级压缩:老的 episodic memory 可以再被摘要,减少体积。

FTS 和 embeddings#

全文检索由 SQLite FTS5 支持:

  • fts_working
  • fts_episodes

触发器会在 insert / delete / update 时同步 FTS 表。

向量相关表是:

  • memory_embeddings(memory_id, embedding_json, model, created_at)
  • episodic_memory.binary_vector
  • 可选 sqlite-vecvec_episodes

reconcileEmbeddingModel() 会检查 memory_embeddings.model 是否和当前 embedding model 一致。如果模型变了,会清空旧 embedding 并后台重建,避免不同维度/不同模型的向量混在一起比较。

facts / annotations / graph#

mnemopi 不只存文本,还会尽量抽结构化信息:

  • memoria_facts
  • facts
  • memoria_timelines
  • memoria_instructions
  • memoria_preferences
  • memoria_kg
  • annotations
  • triples

这部分用于增强 recall,尤其是用户偏好、时间线、实体关系、指令类记忆。

写入流程#

Mnemopi.remember() 最终进入 BeamMemory.remember(),再进入 store.tsremember()

核心流程:

sequenceDiagram
    participant Tool as retain / auto retain
    participant State as MnemopiSessionState
    participant Facade as Mnemopi
    participant Beam as BeamMemory
    participant DB as SQLite
    participant BG as Background jobs

    Tool->>State: rememberScoped(content, options)
    State->>Facade: remember(content, options)
    Facade->>Beam: beam.remember()
    Beam->>DB: insert/update working_memory
    Beam->>DB: FTS trigger updates fts_working
    Beam->>BG: scheduleEmbedding(memoryId, content)
    Beam->>BG: scheduleFactExtraction(extractText)
    Beam->>BG: proactive graph linking

具体机制:

  1. 根据 content + timestamp 生成 memory id。
  2. 如果同一 session 下 content 重复,则更新已有 row 的 importance、timestamp、source 等,而不是插入重复行。
  3. 新 memory 写入 working_memory
  4. 添加 temporal annotations,例如 occurred_on
  5. 触发 FTS 同步。
  6. 调度 embedding。
  7. 如果 extract: true,后台运行 LLM fact extraction。
  8. 如果启用 proactive linking,写入 episodic graph。
  9. 清理 query cache。

这个设计的关键是:remember() 本身是同步可返回的,embedding 和 fact extraction 都是 best-effort 后台任务,不阻塞 durable write。

召回流程#

召回入口有三个:

  • 自动 recall:首轮 prompt 前注入 <memories>
  • recall tool:模型主动搜索记忆。
  • reflect tool:在 mnemopi 里其实是先 recall,再把结果格式化为“Based on recalled memories”。

mnemopi 的召回不是单一路径,而是混合检索。recall.ts 里主要有:

  • query tokenize / normalize
  • synonym expansion
  • FTS query
  • embedding query
  • lexical fallback
  • temporal parser
  • intent-based weight adjustment
  • MMR rerank
  • fact recall

候选来源可以概括为:

query
  -> FTS candidates
  -> vector candidates
  -> lexical fallback candidates
  -> facts candidates
  -> score / dedupe / rerank

评分公式不是单一 cosine similarity。候选分数会综合:

  • dense_score:向量相似度。
  • fts_score:FTS 匹配强度。
  • keyword_score:词面匹配。
  • importance_score:写入时的重要性。
  • recency_score:时间衰减。
  • temporal_score:时间型 query 的额外加权。
  • veracityWeight:可信度权重。
  • currentContentAdjustment:当 query 询问 current/latest 时,提升 current/active 记忆,压低 stale/legacy 记忆。
  • tierWeight:working / episodic 层级权重。

源码里的主要配置权重来自:

vecWeight
ftsWeight
importanceWeight
temporalWeight
temporalHalflife

最终结果会排序、去重、可选 MMR,然后更新 recall count / last recalled。

consolidation:从 working 到 episodic#

mnemopisleep() 是把短期记忆固化成长时记忆的动作。它会找出 eligible 的 working_memory,生成 summary,然后调用 consolidateToEpisodic() 写入 episodic_memory

简化流程:

flowchart TD
    A["working_memory: unconsolidated rows"] --> B["sleep / sleepAllSessions"]
    B --> C["select eligible rows by session"]
    C --> D["summarize / consolidate"]
    D --> E["insert episodic_memory"]
    E --> F["extract facts from summary"]
    E --> G["schedule embedding"]
    D --> H["mark working_memory.consolidated_at"]

这样做的意义是分离两种记忆:

  • working_memory 保留近期原始对话细节,适合短期精确召回。
  • episodic_memory 保留压缩后的长期经验,适合跨 session 延续。

这比简单“所有历史都塞进向量库”更像人类记忆模型:近期细节和长期摘要有不同保真度、不同生命周期。

memory 的调用方式#

oh-my-pi 里的 memory 调用要区分三类入口:

类型 入口 谁触发 作用
Slash command /memory ... 用户 查看、诊断、清理、强制整理 memory backend
Model tool retain / recall / reflect 模型 写入、搜索、基于记忆汇总
自动流程 auto recall / auto retain / consolidation Session lifecycle 首轮注入记忆、定期保存 transcript、结束时固化记忆

Slash command:用户管理 memory#

用户在输入框里按 / 能找到的是 slash command。memory 相关的用户入口是 /memory,而不是 /retain/recall/reflect

常见子命令包括:

/memory view
/memory stats
/memory diagnose
/memory clear
/memory enqueue

这些命令面向“管理 memory 系统”:

  • view:查看当前 backend 注入的 memory 内容。
  • stats:查看 backend 统计信息。
  • diagnose:查看诊断结果。
  • clear / reset:清理当前 backend 的 memory 数据。
  • enqueue / rebuild:强制触发 consolidation / retain 工作。

Model tool:模型主动读写记忆#

retain / recall / reflectmodel-facing tools,不是用户可直接输入的 /retain 斜杠命令。

这意味着:

  1. 用户在 / 菜单里找不到 /retain 是正常的。
  2. 用户可以用自然语言要求模型记住某件事。
  3. 模型判断需要写入时,才会调用 retain tool。

例如用户说:

请记住:我希望技术笔记都用中文,先讲机制再讲配置。

memory.backend = mnemopimemory.backend = hindsight 时,模型可以调用 retain 把这条信息写入长期记忆。

同理,模型遇到这些问题时可以主动调用 recall

我们上次对这个 repo 的发布流程是怎么约定的?
我之前说过这类笔记要怎么写吗?
这个项目之前踩过什么坑?

reflect 则用于“基于记忆做综合回答”。不过要注意:hindsightreflect 是远端服务端 synthesis,而 mnemopi 当前更像 recall 结果的格式化汇总。

自动流程:不需要模型显式调用#

除了工具调用,memory backend 还会挂在 session 生命周期上自动运行:

  1. auto recall
    首轮 prompt 前,根据当前用户问题检索相关记忆,并注入 <memories>

  2. auto retain
    每 N 个 user turn,把当前 transcript 保存到 memory。mnemopi 默认 retainEveryNTurns = 4

  3. consolidation / sleep
    session 收尾时,把 working_memory 中的近期原始记忆整理成 episodic_memory

这三类入口解决的是不同问题:slash command 是用户管理面,model tool 是模型主动记忆读写面,自动流程是后台生命周期面。

工具接口#

retain / recall / reflect 工具只在 memory.backendhindsightmnemopi 时启用。

retain#

retainmnemopi 是同步写入本地 SQLite:

state.rememberScoped(item.content, {
  source: "coding-agent-retain",
  importance: 0.75,
  scope: "bank",
  extract: true,
  extractEntities: true,
  veracity: "tool",
  memoryType: "fact"
})

hindsight 则是进入 session-owned queue,后续 batch flush 到远端。

recall#

recallmnemopi 调用:

state.recallResultsScoped(query)
state.formatScopedRecallWithIds(results)

返回结果带 memory id,方便后续 memory-edit 做 update / forget / invalidate。

reflect#

reflecthindsight 里调用服务端 reflect(),由服务端综合记忆回答。
但在 mnemopi 里,它只是:

  1. 用 query + optional context 做 recall。
  2. 把召回结果格式化为上下文。
  3. 返回 Based on recalled memories: ...

所以 mnemopi 的 reflect 当前更像“召回结果汇总”,不是一个额外 LLM synthesis pass。

配置要点#

主要配置项在 packages/coding-agent/src/config/settings-schema.tspackages/coding-agent/src/mnemopi/config.ts

常用项:

memory:
  backend: mnemopi

mnemopi:
  scoping: per-project
  autoRecall: true
  autoRetain: true
  retainEveryNTurns: 4
  recallLimit: 8
  recallContextTurns: 3
  recallMaxQueryChars: 4000
  injectionTokenLimit: 5000

embedding 相关:

mnemopi:
  embeddingVariant: default        # BAAI/bge-base-en-v1.5
  embeddingVariant: multilingual   # intfloat/multilingual-e5-large
  noEmbeddings: false

显式模型优先级:

mnemopi.embeddingModel
> MNEMOPI_EMBEDDING_MODEL
> mnemopi.embeddingVariant derived default

LLM fact extraction 相关:

mnemopi:
  llmMode: none | smol | remote
  llmBaseUrl: ...
  llmApiKey: ...
  llmModel: ...

当前 loadMnemopiConfig() 中只有 llmMode === "remote" 会把 remote LLM options 传入 providerOptions;否则传 llm: falsesmol 模式在 backend 加载配置时还有额外处理,需要结合 loadMnemopiConfigWithProviders() 看完整模型解析。

设计取舍#

为什么不是只用 Markdown 摘要#

Markdown 摘要适合保存“长期准则”,但不适合回答“上次那条具体决策是什么”。mnemopi 的 SQLite + FTS + embedding 可以做动态 query-time recall,这是 local backend 不具备的。

为什么不是只用向量库#

源码明显没有把 embedding 当成唯一真相。召回同时使用 FTS、关键词、重要性、时间、可信度和 current-sensitive 调整。这是更稳的工程选择,因为:

  • embedding 模型可能未安装或失败。
  • 中文/英文混合 query 可能影响向量质量。
  • 用户问“最新/current”时,时间和 stale 信号比语义相似度更重要。
  • 精确字段、文件名、配置名常常需要 FTS/lexical 命中。

为什么分 working 和 episodic#

working memory 解决“近期高保真”,episodic memory 解决“长期低成本”。如果所有对话都长期保留原文,召回会越来越噪;如果只保留摘要,又容易丢掉近期细节。

为什么 per-project-tagged 在 mnemopi 里不是 tag#

hindsight 后端支持服务端 tag filtering,所以可以单 bank + project tag。mnemopi 当前没有 tag-filtered recall,于是用 project bank + global bank 的组合达到类似效果。这是实现约束下的折中。

风险和待验证问题#

  1. fact extraction 的质量边界
    自动抽取 facts 依赖 LLM 输出质量。如果模型把一次性上下文误抽成长期偏好,会污染未来 recall。

  2. 自动 retain 的粒度
    默认每 4 个 user turn 保存 transcript。长任务里这很有用,但在探索性对话中可能保存过多临时判断。retainEveryNTurnsmemoryType 的策略需要结合真实使用数据调。

  3. reflect 的语义不一致
    hindsightreflect 是服务端 synthesis;mnemopireflect 更接近 recall formatting。如果用户以为两者能力一致,可能会高估 mnemopi reflect 的推理深度。

  4. embedding 模型迁移成本
    reconcileEmbeddingModel() 会在模型变化时清空旧 embedding 并后台重建。大库场景下需要关注重建时间、失败恢复和召回降级表现。

  5. bank 命名与迁移
    mnemopi 已经为旧 bank derivation 做了 legacy bank rescue,但项目路径变化仍可能影响 bank id。长期使用时,bank 管理和迁移工具会变重要。

总结#

oh-my-pi 的 memory 系统可以理解成三种层次:

  • local:把历史经验变成启动时的项目手册。
  • hindsight:把 memory 能力交给远端服务,并支持 mental models。
  • mnemopi:在本地实现一个可检索、可编辑、可固化的长期记忆库。

mnemopi 的核心价值不在“存了文本”,而在它把 agent memory 做成了一个本地数据系统:SQLite 负责可靠持久化,FTS 负责精确检索,embedding 负责语义召回,facts/graph 负责结构化信息,working/episodic 分层负责生命周期管理。

如果要借鉴这个实现,最值得学的是两个点:

  1. memory 要能降级:向量失败时还有 FTS,LLM extraction 失败时原文仍然 durable。
  2. memory 要有生命周期:近期原文、长期摘要、用户显式事实、自动抽取 facts,应该是不同层次,而不是混进同一个无类型向量库。

源码索引#

  • packages/coding-agent/src/memory-backend/types.ts:backend 抽象。
  • packages/coding-agent/src/memory-backend/resolve.ts:backend 选择。
  • packages/coding-agent/src/memory-backend/local-backend.ts:local summary pipeline wrapper。
  • packages/coding-agent/src/hindsight/backend.ts:Hindsight agent 集成。
  • packages/coding-agent/src/hindsight/state.ts:Hindsight session state、auto recall/retain。
  • packages/coding-agent/src/hindsight/client.ts:Hindsight HTTP API client。
  • packages/coding-agent/src/mnemopi/backend.ts:Mnemopi backend 集成。
  • packages/coding-agent/src/mnemopi/state.ts:Mnemopi session state、记忆库隔离策略、auto recall/retain/consolidate。
  • packages/coding-agent/src/mnemopi/config.ts:Mnemopi 配置、bank scope、embedding/LLM provider options。
  • packages/mnemopi/src/core/memory.ts:Mnemopi facade。
  • packages/mnemopi/src/core/beam/index.ts:BeamMemory class。
  • packages/mnemopi/src/core/beam/schema.ts:SQLite schema。
  • packages/mnemopi/src/core/beam/store.ts:remember、update、forget、scratchpad、embedding/extraction 调度。
  • packages/mnemopi/src/core/beam/recall.ts:混合检索和评分。
  • packages/mnemopi/src/core/beam/consolidate.ts:sleep、consolidateToEpisodic、fact extraction。