Codex的分层记忆系统

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 的团队来说,别急着先写一个"永久记忆文件"。先问四个问题:哪些信息不能从代码和文档实时推导?哪些信息可能污染长期偏好?哪些记忆应该被忘掉?模型用到某段记忆后,系统能不能记录这次使用?这四个问题答清楚,记忆系统才不会从助手变成负担。

相关推荐
大鹏的NLP博客4 小时前
工业 AI Agent 引入 DSL 的架构必要性:从概率生成到确定执行
agent·dsl
甘露s7 小时前
Eino DeepAgent 详解:让 Agent 从工具调用走向复杂任务执行
ai·agent·deepagent
安逸sgr7 小时前
AI 编程工具在真实项目中适合做什么?不适合做什么?
人工智能·ai·大模型·agent·智能体
smartfish_liu8 小时前
apl_LLM_agent_harness架构设计
llm·agent
copyer_xyf8 小时前
Elasticsearch:给 RAG 补上关键词检索
agent
吾AI科技8 小时前
Agent的诞生(三):Skill —— 让模型掌握处理复杂流程的能力
ai·agent·智能体
weixin_471383039 小时前
01 agent基础
agent
zzz_23689 小时前
TencentDB-Agent-Memory 深度解析:让多个 Agent 共享项目经验的记忆中枢
java·开发语言·jvm·人工智能·agent·memory·tencent db
武子康10 小时前
WebRTC 已经双向,语音 Agent 为什么还会抢话?
人工智能·llm·agent