☰
Agent记忆持久化实战:Codex硬接TencentDB的冲突与架构拆解
2026/10/1 9:37:41 网站建设 项目流程

上个月我们组在给内部编码助手接 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、key,value 存 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

embedding 字段用 BLOB 存序列化后的向量,数据量小的时候在应用层做向量运算完全够用。如果数据量过了十万条,一定上专门的向量索引,全表扫 cosine 会慢到无法接受。我前期就是全表扫,两万条数据时单次检索已经在百毫秒级了,再翻几倍肯定扛不住。

4.2 embedding 生成与检索参数选择

把记忆变成向量的模型选择很关键。我试过通用 embedding 模型,对代码相关的记忆效果马马虎虎,后来加上领域词表(把项目里的命名规范、模块名加进去)效果明显提升。检索时建议用 cosine 相似度而不是欧氏距离,因为记忆文本长度差异大,余弦对长度不那么敏感,不容易被长文本带偏。

检索参数上,我常用的组合是 top_k=8,再经过重排取 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 嫁接到 Codex

5.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_save,TencentDB 的写入负载飙升。优化方向是两层:内存 buffer 累积,超过阈值后批量 INSERT;写入事务用 READ COMMITTED 隔离级别,降低锁竞争。另外把写操作全部异步化,主线程不等待刷库结果,只在响应里带一条“记忆已记录”的固定消息。这样 Agent 的推理流程不会被写库延迟卡住,用户感知到的响应速度基本不变。

6.4 冲突排查速查表

现象可能原因排查/解决
Agent 忽略检索到的记忆检索摘要太长被截断摘要压缩到 200 字内,加结构化前缀
记忆写入后下轮消失本地会话覆盖外部状态外部记忆设为唯一权威源
检索结果大量不相关domain 过滤缺失或 embedding 版本不一致加 domain 前置过滤,召回后重排
写入延迟拖慢推理同步写库阻塞改异步 buffer 加批量刷库
多条记忆互相矛盾版本控制缺失乐观锁加人工确认的合并任务

最后再分享一点读源码的个人体会。读 Codex 这类项目,不要从头到尾顺着读,那样效率最低。我建议先从工具协议和系统提示组装这两块入手,这两个点是外部系统唯一能影响的入口,也是最容易找到接入点的地方。搞清楚这两个模块,你就知道硬冲突到底硬在哪里,后面再回头看表结构和检索策略,会清晰很多。这次把 TencentDB 接进 Codex 之后,我还在想能不能再加一层定时归纳,让 Agent 每周自动把一周的问题沉淀成项目知识,那就是另一个故事了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询