CHAPTER 4 / 5

Memory 记忆

概念与为什么

Memory 最容易被讲成一句含糊的“agent 记得你”。对 harness 来说,这远远不够。至少要把三种东西拆开:

  1. Durable learned memory —— 从过去的交互中提炼出偏好、反馈、项目经验,写到 session 之外,供未来不相关的 session 召回。
  2. Human-authored instructions —— AGENTS.mdCLAUDE.md 一类由人或团队维护的政策与约定。它们也跨 session,但不是 harness 从对话中“学”出来的。
  3. Resume / session history —— 保存某一条 thread 的原始消息、tool result 或 compaction summary,让同一条对话继续。它持久,却不会自然变成另一条新对话的知识。

这三个层次的差别不在“有没有写盘”,而在 producer、scope、recall path:谁写、在哪些对话之间共享、什么时候重新进入模型上下文。一个完整的 learned-memory loop 至少有四步:

flowchart LR
    A[过去的 session / rollout] --> B[Extract<br/>筛掉噪声与秘密]
    B --> C[Consolidate<br/>去重、归类、更新]
    C --> D[(Durable store<br/>index + detail)]
    D --> E[Recall<br/>预算内选择]
    E --> F[新 session 的 context]
    F -. 新反馈 .-> A

这里每一步都可能坏掉:extractor 把临时任务当成长期偏好,consolidator 留下过期事实,recall 一次塞入太多 token,或者把“用户说过”错误升级成“当前代码仍然如此”。所以 memory 不是越多越好;它是一个带 provenance、freshness 和 context budget 的知识供应链。

本章会用同一把尺看三家:是否真的学习、存储边界在哪里、怎样召回、默认是否开启。结论先说:Claude Code 和 Codex 都有 file-backed learned memory,但 gate 与数据流不同;OpenCode 的新 session 规则连续性主要来自 instruction files,而 session database 只负责特定 session 的 resume / fork。一个例外是选中的 GPT-4/o1/o3 provider path 会加载 Beast system prompt,提示 agent 把用户偏好或“记住”请求写入 .github/instructions/memory.instruction.md;这是 provider-specific、prompt-driven 的 instruction-file memory,不是带专用 store/index/extractor 的 first-class harness memory service,也没有同等级的 autonomous learning pipeline。src/memdir/paths.ts:21-54; codex-rs/features/src/lib.rs:135; packages/opencode/src/session/system.ts:27; packages/opencode/src/session/prompt/beast.txt:113; packages/opencode/src/session/instruction.ts:110; packages/core/src/session/sql.ts:22; packages/opencode/src/session/session.ts:693

三家实现对比

先把名字相近、语义不同的机制摆在同一张表里:

HarnessDurable learned memoryHuman-authored instructionsResume / session continuityFeature / default gateEvidence
Claude Codecanonical Git project scoped 的 memory/: MEMORY.md index + topic Markdown;主 agent 可写,可选 background extractor 补写managed / user / project / local 的 CLAUDE.md family独立的 session-memory summary 可参与 compactioncore auto-memory default-on,但 relevance prefetch、background extraction、team/session modes 各有独立 gatesrc/memdir/paths.ts:21-54,198-258; src/utils/claudemd.ts:1-25; src/services/SessionMemory/sessionMemory.ts:266-375
Codex CLICODEX_HOME/memories/:旧 rollout 经 Phase 1 extraction + Phase 2 consolidation 形成 handbook 与 summary从 project root 到 CWD 收集 AGENTS.mdCODEX_HOME/sessions/YYYY/MM/DD 下的 thread JSONL 可 resume/replaymemories 是 Stable,但 default-off;read/write path 还各自检查 config/runtime gatefeatures/src/lib.rs:930; start.rs:19; agents_md.rs:1; recorder.rs:1524
OpenCode没有 first-class harness memory service;但 selected GPT-4/o1/o3 path 的 Beast prompt 让 agent 可把偏好/remember 请求写到 .github/instructions/memory.instruction.mdglobal/project/configured AGENTS.md / CLAUDE.md 等每轮注入;上述文件是 provider-specific prompt-driven instruction memorySQLite 保存 session/message/part;compaction summary 与 tail 仍属于该 session无 dedicated memory service gate;Beast 行为只随匹配 provider prompt 出现,其余连续性来自 instruction 与显式 resume/forksystem.ts:27; beast.txt:113; instruction.ts:110; sql.ts:22; session.ts:693

