Agent记忆持久化实战:Codex硬接TencentDB的冲突与架构拆解
上个月我们组在给内部编码助手接 TencentDB、做 Agent Memory 持久化时遇到了一个特别尴尬的局面Codex 这类现成的编码智能体自带一套完整的记忆机制外部记忆系统一接入两边立刻开打。这种硬冲突光靠调 prompt 根本解不掉我花了两天把 Codex 源码翻了一遍才把摩擦点一个个看清楚。这篇文章就是这次架构梳理的完整记录从 Agent Memory 的分层讲起结合 TencentDB 做记忆底座的方案拆解 Codex 接入时的硬冲突最后给出一套可落地的接入参考。正在做 Agent 记忆系统或者想把现成编码 Agent 接进自己知识库的开发者应该能从里面找到点有用的东西。1. 先把概念对齐TencentDB、Agent Memory 与 Codex 的边界1.1 Agent Memory 不是“聊天记录”是角色的工作台很多人提到 Agent Memory第一反应就是“把对话历史存下来”。这个理解太浅了。对话历史只是记忆系统里最小的一块真正值得做的记忆分三层工作记忆working memory、情景记忆episodic memory、语义记忆semantic memory。工作记忆对应当前任务里的临时状态比如“正在改 payment 模块文件 A 已重构文件 B 还没动”情景记忆对应过去发生过的具体事件比如“上周二上线时因为漏了 Redis 连接池参数导致报警”语义记忆是提炼后的长期知识比如“这个项目用 Rust 写性能敏感路径别用 Python 重写”。这三层合起来才叫 Agent Memory。如果只存聊天记录那充其量是日志系统真正的记忆系统要做归纳、检索、更新和维护失效。这也是为什么我第一时间想到的是数据库而不是缓存Redis 做工作记忆没问题但情景记忆和语义记忆需要结构化存储和向量检索TencentDB 这种关系型底座加扩展能力的组合更合适。你甚至可以理解成工作记忆是记事本情景记忆是流水账语义记忆是总结成文的经验手册三者的读写频率和生命周期完全不同。1.2 TencentDB 不是一个库是一族存储能力TencentDB 在腾讯云体系里其实是一个产品家族MySQL、PostgreSQL、TDSQL 都在这个品牌下面。做 Agent Memory 时我不需要把鸡蛋放在一个篮子里而是按记忆类型选择存储形态。工作记忆用 MySQL 的高性能表就行记录 agent_id、task_id、key、value、expire_at查询走主键写入走事务语义记忆需要向量检索可以把 embedding 字段存在行里在应用层做相似度计算也可以直接接向量检索能力具体看数据量级。用 TencentDB 做记忆底座的核心优势是事务和审计能力比纯 KV 存储强得多。记忆写入不能丢读取不能脏尤其当多个 Agent 实例并发处理同一个任务时行锁和事务隔离能保证不会出现两个人同时改同一段记忆、最后互相覆盖的情况。这些能力是 Redis 和文件系统都给不了的。我见过不少团队用 JSON 文件直接当长期记忆结果就是一个 Agent 写完另一个 Agent 读到的全是错乱的半行数据改起来欲仙欲死。1.3 Codex 的定位自带记忆机制的编码智能体Codex 这里说的不是单纯的模型而是 Codex CLI / Codex Agent 这类编码智能体形态。它们天然具备一套记忆机制会话历史、系统提示、上下文文件、工具调用结果回流。也就是说Codex 不是“裸模型”它本身就在工作记忆层面做了大量事情。问题恰恰在这里。很多人以为自己只需要“把 Codex 的对话记录同步到 TencentDB”结果发现 Codex 根本不会把内存里的上下文交出来——它的上下文是私有的在进程内部维护。你只能通过外部协议比如工具调用、文件系统去影响它。这就带来了一连串硬冲突。所以读源码之前必须先搞清楚 Codex 的记忆到底由哪些模块构成、在什么时机被读写不然一切设计都是盲人摸象。我一开始也犯了这个错直接设计表结构结果发现接入点都没找对后面全部推翻重来。2. 读源码前的心智模型Codex 的记忆由哪些模块构成2.1 系统提示与上下文窗口记忆的第一战场任何一个编码 Agent 的推理都发生在上下文窗口里窗口就是它的“短期记忆力”。Codex 在启动时会把系统提示、仓库结构、AGENTS.md 等项目文件塞进上下文这些内容占据 token 预算。外部记忆系统如果要把检索结果插进来同样要消耗 token。矛盾就在这里上下文窗口是硬上限外部记忆占得越多模型自己看到的代码就越少。尤其做编码任务时代码片段本身就很大一个仓库的文件列表可能就有几千 token再加外部记忆窗口很快见底。读源码时我特别留意了系统提示是怎么组装的。通常会有专门的模块把 system 内容拼接好包括工具定义、输出规范、安全约束。这个拼接过程是“私有的”外部系统改不动。如果你想让 Agent 知道一条长期记忆只能通过工具调用的方式“告诉”它而不能直接往系统提示里塞。这是很多团队踩坑的地方试图改 Codex 的提示文件注入记忆升级一次就失效因为那个文件是安装包的一部分不是给用户改的。2.2 会话历史结构化事件流而不是纯文本Codex 的会话历史组织方式非常值得学习。它不是把每轮对话存成一段字符串而是按事件event的方式记录user message、assistant message、tool call、tool result每个事件带时间戳和元数据。这个设计的好处是回放能力极强Agent 可以精确知道“哪次工具调用返回了什么”。但对外部记忆系统来说事件流反而是难点。TencentDB 里存的是结构化的行agent_id、memory_key、content、vector、created_at而 Codex 的历史是事件队列两者之间没有天然映射。想把 Codex 的历史转成可检索的长期记忆必须做事件归约event reduction把多个工具调用和消息聚合成一条“任务结果”再提炼成记忆条目。这个聚合逻辑如果写得不好记忆库很快会被噪音淹没。我见过有人直接把原始事件全量导进库检索时永远捞到一堆碎片Agent 看了比不看还懵。2.3 文件级记忆AGENTS.md 与其上下文注入Codex 这类工具通常支持从项目文件里读取“给 Agent 的说明”比如 AGENTS.md里面可以写项目约定、命令用法、易错点。这是非常轻量的长期记忆它以文件形式存在于仓库里每次会话启动时被自动注入上下文。这种机制好看但有限。它能表达的知识是“静态显式”的比如“构建命令是 make build测试命令是 make test”但表达不了“动态隐式”的知识比如“根据上周的监控数据payment 模块在高峰期有连接池瓶颈”。后者需要从历史中归纳这是 TencentDB 加检索要做的事不是 AGENTS.md 能替代的。所以我的建议是AGENTS.md 管工程约定TencentDB 管业务与运行记忆两者互补不要互相替代。如果你把动态决策硬塞进 AGENTS.md文件会越来越臃肿最后整个上下文全是废料。2.4 工具调用的结果回流记忆写入的真实时机真正能让外部记忆系统“趁虚而入”的是 Codex 的工具调用协议。Agent 在推理过程中会调用工具工具返回结果后结果会被放回上下文继续推理。如果我们把“记忆查询”“记忆写入”封装成工具那外部记忆系统就能以工具的形式接入 Codex 的推理循环。这是目前最干净的接法没有之一。读源码时要关注的就是工具定义的 schema 以及工具执行后的结果如何被格式化。Codex 对工具结果往往有长度限制比如只取前 N 字符超过部分截断。这就意味着从 TencentDB 检索回来的记忆如果太长会被截断Agent 只能看到一部分。所以检索结果一定要做摘要化、压缩化最好是 300 字以内的结构化要点而不是长篇原文。我第一次接入时就是把整段历史决策丢回去结果 Agent 只看到前半段后半段直接被截断相当于记了个残缺的记忆。3. 硬冲突现场拆解Codex 接外部记忆的四个摩擦点3.1 令牌预算争夺外部记忆挤占原生上下文第一个硬冲突就是 token 预算。我实测下来一个中型仓库扫描完文件结构系统提示加项目上下文可能已经占用 8k 到 12k token。再加上对话历史和工具结果一个 128k 上下文的模型实际可用空间并不宽裕。接入外部记忆后如果每条检索结果都完整塞入一次查询召回 5 条、每条 500 token就是 2500 token 的额外开销。而且最要命的是编码任务的核心是代码本身代码 token 是刚需不能被记忆压缩。解决办法不是减少记忆而是控制记忆的“形态”。检索返回的不是原文而是结论每条记忆带来源、时间、置信度让 Agent 自行决定是否采信召回数量上限控制在 3 到 5 条提供“展开详情”的二次工具只有 Agent 觉得重要时才拉全文。这样就把记忆对 token 的侵占降到最低。我在代码里给每条记忆加了 summary 字段Agent 默认只看到 summary只有明确调用 detail 工具才能看到 content效果立竿见影。3.2 数据模型错位事件流与行存储 / 向量存储之间缺一座桥第二个冲突在数据层。Codex 内部是事件流TencentDB 是行加向量。想同步就得有一个转换层。很多人忽略这个转换层的复杂度一条历史消息要经历清洗、去重、摘要、向量化、归并才能成为一条合格记忆。直接原样存库等于把日志变成了“记忆”检索的时候什么都能捞出来但什么都不是重点。我在项目里做了一个 memory_ingest 管道原始事件进原始表定时任务做聚合只把“结论性信息”写入长期记忆表。这个管道的核心指标是信息密度一条记忆必须能让 Agent 在 20 字内说出它解决了什么。如果做不到就说明归约得不够狠。比如原始工具返回了 500 行日志归约后应该变成“超时原因是 Redis 连接池耗尽已调大 maxTotal 到 50”而不是把 500 行日志原封不动存进去。3.3 写入时机冲突每次工具调用都写库一致性怎么保第三个冲突是性能。Codex 在推理过程中会连续调用多次工具如果每次工具执行都同步写 TencentDB很快会遇到两个问题一是写入延迟拖慢推理二是高并发下的行锁竞争。尤其多个任务同时跑时同一个 agent_id 的写入会频繁冲突。我最后的方案是两层缓冲内存里有个 memory_buffer攒够 N 条或者超过 T 秒再批量刷库刷库时用事务保证一批要么全成功要么全失败。长期记忆的写入异步化短期记忆工作记忆仍可以同步写因为工作记忆要保证实时性。分而治之之后实测写入压力下降了 80% 以上。我们压测时 20 个 Agent 并发写同一任务域同步写模式下事务冲突率接近 15%改异步批量后降到了 1% 以内。3.4 检索插入的上下文污染向量召回打断代码任务的序列性第四个冲突比较隐蔽是语义层面的。编码任务天然是序列化的改完 A 文件再看 B 文件推理链是一步一步的。而向量检索的召回是跳跃的它可能召回历史上关于 C 模块的决策而这个决策跟当前任务毫无关系一旦插入上下文反而干扰模型注意力。这就是上下文污染。我的做法是给检索加条件过滤。Codex 在调用记忆工具时必须带上当前任务域模块名、关键词TencentDB 端用 SQL 先按 domain 过滤再做向量相似度排序。召回结果带 section 字段Agent 会忽略与当前模块不相关的记忆。这一步看起来简单实际对效果提升非常明显。之前不设过滤时检索 payment 超时问题经常把 auth 模块的历史决策捞进来模型看完直接跑偏加了 domain 过滤之后这种问题基本绝迹。4. TencentDB 侧的记忆底座设计表结构、向量检索与版本控制4.1 工作记忆与长期记忆的分表设计我在 TencentDB 里建了四张核心表分别对应工作记忆、情景记忆、语义记忆、记忆索引。工作记忆表是宽表结构主键是 agent_id、task_id、keyvalue 存 JSON支持 expire_at 做自动过期情景记忆表记录事件包括 task_id、action、result_summary、occurred_at语义记忆表是核心字段包含 domain、content、summary、embedding、confidence、created_at、source_ref。四张表分开的好处是生命周期不同工作记忆可以随时删情景记忆保留 30 天语义记忆原则上不删。表结构示例如下用 MySQL 语法示意CREATE TABLE memory_working ( agent_id VARCHAR(64) NOT NULL, task_id VARCHAR(64) NOT NULL, memory_key VARCHAR(128) NOT NULL, memory_value JSON NOT NULL, expire_at DATETIME NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY(agent_id, task_id, memory_key), KEY idx_expire(expire_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE memory_semantic ( id BIGINT AUTO_INCREMENT PRIMARY KEY, domain VARCHAR(128) NOT NULL, content TEXT NOT NULL, summary VARCHAR(512) NOT NULL, embedding BLOB COMMENT vector representation, confidence FLOAT DEFAULT 1.0, source_ref VARCHAR(256) DEFAULT , created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_domain(domain), KEY idx_created(created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;embedding 字段用 BLOB 存序列化后的向量数据量小的时候在应用层做向量运算完全够用。如果数据量过了十万条一定上专门的向量索引全表扫 cosine 会慢到无法接受。我前期就是全表扫两万条数据时单次检索已经在百毫秒级了再翻几倍肯定扛不住。4.2 embedding 生成与检索参数选择把记忆变成向量的模型选择很关键。我试过通用 embedding 模型对代码相关的记忆效果马马虎虎后来加上领域词表把项目里的命名规范、模块名加进去效果明显提升。检索时建议用 cosine 相似度而不是欧氏距离因为记忆文本长度差异大余弦对长度不那么敏感不容易被长文本带偏。检索参数上我常用的组合是 top_k8再经过重排取 top_3。召回范围先按 domain 过滤如果 domain 匹配的结果不足 3 条再放宽到全部向量空间补充。score 阈值一般设在 0.75 以上低于这个值召回的基本是噪音。这套参数要从自己的数据里调别人的经验只能当起点。我们最初照搬开源项目的默认阈值 0.5召回了大量无关结果提到 0.75 之后才稳定。4.3 记忆写入的幂等与版本控制记忆系统最容易被忽视的是幂等。Agent 重试、工具重复调用、消息重放都会导致同一条记忆被写两次。解决方法是给每条记忆一个 dedup_key比如 source_ref 加 event_id 拼接成唯一键插入时用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE。版本控制则用 updated_at 加乐观锁读取时记录 version写入时检查 version 是否变化变了就放弃或合并。长期记忆还有一个“合并”逻辑。比如 Agent 今天记录“payment 模块性能差”明天记录“payment 模块连接池配置导致性能差”这两条应该合并而不是共存。这块可以用定时任务扫描同 domain 下的记忆用 embedding 相似度判断是否重复再由人工或规则确认合并。自动合并有风险我的经验是先标 suspected_duplicate人工确认后再真正合。刚开始我试过全自动合并结果两条含义相近但场景不同的记忆被强行合并把原本正确的上下文搞丢了后面再也不敢不审核。5. 可落地的接入方案用工具协议把 TencentDB 嫁接到 Codex5.1 整体架构思路前面分析的冲突已经有解了落地方案的核心就一句话不接管 Codex 的记忆只做外挂。Codex 自己管会话历史我们通过工具协议让它有需要时查询 TencentDB、有结论时写入 TencentDB。数据流向是Codex 推理 → 调用 memory_search 或 memory_save → 服务端读写 TencentDB → 返回结构化摘要 → Codex 继续推理。简单说Codex 是大脑TencentDB 是外置硬盘两者之间走的是“按需读取、显式写入”的协议而不是试图共享内存。5.2 工具定义与回调注入在 Codex 的工具配置里加两个函数schema 大概长这样{ name: memory_search, description: 根据主题检索项目历史记忆返回结构化摘要。返回的是摘要不是原文需要详情时再调用 memory_detail。, parameters: { type: object, properties: { query: {type: string, description: 检索主题如 payment timeout}, domain: {type: string, description: 模块域如 payment}, limit: {type: integer, default: 3} }, required: [query] } }memory_save 类似参数改成 memory_key、content、domain、source_ref。这里有个细节工具描述里一定要写明“返回的是摘要不是原文”。因为 Agent 会依据描述来决定是否调用、怎么解析结果描述写得越清楚实际表现越稳。我一开始描述写得太简短Agent 经常把检索结果当成最终答案直接输出后来加了一句话“结合当前代码上下文判断”行为立刻正常了。5.3 上下文组装策略与防冲突设计查询结果不是直接拼接丢给模型的。我做了四步处理第一步truncate每条摘要不超过 200 字第二步结构化前缀每条带来源、时间、置信度格式类似“[source_ref | 2025-01-10 | confidence 0.9]”第三步数量限制默认最多 3 条第四步重要记忆置顶直接当作“用户提示”的一段落放在对话开头让模型在推理前就知道这些约束。实际效果比我预期好。原来 Agent 经常在同一个问题上反复踩坑接上记忆后只要是之前沉淀过的决策基本不会再犯。跨会话的连续性有了之后Agent 表现得像“越用越聪明”——上周修过的 bug这周再遇到类似场景它会主动说“这个之前处理过方案是调整连接池参数”而不是重新从头排查一遍。6. 常见问题与排查技巧实录6.1 记忆丢失本地会话历史与外置记忆读写竞争遇到最诡异的问题是Codex 明明在会话中保存了记忆下一轮却“忘了”。排查后发现Codex 的本地会话历史会在特定时机覆盖外部记忆系统的状态外部写入被本地状态重置掩盖。解决办法是让外部记忆成为唯一权威源本地会话只读不回写。工具调用的记录也不要依赖 Codex 持久化每次会话启动时重新从 TencentDB 加载已沉淀的记忆。这个问题的本质是数据主权之争谁拥有记忆的最终解释权谁就能决定记忆的存续。6.2 向量召回质量差embedding 漂移与重排检索出来的东西相关性差常见两个原因一是 embedding 模型换了版本新旧向量分布不一致导致相似度失真二是召回后没有重排。我的做法embedding 版本写入每条记忆的 meta 里检索时只比较同版本向量召回 top_8 后在应用层用关键词重合度做一次重排把同时命中语义和关键词的结果排在前面。重排逻辑很简单就是给召回结果加一个关键词命中分数跟 cosine 分数做加权效果比纯向量排序稳定得多。6.3 写库风暴异步批处理与事务隔离高并发场景下大量 Agent 同时调 memory_saveTencentDB 的写入负载飙升。优化方向是两层内存 buffer 累积超过阈值后批量 INSERT写入事务用 READ COMMITTED 隔离级别降低锁竞争。另外把写操作全部异步化主线程不等待刷库结果只在响应里带一条“记忆已记录”的固定消息。这样 Agent 的推理流程不会被写库延迟卡住用户感知到的响应速度基本不变。6.4 冲突排查速查表现象可能原因排查/解决Agent 忽略检索到的记忆检索摘要太长被截断摘要压缩到 200 字内加结构化前缀记忆写入后下轮消失本地会话覆盖外部状态外部记忆设为唯一权威源检索结果大量不相关domain 过滤缺失或 embedding 版本不一致加 domain 前置过滤召回后重排写入延迟拖慢推理同步写库阻塞改异步 buffer 加批量刷库多条记忆互相矛盾版本控制缺失乐观锁加人工确认的合并任务最后再分享一点读源码的个人体会。读 Codex 这类项目不要从头到尾顺着读那样效率最低。我建议先从工具协议和系统提示组装这两块入手这两个点是外部系统唯一能影响的入口也是最容易找到接入点的地方。搞清楚这两个模块你就知道硬冲突到底硬在哪里后面再回头看表结构和检索策略会清晰很多。这次把 TencentDB 接进 Codex 之后我还在想能不能再加一层定时归纳让 Agent 每周自动把一周的问题沉淀成项目知识那就是另一个故事了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →