记忆 · 从历史会话里提炼经验

打开 memories 后,每开一轮新对话,Codex 都会在后台把闲置已久的旧会话分两阶段提炼成 ~/.codex/memories 下的记忆:Phase 1 用一次模型调用把单个会话抽成原始记忆存进 SQLite,Phase 2 由一个锁死权限的整合子代理把它们归并成 MEMORY.md 与 memory_summary.md。之后每个会话把摘要注入开发者指令,模型引用了哪条记忆就给它记一次使用,决定它下次还能不能留下。

作者 David更新于 第 42 篇(共 57 篇)

记忆 · 从历史会话里提炼经验

记忆是一个默认关闭的功能开关 memories(稳定)。打开后,Codex 会把你过去的会话提炼成一个本地的记忆目录,新会话开始时先读一份摘要,需要时再翻细节。TUI 的 /memories 可以分别开关“使用记忆”和“生成记忆”,或者清空全部记忆;相关配置见手册的配置参考与斜杠命令。这一篇看记忆从哪里来、存在哪里、怎么被用上、怎么清掉。

三个 crate,读写分开

crate目录职责
codex-memories-writecodex-rs/memories/write后台管线:Phase 1 抽取、Phase 2 整合、记忆目录的 git 基线与清理
codex-memories-readcodex-rs/memories/read解析回复里的记忆引用、读取行为的遥测分类
codex-memories-extensioncodex-rs/ext/memories扩展:把记忆摘要注入提示词,可选地提供专用记忆工具

数据分两处放。记录与任务在单独的 SQLite 库里:memories_1.sqlite(memories.version = "v2" 时是 memories_v2_1.sqlite),默认在 ~/.codex 下,表是 stage1_outputs(每个会话一行原始记忆)和 jobs(任务租约与重试)。文件在 ~/.codex/memories/(v2 是 memories_v2/),是给模型读的。

codex-rs/memories/README.md 有两处过时:它说 Phase 1、Phase 2 的编排“still lives in codex-core under codex-rs/core/src/memories/”,但 v0.158.0 没有这个目录,编排就在 memories/write 里;它列的读取模板 read/templates/memories/read_path.md 实际在 ext/memories/templates/memories/read_path.md。README 里对两个阶段的描述大体仍与代码一致。

什么时候跑

README 说管线在根会话启动时触发,代码里的触发点其实是 app-server 的 turn_start_inner:一次 turn/start 真的开了新一轮(不是插进进行中的轮次)、并且配置了主执行环境,就调用 start_memories_startup_task。它在临时会话、功能没开、子代理会话时直接返回;每个要生成的版本各起一个后台任务(memories.dual_write = true 时 v1、v2 都跑),状态库打不开就跳过。任务里先建好目录(记忆根目录是符号链接会拒绝),然后按这个顺序走:

            // Clean memories to make preserve DB size. This does not consume tokens so can be
            // done before the quota check.
            phase1::prune(context.as_ref(), &config).await;

            if !guard::rate_limits_ok(&auth_manager, &config).await {
                context.counter(
                    MEMORY_STARTUP,
                    /*inc*/ 1,
                    &[("status", "skipped_rate_limit")],
                );
                return;
            }

            // Run phase 1.
            phase1::run(Arc::clone(&context), Arc::clone(&config)).await;
            // Run phase 2.
            phase2::run(context, config, parent_permission_profile).await;

(codex-rs/memories/write/src/start.rs:75)

prune 删掉当前没被 Phase 2 选用、最后一次使用(没用过就看会话更新时间)早于 memories.max_unused_days(默认 30 天)的原始记忆,每次最多 200 条。额度检查只在认证走 Codex 后端(如 ChatGPT 登录)时生效:已经撞上额度上限,或主、次额度窗口任一已用超过 100 - min_rate_limit_remaining_percent(默认要求剩余 25%),就不跑;其它认证方式查不到额度,按“允许”处理。每开一轮都会走到这里,靠数据库里的租约和锁保证不重复干活。

图表加载中…

Phase 1:一次模型调用,抽一个会话

claim_stage1_jobs_for_startup 从状态库的 threads 表里挑会话。来源只限 INTERACTIVE_SESSION_SOURCES(CLI、VS Code 和两个自定义来源 atlas、chatgpt,所以 codex exec 的会话不算),再加上这几条:

        builder.push(" AND threads.memory_mode = 'enabled'");
        builder
            .push(" AND threads.id != ")
            .push_bind(current_thread_id.as_str());
        builder
            .push(" AND ")
            .push("threads.updated_at_ms")
            .push(" >= ")
            .push_bind(max_age_cutoff);
        builder
            .push(" AND ")
            .push("threads.updated_at_ms")
            .push(" <= ")
            .push_bind(idle_cutoff);

(codex-rs/state/src/runtime/memories.rs:241)

即:会话的 memory_mode 是 enabled、不是当前会话、最近 max_rollout_age_days(默认 10 天)内动过、但已闲置 min_rollout_idle_hours(默认 6 小时)以上,并且在上次抽取之后又有更新。按更新时间倒序最多扫 5000 个,每次最多认领 max_rollouts_per_startup(默认 2)个,每个带 1 小时租约。

抽取不开线程,而是直接用 ModelClient 发一次请求:读出会话的 rollout,丢掉开发者消息、推理、压缩记录等,只留用户与助手消息、工具调用和输出,序列化后过一遍 redact_secrets 脱敏;系统提示是 stage_one_system.md(开头就是 “Memory Writing Agent: Phase 1”,并说明没有值得记的就什么都别写),输出用严格的 JSON schema 约束成 raw_memory、rollout_summary、rollout_slug 三个字段,结果再脱敏一次。模型默认取模型提供方给的偏好(默认实现是 gpt-5.6-luna,Bedrock 另有映射),推理强度 low,可用 memories.extract_model 改;最多 8 个会话并发。成功的写进 stage1_outputs,没产出的记为 succeeded_no_output,失败的 1 小时后重试。

Phase 2:一个锁死权限的整合子代理

Phase 2 先抢全局锁:已有人在跑、正在退避、或者上次成功不到 6 小时,就跳过。拿到锁后:

  1. 确保记忆根目录是一个 git 仓库,作为比对基线;
  2. 从 stage1_outputs 里只看最近 max_unused_days 内用过(没用过就看会话更新时间)的记录,按 usage_count 从高到低、再按最近使用时间排序,取前 max_raw_memories_for_consolidation(默认 256)条,同步成 rollout_summaries/ 下的逐会话摘要和合并的 raw_memories.md,顺带删掉 extensions/ 下超过 7 天的资源文件;
  3. 与基线做 git diff:没有变化、且成品文件都合法,就直接记成功;否则把差异写进 phase2_workspace_diff.md(上限 4 MiB),启动整合子代理。

整合子代理是一个内部线程(SessionSource::Internal(InternalSessionSource::MemoryConsolidation)),工作目录就是记忆根目录,配置被这样收紧:

        agent_config.cwd = root.clone();
        // Consolidation threads must never feed back into phase-1 memory generation.
        agent_config.ephemeral = true;
        agent_config.memories.generate_memories = false;
        agent_config.memories.use_memories = false;
        // Background memory work must not send user-facing completion notifications.
        agent_config.notify = None;
        agent_config.include_apps_instructions = false;
        agent_config.mcp_servers = Constrained::allow_only(HashMap::new());
        // Approval policy
        agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
        // Consolidation runs as an internal worker and must not recursively delegate.
        let _ = agent_config.features.disable(Feature::Collab);
        let _ = agent_config.features.disable(Feature::MemoryTool);
        let _ = agent_config.features.disable(Feature::Apps);
        let _ = agent_config.features.disable(Feature::Plugins);

(codex-rs/memories/write/src/phase2.rs:304)

父会话用 Codex 管理的沙箱时,子代理只能写记忆根目录、不能联网;父会话关了沙箱则沿用父会话的选择。模型默认同样取提供方的偏好(默认实现是 gpt-5.6-terra,可用 memories.consolidation_model 改),推理强度 medium。提示词 consolidation.md 要它维护几样东西:必须以一行 v1 开头、会被注入提示词的 memory_summary.md,供检索的手册 MEMORY.md,以及可选的 skills/。子代理跑的时候每 90 秒给锁续一次租约;结束后校验成品,重置 git 基线,把用到的记录标成 selected_for_phase2,任务记成功。

下一个会话怎么用上

读的一侧是扩展(机制见上一篇)。codex-memories-extension 在功能开着、且 memories.use_memories 为真时,作为 ContextContributor 读出 memory_summary.md,截到 2500 token,填进模板 read_path.md,作为一段开发者指令放进上下文。模板告诉模型:摘要已经给你了;MEMORY.md 是可搜索的主索引;再往下是 skills/ 和 rollout_summaries/;动手前先做一次“快速记忆检索”,最好不超过 4 到 6 步。模型用普通的 shell 工具去读这些文件;打开 memories.dedicated_tools 则会多一组 memories 命名空间的工具:list、read、search、add_ad_hoc_note。

回路靠引用闭合。模板要求用到记忆时,在最终回复末尾附一个 <oai-mem-citation> 块,列出引用的文件行号和 rollout_ids。内核在条目完成时把这个块从可见文本里剥掉,用 codex-memories-read 解析出 id,再调 record_stage1_output_usage 给对应记录的 usage_count 加一、把 last_usage 设为当前时间(codex-rs/core/src/stream_events_utils.rs:201)。被引用得多的记忆在 Phase 2 排得靠前,久不被引用的超过 max_unused_days 就被清理掉。用户明确要求改记忆时,模板要求模型只往 extensions/ad_hoc/notes/ 写一个小笔记文件,由下一次 Phase 2 归并,而不是直接改记忆文件。

怎么关、怎么清

  • memories.generate_memories = false:新建的会话在状态库里记为 memory_mode = "disabled",Phase 1 不会挑它;
  • memories.use_memories = false:不再注入摘要;
  • memories.disable_on_external_context = true:会话里出现网页搜索、工具搜索等外部内容时,把它标成 polluted,同样不参与抽取;
  • 清空:TUI /memories 里的 “Reset all memories” 走 app-server 的实验方法 memory/reset;隐藏的 CLI 子命令 codex debug clear-memories 做同样的事。两者都清空两个版本记忆库里的 stage1_outputs 与记忆相关的 jobs,再清空 memories、memories_v2、memories_extensions 三个目录的内容(目录本身保留,是符号链接的拒绝处理)。

和《从 LLM 到 Coding Agent》对照

那本的会话历史用 append-only 的 JSONL 把一次会话落盘,解决的是“活过进程”;Codex 的记忆以这些 rollout 为原料,把许多次会话蒸馏成跨会话的经验。注入方式也对得上那本的上下文注入:项目记忆一类稳定内容前置,按相关性检索的细节按需取用。Codex 把截断后的 memory_summary.md 放进开发者指令,MEMORY.md 与逐会话摘要则让模型自己去翻。

对照 Grok Build 的记忆,两边骨架很像:grok 的 /flush 把单个会话写成日志、/dream 把多次会话合并进 MEMORY.md,对应 Codex 的 Phase 1 与 Phase 2。差别在触发与检索:grok 在压缩前、空闲时或手动触发,检索用 SQLite 的 FTS5 加向量混合搜索;Codex 只在新一轮开始时于后台处理闲置已久的旧会话,检索交给模型自己搜文件,再用回复里的引用给记忆计数。


上一篇:扩展 API · goal 与内部扩展点 · 下一篇:从别的 agent 迁移 · 导入 Claude Code 与 Cursor 的配置

本页目录