从 AGENTS.md、会话上下文到两阶段长期记忆管道,拆清 Codex 如何提取、整合、遗忘和取用记忆,并给出一个可运行的 Python 最小实现。
很多 AI 编程助手都有这个毛病:这一轮像老同事,下一轮像新实习生。你刚教完项目偏好、命令习惯、踩坑记录,开个新会话又得重讲。
问题不只是模型上下文不够长,而是记忆没有变成一条可维护的管道。Claude Code 更依赖人手写 CLAUDE.md;Codex 的新方向,是把"什么值得记、什么时候忘、用到后如何反馈"拆成后台流程。官方 codex-rs/memories README 已把这条链分成 read path 和 write path;记忆能力仍属于实验特性,通常需要在配置里显式开启。
# ~/.codex/config.toml
[features]
memories = true
1. 先分层:不要把所有东西塞进一个文件
Codex 的记忆不是一份超大的备忘录,而是三层东西各管各的:
-
AGENTS.md:静态规则。比如代码风格、目录约定、团队偏好。 -
当前会话上下文:这一轮刚说过什么、工具刚返回什么。
-
长期记忆:从旧会话里提炼出来、未来还会改变行为的经验。
这一步很重要。AGENTS.md 已经会按工作目录重新注入,如果再被长期记忆管道反复抽取,只会制造重复和污染。

from pathlib import Path
def collect_agents(start: str) -> str:
cur = Path(start).resolve()
files = []
for parent in [cur, *cur.parents]:
doc = parent / "AGENTS.md"
if doc.exists():
files.append(doc.read_text(encoding="utf-8"))
return "\n".join(reversed(files))
print(collect_agents("."))
2. Phase 1:先从旧会话里捞候选
长期记忆最难的不是"写摘要",而是判断什么信息未来还值钱。Codex 的写入路径会在根会话启动时检查多层条件:不是临时会话、记忆功能已开启、不是子代理、状态库可用,然后后台处理旧 rollout。
Phase 1 做的事很像粗筛:读取旧会话,过滤掉 AGENTS.md、skill 注入这类系统上下文,再提取 raw memory、rollout summary 和索引信息。它不应该把历史里的"指令文本"当成新系统指令执行,只能当数据处理。

def should_extract(session: dict) -> bool:
checks = [
not session.get("ephemeral", False),
session.get("memory_enabled", False),
not session.get("is_sub_agent", False),
session.get("state_db_ok", False),
]
return all(checks)
case = {"ephemeral": False, "memory_enabled": True, "is_sub_agent": False, "state_db_ok": True}
print(should_extract(case)) # True
3. Phase 2:整合比提取更关键
Phase 1 只得到零散候选。真正决定记忆质量的是 Phase 2:拿到一批候选后,在受控工作区里合并、去重、排序,并删除已经不值得保留的条目。
这里最值得借鉴的是 Selection Diff:新选中的进 added,继续保留的进 retained,上轮有但本轮不再保留的进 removed。很多记忆系统只会追加,最后一定变成垃圾堆。Codex 把"遗忘"做进流程,这才像长期记忆。

selection_diff = {
"added": ["用户偏好 Python 3.11 示例"],
"retained": ["回答默认用中文"],
"removed": ["一次性调试路径 /tmp/foo"],
}
for name, items in selection_diff.items():
print(f"{name}: {len(items)}")
4. 读取:先看总览,再按需翻细节
memory_summary.md 不应该变成"每次把所有历史都塞进 prompt"。更合理的做法是:先给模型一个总览,让它知道有什么可查;如果问题真的需要旧信息,再去更细的 MEMORY.md、skills 或 rollout summaries 里找。
Codex 的 read path 还有一个闭环:回答如果引用了某段记忆,系统可以从 citation 里回写使用次数和最近使用时间。常用记忆下次更容易被保留;长期没用的记忆会被淘汰。

