news 2026/10/1 9:37:03

Codex长期记忆难接?MCP桥接TencentDB Agent Memory实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Codex长期记忆难接?MCP桥接TencentDB Agent Memory实战

这段时间我在做一件听起来顺理成章、做起来却很折磨人的事:把腾讯云 TencentDB Agent Memory 当作 Codex CLI 的长期记忆后端,让同一个编码 Agent 在不同会话之间记得项目背景、记得上次改过哪几个文件、记得已经踩过的坑。单元测试里记忆写入和向量检索都正常,召回结果也漂亮,可真接到 Codex 上,思维链路跑得通,代码照写,唯独“记忆”像根本不存在。查了两天,把两边关键源码翻完,结论不是配置写错,也不是网络时延,而是架构层面的硬冲突——两边对“记忆”这个词的理解完全不是一个东西。这篇文章把整个梳理过程、源码定位和最终采用的桥接方案完整记下来,给同样想给 Agent 接持久记忆的人一个参考。

1. 先别急着调参:还原一下冲突现场

1.1 我最初设想的集成路径

先说背景。我手上有个长期维护的代码库,用 Codex CLI 在终端里做日常编码辅助。Codex 本身只有会话内上下文,关掉终端它就“失忆”了,所以我想给它加一层长期记忆:把每次会话里产生的关键信息——需求背景、文件结构、踩坑记录、决定用的方案——抽出来,写进 TencentDB Agent Memory。

我最初的架构设想很直白:

  • Codex CLI 作为交互入口;
  • 通过 MCP 协议暴露一个记忆工具集,供 Codex 在对话中调用;
  • 记忆工具内部走 TencentDB Agent Memory 的 SDK,做向量化、写入、检索;
  • 每次会话结束后,把本地会话记录汇总、固化回记忆库。

这个方案在当时看起来是最“正统”的:MCP 是 Codex 官方支持的扩展方式,TencentDB Agent Memory 官方也主打“给 Agent 用的一站式记忆存储”。结果一联调就露馅。

1.2 冲突的三种典型表现

我在不同阶段踩到了三种非常典型的现象:

现象一:新会话对历史完全没有感知。我手动把一批记忆写进库,向量检索单独测都能搜到。但只要开一个新的 Codex 会话,问“我们之前是不是定过这个模块的命名规范”,模型一律回答“我没有之前会话的记录”。

现象二:记忆明明被工具搜到了,模型却不会用。我换了思路,让模型在开场时主动调用记忆检索工具。工具确实返回了文本,但模型经常把检索结果当成“工具输出”来看待,不会把它当作背景知识吸收进推理。更麻烦的是,返回内容一旦长了,回答风格还会被带偏,好像突然换了个说话习惯。

现象三:自动注入记忆后,上下文直接被挤爆。我试过在启动脚本里预取记忆、强行拼进 prompt,结果长会话里记忆块占了几千 token,本来够用的上下文窗口变得捉襟见肘,代码文件反而放不进去了。

这三件事指向同一个结论:不是我不会调参数,而是这两个系统的数据模型和运行机制不匹配。

1.3 为什么我愿意称之为“硬冲突”

“硬冲突”这个说法,是我在翻源码之后才敢用的。它区别于“软冲突”——软冲突调调参数、换个 prompt 模板就能解决;硬冲突是接口契约层面的分歧,除非改一端的实现,否则绕不过去。

具体来说,TencentDB Agent Memory 眼里的“记忆”是一个个可独立度量的条目:有内容、有重要性、有访问时间、有向量表示,可以被精确检索。而 Codex 眼里的“记忆”是线性消息流:一条条消息按顺序排列,靠模型窗口硬记。这两套模型之间没有天然的翻译层,这才是所有问题的根源。

2. TencentDB Agent Memory 的架构拆解

2.1 存储模型:一行记忆里到底塞了什么

先把 TencentDB Agent Memory 这一侧拆清楚。它本质是一个“为 Agent 设计的数据库”,底层兼顾了结构化数据和向量数据。以我手头接触到的 SDK 为例,一条记忆记录的核心字段大概是这样:

字段类型说明
memory_id字符串记忆唯一 ID,写入时自动生成
agent_id字符串属于哪个 Agent,租户级隔离
session_id字符串来源会话 ID,可空
content文本记忆正文,模型直接可读
content_type枚举fact / decision / error / preference 等
embedding向量文本向量,用于相似度检索
importance浮点重要性评分,比如 0.0~1.0
access_count整数被检索次数,用于温热管理
created_at / last_access_at时间记录时间与最后访问时间
metadataJSONB自定义扩展字段,按需过滤

这个设计有一个很关键的点:记忆是“扁平条目”,不是“对话消息”。它不关心这条记忆来自哪一轮对话的哪一条消息,只关心它本身的内容、权重和可检索性。这是一个面向“查找”的数据模型,而不是面向“重放”的数据模型。

2.2 写入链路:谁在负责把对话变成记忆

实际写入流程比我预想的更复杂。调用方给出一条文本,SDK 内部大致做了四件事:

  1. 对文本做向量化,生成 embedding;
  2. 做近似去重,计算新文本与已有记忆的余弦相似度,超过阈值(比如 0.92)就认为是重复;
  3. 把向量、元数据、正文组装成一个事务写入数据库;
  4. 返回新的 memory_id。

这套链路在 LangChain、LlamaIndex 这类框架里通常是以 Memory 抽象类或 Retriever 接口提供的。你只要调add()或save_context(),框架层面会处理向量化和去重。

但这里有个容易被忽略的隐含假设:写入之前,必须有人负责“提炼”。数据库只做存储和检索,它不会自动把一段几千字的对话总结成“项目决定使用 X 方案”这样一句话。这个“提炼”动作,要么由 orchestration 层的 LLM 做,要么由调用方手动做。在传统 LangChain 应用里,RunnableWithMessageHistory 这类组件会替你完成提炼;但在 Codex 这种模型主动驱动的 Agent 里,没有人替你做这件事。

2.3 检索链路:数据库是怎么把记忆捞出来的

检索链路才是 TencentDB Agent Memory 的核心价值所在。一次查询大概走这几步:

  1. 用户拿到一个自然语言查询;
  2. 查询文本向量化;
  3. 在向量索引上找 top-K 相近的候选(比如 K=20);
  4. 用 metadata 过滤掉不符合条件的条目,比如限定 agent_id、时间范围、importance 下限;
  5. 对剩余结果按相似度和其他规则排序,返回最终 top-N。

这一步会暴露另一个隐含假设:调用方是“主动查询”的。检索是被动触发的,必须有一个外部逻辑决定“现在该查记忆了”,然后把查询词准备好。这个假定在 LangChain 里成立,因为编排器控制每一步;但放到 Codex 里,这个编排器变成了模型自己,它想不想查记忆、什么时候查,完全不受你控制。

2.4 它天生适合当“记忆”的几个设计点

TencentDB Agent Memory 能被官方定位成 Agent 专用存储,是有道理的:

  • 混合存储:向量和元数据在一套库里,不用像老方案那样“向量库管语义、关系库管元数据”搞两套系统同步;
  • 托管运维:扩缩容、备份、监控都不用自己管;
  • TTL 和重要性:支持按时间衰减、按重要性过滤,避免记忆无限膨胀;
  • 租户隔离:不同项目、不同 Agent 可以完全隔离。

这些能力单独拿出来都是好用且必要的。问题在于,这些设计面向的是“应用方自己编排记忆流程”的场景,而 Codex 不是这种场景。

3. Codex 的会话模型与记忆扩展点

3.1 Codex 眼里的“记忆”是什么

很多人以为 Codex 有记忆能力,其实它的“记忆”来自三个地方:

  • 项目上下文文件:主要是 AGENTS.md,由用户在项目里显式书写,属于静态文本;
  • 本地会话记录:每次会话存成一个 JSONL 文件,记录全部消息和工具调用,但只是在本地躺着,默认不参与新会话;
  • 上下文窗口:模型当前能看到的所有内容,包括系统提示、历史消息、工具结果。

换句话说,Codex 的“记忆”是瞬态的、线性的,依靠上下文窗口硬存。它没有“长期记忆”的概念,也不存在一个对外暴露记忆增删改查的接口。如果你希望它“记得”上上次的会话内容,唯一原生手段是手动把历史录进新的提示词,或者把关键信息写进 AGENTS.md。

这就是第一块拼图:Codex 的设计哲学里,记忆是上下文工程的一部分,而不是存储架构的一部分。

3.2 从 CLI 启动到单轮请求的完整链路

为了定位冲突,我把 Codex CLI 的主链路读了一遍。简化后的执行流程是:

  1. 读取配置,包括模型参数、MCP 服务器配置、工作区信息;
  2. 创建或恢复会话,会话内容从本地 JSONL 读入;
  3. 进入 turn loop(对话轮次循环);
  4. 每一轮:构造请求上下文 → 调用模型 → 如果模型要求调用工具,就执行工具并把结果追加进上下文 → 重复;
  5. 用户输入结束或达到终止条件后,退出循环,把完整会话写入本地 JSONL。

关键在第 4 步的“构造请求上下文”。Codex 在这里做的事情是:把 AGENTS.md 内容、系统提示、历史消息、最近工具结果按顺序拼成一个请求体,送给 Responses API。整个过程是同步、一次性的。它没有一个“在构造上下文之前执行外部代码”的钩子。

我用伪代码表示一下这个 turn loop:

def run_turn(user_input): context = build_context( system_prompt=load_system_prompt(), project_docs=load_project_docs(), # AGENTS.md 等 transcript=load_current_transcript(), ) response = call_responses_api(context) while response.has_tool_calls(): for call in response.tool_calls: result = execute_tool(call) # MCP 工具在这里才执行 response = call_responses_api(context + result) return response

注意这个顺序:工具调用发生在模型“决定调用”之后,而不是上下文构造之前。这对记忆是致命的。

3.3 Codex 官方留下的三条扩展路径

Codex 不是完全不能扩展,它给了三条路:

  • AGENTS.md:写死在项目里的说明文件,模型每轮都会读到。它是注入静态知识的合法渠道,但只能放静态内容,无法动态查库;
  • MCP 工具:通过codex mcp add注册外部工具,模型在对话中决定是否调用。这是动态能力接入的官方通道,但受制于“模型必须主动调用”这个前提;
  • SDK 定制:用官方提供 Codex 的 Python/TypeScript 包自己封装交互流程,在调用底层接口前做任何想做的事。这是自由度最高的路径,但需要放弃现成的 CLI 交互体验。

这三条路我都试了一遍,最后确认:只靠其中任何一条,都接不好持久记忆。真正的解法是把多条路径组合起来,再在外部补一个“会话结束沉淀”的环节。这个后面细说。

4. 读源码之后,我定位到的四个硬冲突

4.1 冲突一:记忆是“片段”,Codex 要的是“消息”

这是第一个在数据模型层面撞上的冲突。

TencentDB Agent Memory 返回的检索结果是扁平文本片段,天然没有“这条记忆是用户说的还是助手说的”这种角色属性,也没有“它应该插在对话序列的哪个位置”的顺序属性。而 Codex 提交给 Responses API 的上下文,是一个严格有序的 messages 数组,每条消息都有明确的 role(system / user / assistant / tool)。

这就导致一个尴尬的问题:你想把记忆塞进去,塞在哪?

  • 塞进 system,模型会把记忆内容当成系统指令,有时照搬执行很危险;
  • 塞进 user,模型可能把它当成当前用户输入,产生错误的对话流;
  • 塞进 assistant,等于伪造了自己的发言,后续引用会出问题。

我试过最“不坏”的做法是:把记忆块打包成一条 user 消息,前面加一句“以下是从长期记忆中检索到的历史信息,仅供参考”。实测能用,但污染消息流,而且一旦多条记忆内容互相矛盾,模型会陷入混乱。这个冲突,我在前面提的“现象二”里就是这么踩出来的。

4.2 冲突二:拉取的时机对不上请求的时机

记忆检索是个“事前动作”,但 Codex 的上下文构造是个“同步封装”。

你需要记忆来支撑本轮回答,所以理想时序应该是:用户输入 → 查记忆 → 组装上下文 → 调模型。但 Codex 的内部逻辑是:用户输入 → 组装上下文 → 调模型 → 模型决定调工具 → 才查记忆 → 再把结果喂回模型。

这就多了一个“模型的中间决策环节”。如果模型这一轮压根没想到要查记忆,那这轮回答就是无记忆的;而如果我们硬把记忆查询做成一个工具让模型“必须调用”,那每次对话都额外消耗一次模型往返,延迟和成本都上去了。

更麻烦的是,工具调用返回的记忆,本质上是“事后补救”。模型看到检索结果时,可能已经基于无记忆的上下文写出了第一版推理,不一定会回头修正。

4.3 冲突三:写回的路径根本不存在

读 TencentDB Agent Memory 的 SDK 时,我发现一个问题:它提供了完美的写入接口,但没有提供一个“接住 Codex 会话结束事件”的挂载点。

Codex 这边,每次会话结束只是默默把 JSONL 写到本地;TencentDB Agent Memory 这边,add()接口安安静静等着被调用。两边之间没有任何机制自动触发“把这次会话沉淀成记忆”。

理论上,Codex 可以在对话过程中让模型自己调用memory_add工具来写记忆,但让模型“自觉做记忆管理”这件事本身就很不可靠:它可能在无关紧要的地方疯狂写入,也可能整个会话一份都不写。靠 prompt 约束,效果全看模型心情。

这就是为什么我在最终方案里增加了一个“会话结束后的外部固化脚本”,而不是依赖模型自觉。

4.4 冲突四:检索排序逻辑和 Codex 的 token 预算打架

TencentDB Agent Memory 的检索排序,优化目标是“语义相似度”。它默认只关心“这条记忆和查询像不像”,不关心“在固定的 token 预算下,哪些记忆值得占用空间”。

可 Codex 的上下文是一个有限资源。一个中等规模项目,AGENTS.md 加历史消息就能花掉几万 token。这时候你再往里面塞 top-5 的记忆,每条记忆平均 300 token,就是 1500 token 的额外开销。如果检索结果里有几条只是“字面相似但实际毫无用途”的旧记录,浪费就更明显。

我在源码里也确认了 TencentDB Agent Memory 的检索结果是带score和importance的,但 Codex 这一侧没有任何消费这些元数据的逻辑。两边各自的“排序指标”不对齐,结果就是:数据库觉得“这条最相关”,Codex 却觉得“这玩意占地方”。

所以后面桥接方案里,我必须在网关层自己做一道“二次筛选”,按重要性和新鲜度压缩记忆块,再交给模型。

5. 我采用的桥接方案:用 MCP 包一层“记忆网关”

5.1 方案选型:为什么不改 Codex 源码

面对四个硬冲突,最直接的思路是 fork Codex 改源码,在上下文构造前硬插一个记忆检索步骤。这确实能打通“事前注入”的问题,但我不推荐。

原因有三个:第一,Codex 迭代很快,我 fork 的版本撑不过两轮升级;第二,改源码需要重新编译和维护,个人项目扛不住;第三,Codex 上游其实也在演进记忆能力,现在做深度定制,很可能做几个月就被官方功能覆盖了。

所以我的选择是:保留 Codex CLI 原样,在 MCP 工具层解决“动态读”,在外部脚本层解决“自动写”,在 AGENTS.md 层解决“模型自觉性”,再用一个轻量 Python 服务把三者串起来。这样所有改动都在“外围”,升级 Codex 不受影响。

5.2 记忆网关的组成与实现

整个桥接方案由四块组成:

  1. 一个 MCP 服务器,暴露三个工具:memory_search、memory_add、memory_recent;
  2. 一份 Codex 的 MCP 配置,把记忆网关挂上去;
  3. 一份 AGENTS.md,告诉模型“什么时候查、什么时候写”;
  4. 一个会话结束后的固化脚本,把本地 JSONL 里的关键信息提炼成记忆条目。

先看 MCP 服务器的核心代码,我用 Python 写,尽量精简:

# memory_bridge.py from mcp.server.fastmcp import FastMCP import agent_memory_sdk as sdk # TencentDB Agent Memory 的 Python SDK,示例命名 mcp = FastMCP("memory-bridge") @mcp.tool() def memory_search(query: str, limit: int = 5) -> str: """按语义检索与当前问题相关的历史记忆,返回文本列表。""" hits = sdk.search(query=query, top_k=limit, agent_id="my-project") if not hits: return "没有检索到相关记忆。" return "\n---\n".join(f"[重要性 {h.importance:.1f}] {h.content}" for h in hits) @mcp.tool() def memory_add(content: str, importance: float = 0.5) -> str: """把一条值得长期保留的信息写入记忆库。""" sdk.add(content=content, metadata={"importance": importance}) return "已写入记忆。" @mcp.tool() def memory_recent(hours: int = 24, limit: int = 10) -> str: """获取最近指定时间内写入的记忆,用于会话开始时的背景补全。""" items = sdk.recent(hours=hours, agent_id="my-project", top_k=limit) if not items: return "最近没有写入记忆。" return "\n---\n".join(i.content for i in items)

然后把它挂进 Codex 的配置,我这边用的是~/.codex/config.toml:

[mcp_servers.memory] command = "python" args = ["memory_bridge.py"]

AGENTS.md 里我写了一份很明确的规范,让模型的行为可预期:

## 记忆使用规范 - 每次会话开始,先调用 memory_recent 查看近期进展,再调用 memory_search 检索与本任务直接相关的记忆。 - 当确定了关键决策、踩坑经验、模块约定时,调用 memory_add 写入,importance 按影响范围给 0.3~0.9。 - 记忆检索结果只作为背景参考,如果与当前代码冲突,以当前代码为准。 - 不要写入临时性细节(如当前行号、临时调试输出),不要写入显而易见的事实。

这一步把“记忆读取”从系统级前置动作,变成了模型可执行的工具调用。虽然不是 100% 可靠,但配合 AGENTS.md 的强制语气,实测大多数会话都会遵守。

5.3 会话结束时的记忆固化脚本

模型自觉写记忆靠不住,所以我还做了一个外部兜底:用一个包装脚本包住codex exec命令,会话结束后自动解析最新生成的 JSONL 会话记录,让一个便宜的总结模型提炼出该保留的条目,统一写入记忆库。

简化版本的思路是这样的:

codex exec "$@" LATEST_SESSION=$(ls -t ~/.codex/sessions/*.jsonl | head -1) python summarize_session.py "$LATEST_SESSION"

summarize_session.py里做的事情不复杂:把会话里所有 user 消息和 assistant 消息抽出来,喂给一个文本模型,让它输出三条以内的“值得长期保留的事实”,每条不超过 80 字,然后逐条调memory_add写入,importance 由总结模型给定。

实测下来有几个注意点:

  • 去重要控制好:同一个项目反复跑,很容易写出重复记忆。我利用 TencentDB Agent Memory 的相似度去重机制,把去重阈值调高了一些,保证同义表述也能命中。
  • 只总结决策与坑,不总结过程:会话里大部分内容是“改了这个函数再跑测试”,这种过程性内容没有长期价值,固化前就要过滤掉。
  • 时间粒度要统一:固化脚本建议用 cron 或终端集成跑,不要每次手动执行,否则坚持不下来。

5.4 实测效果与仍然存在的边界

接入之后,效果是肉眼可见的:新会话开头,模型会主动说“我先查一下相关记忆”,然后给出之前定过的规范和踩坑记录。至少“同一个问题问两遍”这种蠢事很少再发生了。

但也要说实话,这个方案不是银弹。边界很清楚:

  • 依赖模型自觉:AGENTS.md 能提高自觉率但不是 100%,偶尔还是会忘;
  • 时效性差:固化脚本是“会话结束后”才写记忆的,会话中途崩溃、被强杀,本轮内容就丢了;
  • 并发会话会打架:同一个项目同时开两个 Codex 会话时,记忆写入需要额外的锁和版本控制,否则后写的把先写的覆盖了;
  • 检索结果仍可能“占着位置没用”:网关层的二次压缩只是缓解,没有根治排序指标不对齐的问题。

这个方案我用了大概三周,整体收益大于成本,尤其是对跨天、跨周的项目维护场景价值很明显。

6. 常见问题排查与速查表

把这段时间踩过的坑整理成一张速查表,遇到问题先对号入座:

症状可能原因排查与解决
Codex 从不调用记忆工具AGENTS.md 没被加载,或规范语气不强确认 AGENTS.md 在项目根目录且命名正确;把“先查记忆”写成命令式语气
memory_search 总是返回空agent_id 过滤过严,或记忆库里本来就空先不传 agent_id 试一次;检查固化脚本是否真的成功写入了
模型把记忆当用户输入记忆以 user 角色注入,模型无法分辨改用 MCP 工具返回值注入,不要直接拼进消息流
上下文长度经常超限记忆检索 top_k 太大,或记忆块本身过长调小 limit;在网关层按 importance 截断,只保留摘要部分
重复写入大量相同记忆去重阈值太低,同义句被判为不同内容调高相似度阈值;固化脚本里先 search 再 add
会话中途崩溃,本轮记忆全丢固化脚本只在会话正常结束时执行把固化逻辑改成轮询本地 JSONL 文件变化,增量更新

如果你也想复现这套方案,我的建议是:先把最小闭环跑起来——只接一个 MCP 服务器,手动memory_add几条记忆,然后在新会话里问“有没有关于 X 的记录”。确认模型能搜到、能引用之后,再加自动化固化脚本。千万不要一上来就把四个组件全铺开,出了问题根本不知道是哪一环坏了。

我个人在这几天的实践里,最大的体会是:给 Agent 接记忆,难的从来不是存储,而是“时机的适配”。数据库做得再好,如果 Agent 的运行时没有在正确的时机给你入口,一切能力都等于不存在。所以在选型前,先花时间读一读 Agent 的 turn loop 源码,搞清楚它在哪里暴露钩子、在哪里是封闭的,能帮你省掉后面几周的折腾。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 9:36:50

linux-command 命令详解:grpunconv 关闭群组投影密码的原理与实战

文档教程 【免费下载链接】linux-command Linux命令大全搜索工具,内容包含Linux命令手册、详解、学习、搜集。https://git.io/linux 项目地址: https://gitcode.com/GitHub_Trending/linux/linux-command 点击查看 免费下载 grpunconv 是 Linux 影子密码…

作者头像 李华
网站建设 2026/10/1 9:34:46

端侧视觉检索:TensorFlow.js与Web Worker实现零云端成本本地图片搜索

做端侧视觉检索这几个月,我最大的体会是:真正的隐私不是“上云后加密”,而是“根本不上云”。标题里那句话——“0 云端成本与 100% 隐私安全”——听起来有点营销味,但它确实把我做的一个东西概括完了:用 TensorFlow.…

作者头像 李华
网站建设 2026/10/1 9:33:30

systemd CPUAffinity覆盖taskset:容器绑核失效与CPU争抢排查

1. 现场还原:一条 systemd 命令让两个容器在同一个核上硬碰硬干这行久了,你会遇到一种特别有意思的故障:systemd 命令一执行,容器里的 CPU 使用率开始“打架”,进程明明之前绑核正常,重启之后就错位了。我遇…

作者头像 李华
网站建设 2026/10/1 9:32:43

xinput1_3.dll丢失导致游戏打不开、手柄不识别?6种安全修复方法详解

1. 先搞清楚 xinput1_3.dll 到底管什么,别急着乱下文件很多人一看到弹窗写着“xinput1_3.dll 丢失”,第一反应就是去搜索引擎里找一个同名文件下载下来丢进系统目录。我见过太多人这么干,结果要么游戏照样打不开,要么系统被塞进一…

作者头像 李华
网站建设 2026/10/1 9:32:40

EarthVerse:面向动态地球系统与自然灾害的科学智能体评测基准

EarthVerse:面向动态地球系统与自然灾害的科学智能体评测基准 原论文信息:arXiv:2608.23525v1 数据集HuggingFace:https://huggingface.co/datasets/miracle10/EarthVerse GitHub仓库:https://github.com/CuiZHIQ/Earth‑Verse 项目主页:https://cuizhiq.github.io/EarthV…

作者头像 李华