Claude Code:project-scoped topic memory

Claude Code 把 human-authored instructions 与 auto memory 放在相邻但不同的路径里。前者按 managed → user → project → local 的层级发现,离 CWD 更近的文件后加载、优先级更高;auto memory 则由独立 gate 和目录解析器管理。src/utils/claudemd.ts:1-25; src/memdir/paths.ts:21-54

auto-memory 根目录默认按 canonical Git root namespace,所以同一 repository 的 worktree 共享一份 topic corpus;入口是该目录下的 MEMORY.md。这个 core gate 默认开启,但 --bare、无持久存储的 remote mode、环境变量或 settings 都能关闭它。src/memdir/paths.ts:198-258; src/memdir/paths.ts:21-54

默认 delivery 是把存在的 MEMORY.md 作为 AutoMem context file 读入。另一条 relevance-prefetch 路径由 tengu_moth_copse gate 控制:开启时不静态注入 index,而是扫描 topic file 的 frontmatter,交给 Sonnet side query 从 manifest 中最多选 5 个文件,再以 relevant_memories attachment 注入。src/utils/claudemd.ts:979-1006; src/utils/claudemd.ts:1136-1151; src/memdir/memoryScan.ts:21-73; src/memdir/findRelevantMemories.ts:18-31; src/utils/attachments.ts:2196-2241

这条 selective recall 有硬预算:MEMORY.md 最多加载 200 行 / 25,000 bytes;每个动态 topic 最多 200 行 / 4,096 bytes,当前 transcript 的累计动态注入上限为 60 KiB。src/memdir/memdir.ts:34-102; src/utils/attachments.ts:269-289

写入也有两条路。主 agent 可直接维护 topic 和 index;另一个 feature-gated background fork 只在主 agent 没有写过同一段消息时补做 extraction,最多运行 5 turns,失败只记录、不阻塞用户回答。src/memdir/paths.ts:57-76; src/services/extractMemories/extractMemories.ts:112-148; src/services/extractMemories/extractMemories.ts:329-359; src/services/extractMemories/extractMemories.ts:395-427; src/services/extractMemories/extractMemories.ts:463-499

别把它与 session memory 混在一起。后者只在 main REPL thread、session-memory gate 与 auto-compact 条件满足时更新当前 session 的 summary,随后 compaction 可以消费它;resume 分支也只是继续这条 session 的摘要边界。src/services/SessionMemory/sessionMemory.ts:266-375; src/services/compact/sessionMemoryCompact.ts:505-565

Codex CLI:两阶段全局 handbook

Codex 的 learned memory 根在 CODEX_HOME/memories/,不是当前 repository 的 MEMORY.md。feature inventory 把 MemoryTool 描述为 startup extraction + file-backed consolidation;在 pinned SHA 中它的 stage 是 Stable,但 default_enabled: falsecodex-rs/features/src/lib.rs:135; codex-rs/features/src/lib.rs:930

write path 只在 non-ephemeral root session、feature 已开启且 State DB 可用时异步启动;rate-limit guard 通过后,先跑 Phase 1,再跑 Phase 2。codex-rs/memories/write/src/start.rs:19; start.rs:48

consolidator 被刻意降权:自身不能生成/读取 memory,没有 MCP/apps/collab,approval 固定为 Never;在 managed sandbox 下只允许写 memory root 且不许联网。成功结果必须留下 MEMORY.md,以及首行为 v1memory_summary.mdcodex-rs/memories/write/src/phase2.rs:319; codex-rs/memories/write/src/workspace.rs:48

read path 又独立检查 MemoryTool && use_memories;通过后只把 token-bounded memory_summary.md 包成 developer policy,而不是把全部 raw memory 都塞进 prompt。dedicated_tools=true 时才额外暴露 scoped list/read/search/note tools;本地 search 是 file/line matcher 并排序、分页。codex-rs/ext/memories/src/extension.rs:40; codex-rs/ext/memories/src/prompts.rs:23; codex-rs/ext/memories/src/local/search.rs:51

这里的 session history 是 evidence source,不是 memory product 本身。普通 thread 仍独立写入按日期分目录的 rollout JSONL;AGENTS.md 又是从 project root 到 CWD 拼接的 human-authored instructions。codex-rs/rollout/src/recorder.rs:1524; codex-rs/core/src/agents_md.rs:1

OpenCode:provider-specific instruction-file memory + resumable database,没有 autonomous learner