from datetime import datetime, timezone
usage = {"usage_count": 3, "last_usage": None}
usage["usage_count"] += 1
usage["last_usage"] = datetime.now(timezone.utc).isoformat()
print(usage["usage_count"])
最小实现
下面这份脚本不依赖外部 API。它用 SQLite 模拟历史会话,Phase 1 抽取偏好,Phase 2 去重整合,最后把 memory_summary.md 注入下一轮问题。

import sqlite3
from pathlib import Path
ROOT = Path(".")
DB = ROOT / "memory_demo.db"
OUT = ROOT / "memory_summary.md"
conn = sqlite3.connect(DB)
cur = conn.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS sessions(id INTEGER PRIMARY KEY, content TEXT)")
cur.execute("CREATE TABLE IF NOT EXISTS raw_memories(id INTEGER PRIMARY KEY, memory TEXT UNIQUE)")
cur.execute("DELETE FROM sessions")
cur.execute("DELETE FROM raw_memories")
sessions = [
"用户说:以后代码示例默认用 Python 3.11。",
"用户说:回答尽量简洁,先给结论再展开。",
"用户又提到:代码示例请继续使用 Python 3.11。",
]
cur.executemany("INSERT INTO sessions(content) VALUES(?)", [(s,) for s in sessions])
conn.commit()
rows = list(cur.execute("SELECT content FROM sessions"))
for (content,) in rows:
if "Python 3.11" in content:
cur.execute(
"INSERT OR IGNORE INTO raw_memories(memory) VALUES(?)",
("代码示例默认用 Python 3.11",),
)
if "先给结论" in content or "简洁" in content:
cur.execute(
"INSERT OR IGNORE INTO raw_memories(memory) VALUES(?)",
("回答风格:先结论,后展开,保持简洁",),
)
conn.commit()
memories = [row[0] for row in cur.execute("SELECT memory FROM raw_memories ORDER BY id")]
summary = "# Memory Summary\n\n" + "\n".join(f"- {m}" for m in memories)
OUT.write_text(summary, encoding="utf-8")
question = "请给我一个 SQLite 示例。"
prompt = OUT.read_text(encoding="utf-8") + "\n\n用户问题:" + question
print(prompt)
print("\n预期效果:回答应先给简洁结论,并使用 Python 3.11 示例。")
conn.close()
预期输出:
# Memory Summary
- 代码示例默认用 Python 3.11
- 回答风格:先结论,后展开,保持简洁
用户问题:请给我一个 SQLite 示例。
预期效果:回答应先给简洁结论,并使用 Python 3.11 示例。
边界说明
第一,当前会话压缩不等于长期记忆。压缩只是在一个 thread 里延长上下文寿命,长期记忆则来自旧会话的后台抽取和整合。
第二,AGENTS.md 是静态规则层,不应被长期记忆反复学习。它已经会随工作目录进入上下文。
第三,网络搜索、外部 MCP、一次性调试输出这类临时信息容易污染长期记忆。工程上要给它们打标,必要时直接禁止进入 Phase 2。
第四,源码仍在快速变化。本文重点讲设计模式;具体模型名、默认扫描窗口、文件路径上限等参数,发布时应以 OpenAI Codex 仓库当前实现为准。

def should_persist(source: str, polluted: bool) -> bool:
if source in {"agents_md", "skill_injection"}:
return False
if polluted:
return False
return True
print(should_persist("chat", polluted=False)) # True
结尾
如果只看表面,Codex 的记忆像是自动版 CLAUDE.md。往里拆,它真正有价值的是一条闭环:先分层,再提取,再整合,再按需读取,最后用使用反馈决定下一轮保留什么。
对做 agent 的团队来说,别急着先写一个"永久记忆文件"。先问四个问题:哪些信息不能从代码和文档实时推导?哪些信息可能污染长期偏好?哪些记忆应该被忘掉?模型用到某段记忆后,系统能不能记录这次使用?这四个问题答清楚,记忆系统才不会从助手变成负担。