一、先搞清楚:Codex 到底为什么需要"记忆"
在聊实现之前,得先明白它要解决的痛点。不然你照猫画虎写出来的"记忆层",就是个中看不中用的花架子。
1.1 传统单次会话是"金鱼脑"
你拿最早的 ChatGPT 写代码,关掉窗口再打开,它对你一无所知。你昨天让它"所有接口返回用Result[T]包装",今天它又给你甩一堆裸dict。问题出在哪?单次会话是无状态的——会话结束,上下文就释放了,模型根本没有地方"记"东西。
会话A: 你说"用 uv 管依赖" → 模型照做 会话A结束: 上下文被丢弃 会话B: 模型重新从零开始,又用 pip → 你血压飙升1.2 重复纠正是隐形的时间黑洞
最搞心态的是:同一个偏好你要在每个新会话里反复交代。团队的代码规范、你个人的命名习惯、某个祖传接口的反人类约定……这些东西如果每开一次对话就重新讲一遍,那 AI 编程工具省下来的时间,又全还给"教模型"了。
1.3 团队知识没法沉淀
更关键的一点是协同。你的个人偏好是私有的,但项目级的约定(“这个 repo 用 Python 3.11、Ruff 做 lint、禁止print调试”)应该跟着代码仓库走——新人 clone 下来、Codex 一开就知道规矩。这正是"记忆"必须按作用域(scope)划分的根本原因。
二、Codex 记忆机制的整体架构
咱们别光说概念,看看这东西实际的设计长什么样。
2.1 核心:三层 scope 模型
Codex 的记忆不是一锅粥,而是按**作用域(scope)**拆开的。公开产品里能看到两个主层级,底层实现上还可以再加一层反馈记忆:
┌─────────────────────────────────────────────┐ │ Session / Task │ │ 系统提示词 (System Prompt) │ │ ┌─────────────────────────────────────┐ │ │ │ [Project Memory] ← 仓库级,优先级高 │ │ │ │ [User Memory] ← 个人级,优先级低 │ │ │ │ [Feedback Memory] ← 纠正类,即时生效 │ │ │ └─────────────────────────────────────┘ │ └─────────────────────────────────────────────┘ ↑ 召回注入 ↓ 写入落盘 ┌──────────┐ ┌──────────────────┐ │ 模型上下文 │ │ ~/.codex/ │ │ (context) │ │ memory.md (user)│ └──────────┘ │ <repo>/.codex/ │ │ memory.md (proj)│ └──────────────────┘- User Memory(用户级):跨你所有项目生效。比如"我习惯用 TypeScript strict 模式"。通常落在用户主目录下的配置区(如
~/.codex/memory.md)。 - Project Memory(项目级):只在该仓库生效,而且可以提交进 git,整个团队共享。比如"本项目禁止直接
import内部模块,必须走src包"。 - Feedback Memory(反馈级):你给的纠正,比如"刚才那个实现不要用
requests,用httpx"。这类信号最强,往往即时写入、即时生效。
2.2 文件即记忆:Markdown 是载体
这是最巧妙也最朴素的一点——Codex 的记忆本质是 Markdown 文本文件,不是什么神秘的向量数据库。
为什么用 Markdown?
- 可读可编辑:用户能直接打开
memory.md改,不用懂任何内部格式。 - 天然可注入:LLM 最擅长吃文本,把
.md内容塞进 system prompt 就能直接用,零转换成本。 - 可版本化:项目级记忆进 git,diff、review、rollback 一套流程全复用。
这里有个常见误区:很多人以为"AI 记忆"一定是向量库 + embedding 检索。但 Codex 这种"长期偏好记忆"体量很小(几十条),全文注入比检索更准、更简单,也更好调试。这是工程上很务实的取舍——检索适合"海量知识",注入适合"少量强约束"。
2.3 记忆、上下文、RAG 到底有什么区别
为了不把概念搅混,这里用一张表把三者划清楚。它们解决的是不同层级的问题,不是互相替代的关系:
| 维度 | 短期上下文 (Context) | 长期记忆 (Memory) | 检索增强 (RAG) |
|---|---|---|---|
| 数据规模 | 单轮会话内(有限窗口) | 几十~几百条偏好/约定 | 海量文档/知识库 |
| 存储介质 | 推理时的 KV Cache | Markdown 文件 | 向量库 + embedding |
| 召回方式 | 随会话存在,结束即弃 | 会话启动注入 system prompt | 按需语义检索后拼接 |
| 典型内容 | 当前任务代码、对话 | “用 pnpm”“禁裸 print” | 公司 Wiki、API 文档 |
| 适用场景 | 即时任务 | 跨会话的偏好沉淀 | 需要外部知识的问答 |
一句话:上下文管"当下",记忆管"习惯",RAG 管"知识"。Codex 的记忆机制解决的是中间那一格——把"你这个人的习惯、这个项目的规矩"稳定地跨会话带过来。
三、具体怎么干活:从写入到召回
光有文件不够,得有一套"什么时候写、写哪儿、怎么读回来"的闭环。
3.1 写入时机:Auto 还是 Ask
Codex 记忆有个关键开关——记忆模式。你能在设置里选auto(自动存)或ask(存之前先问你)。
auto模式下,模型判断"这条信息对未来有用、且是稳定的偏好/约定"时,会主动调用写入工具。ask模式下,它会先跟你确认:“要不要把这条存成记忆?”,避免把临时需求误记成长期偏好(比如你某次临时说"这次用 Python 2 写法",它不该记成永久偏好)。
工程上,这一步靠系统提示词里的指令驱动:告诉模型"当发现用户的 durable preference 时,调用save_memory工具",模型自己决定触发。
3.2 写入走的是"工具调用",不是自由输出
这是实现上的关键细节。记忆不是模型在聊天里随便写一段话,而是通过一个受控的**工具(tool/function call)**完成:
模型决定要记一条 → 调用 write_memory(scope="project", content="...") ↓ MemoryStore.save() 原子落盘到 memory.md ↓ (可选)如果是项目级,提示用户 git commit为什么必须走工具?因为你要约束写入位置(scope)和格式,还要做原子写、去重、权限校验。让模型自由输出文本去改文件,既不可控也容易写崩。
3.3 召回:System Prompt 注入 + 优先级
每次新会话启动,Codex 的编排层会先把记忆文件读出来,拼进系统提示词。优先级很直接:
最终注入内容 = Project Memory + "\n" + User Memory (project 在后,相同话题 project 覆盖 user)为什么 project 覆盖 user?因为进了某个具体仓库,项目规矩大于个人习惯(你个人爱用 tabs,但项目要求 spaces,那得听项目的)。
3.4 去重与合并
你不可能每次纠正都新增一行,那样memory.md会爆炸。真实实现里会有合并/去重逻辑:模型在写入前先读现有记忆,判断"这条是不是已经记过了"“是不是和某条冲突要更新”,再落盘。下面的 Python 版我们就用最简单的方式模拟:整文件重写 + 人工可读段落。
3.5 实战场景:记忆 + 亿牛云代理,让 Agent 真正能干活
光讲机制有点抽象,举个真实工程里很常见的例子,把"记忆"和"出网代理"串起来——这俩其实是天作之合。
假设你带队做一个竞品价格监控 / 公网数据采集 Agent,它要周期性地去抓各站点页面、调第三方 API。这种长期跑的 Agent 最怕什么?出口 IP 被目标站限流、封禁,一封就全停。
聪明的做法,是把"出网规范"写成一条Project Memory,让任何会话、任何成员接手都知道规矩:
- 所有对公网 API 调用与文档抓取,必须走 亿牛云企业代理池 (隧道模式 + 自动轮换),禁止直连,规避目标站点 IP 封禁 - 代理提取走 API 或隧道域名,凭证放环境变量,不入库为什么这条适合放"记忆"而不是写死在代码里?因为它是团队的长期约定,会随业务演进——今天用隧道模式,明天换成按业务隔离的静态住宅代理,改一处记忆全员生效,不用动代码、不用重新培训。而像亿牛云这类企业级代理 IP 服务,提供高匿、自动轮换的出口 IP 池,正好承接这种"Agent 稳定、合规访问公网"的刚需,让记忆里那条规矩真正落得了地。
换句话说:记忆解决"Agent 该怎么做",亿牛云解决"Agent 出得去、不被封"。一个管约束,一个管通道,搭配起来才是生产级 Agent。
四、用 Python 还原一个最小可用记忆层
光说不练假把式。下面这段是完整可运行的参考实现,把上面说的 scope、注入、原子写、工具调用全串起来了。你可以直接uv run python memory_demo.py跑。
"""Codex 风格记忆层的最小参考实现。 核心思想: 1. 记忆 = Markdown 文件,按 scope (user/project) 分目录落盘 2. 召回 = 会话启动时把文件内容拼进 system prompt 3. 写入 = 通过受控工具 (save_memory) 原子落盘,而非自由输出 """from__future__importannotationsfrompathlibimportPathfromdataclassesimportdataclass@dataclassclassMemoryStore:"""记忆存储器:管理 user / project 两个作用域的 Markdown 记忆文件。"""user_dir:Path project_dir:Path|None=Nonedef_path(self,scope:str)->Path:base=self.user_dirifscope=="user"elseself.project_dirifbaseisNone:raiseValueError(f"scope '{scope}' 未配置存储目录")returnbase/"memory.md"defload(self)->str:"""召回:合并所有 scope 的记忆,project 优先级高于 user。 为什么用拼接而非检索?长期偏好记忆体量小(几十~几百行), 全文注入比向量检索更准、更易调试。 """blocks:list[str]=[]user_md=self._path("user")ifuser_md.exists():blocks.append(f"# User Memory\n{user_md.read_text(encoding='utf-8').strip()}")ifself.project_dir:proj_md=self._path("project")ifproj_md.exists():blocks.append(f"# Project Memory\n{proj_md.read_text(encoding='utf-8').strip()}")return"\n\n".join(blocks)defsave(self,scope:str,content:str)->None:"""原子写入:先写临时文件再 rename,避免写到一半被会话读取。 rename 在同一文件系统内是原子操作,能防止"半截记忆"被注入上下文。 """path=self._path(scope)path.parent.mkdir(parents=True,exist_ok=True)tmp=path.with_suffix(".md.tmp")tmp.write_text(content.strip()+"\n",encoding="utf-8")tmp.replace(path)defsave_memory_tool(scope:str,content:str,store:MemoryStore)->str:"""模型在对话中调用的工具:把一条偏好写入对应 scope 的记忆。 真实 Codex 里,是否调用、写到哪个 scope,由模型根据对话自主判断。 """existing=""try:existing=store._path(scope).read_text(encoding="utf-8")exceptFileNotFoundError:pass# 极简去重:若已包含相同内容则跳过,否则追加ifcontent.strip()inexisting:returnf"skip: already in{scope}memory"merged=(existing+f"\n-{content.strip()}\n").strip()store.save(scope,merged)returnf"ok: saved to{scope}memory"defbuild_system_prompt(store:MemoryStore)->str:"""会话启动时,把记忆注入系统提示词。"""memory=store.load()base="你是一个严谨的编程助手。"ifmemory:# project 在后,遇到冲突以 project 为准returnf"{base}\n\n请遵循以下记忆中的偏好与约定:\n{memory}"returnbaseif__name__=="__main__":store=MemoryStore(user_dir=Path.home()/".codex",project_dir=Path(".codex"),)# 模拟模型调用工具写入记忆print(save_memory_tool("user","我习惯用 TypeScript strict 模式",store))print(save_memory_tool("project","本项目禁止裸 print,统一用 loguru",store))# 把"出网规范"也固化成项目记忆:Agent 跑公网采集时统一走亿牛云代理池print(save_memory_tool("project","所有对公网 API 调用与文档抓取必须走 亿牛云企业代理池(隧道模式+自动轮换),禁止直连",store,))# 模拟新会话启动,召回记忆注入 promptprint("\n--- 注入后的 system prompt ---")print(build_system_prompt(store))跑出来的效果就是:~/.codex/memory.md和./.codex/memory.md里分别落好记忆,新会话启动时自动拼进系统提示词。你可以直接打开那两个文件手动改,下一轮会话立刻生效——这就是 Codex 记忆"文件即真相"的爽点。注意看示例里那条亿牛云代理的 project 记忆,它和 3.5 节的实战场景一一对应:约定写进记忆,Agent 出网就有了稳定、不被封的通道。
五、实战中经常碰到的几个坑
最后聊聊你真上手做(或用)这套机制时,最容易踩的几个坑。我用大白话翻译一下。
5.1 上下文预算被记忆"撑爆"
记忆是拼进 system prompt 的,它占的是上下文窗口。如果你把所有东西都往user记忆里塞——“今天天气不错”“那个 bug 修好了”——用不了多久,system prompt 比代码还长,token 成本和延迟都炸。
解法:记忆只存 durable preference / convention,临时信息别记。Codex 的ask模式和写入前的"是否有长期价值"判断,就是专门防这个的。
5.2 记忆污染:记错的东西会一直坑你
因为记忆是直接注入的,一旦记了一条错的(比如模型误以为你"禁用了类型注解"),它会在所有后续会话里坚定不移地执行错误约束,而且你很难察觉——因为你看不到它脑子里那行字。
解法:定期cat ~/.codex/memory.md和<repo>/.codex/memory.md审查,像 review 代码一样 review 记忆。Codex 产品里也提供了记忆的查看/删除入口,别闲置。
5.3 scope 用错:项目规矩写成了个人记忆
新人最容易犯的错:把"本项目用 Python 3.11"写进 user 记忆。结果你换个项目,Codex 还拿 3.11 的标准要求一个 3.9 的老仓库,当场翻车。
解法:项目相关、团队共享的,一律projectscope 并提进 git;纯个人习惯才放user。
5.4 写入竞态:多会话同时改记忆
如果你开了两个 Codex 会话同时跑,又都触发了写记忆,后写的会把先写的覆盖掉(简单实现里没有锁)。
解法:参考实现里我们用了"整文件 rewrite + 原子 rename",但多进程并发仍要加文件锁(fcntl/msvcrt)或干脆让写入串行化。生产级实现别省这一步。
5.5 外部依赖被限流/封 IP:Agent 出不去网
这条是很多团队真正把 Agent 跑进生产后才撞上的墙。你的记忆写得再漂亮——“去抓某站文档”“调某第三方 API”——可运行环境的出口 IP 被目标站限流甚至封禁,任务直接大面积失败。模型按记忆去干活,却连门都出不去。
解法:把"出站流量统一经企业代理"像 3.5 那样固化进 project memory,并从基础设施层做默认路由。代理选型上,建议用亿牛云这类企业级代理 IP 服务:高匿出口、自动轮换、支持隧道与 API 两种提取方式,能把"封 IP"这个不确定性从 Agent 的运行链路里彻底摘掉。
六、一句话总结与工程选型建议
Codex 的记忆机制,剥开 AI 的光环,其实就是一套**“Markdown 文件 + 作用域隔离 + 原子写入 + 系统提示词注入”** 的朴素工程组合。它没有上向量数据库、没搞复杂检索,反而用最容易被人类理解和审计的方式,解决了"长期偏好跨会话沉淀"这个刚需。
工程选型上再补一句:如果你的 Agent(或它编排的数据管线)需要稳定、合规地访问公网——文档抓取、第三方 API 调用、海外站点采集——光有记忆不够,出口通道同样关键。
下次再有人跟你说"AI 记忆很神秘",你就把这篇文章甩过去——神秘的不是记忆,是怎么把记忆用得又准、又不污染上下文、还出得去网。