OpenCode 的 durable continuity 首先来自 instructions。它在 global 位置识别 AGENTS.md 与兼容的 ~/.claude/CLAUDE.md,project 侧在 AGENTS.md / CLAUDE.md / deprecated CONTEXT.md family 中选择匹配,再加 configured files/URLs;每次 ordinary model call 都把这些内容与 MCP/skill catalogue 组装进 system array。另有一个不能略过的 provider-specific path:当 model API ID 匹配 gpt-4o1o3 时,system.provider() 选择 Beast prompt;该 prompt 声明 .github/instructions/memory.instruction.md 为用户偏好的 memory,要求空文件时创建带 applyTo: '**' front matter 的文件,并在用户要求 remember/add memory 时让 agent 更新它。这是 prompt 指导 agent 进行普通 instruction-file 编辑,不是 runtime 自动抽取或服务层写入。packages/opencode/src/session/instruction.ts:60; instruction.ts:110; packages/opencode/src/session/prompt.ts:1257; packages/opencode/src/session/system.ts:27; packages/opencode/src/session/prompt/beast.txt:113

session continuity 则来自 SQLite。默认 database file 位于 XDG data path;schema 把 session、message、part、todo 分表,核心关系都带 session_id。普通 createNext 只建新 metadata;只有显式 fork 才克隆源 session 的 messages/parts。loop 也只从指定 sessionID 读取 compacted view。packages/core/src/global.ts:10; packages/core/src/database/database.ts:43; packages/core/src/session/sql.ts:22; packages/opencode/src/session/session.ts:501; session.ts:693; packages/opencode/src/session/prompt.ts:1092

compaction 同样没有跨过 session 边界:它生成 summary: true 的 assistant message,并按 budget 保留 recent tail;之后 filterCompacted 只把同一 session 的视图重排成 compaction request、summary、tail 与后续消息。packages/opencode/src/session/compaction.ts:188; compaction.ts:328; packages/opencode/src/session/message-v2.ts:521

因此本章只做一个窄、可复查的 absence claim:在 pinned revision 的已检查 runtime 路径中,memory 搜索确实找到了 Beast prompt 的上述 instruction-file path;宽泛的 semantic/retrieval 搜索也会遇到 prompt-level references。把这些排除为“专用 memory subsystem”的证据并不诚实。相反,对 packages/opencode/src packages/core/src(memory[_-]?store|memory[_-]?file|memory\.md|MEMORY\.md|memdir|write.?memory|save.?memory|recall) 实现导向搜索,没有找到 dedicated memory service、tool、schema/index 或 automatic cross-session distillation;这只描述 inspected runtime 的结果,不外推到外部 plugin、provider 或未检查路径。正面证据仍是 instruction injection、Beast 的 provider-specific prompt-driven file write 和 session-scoped DB/read path;MemoryState 这个名字只是持有 messages[] 的 in-process adapter。packages/opencode/src/session/system.ts:27; packages/opencode/src/session/prompt/beast.txt:113; packages/core/src/session/message-updater.ts:6

代码走读

这章走读 Codex 的 two-phase file-backed memory write path。理由不是它“最好”,而是这个开源路径把 raw rollout → structured extraction → serialized consolidation → later prompt injection 四段都公开出来;Claude Code 可以描述但不适合大段引用,OpenCode 虽有 Beast prompt 驱动的 instruction-file 写入,却没有对等的 first-class autonomous learned-memory writer。

入口:先证明这个 session 有资格学习

startup function 第一层就排除 ephemeral、feature-disabled 和 subagent session;State DB 不存在也直接退出。真正执行时 Phase 1 与 Phase 2 的顺序写在同一个 async task 里:

if config.ephemeral
    || !config.features.enabled(Feature::MemoryTool)
    || source.is_non_root_agent()
{
    return;
}
// ... State DB + rate-limit guards ...
phase1::run(Arc::clone(&context), Arc::clone(&config)).await;
phase2::run(context, config, parent_permission_profile).await;

codex-rs/memories/write/src/start.rs:32 — eligibility gate; :76 — ordered Phase 1 → Phase 2 call.

这不是装饰性 gate。它把“哪些对话允许成为学习源”与正常 answer loop 分开:subagent 的临时探索不会递归污染全局 memory,ephemeral session 的承诺也不会被后台任务偷偷破坏。

Phase 1:把一条 rollout 变成可合并的 record

