【AI应用开发|Agent记忆】—— Codex 与 Claude Code 持久化记忆对比:两条不用向量库的路线

Codex 与 Claude Code 的持久化记忆:两条不用向量库的路线

一提「给 Agent 加长期记忆」,很多人脑子里立刻蹦出那套标准答案:切块、Embedding、向量库、相似度 Top-K、RAG。这套方案在知识库场景确实能打,但落到「Agent 记住用户偏好」这件事上,它反而是被市场淘汰最快的一档。

OpenAI 的 Codex 和 Anthropic 的 Claude Code,两家头部 coding agent 都做了跨会话记忆,都没有用 Embedding,也都没有向量库。一个把记忆做成离线两阶段管线 + SQLite 状态机 + 四层 Markdown;另一个做成回合内直接写文件 + 分层目录 + 按需召回。

这篇文章把两套实现拆开对比。Codex 侧依据 openai/codex 仓库的 Rust 源码(codex-rs/memories/{read,write} + codex-rs/memories/README.md);Claude Code 侧依据其泄露源码的静态分析仓库(src/memdir/src/services/SessionMemory/src/tools/AgentTool/)。两边都给了文件路径,你可以自己对着看。


一、先厘清:Agent 的「记忆」到底在解决什么

聊实现之前先把概念钉死,不然后面全是误解。

1.1 上下文窗口不是记忆

模型的上下文窗口是一次请求内 能看到的东西,请求结束就没了。所谓「Agent 记忆」要解决的是:下一次开会话时,怎么让模型知道你上次教过它什么。这两件事的边界很清楚:

上下文窗口 持久化记忆
生命周期 单次请求 跨会话、跨天、跨月
载体 模型输入 磁盘文件 / 数据库
谁管理 框架自动裁剪 框架 + 模型 + 用户共同维护
成本形态 每回合都付 token 生成一次,注入多次

一句话:记忆的本质是「把这一次的 token 花在下一次不用再花」

1.2 为什么第一反应总是向量库,而这两个产品都没走

向量库方案的隐含假设是:记忆条目量大、查询词和存储文本用词不一致、并且需要一个独立检索服务来兜底。但在 Agent 记忆这个场景里,这三条都不太成立:

  • 个人用户的记忆条目通常是几十到几百条,不是百万级文档;
  • 记忆内容往往是「用户偏好」「项目约定」这类短事实,短文本的语义相似度噪声极大
  • 后台「记忆代理」最难的不是检索,而是判断什么时候该写------这个时机问题向量库根本不管。

真实项目里踩的坑也很具体:嵌入有损、检索命中不相关的、排序不可解释、每回合跑 embedding 的成本、以及最致命的------出问题时你没法指着某几个字节说「就是它害的」

Codex 和 Claude Code 都绕开了这条路,代价是接受「换个说法就搜不到」这个缺陷(后面第五章细说)。

1.3 对比口径

只谈 harness 层的开发者记忆机制,不谈 ChatGPT 网页端那种消费级「记忆功能」(那个是模型侧通道,两码事)。Codex 侧还有个容易混淆的点:AGENTS.mdMemories 不是一件事。

AGENTS.md Memories(本文主角)
谁写 人写 Agent 自己生成
放哪 仓库里,进版本控制 ~/.codex/memories/,本机状态
作用 项目宪法,必须遵守 经验沉淀,下次少走弯路
大小上限 默认 32 KiB,超出静默截断 无硬上限,靠淘汰机制控制

Codex 自己也知道这两者容易串味,所以在 Phase 1 抽取时会主动把 AGENTS.md 的注入片段从输入里剔除,防止小模型把「项目宪法」再抄一份成「经验」。


二、Codex 的记忆管线:从 rollout 到四层 Markdown

先给结论:Codex 的记忆是会话外(off-session)重批处理 + 会话内(in-session)轻注入。生成记忆的算力和当前对话完全解耦,用户在当前回合几乎不付额外成本。

流水线一句话概括:

复制代码
原始记录(rollout)
  → 结构化解析(Phase 1:小模型抽成 raw_memory + rollout_summary)
  → SQLite 落库(stage1_outputs 表)
  → 归纳整合(Phase 2:大模型子代理 morpheus 合并)
  → 四层 Markdown 持久化(总览 / 索引 / 任务记录 / 标准流程)

2.1 两张表把架构说清楚了

Codex 用 SQLite 做状态层,核心就两张表:

粒度 关键字段
stage1_outputs 一条 rollout 一行 raw_memoryrollout_summaryrollout_slugcwdusage_countlast_usageselected_for_phase2
jobs Phase 1 每 thread 一行;Phase 2 全局一行 kindstatuslease_untilretry_atinput_watermark

jobs.kind = 'memory_stage1'job_key 是 thread_id;kind = 'memory_consolidate_global'job_key 固定为常量 "global"并发边界全靠 BEGIN IMMEDIATE 和原子条件 INSERT 撑住,不指望外部进程礼貌行事。Phase 1 有 8 路并发 + 3600 秒租约 + 3600 秒退避;Phase 2 是全局唯一锁 + 90 秒心跳。这组参数就是一套「避免雪崩」的完整约束,想 fork 的人只改一个必挂。

2.2 Phase 1:把一条 rollout 抽成三个字段

Phase 1 干的事很朴素------读 rollout,喂给一个便宜模型,拿回结构化输出:

  • raw_memory:详细的原始记忆
  • rollout_summary:精简的会话摘要
  • rollout_slug:文件名用的短标识(可选)

工程细节才是关键:

  • Schema 强约束。输出走 JSON Schema strict 模式,模型只能吐这三个键,多一个字段直接解析失败。
  • 双重脱敏 。序列化阶段过一次 redact_secrets,模型输出阶段再过一次,prompt 里还明确要求模型自己也做一次。三道防线,任何一道漏了都不会被自动补救。
  • 注入即排除 。序列化时显式丢弃 developer 角色消息、AGENTS.md 注入片段、<skill> 片段。理由很直白:这些是「下一次会话本来就该看到」的内容,不该被当成「经验」再提炼一遍,否则 MEMORY.md 里会堆满规则副本。
  • 输入要截断。按模型有效 context window 的 70% 作为 rollout 输入上限,给系统指令、输出、推理留 30%。取不到模型元数据时回退 150,000 tokens。
  • 失败要退避。抽取失败按 1 小时退避重试,不会出现「启动一次连撞 50 次模型」。

结果写回 SQLite。注意这一步:rollout 是原料,raw_memory 是初加工产物,DB 是存放初加工产物的仓库

2.3 SQLite 到底是「临时存储」还是「事实源」

✋ 这里要纠正一个流传很广的说法:Codex 的 SQLite 不是临时缓存,它是事实源(source of truth)

~/.codex/memories/raw_memories.mdrollout_summaries/ 这两个文件,每一次 Phase 2 跑的时候都会从 SQL 重新生成。就算你手工把它们删了,下一次 Phase 2 会原样补回来------除非这条 thread 在 DB 里也已经被 prune 掉了。

这个设计取向和很多开源记忆项目恰好相反(那些项目通常主张「Markdown 是事实源,SQLite 只是可重建的索引」)。Codex 的排序是:

复制代码
SQLite(不可重建,事实源) → Markdown 派生物(可重建) → 注入 prompt 的摘要(截断后)

好处是数据一致性有保证、并发安全能靠事务兜住;代价是记忆的「真相」在 DB 里,用户看到的 Markdown 只是投影。

2.4 Phase 2:用 git diff 决定「要不要干活」

Phase 2 是全局串行的合并阶段,流程是线性的十步:

  1. 抢全局锁
  2. 准备 workspace(确保 git baseline 存在)
  3. 构造被严格降级的 agent 配置
  4. 按选择规则加载要参与合并的 stage-1 输入
  5. 同步 rollout_summaries/raw_memories.md
  6. 用 git diff 判断 workspace 是否有变化
  7. 有变化才写 phase2_workspace_diff.md(上限 4MB)
  8. spawn 子代理(内部代号 morpheus)
  9. 后台接管心跳、ownership 校验、baseline 重置
  10. 上报指标

有两个设计特别值得说:

第一,用 git 工作区当 dirty 指示器。 ~/.codex/memories/ 本身是个 git 仓库。上一轮成功的状态是 baseline,本轮先同步文件、再 diff。没变化就直接 succeeded_no_workspace_changes 退出,连模型都不调 。这个设计让「用户手工编辑记忆」变成了可被感知的输入------你在 MEMORY.md 里删了一段、加了一条,下次 Phase 2 跑起来时 morpheus 通过 diff 能「看见」你的改动,而不是无脑覆盖。

⚠️ 注意:这个 baseline 每次成功后都会被重置 ,官方 PR 里写得很直接------不打算通过 .git 保留历史。所以 git log ~/.codex/memories 看不到记忆的演化史,想做审计得自己另外备份。

第二,morpheus 是被阉割过的子代理。 它跑在:ephemeral = truegenerate_memories = falseuse_memories = false、无网络、无审批、只有本地写权限、collab 被禁用。前三项是为了杜绝自递归(合并代理自己再生成记忆,会无限套娃),禁网络和禁 collab 是为了把一个「改文件的模型」关进最小的盒子里。

2.5 四层 Markdown:总览 / 索引 / 任务记录 / 标准流程

合并完成后,~/.codex/memories/ 里是这样一套结构:

文件 角色 何时被读
总览 memory_summary.md 导航层,下次会话第一眼看到的东西 每回合注入(截断后)
索引 MEMORY.md 长期手册,可检索的知识主体 模型按需 grep
任务记录 rollout_summaries/*.md 证据层,每个会话一份回放 命中时才打开
标准流程 skills/<name>/SKILL.md 可复用流程,能带脚本和模板 命中时才打开

MEMORY.md 的结构是被 schema 管住的,按任务组组织,每个任务块要按固定顺序给子节:

markdown 复制代码
# Task Group: <cwd 或工作流分组>
applies_to: cwd=/Users/xxx/work/api-service

## Task 1: <任务描述, outcome=success|partial|fail|uncertain>

### rollout_summary_files
- 2026-02-17T21-23-02-LN3m-weekly_memory_pivot.md (cwd=/Users/xxx/work)

### keywords
- model routing, gateway api, prompt cache

### Preference signals
- 用户说过:"先追实际路由路径再回答"
  -> 回答模型选择问题前,先查网关路由配置

### Reusable knowledge
- 网关 portal 有 per-model 容量看板 /portal/capacity

### Failures and how to do differently
- 直接用 CLI 查 GPU 容量撞了鉴权墙 -> 改走申请表单

### References
- /portal/capacity, /portal/request

applies_to: cwd= 这行是跨项目隔离的关键:Codex 只有一个全局记忆目录,不分项目,靠内容里的 cwd 标注 + 读路径过滤来实现多项目共存。合并模型的 prompt 是 841 行,很大一部分就在教它怎么在多次更新中维持这套 schema。

2.6 淘汰机制:调用次数 + 最后使用时间

这是整套系统里最「反 RAG 直觉」的一块。大多数 RAG 框架只管写入,不管退役;Codex 把退役做成了三条口子。

先说排序和筛选。Phase 2 选记忆的 SQL 长这样:

sql 复制代码
-- 内层:谁进 top-N
ORDER BY
    COALESCE(so.usage_count, 0) DESC,                        -- 被引用过几次
    COALESCE(so.last_usage, so.source_updated_at) DESC,      -- 最近一次用是什么时候
    so.source_updated_at DESC,
    so.thread_id DESC
LIMIT ?

-- 外层:进 top-N 的集合按 thread_id 升序固定
ORDER BY selected.thread_id ASC

外层那个 ORDER BY thread_id ASC 不是为了排序好看,是为了raw_memories.md 的文本顺序在多次连续运行里保持稳定,否则排名一变,git diff 全是噪声。

淘汰规则:

机制 作用对象 怎么算
max_unused_days(默认 30) 记忆条目 last_usage 超窗的直接不进选择集
usage_count 排序 记忆条目 被引用越多越靠前
prune_stage1_outputs_for_retention DB 记录 按时间窗硬删除,每次启动最多 200 行
selected_for_phase2 = 0 DB 记录 从未进过 baseline 的优先淘汰
mark_thread_memory_mode_polluted thread 主动遗忘 API

⚠️ 两个坑:

  1. prune 跑在限速检查之前。源码注释说得很清楚------它不花 token,所以可以提前跑。结果就是:即使 Phase 1/2 因为配额不足被跳过,清理照样执行。极端情况下会出现「还没合并就被先清掉」。
  2. usage_count 依赖模型自觉引用 。它靠模型在最终回复里输出 <oai-mem-citation> 块来计数,模型忘了引用,信号就丢了。这是个会静默失效的回路------模型升级后引用行为一变,淘汰逻辑就跟着漂。

至于「只有低频又长期闲置的才会被清理」------这句话基本准确,但要补一句:高使用频率的记忆即使很久没用,因为 usage_count 排在前面,仍然能进选择集;真正被清掉的是「既没被引用过、又过了时间窗」的那批。

2.7 读路径:只注入一个被截断的摘要

写路径六千多行,读路径三百来行。这个比例本身就是设计态度。

会话启动时,Codex 只做一件事:

rust 复制代码
// 伪代码,对应 read 路径的注入函数
let summary = read("~/.codex/memories/memory_summary.md");
let summary = truncate_text(summary, Tokens(SUMMARY_TOKEN_LIMIT));  // 硬截断
// 把截断后的 summary 塞进 developer 指令模板,其余文件靠模型自己 grep

其余 MEMORY.mdrollout_summaries/skills/ 全部不注入 ,模型需要时自己用 shell 去 read / grep。社区文章普遍写的「5K token 上限」来自早期版本,源码分析时点(v0.137 附近)这个常量已经收紧到 2,500 tokens;再算上模板自身约 1,500 tokens,等于每回合花约 4,000 tokens 教模型「怎么用记忆」。

反馈闭环在这里闭合:

xml 复制代码
模型回复里带 <oai-mem-citation>
  → parse_memory_citation 按行解析
  → record_stage1_output_usage 事务更新 usage_count / last_usage
  → 下一次 Phase 2 排序时生效

另外还挂了一个独立的只读 MCP 服务(list / read / search 三个工具),意味着别的客户端也能挂载 Codex 的记忆目录 。路径校验严格到什么程度:禁止 ..、禁止根目录、禁止隐藏文件、逐层拒绝 symlink,等于把记忆目录当 chroot 监狱管。

2.8 什么情况下记忆系统根本不跑

四道前置门,任何一条没过就整体跳过:

  • 会话是 --ephemeral 或一次性 exec
  • 用户关掉了 Feature::MemoryTool
  • 当前会话是子代理(包括 Phase 2 自己的 morpheus)
  • state DB 不可用

还有一条容易忽略的触发条件:会话必须空闲够久才有资格合并 。mem0 的拆解写的是默认 6 小时,Codex 配置里对应 min_rollout_idle_hours(会被 clamp 到 1~48 小时)。含义是------今天学到的东西,通常要等明天那次启动才会真正进记忆


三、Claude Code 的记忆体系:回合内写、分层目录、按需召回

Claude Code 走的是完全相反的路线。它没有两阶段管线,没有 SQLite,没有后台抽取任务(后期补了一个 auto-dream 做整合)。核心就一句话:让 agent 在回合内,用和写代码完全相同的文件工具,把记忆写成 Markdown

3.1 四层记忆,各管一件事

先看职责划分:

解决的问题 存哪
Auto Memory 用户 / 项目的长期协作信息 <memoryBase>/projects/<sanitized-git-root>/memory/
Session Memory 当前会话摘要,服务长会话与 compact Session Memory 目录
Agent Memory 某类 agent 的专属长期记忆 user / project / local 三种 scope
Team Memory 团队共享的 repo 级知识 带同步机制的共享层

它们没被塞进一套 schema,而是各自有独立目录、独立 prompt、独立更新策略。设计意图是透明(用户能直接打开看)、可治理(单独开关)、可组合(同时存在但职责不同)

3.2 底层约定:MEMORY.md 是索引,不是正文

src/memdir/memdir.ts 里三个常量定了整套规矩:

typescript 复制代码
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000

含义:

  • 每条记忆是一个独立 Markdown 文件
  • MEMORY.md 只维护索引,一行一条,- [标题](文件.md) --- 一句话钩子不超过约 150 字符,不写正文
  • 会话启动时只保证模型看到 MEMORY.md,需要细节再去读具体文件
  • 超过 200 行 / 25KB 会被硬截断(超了会在注入内容里附带警告)

写入是两步法 ,系统 prompt 里写得很死:先写 topic 文件,再在 MEMORY.md 加一行指针。**「绝不要把记忆正文写进 MEMORY.md」**是硬规矩。

为什么不在一个大文件里堆?因为大文件会同时带来 prompt 爆炸、更新冲突、历史垃圾无法治理、单条错误污染整份上下文。

3.3 写入时机:主回合内,看得见

Claude Code 的写入发生在用户还在键盘前的时候 :agent 判断「这个值得记」,当场调 Write / Edit 落盘。用户可以看着文件生成,也可以当场反对。

这带来一个 Codex 没有的性质:记忆的写入是可审计的人类动作 ,而不是后台管线的产物。代价是复杂度转移给了模型------它得自己守规范,系统没有 schema 校验器会拒绝一个 type: foo 的文件。

记忆被约束成四种类型(memdir/memoryTypes.ts):

类型 内容 特点
user 用户角色、目标、知识背景 写得少,偏静态
feedback 用户对工作方式的纠正与肯定 数量最多,含规则的「为什么」和「怎么用」
project 项目代号、业务背景等非代码信息 不进版本控制的那些上下文
reference 需要反复查的技术细节 自由格式

关键约束写在注释里:代码模式、架构、git 历史、文件结构这些能从当前项目状态推导出来的东西,不该存成记忆。这条边界是这套设计能保持干净的根本原因。

3.4 召回:不灌注全文,让轻量模型挑 5 个

src/memdir/findRelevantMemories.ts 的做法:

scss 复制代码
memoryDir
  → scanMemoryFiles()        只读文件头
  → 过滤 alreadySurfaced     别每轮都挑同一批
  → formatMemoryManifest()   生成「文件名 + 描述」清单
  → sideQuery(轻量模型)      从清单里选
  → 最多返回 5 个文件路径

两个细节:选的是文件名不是正文 (不先把所有记忆全文塞进去),以及 MEMORY.md 本身不参与这次选择 (它已经单独注入 system prompt 了)。选择器用的是默认 Sonnet,max_tokens 只有 256。

这是一台轻量检索器,不是向量库。

3.5 Session Memory:长会话的摘要层

Session Memory 不解决「跨会话记住什么」,它解决长会话跑到一半怎么不撑爆

触发阈值写死在 sessionMemoryUtils.ts

typescript 复制代码
export const DEFAULT_SESSION_MEMORY_CONFIG = {
  minimumMessageTokensToInit: 10000,    // 没到 1 万 token 不启用
  minimumTokensBetweenUpdate: 5000,     // 涨了 5 千 token 才考虑更新
  toolCallsBetweenUpdates: 3,           // 至少 3 次工具调用
}

判断逻辑里有个容易被忽略的巧思:token 阈值始终必要 ,然后要么「双阈值都满足」,要么「token 够了且上一轮没有 tool_use」------后者是在找自然断点,防止在工具调用链中间截断,生成一份孤立摘要。

文件权限也说明它被当成敏感状态而不是普通缓存:

typescript 复制代码
await fs.mkdir(sessionMemoryDir, { mode: 0o700 })   // 目录:仅属主
await writeFile(memoryPath, '', {
  mode: 0o600,   // 文件:仅属主可读写
  flag: 'wx',    // 不存在才创建,防覆盖已有记忆
})

更新由一个后台 forked subagent 完成,权限被掐到极致------只允许对唯一那个精确路径调 FileEdit,连 Read 和 Write 都被拒。这是个严密沙箱化的摘要代理。

配套的 compact 也很有意思:上下文要压缩时,它不再额外调一次 API 让模型总结 ,而是直接拿 Session Memory 当断点;裁切消息时优先保证尾部保留下限 token 的原文;如果切断位置落在 tool_use / tool_result 链中间,会把索引往前平移把这些记录包进来,绝不切出孤立的 tool_result(否则会直接吃厂商 API 的校验错误)。压缩完还会把文件附件和工具能力声明重新灌回去,让模型「醒来」时技能蓝图依然齐装满员。

3.6 Agent Memory:记忆可以随项目分发

Agent Memory 是「某个 agent 类型的长期记忆」,三种 scope:

scope 典型目录 用途
user <memoryBase>/agent-memory/<agentType>/ 跨项目复用
project <cwd>/.claude/agent-memory/<agentType>/ 项目内共享
local <cwd>/.claude/agent-memory-local/<agentType>/ 本机本地

agentType 会先做路径清洗(插件命名空间里的 : 会被换成 -),local scope 在 remote 环境里会重定位到远端挂载盘的 project namespace 下------它不是真的「只在本地磁盘」

写权限是自动注入的:agent 定义里声明了 memory,系统就强制把 FileWriteTool / FileEditTool / FileReadTool 加进这个 agent 的工具列表,并把记忆 prompt 拼到它的 system prompt 后面。这两步缺一不可------只给 prompt 不给工具,agent 看得到规则没法落盘;只给工具不给 prompt,agent 有权限却不知道怎么维护。

最有意思的是 snapshot 机制 :snapshot 目录固定在 <cwd>/.claude/agent-memory-snapshots/<agentType>/,用 snapshot.json(记录 updatedAt)和 .snapshot-synced.json(记录本地同步自哪个时间点)两个文件,判定三种动作:

  • 没有 snapshot → 什么都不做
  • 本地记忆目录里一个 .md 都没有 → 初始化(把 snapshot 拷进去)
  • 本地有记忆但没有同步标记,或 snapshot 比本地新 → 提示可更新

这一手把 agent memory 从「运行时副产物」提升成了「可分发的角色资产」:项目不仅能下发 prompt,还能一起下发这个 agent 已经积累好的协作经验,并且支持后续升级。

3.7 auto-dream:补上的后台整合

Claude Code 后期引入了一个后台整合流程(src/tasks/DreamTask/DreamTask.ts 是它在 UI 上的可见化外壳)。forked 出来的 dream agent 按四阶段跑:orient(定位)/ gather(收集)/ consolidate(整合)/ prune(清理),配一把 consolidation lock 防止并发打架。

这算是向 Codex 那种「离线整合」思路靠了一步,但入口依然是显式的、可被用户察觉的(UI 里有 pill 和对话面板能看),不像 morpheus 那样几乎全黑。


四、逐维度硬碰硬

4.1 总览对比

维度 Codex Claude Code
记忆载体 ~/.codex/memories/*.md + git baseline + SQLite 项目/用户级目录下的 Markdown 文件
是否自动生成 是(两阶段管线) 部分(回合内主动写 + auto-dream 整合)
写入时机 会话外(空闲数小时后,下次启动跑) 回合内同步
事实源 SQLite(Markdown 是可重建的派生物) 文件(没有 DB)
索引 memory_summary.md(注入)+ MEMORY.md(手册) MEMORY.md(注入,上限 200 行 / 25KB)
召回方式 注入截断摘要 + 模型自己 grep 轻量模型选 ≤5 个文件 + 索引常驻
淘汰 usage_count / last_usage / 时间窗 / prune 无自动衰减,靠读取时的年龄提醒
脱敏 三道模式匹配 + 多次 redact 文件权限 0600/0700 + 团队同步侧扫描
跨项目隔离 单一全局目录 + 内容里 cwd: 标注 每个 cwd 一个编码后的独立目录
分发 / 协作 无(本机、按用户) Agent Memory snapshot + Team Memory 同步
可观察性 主要是 OTel 指标,用户侧缺诊断命令 文件可直接打开,UI 有文件选择器
代码量 记忆三 crate + 状态层约一万一千行 分模块,相对轻

4.2 三组根本分歧

分歧一:写入放回合内还是回合外。

Codex 赌「当前回合越便宜越好」,代价是新鲜度延迟------今天学会的东西明天才生效(<oai-mem-citation> 的计数是即时更新的,但内容合并要等)。Claude Code 赌「当场写、当场可见」,代价是模型必须自己守写入规范,而且用户会看到写入动作。

分歧二:事实源是数据库还是文件。

Codex 把 DB 当权威,文件是投影。好处是并发安全、可恢复、幂等;代价是「真相」不在用户能直接读的地方。Claude Code 把文件当权威,好处是 cat 就能看、git diff 就能审;代价是没有任何机制保证文件结构不被写坏。

分歧三:淘汰靠机制还是靠人。

Codex 有完整的自动退役(usage 衰减 + 时间窗 + prune + 污染标记)。Claude Code 完全没有 usage_count、没有 last_usage、没有 max_unused_days ------一个第 1 天写的记忆,第 365 天还在 MEMORY.md 里,除非 agent 或用户手动删。它的替代方案是在读取时提醒年龄,让「过时」在用的那一刻暴露出来,而不是在存储时被静默清掉。

4.3 各自的代价

主要代价 具体表现
Codex 新鲜度延迟 快速演进的偏好(新项目、新代号、新规则)要等一轮空闲窗口
Codex 依赖模型自觉 不引用 → usage_count 不涨 → 好记忆被误淘汰,且静默失败
Codex 用户侧黑箱 没有 codex memories status 类的诊断命令,只能看 OTel
Codex 无跨设备 / 无团队 换台机器、换个容器,记忆从零开始
Claude Code 无自动衰减 过时记忆会持续污染上下文,靠人清理
Claude Code 索引硬上限 MEMORY.md 200 行封顶,记忆多了必然挤占
Claude Code 检索靠词面 只记得意思、不记得原词时召回不稳
Claude Code 单工具锁定 记忆不跨工具,换 Codex 得重头养

五、为什么都不用 Embedding 和向量库

拆完两套实现,可以回答最初那个问题了。

5.1 三条被反复验证的判断

  1. 嵌入是有损的,短事实上的语义相似度尤其吵。 「用户不喜欢连字符」和「用户喜欢简洁写作」在向量空间里可能挨得很近,但它们是两条不同的规则。
  2. 后台记忆代理最难的不是检索,是触发时机。 什么时候该写、写哪一条、和已有条目冲突了怎么办------这些问题向量库一个都答不了。
  3. 可调试性。 cat 一个文件能看的东西,比 query 一个黑盒索引要可信得多。生产上出问题时,你能指着具体的字节说「就是它」。

Codex 侧更直接:主代理模型本身就足够强,模型自己 grep 比再架一层检索服务更划算,还少了一个服务依赖和一层排序不确定性。

5.2 赢下来的组合其实很朴素

组成 说明
一个 LLM 判断什么值得记、怎么合
一堆 Markdown 唯一存储格式,人可读可改
文件工具 + bash Read / Write / Edit / grep,就这些

这套「文本文件 + grep」的组合在软件工程里用了四十年,现在被搬到了 Agent 记忆上。有意思的问题不在数据结构,而在「读写时的纪律」------记忆在 prompt 里放哪、谁能写、怎么防止 prompt cache 每轮失效、什么时候淘汰。

5.3 诚实说清楚它的代价

不用向量库不是没有代价的,两套实现都栽在同一个坑里:

  • 换个说法就搜不到。你记得讨论过「部署时端口冲突」,但记忆里写的是「修改 docker-compose 的 port mapping」------grep 找不到。这是纯文本检索的物理上限。
  • 规模上不去MEMORY.md 涨到几十 MB 时,靠模型 grep 的效率会成为瓶颈。这是 Codex 当前没解决、社区也还没共识的开放问题。
  • 不是制度替代品 。团队规范、部署要求、安全边界这些「必须始终生效」的规则,应该写进 AGENTS.md / CLAUDE.md 这类静态文件,而不是指望自动生成的记忆。两家的官方文档都说了这件事------记忆是辅助召回层,不是规则的唯一来源。

5.4 别忽略的选型信号

Codex 的 Memories 在发布时不支持 EEA、英国、瑞士地区 ,那些用户只有 AGENTS.md 这一层。另外它是本机状态、按用户隔离,没有官方跨设备同步,也没有团队共享。如果你的场景要求「团队共享 + 跨设备」,原生方案不够用,得看外部记忆层(例如 Mem0 这类 MCP 接入方案)或者自建。


六、你真正需要记住的 6 件事

  1. 记忆不等于向量库。 两家头部 coding agent 的跨会话记忆都用 Markdown + grep,靠的是模型自身的检索能力,不是嵌入检索。
  2. Codex 是「会话外重、会话内轻」。 两阶段管线(小模型抽取 + 大模型合并)、SQLite 事实源、git baseline 做 dirty 检测,读路径只注入一个被截断的摘要。
  3. Codex 的 SQLite 不是临时存储,是权威数据源。 raw_memories.mdrollout_summaries/ 每次都会从 SQL 重建。
  4. Codex 的四层是「总览 / 索引 / 任务记录 / 标准流程」。 分别对应 memory_summary.mdMEMORY.mdrollout_summaries/skills/,前一层注入,后三层按需读。
  5. Claude Code 是「回合内写、分层目录、按需召回」。 MEMORY.md 只是索引(200 行 / 25KB 封顶),正文一条一个文件;召回用一个轻量模型从清单里挑不超过 5 个。
  6. 淘汰策略是两套设计最大的分水岭。 Codex 用 usage_count + last_usage + 时间窗自动退役;Claude Code 不衰减,改用读取时的年龄提醒把「过时」暴露在使用现场。

验证清单

  • 打开 ~/.codex/memories/,确认能看到 memory_summary.mdMEMORY.mdraw_memories.mdrollout_summaries/skills/ 五类内容
  • 在 Codex 配置里找到 [memories] 段,确认存在「写入开关」和「读取开关」两个独立配置
  • 确认 ~/.codex/memories/ 下存在 .git/,并验证 git log 看不到历史(baseline 每次成功的合并都会被重置)
  • 打开 ~/.claude/projects/<编码后的cwd>/memory/MEMORY.md,数一下行数是否在 200 行以内
  • 抽查一份 feedback_*.md,确认正文没有写进 MEMORY.md(索引里应该只有一行指针)
  • 删除一条 MEMORY.md 里的索引行,观察下一次会话是否还会召回这个文件(验证「索引即入口」)
  • 对你自己的场景做一次判断:你需要「团队共享 + 跨设备」,还是「本机单人」?前者原生方案不够用

参考资源

官方文档

源码

  • openai/codex 记忆管线设计说明:github.com/openai/code...README.mdwrite/src/ 是核心)
  • 本地 Claude Code 源码分析仓库:claude-code-analysis(重点看 analysis/04-agent-memory.mdsrc/memdir/src/services/SessionMemory/src/tools/AgentTool/agentMemory*.ts

第三方拆解(可交叉验证)

  • mem0 :《Codex CLI Memory: How It Works + What Mem0 Adds》
  • Nicolas Bustamante:《Agent Memory Engineering》(Hermes / Codex / Claude Code 三家横向对比,含淘汰策略细节)
  • codex-source-analysis 第 20 章《记忆系统》(两阶段管线的逐文件源码分析)
相关推荐
用户3134672143541 小时前
Agent 开发学习笔记(六):LCEL:把组件串成链
langchain·agent
葡萄城技术团队1 小时前
GcExcel V9.2 新特性揭秘:公式对象的无损往返
后端
蜗牛互联网1 小时前
Claude放宽生命科学限制:代价是验证、分级和30天留存
java·人工智能·后端
上进小菜猪1 小时前
被 32 位整数卡了 30 年脖子:PostgreSQL 事务号困局与 V9 的 64 位解法
后端
小马过河R1 小时前
开篇|3天从0到1入门AI应用开发
人工智能·语言模型·llm·agent·ai编程·deepagent
知识的搬运工旺仔3 小时前
CREATE INDEX CONCURRENTLY:线上建索引不阻塞 DML 的代价与坑
数据库·后端·sql
liulilittle3 小时前
Linux AI 开发环境搭建实录
linux·ai·llm·agent·hyper-v·dev·opencode
小蒜学长3 小时前
大学生健康饮食的智慧管理系统(代码+数据库+LW)
java·后端·springboot·大学生·健康饮食