ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Codex的分层记忆系统

Codex的分层记忆系统

从 AGENTS.md、会话上下文到两阶段长期记忆管道,拆清 Codex 如何提取、整合、遗忘和取用记忆,并给出一个可运行的 Python 最小实现。

很多 AI 编程助手都有这个毛病:这一轮像老同事,下一轮像新实习生。你刚教完项目偏好、命令习惯、踩坑记录,开个新会话又得重讲。

问题不只是模型上下文不够长,而是记忆没有变成一条可维护的管道。Claude Code 更依赖人手写CLAUDE.md;Codex 的新方向,是把“什么值得记、什么时候忘、用到后如何反馈”拆成后台流程。官方codex-rs/memoriesREADME 已把这条链分成 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 的团队来说,别急着先写一个“永久记忆文件”。先问四个问题:哪些信息不能从代码和文档实时推导?哪些信息可能污染长期偏好?哪些记忆应该被忘掉?模型用到某段记忆后,系统能不能记录这次使用?这四个问题答清楚,记忆系统才不会从助手变成负担。

返回列表