Phase 1 先通过 State DB lease 一批符合 source / age / idle / count 限制的 interactive rollouts,避免多个 startup worker 重复消费同一输入。codex-rs/memories/write/src/phase1.rs:149

对每条被 claim 的 rollout,writer 读取 JSONL、只序列化允许的 response items,再要求模型按 strict schema 回传三个字段。解析成功后先 redaction,再交给后续存储:

let (rollout_items, _, _) = RolloutRecorder::load_rollout_items(rollout_path).await?;
let rollout_contents = serialize_filtered_rollout_response_items(&rollout_items)?;
// ... build prompt ...
prompt.output_schema = Some(output_schema());
prompt.output_schema_strict = true;

let mut output: StageOneOutput = serde_json::from_str(&result)?;
output.raw_memory = redact_secrets(output.raw_memory);
output.rollout_summary = redact_secrets(output.rollout_summary);

codex-rs/memories/write/src/phase1.rs:290 — rollout filtering; :308 — strict output schema; :318 — parse + secret redaction.

这里有两个重要边界。第一,raw transcript 不直接升级成 memory,中间先经过 schema-constrained distillation。第二,redaction 放在 model output 之后,因为 extractor 可能把原日志中的 secret 复制进自己的总结。它降低风险,但不是“模型从未见过 secret”的保证。

Phase 2:用 global lock 把 records 合成 handbook

Phase 2 解决的是 shared mutable state。多个 rollouts 可以各自抽取,但 MEMORY.md 不能由多个 consolidator 同时改。实现先拿 global claim,建立 Git baseline,同步当前 DB selection,再用 workspace diff 判断是否真的需要启动 agent。删去各分支的错误处理后,control flow 可以缩写成:

let claim = match job::claim(context.as_ref(), db.as_ref()).await { /* ... */ };
if let Err(err) = prepare_memory_workspace(&root).await { /* ... */ return; }
let raw_memories = match db
    .memories()
    .get_phase2_input_selection(max_raw_memories, max_unused_days)
    .await
{ /* ... */ };
if let Err(err) = sync_phase2_workspace_inputs(&root, &raw_memories).await {
    /* ... */
    return;
}
let workspace_diff = match memory_workspace_diff(&root).await { /* ... */ };

codex-rs/memories/write/src/phase2.rs:63 — global lock; :103 — DB-backed selection; :125 — workspace sync/diff; :153 — unchanged fast path.

真正启动前,worker config 禁掉 recursive memory、MCP、apps、plugins 与 collaboration;managed sandbox 只留 memory root writable 且 network_access: false。这让 consolidator 的能力刚好够整理文件,不够顺手去“验证”互联网或再派一个 agent。codex-rs/memories/write/src/phase2.rs:319

最终不是“agent 正常退出”就算成功。validator 要求 MEMORY.md 是文件,还要求 memory_summary.md 第一行恰好为 v1;只有 artifact contract 成立,baseline 才有资格向前推进。codex-rs/memories/write/src/workspace.rs:48

Read path:summary 先行,detail 按需

到了未来 session,extension 不会倒灌所有 Phase-1 records。它从固定 root 读取 memory_summary.md,按 token policy 截断,再渲染成 developer instructions:

let base_path = codex_home.join("memories");
let memory_summary_path = base_path.join("memory_summary.md");
let memory_summary = fs::read_to_string(&memory_summary_path).await.ok()?;
let memory_summary = truncate_text(
    &memory_summary,
    TruncationPolicy::Tokens(MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_SUMMARY_TOKEN_LIMIT),
);

codex-rs/ext/memories/src/prompts.rs:27 — fixed path + bounded summary read.

因此它采用的是 progressive disclosure:summary 负责告诉主 agent“可能有什么”,detail 留在 files / rollout summaries,必要时再由 scoped tools 查。代价也很直接:summary 写漏的内容,后续 agent 连搜索关键词都未必想得到。

设计权衡

