为Codex打造跨会话长期记忆:OpenViking设计与实战

发布时间:2026/9/28 19:07:51
为Codex打造跨会话长期记忆:OpenViking设计与实战
大概每一个用过 Codex 写代码的人都经历过那种“明明上次已经聊过的内容这次还得重新解释一遍”的崩溃瞬间。代码改到一半新起一个会话Codex 就像失忆了一样连我们上午刚定下的命名规范、目录结构、依赖版本都忘得干干净净。这个痛点不是一个 “复制粘贴上下文” 能解决的它背后是 Agent 体系里一个真正的短板没有长期记忆。所以我做了 OpenViking一个专门给 Codex 补上“跨会话记忆”的本地工具。这篇文章不聊虚的我会把 Codex 为什么健忘、Agent 记忆到底该怎么分层、OpenViking 的设计思路以及怎么接入 Codex 的完整实操过程全部写一遍所有步骤都给到你。如果你正在用 Codex CLI 或桌面版又被它的“金鱼记忆”折磨过或者你本身在做 Agent 类应用想知道短期、长期、永久记忆分别怎么落地那么这篇文章应该能直接帮你省掉几个晚上的折腾时间。我会按自己的实践来写也会把踩过的一些坑放在最后。1. 为什么 Codex 需要长期记忆1.1 Codex 默认状态下有多健忘先说结论Codex 默认不是“没有记忆”而是它的记忆被限定在了单次会话里。每次新开一个对话它拿到的历史信息大概率只有当前窗口里可见的内容旧会话里的关键结论、代码决策、用户偏好全都不可见。这种设计在纯聊天的场景里问题不大但放在写代码这件事上就非常致命。写代码是一个强状态过程你上午定了数据库表结构下午让它写接口它如果不知道表结构就可能把字段名写错你周一确定了“所有错误码统一用业务前缀”周三让它写新模块它可能又按自己的习惯生成一套新风格。我自己的体感是Codex 单次会话内非常强一旦跨会话退化程度令人崩溃。你可以在新会话里反复贴以前的摘要来“喂”它但这样有两个问题。第一摘要本身会丢失细节写代码最怕的就是细节丢失。第二上下文窗口不是无限的当你把历史摘要、代码片段、当前需求全部塞进窗口很快就会被截断截断后的 Codex 会选择性遗忘而且你根本不知道它忘了哪部分。所以我们需要的不只是“把上一轮对话文本存下来”而是要一个真正能按需检索、自动沉淀、可以在新会话里随时被 Codex 调用的长期记忆系统。OpenViking 就是为了补这个空缺才做的。1.2 Agent 记忆的分级模型在讲 OpenViking 之前有必要把 Agent 记忆的分层问题讲清楚。现在做 Agent 的人基本都认同记忆可以分成三层短期记忆、长期记忆和永久记忆。这三层的边界虽然各家定义不完全一样但核心思路是共通的。短期记忆也叫工作记忆是 Agent 在当前会话里能直接看到的内容。它就在上下文窗口里相当于你桌面上的草稿纸所有正在处理的信息都摊在这里。但它有容量限制关掉会话就不存在了。长期记忆是跨会话保存的、和具体项目或任务相关的知识。比如“这个项目用 Rust 写核心库”“用户上次说不要用 Lombok”“支付模块还没有做幂等”。这些信息需要被结构化保存并且在合适的时候重新回到上下文里。永久记忆则是更高一层的东西通常和用户本身强相关比如“用户习惯用四个空格缩进”“用户希望每个函数都有文档注释”“用户所在时区、常用工具链”。它不依赖具体项目换了项目也依然有效。很多人第一次做记忆功能的时候会掉进一个陷阱把所有历史消息不分青红皂白全部存下来然后一股脑塞进提示词。这既不经济也不可靠。正确的做法是分层短期记忆负责实时响应长期记忆负责项目沉淀永久记忆负责用户偏好。OpenViking 主要做长期记忆这一层同时把一部分永久记忆的兜底也接了进来后面我会具体讲。2. OpenViking 记忆机制的整体设计2.1 记忆存储选型为什么不用一张大文本在最开始设计的时候我考虑过最简单的方案把记忆写进一个AGENTS.md或者MEMORY.md文件每次会话开始前让 Codex 读一遍。这样做确实能实现“跨会话可读”但它有两个硬伤。第一个硬伤是上下文污染。项目跑了三个月之后记忆文件里可能有几百条内容如果全部读进上下文几千甚至上万个 token 就没了。Codex 的可利用上下文是有限的你拿来读记忆就少了空间去放代码和任务上下文。第二个硬伤是检索困难。你想让 Codex 只“想起”和当前任务相关的记忆比如这次只和数据库迁移有关就别把前端样式那些历史记录塞给它。纯文本文件没法做细粒度检索除非你自己去正则匹配但那样很脆弱。所以 OpenViking 的存储我选了两层结构底层用 SQLite 存记忆条目每条记忆是结构化的包含内容、类型、标签、时间戳、来源会话、命中计数等字段上层按需加载对外只暴露“写入”和“检索”两类能力。如果项目需要向量检索我再在 SQLite 的基础上挂一层向量索引。这样既保留了本地化的简单可靠又具备了真正的检索能力。为什么不直接上云端的向量数据库原因很简单记忆是高度私密的项目数据我不希望每次 Codex 回忆点东西都要把内容传到外部服务而且本地跑一个轻量级向量索引对普通项目来说性能完全够用还省了 API 费用和网络依赖。2.2 记忆要如何被 Codex 读到存储只是第一步关键是 Codex 怎么把记忆“想起来”。我试过三种接入方式简单对比一下。第一种是启动提示词注入。在每次会话开始前由 OpenViking 生成一段“历史记忆摘要”通过 Codex 的配置指令注入进去。好处是零额外工具调用Codex 天然就会带上坏处是摘要依然是“提前生成”的如果摘要生成时没覆盖到当前问题Codex 还是想不起来。第二种是直接当 MCP 工具给 Codex 调用。MCP 是现在 Agent 接入外部能力的主流协议Codex 对 MCP 的支持已经比较好了。OpenViking 对外暴露两个核心工具search_memory和save_memory。Codex 在对话中如果判断需要历史信息就会自己去调用search_memory把检索结果读进上下文如果聊出了新的重要结论也会自己调用save_memory存下来。这是我最推荐的方案也是我最终实现的方案。第三种是在项目里维护一个精简版的AGENTS.md只放最高频的几条项目事实比如“目录结构见 docs/project-structure.md”“所有命令通过 Makefile 进入”。OpenViking 可以定期自动整理这个文件而不是直接管理一个大而全的记忆库。最终我选的是以第二种为主、第三种为辅。MCP 工具的好处是“按需取用”不需要的时候完全不占上下文需要的时候 Codex 才会主动去查。这比无脑注入摘要聪明得多也更接近人类“回忆”这个过程。2.3 记忆内容与粒度控制有了存储和读取的机制还有一个关键问题到底该记什么OpenViking 的记忆粒度是我踩坑最多的地方。最开始我恨不得把 Codex 说的每句话都存下来结果是检索出来的东西全是噪音。后来我整理了一套记录规则。值得记的东西包括项目结构和高层模块划分、技术栈选型及理由、已经拍板的设计决策、用户明确说过的偏好、常见的坑和规避方式、尚未完成的任务及其上下文。不值得记的东西包括临时变量的命名、一次性调试步骤、具体的报错日志全文、敏感凭据和密钥、会话里其他无关闲聊。记忆条目必须原子化一条只讲一件事。如果一条记忆里既写了“后端用 FastAPI”又写“用户喜欢 RESTful 风格”那检索的时候这两件事就会互相污染。我会把这类内容拆成两条分别打标签这样搜索“RESTful”的时候不会把 FastAPI 那条无关细节也带出来。另外条目必须有来源和过期时间的概念。来源是哪个会话、哪个时间点过期时间是这条信息大概率在多长时间内还有效。比如“目前使用 Node 20”可以设置一年后失效而“用户要求所有接口返回统一格式”就是永久记忆不需要过期。3. 实操把 OpenViking 接入 Codex3.1 准备环境与安装下面进入正题。我用的是一个本地 Python 服务加 MCP 协议的方式整个过程大概分三步装 OpenViking、启动记忆服务、在 Codex 里注册这个 MCP Server。先准备环境。建议用 Python 3.10 以上版本创建一个干净的虚拟环境避免依赖冲突。安装本身可以直接通过 pip 来做。如果 OpenViking 发布到了 PyPI命令就是python -m venv .venv source .venv/bin/activate pip install openviking装好之后先初始化一个记忆目录。我习惯把记忆库放在项目根目录下的.openviking/里这样能跟着项目走换机器也能直接拷贝。初始化命令openviking init --path .openviking openviking statusstatus会显示当前记忆库的路径、条目数量和存储状态。如果这里能正常输出说明基础环境没问题。如果你不想用虚拟环境也可以直接用pip install --user安装但我不推荐。因为后面的 MCP Server 进程需要长期运行虚拟环境能帮你把依赖隔离好省得以后升级别的包把 OpenViking 搞挂了。3.2 配置 Codex 连接 MCP ServerOpenViking 本质上是一个本地 MCP Server所以接入 Codex 的步骤就是注册一个 MCP。Codex 的配置目录在当前用户的~/.codex/下主要配置文件是config.toml。Codex 支持通过命令行直接添加 MCP也支持手改配置。我推荐先用命令行添加让 Codex 帮你写对格式。命令大概是这样codex mcp add openviking \ --stdio \ --command python \ --args /path/to/your/openviking_server.py如果你用的 Codex 版本还不支持codex mcp add那可以直接编辑~/.codex/config.toml在文件里加一段 MCP 配置。大致形式如下[mcp_servers.openviking] command python args [/path/to/your/openviking_server.py] env { OPENVIKING_HOME /path/to/your/.openviking }配置好之后先确认 Codex 能看到这个服务运行codex mcp list如果输出里有openviking并且状态正常说明 Codex 已经识别到了。这个时候你在一个 Codex 会话里直接问一句“你有哪些记忆工具可以用”它应该能回答出search_memory和save_memory这两个工具。这里有一个容易踩的坑路径写错或者 Python 环境不对会导致 MCP Server 启动失败。Codex 启动 MCP Server 时用的python命令必须是你装了 OpenViking 的那个 Python。所以我前面才建议用虚拟环境然后在配置里把command写成虚拟环境里的 Python 绝对路径比如.venv/bin/python而不是裸写python。3.3 记忆写入与检索的真实调用OpenViking 的 MCP Server 核心逻辑其实不复杂无非是两个工具save_memory和search_memory。我给你看一下我这里的实现骨架你可以照着理解。先看服务端核心代码使用 FastMCP 的话大概是这个样子from fastmcp import FastMCP import sqlite3 mcp FastMCP(openviking) MEMORY_DB /path/to/.openviking/memory.db mcp.tool() def save_memory(content: str, tags: str , entry_type: str project_fact) - str: 存入一条长期记忆。content 是记忆内容本身tags 是逗号分隔的标签 entry_type 可选 project_fact/preference/decision/lesson。 conn sqlite3.connect(MEMORY_DB) conn.execute( INSERT INTO memories (content, tags, entry_type, created_at, hit_count) VALUES (?,?,?,datetime(now),0), (content, tags, entry_type), ) conn.commit() conn.close() return memory saved mcp.tool() def search_memory(query: str, limit: int 5) - str: 根据 query 在长期记忆中检索相关条目返回 markdown 格式的历史记忆列表。 limit 控制在 1-10 之间默认返回 5 条。 conn sqlite3.connect(MEMORY_DB) rows conn.execute( SELECT content, tags, entry_type, hit_count FROM memories WHERE content LIKE ? ORDER BY hit_count DESC LIMIT ?, (f%{query}%, limit), ).fetchall() conn.close() if not rows: return 没有找到相关历史记忆 return \n.join(f- [{entry_type}] {content} (命中 {hit_count} 次) for content, tags, entry_type, hit_count in rows)实际项目里search_memory不会用简单的LIKE查询我会在 SQLite 里挂一个向量索引对 query 做嵌入向量相似度检索。但原理是一样的Codex 调用这个工具传入一句话它返回几条最相关的历史记忆。Codex 判断要不要调用工具靠的是工具描述。所以工具描述一定要写得足够清楚。search_memory的描述里我专门写了一句当用户提到“上次”“之前的决定”“我们说过”“你忘了”这类表述时应该主动检索历史记忆。如果不这么写Codex 经常不会主动调它会选择“假装记得”。保存逻辑也是类似。save_memory的描述里我写了当用户明确表达一个新决定、给出偏好或者总结出一条可复用的经验时应该调用这个工具。平时你不不需要管它Codex 自己会判断。如果你想手动测试也可以直接在对话里说“记住这个项目的测试环境域名是 test.internal.example.com”它就会去调用save_memory。对于不想依赖 Codex 自动判断的用户还有一个更粗暴的办法在AGENTS.md里写一句“每次会话开始时请先调用 search_memory 检索与本任务相关的历史记忆”。这样 Codex 就会在开场阶段主动去查。我实测下来这种方式最稳但会稍微增加每次会话的启动时间。4. 记忆的管理与维护避坑指南4.1 从短期到长久的晋升机制很多人以为长期记忆功能装上就完事了其实这才刚刚开始。记忆库如果没人维护三个月后就会变成垃圾堆。所以我给 OpenViking 设计了一套“记忆晋升”机制。短期记忆先进入 staging 表不直接进入长期记忆库。比如 Codex 在会话里可能为了调试记录了一堆临时状态这些不应该污染长期记忆。什么时候晋升两个条件一是有明确的结构化字段比如decision或preference类型二是被查询命中过多次说明它确实有复用价值。命中次数越高优先级越高。我自己的规则很简单save_memory保存的内容默认进入长期库但临时调试记录会通过一个expires_at字段标记为短期24 小时后自动降级。条目如果长期没有被检索命中后台会定期把它标记为“低频记忆”压缩到归档区不再参与常规检索。这样既保证了高频信息快速可用又不用担心存储无限膨胀。4.2 如何避免记忆污染记忆污染是我遇到的最头疼的问题。Codex 有时候会把一次性的错误信息当成项目事实存下来比如“这个接口报 401 错误”。如果这条记忆被检索到新会话里的 Codex 就会被误导白白去排查一个已经解决的问题。为了对抗污染我做了两件事。第一save_memory工具的参数里必须带entry_typeCodex 在保存时要判断这是“一条持久的项目事实”还是“一个临时状态”。如果 Codex 拿不准类型我会要求它先问用户而不是自作主张。第二所有记忆条目支持手动删除和修正而且整个记忆库会纳入 Git 管理。我会在.openviking/目录里初始化一个 Git 仓库每次记忆批量变更后提交一个 commit。这样万一发现某条记忆是错的我可以直接回滚到之前的版本。另一个实用技巧是设白名单路径。有些记忆内容会带文件路径比如“xx模块的代码在 src/utils/helper.py”。如果这条路径是临时生成的或者已经被重命名新会话再用就会踩坑。所以在保存记忆的时候OpenViking 会检查内容里的路径是否存在于当前项目里如果路径不存在就提示 Codex 确认后再保存。这个小机制帮我挡掉了至少一半的过期信息。4.3 上下文窗口与性能优化接入 MCP 工具之后还需要注意性能。search_memory虽然按需调用但检索结果一多还是会吃掉不少上下文。我建议把默认返回条数控制在 3 到 5 条之间每条 200 字以内。如果检索出来一堆无关结果宁可少也不要多。Codex 读太多无关记忆会分散注意力甚至被带偏。向量检索的阈值也很关键。我在 OpenViking 里默认只返回相似度大于等于 0.35 的结果如果query和所有记忆条目的相似度都低于这个阈值就返回“没有相关历史记忆”。这个阈值不能定太高否则很多其实有用的记忆被过滤掉也不能定太低否则随便搜索一句都会被匹配出一堆不相干内容。0.35 是我自己在几个不同项目上试出来的一个平衡点你可以根据项目调。还有启动时的加载逻辑。OpenViking 不是每次都要把所有记忆条目扫描一遍那样太慢。实际的做法是在服务启动时只加载索引元信息真正的条目内容等search_memory被调用时才去读取。这样即使记忆库有几万条数据Codex 的启动速度也不会受影响。5. 常见问题与排查实录5.1 Codex 好像完全不调用记忆工具这是最常遇到的问题。你在 Codex 里问“还记得上次说的那个接口规范吗”它直接说“我不记得”完全不理会search_memory。遇到这种情况先别急着改代码按顺序排查。第一确认 MCP Server 真的活着。运行codex mcp list如果显示 openviking 异常说明服务没起来。第二确认工具描述是否足够明显。Codex 是否调用工具很大程度取决于工具定义里的提示词。我建议在search_memory的描述里直接写明“当用户试图唤起跨会话记忆时必须调用此工具”。第三如果还是不行就在config.toml里给 Codex 加一段全局 instruction例如[instructions] 使用记忆工具 当用户提到之前做过的事情时调用 openviking 的 search_memory 工具不要凭空猜测。这样相当于给 Codex 下了一个硬性指令它不调用都不行。5.2 检索出来的记忆质量很差如果你发现 Codex 调用了search_memory但返回的结果和当前问题关系不大那大概率是检索逻辑太原始或者记忆条目太大。我用LIKE查询的阶段经常搜“数据库”出来一堆包含“数据库”这个词但毫无关联的内容。换向量检索是正解。让每条记忆先做向量化查询的时候用相似度排序能够把“语义相关但措辞不同”的信息找出来。如果你不想引入额外依赖至少也要做分词加倒排索引不要直接LIKE。另外检查一下记忆条目是否保持了原子性。如果一条记忆又臭又长包含三四个主题那么不管用什么检索方式精度都上不去。还有一个容易被忽略的点Codex 传给search_memory的 query 不一定是你用户的原话它可能会自己改写。我在日志里就见过用户说“上次那个报错怎么解决的”Codex 调用工具时传入的 query 是“某接口报错 401 的解决方式”。这其实是好事但前提是你的记忆库里有这条内容的原文。所以保存记忆时关键词和标签要尽量覆盖可能被检索到的说法。5.3 常见报错和配置问题速查在实际接入过程中你可能会碰到一些 Codex 自身的报错。这里我整理几个我碰到过、也验证过解决方案的情况。codex is ignoring 1 unrecognized configuration setting. check for typos or d...这个报错通常是config.toml里写了当前 Codex 版本不认的配置项。比如把某个字段名拼错或者版本升级后旧字段被移除了。解决办法就是检查报错里提到的那个配置项把它删掉或改成新版的字段名。不要无视它因为被忽略的配置项很可能是你期望生效的记忆相关设置。codex auth token is unavailable这个意思是登录态失效或者没有正确的凭证。解决办法是重新进行 Codex 的登录流程。不要把 token 硬编码进配置文件容易失效也容易泄漏。注意这不是 OpenViking 的锅但如果你配置了 MCP 环境变量要确认没有把登录凭证误传进子进程Codex 不会通过 MCP 环境变量去拿认证信息。还有一个和模型选择有关的报错the gpt-5.6-sol model is not supported when using codex with a chatgpt acc。这个翻译过来就是你用 ChatGPT 账号登录的时候Codex 不支持你配置的这个模型。解决方式是在config.toml里把模型改成你账号可用的型号或者换用支持的登录方式。这个错误和 OpenViking 无关但会阻断整个会话所以也值得排查一下。我自己在实际项目中把 OpenViking 跑了一个多月最大的体会是给 Agent 加记忆最难的不是写代码而是“记什么”和“忘什么”的平衡。记太多了检索噪音大到没法用记太少了又跟没记一样。OpenViking 现在还在不断迭代最近的计划是支持多项目共享的永久记忆层把用户偏好从具体项目里抽离出来。如果你也在折腾 Codex 的记忆方案欢迎留言交流坑这种东西早踩早超生。