决策点Claude CodeCodex CLIOpenCode
学习单元semantic topic file + index (src/memdir/memoryScan.ts:21)rollout record → global handbook (phase1.rs:282; phase2.rs:103)无 first-class learned unit;Beast path 只有 prompt-driven 的 preference instruction file (beast.txt:113)
默认范围canonical Git project (src/memdir/paths.ts:198)CODEX_HOME user/global (prompts.rs:30)Beast file 是 project .github/instructions/;其他 instruction 可 global/project,history 按 session (system.ts:27; sql.ts:22)
写入者main agent + gated fallback extractor (src/services/extractMemories/extractMemories.ts:112)extraction model + serialized consolidator (start.rs:76)用户/agent 普通文件编辑;匹配 provider 时 Beast prompt 要求 agent 按 remember 请求维护该文件 (beast.txt:123)
首次召回index,或 gated side-model 选最多 5 topics (src/memdir/findRelevantMemories.ts:18)bounded memory_summary.md developer policy (prompts.rs:23)ordinary discovered instructions 每轮 eager injection;Beast memory file 由模型按 prompt 读写,未见自动 loader (instruction.ts:110; prompt.ts:1257; beast.txt:113)
详细召回topic attachment / 显式 file read (src/utils/attachments.ts:2196)可选 dedicated local tools (extension.rs:98)没有 dedicated memory recall;resume/fork 仅同一 session (session.ts:693)
stale 防线prompt 要求先验证当前文件/资源 (memoryTypes.ts:197)consolidation + source rollout trace;当前性仍需 agent 判断prompt-driven 文件依赖 agent/用户维护;session summary 只代表当时上下文 (beast.txt:123)

三组取舍最值得带走:

再补一个窄的 retrieval honesty check:对 Codex pinned memory runtime 的 memories/read/srcmemories/write/srcext/memories/srcstate/src 搜索 embedding|vector|semantic|similarity|nearest.?neighbor|faiss|qdrant|pinecone|chrom(a|adb),没有发现 vector/semantic retrieval implementation;正面实现是 scoped file matcher。这个结论只覆盖上述目录与 SHA,不外推到外部 plugin/service。codex-rs/ext/memories/src/local/search.rs:51

迁移到你自己的 agent

  1. 先画 boundary map,再写 memory API。 至少分开 instructions/sessions/memories/ 三个 store;每个都写清 producer、scope、retention 与 injection point。文件都叫 Markdown 不代表语义相同。
  2. 默认从两层文件结构起步。 用一个短 memory_summary.md / MEMORY.md 做 orientation,detail 按 topic 或 source rollout 拆开。每轮只注入 index/summary,让 agent 按需 read/search,不要把整个知识库塞进 prompt。
  3. 把 write pipeline 分成 extract 与 consolidate。 extract 可以并行处理独立 session,输出严格 schema;consolidate 必须串行处理 shared index、去重和 stale update。共享文件前加 lease/lock,完成后验证必需 artifact,别把“进程退出 0”当作 memory 正确。
  4. 给每条 memory 留 provenance。 最少记录 source session / timestamp / scope;如果回答依赖旧事实,让 agent 能回到 evidence。memory 是线索,不是当前事实的替代品。
  5. 在模型输出后做 secret scan / redaction。 extractor 已经看过 transcript,所以 redaction 保护的是 durable store 与未来 recall,不是回溯性保密。敏感 session 是否进入 extraction,还要在 claim 阶段先过滤。
  6. 把 freshness 写进 read policy。 memory 提到 file、function、flag、price 或“当前”状态时,先查 live source;冲突时相信当前观察,并更新或删除 stale record。Claude Code 的 memory policy 明确把这条规则放进 recall prompt。src/memdir/memoryTypes.ts:197-255
  7. 所有自动化都要有独立 gate。 至少拆 generate_memoriesuse_memoriesdedicated_tools、scope 与 ephemeral opt-out。Codex 的 read/write gate 独立,Claude Code 也把 core enablement、prefetch 与 extractor 分开;这比一个总开关更容易解释隐私与成本。codex-rs/ext/memories/src/extension.rs:40; src/memdir/paths.ts:21-76; src/utils/claudemd.ts:1136-1151
  8. 限制 prompt budget,也限制 worker capability。 对 index、单文件、单 turn、整条 transcript 分别设 cap;consolidator 只开放 memory root 写权限,关闭 network、MCP 与 recursive agents。context pressure 和 blast radius 是两套预算,都要算。
  9. 没有 autonomous memory 也可以是正确设计。 如果产品更看重显式控制,可以采用 OpenCode 的大部分路线:instruction files 负责跨 session 规范,SQLite/JSONL 负责同 session resume,skill 负责可复用流程;也可以像 Beast provider path 一样,只在明确 remember 请求时提示 agent 写一个 project instruction file。关键是 UI 和文档要把这种 prompt-driven 文件记忆与自动 learned-memory service 分开,不要把“保存过”包装成“学会了”。