1. 从一次真实的踩坑说起为什么我要去翻 TencentDB Agent Memory 的源码事情的起因很简单。我在做一个基于 Codex 的本地 Agent 项目想让它在多轮对话里记住用户之前说过的偏好比如“我习惯用 Python 而不是 Node”“数据库统一走 PostgreSQL”“日志级别默认 WARN”。最开始我用的是最土的办法——把历史对话拼成一个超长的 prompt 塞进上下文。跑了两天就崩了token 消耗爆炸响应延迟从 1.2 秒涨到 8 秒而且一旦对话轮次超过 30 轮模型开始出现“记忆错乱”把三天前的一个临时变量当成当前任务的约束条件。这时候我意识到Agent Memory 不是“把历史记录塞进 prompt”这么简单的事。它本质上是一个独立的存储与检索子系统需要解决三个核心问题写什么、怎么存、怎么取。而 TencentDB 在这块做了一套相对完整的 Agent Memory 架构我决定直接读源码搞清楚它的设计逻辑顺便验证一个我一直在怀疑的事情——Codex 这类工具在接入外部记忆系统时存在一个结构性的硬冲突。这篇文章就是我读源码 实际接入测试的完整记录。适合正在做 Agent 记忆系统、或者准备把 Codex 接入自有存储层的朋友参考。我会把架构拆开讲把冲突点标出来把踩过的坑和绕过去的办法都写清楚。2. TencentDB Agent Memory 的整体架构设计思路2.1 为什么不是简单的向量数据库加一层封装很多人第一次听到“Agent Memory”会下意识觉得不就是把对话 embedding 一下存进向量库需要的时候做相似度检索吗我一开始也是这么想的直到我把 TencentDB Agent Memory 的模块依赖图拉出来才发现它的分层比想象中复杂得多。它的核心设计思路可以概括为四层分离接入层Ingestion Layer负责接收来自 Agent 的原始交互数据做初步的格式归一化和敏感信息过滤。记忆抽象层Memory Abstraction Layer这是整个架构里最关键的一层它把原始数据拆成三类记忆——工作记忆Working Memory、情景记忆Episodic Memory、语义记忆Semantic Memory。这个分类不是拍脑袋定的后面我会详细讲它的依据。存储引擎层Storage Engine Layer底层用的是 TencentDB 自身的多模存储能力结构化数据走关系表非结构化走向量索引时序特征走列存。检索与召回层Retrieval Layer对外暴露统一的 recall 接口内部做多路召回 重排序。这个分层的好处在于每一层可以独立替换。比如你不想用它的向量索引可以换成自己搭的 FAISS你不想用它的记忆分类策略可以在抽象层做自定义。这种设计在工程上很务实因为 Agent 场景的需求变化太快耦合太紧的系统活不过三个月。2.2 三类记忆的划分依据与实际含义我重点看了记忆抽象层的实现。它把记忆分成三类这个划分直接对应了 Agent 运行时的不同时间尺度工作记忆对应的是当前任务上下文生命周期最短通常只在一个 session 内有效。源码里它的存储结构是一个带 TTL 的 KV 表默认过期时间是 30 分钟。这里有个细节值得注意工作记忆的写入是同步阻塞的因为 Agent 下一步动作可能立刻就要读它。情景记忆对应的是“发生过什么”比如用户在某次对话里提到了某个偏好、某个任务失败了、某个工具调用返回了异常。它的存储是追加写的日志结构带时间戳和 session_id 索引。读取时主要按时间范围 语义相似度做混合检索。语义记忆对应的是从多次交互中提炼出来的稳定知识比如“这个用户偏好简洁回答”“这个项目的技术栈是 Java MySQL”。它的写入是异步的需要经过一个提炼过程通常由后台任务完成。提示三类记忆的写入时机和同步策略完全不同这是很多人在接入时最容易搞混的地方。如果你把语义记忆也做成同步写入Agent 的响应延迟会直接翻倍。2.3 存储引擎的选型逻辑底层存储这块源码里能看到它做了明确的读写分离。工作记忆走的是内存 本地持久化追求的是低延迟情景记忆走的是分布式 KV追求的是写入吞吐语义记忆走的是向量索引 关系表追求的是检索质量。我实测下来这个选型逻辑是合理的。但有一个隐藏成本三套存储意味着三套运维。如果你是在本地做单机 Agent这套架构会显得过重。我的建议是单机场景可以只保留工作记忆 语义记忆两层情景记忆直接用本地文件追加写等规模上来了再迁移。3. 核心细节解析记忆的写入、提炼与召回3.1 写入路径的完整链路我把写入路径的源码从头跟了一遍完整链路是这样的Agent 产生一次交互用户输入 模型输出 工具调用结果接入层做格式归一化统一成内部 schema敏感信息过滤这里用的是规则 轻量分类器路由到记忆抽象层根据内容类型决定写入哪类记忆工作记忆直接写 KV情景记忆追加写日志语义记忆进入提炼队列写入完成后返回一个 memory_id供后续引用这里有个设计我觉得很聪明memory_id 是全局唯一的跨三类记忆通用。这意味着你拿到一个 memory_id可以反查它属于哪类记忆、原始内容是什么、被哪些后续记忆引用过。这个引用链在调试 Agent 行为时非常有用。3.2 语义记忆的提炼过程语义记忆的提炼是整个架构里最复杂的部分。源码里它走的是一个两阶段流程第一阶段是候选提取。系统会定期扫描情景记忆找出高频出现的模式。比如用户连续三次要求“回答简短一点”这就会被标记为候选。第二阶段是置信度评估。候选模式会经过一个打分函数综合考虑出现频次、时间衰减、跨 session 一致性。只有超过阈值的候选才会被写入语义记忆。这个阈值在源码里默认是 0.75我建议根据你的场景调整。如果你的 Agent 面向的是高频短交互场景比如客服可以降到 0.6如果是低频长交互比如代码助手建议提到 0.85避免把偶然偏好当成稳定偏好。3.3 召回策略的多路融合召回层用的是三路并行 重排序的结构第一路基于 memory_id 的精确查找用于已知引用的场景第二路基于时间窗口的范围检索用于“最近发生过什么”的场景第三路基于向量相似度的语义检索用于“相关记忆是什么”的场景三路结果合并后会经过一个重排序模型最终返回 top-k。这里的 k 默认是 5源码里有个细节工作记忆的权重系数是 1.5情景记忆是 1.0语义记忆是 0.8。这个权重设计的意思是越接近当前上下文的记忆越重要。我实测下来这个权重在代码助手场景下偏保守。如果你做的是需要长期一致性的任务比如持续几天的项目开发建议把语义记忆的权重提到 1.2 左右否则 Agent 会“忘记”之前确立的约定。4. Codex 接入记忆系统的硬冲突问题到底出在哪4.1 冲突的表象endpoint 不匹配我第一次尝试把 Codex 接到这套记忆系统时遇到的是最直接的问题Codex 的请求走的是/responses端点而记忆系统的接入层默认监听的是/memory/ingest。这两个接口的 schema 完全不兼容。Codex 发过来的请求体里对话历史是嵌套在messages数组里的每条消息带role和content。而记忆系统期望的是扁平的interaction对象带session_id、turn_index、raw_input、raw_output这些字段。我一开始想的是写个适配层做转换但转换过程中发现了一个更深的问题。4.2 冲突的根源状态管理模型的差异Codex 的设计假设是无状态请求。每次请求都携带完整的对话历史服务端不保存任何状态。这是为了水平扩展和容错。而 Agent Memory 的设计假设是有状态会话。它期望客户端只发送增量信息由记忆系统来维护完整的状态。这两个假设是根本矛盾的。你要么让 Codex 每次发全量历史那记忆系统就退化成了一个纯存储失去了状态管理的意义要么让 Codex 只发增量那你就得改 Codex 的客户端逻辑而它本身不提供这个能力。注意这不是配置问题是架构层面的冲突。任何试图通过“改个 endpoint”或“加个中间件”来解决的方案都只能解决表象解决不了根源。4.3 我尝试过的三种绕行方案方案一全量历史 记忆系统做去重。让 Codex 照常发全量历史记忆系统在写入时做去重只保留新增部分。这个方案能跑通但代价是每次请求的 payload 都很大网络传输和解析开销高。我实测在 50 轮对话后单次请求体到了 200KB 左右。方案二客户端拦截 增量改写。在 Codex 客户端和记忆系统之间加一个代理拦截请求把全量历史替换成增量。这个方案技术上可行但维护成本高而且 Codex 版本升级时容易失效。方案三双写模式。Codex 保持无状态记忆系统独立维护一份完整状态。两边各存各的通过 memory_id 做关联。这个方案最稳但存储成本翻倍而且存在一致性问题。我最后选的是方案三的变体Codex 侧只保留最近 5 轮对话更早的历史由记忆系统按需召回后注入。这样既控制了 payload 大小又保证了长期记忆的可用性。5. 实操过程从零搭一套可用的接入方案5.1 环境准备与依赖确认我用的环境是 Python 3.11 本地部署的 TencentDB 实例。依赖这块有几个坑要注意向量索引依赖的底层库对 glibc 版本有要求CentOS 7 默认版本偏低需要手动升级记忆系统的 SDK 目前只提供了 Python 和 Java 两个版本其他语言需要走 HTTP 接口本地部署时工作记忆的 TTL 清理任务默认是 60 秒跑一次高频场景下建议调到 10 秒5.2 接入层的适配代码适配层的核心逻辑是把 Codex 的请求转换成记忆系统能识别的格式。关键代码如下def adapt_codex_request(codex_payload, session_id): messages codex_payload.get(messages, []) # 只取最后一条用户消息作为当前输入 current_input for msg in reversed(messages): if msg[role] user: current_input msg[content] break # 历史消息作为情景记忆的补充 history [ {role: m[role], content: m[content]} for m in messages[:-1] ] return { session_id: session_id, turn_index: len(messages), raw_input: current_input, history_context: history, timestamp: int(time.time()) }这段代码的关键点是把当前输入和历史上下文分开处理。当前输入走工作记忆历史上下文走情景记忆。这样召回时可以根据需要分别取用。5.3 召回结果的注入策略召回结果怎么注入回 Codex 的请求这是另一个需要仔细设计的地方。我的做法是在 system prompt 后面追加一个记忆区块def inject_memory(codex_payload, recalled_memories): memory_block \n.join([ f[{m[type]}] {m[content]} for m in recalled_memories ]) system_msg { role: system, content: f以下是相关历史记忆供参考\n{memory_block} } # 插入到 system 消息之后 codex_payload[messages].insert(1, system_msg) return codex_payload这里有个细节记忆区块要标注类型。我实测发现如果不标注类型模型会把工作记忆和语义记忆混为一谈导致行为不稳定。标注之后模型能区分“这是当前任务的临时约束”和“这是长期偏好”。5.4 完整链路的联调记录联调时我按这个顺序逐步验证先单独测写入发一条消息确认三类记忆都正确落库再单独测召回手动构造查询确认三路召回都能返回结果然后测端到端发消息 → 写入 → 下一轮召回 → 注入 → 模型响应最后测边界空历史、超长历史、并发写入第三步跑通的时候我观察到响应延迟从纯 Codex 的 1.2 秒涨到了 2.1 秒。其中写入占 0.3 秒召回占 0.4 秒注入和序列化占 0.2 秒。这个开销在可接受范围内但如果你的场景对延迟敏感可以考虑把写入改成异步。6. 常见问题与排查技巧实录6.1 记忆召回不准确的三类原因我踩过的坑里召回不准是最常见的。总结下来有三类原因问题现象根本原因解决方向召回结果与当前任务无关向量索引的 embedding 模型与场景不匹配换用领域微调过的 embedding 模型该召回的记忆没召回时间衰减系数设置过激调低衰减速率或对语义记忆关闭衰减召回了过时的记忆缺少时效性过滤在重排序阶段加入时间惩罚项第一类问题最隐蔽。我一开始用的是通用 embedding 模型在代码场景下召回质量很差。换成代码领域微调过的模型后召回准确率从 62% 提到了 81%。6.2 写入冲突与并发问题并发写入时我遇到过一次数据覆盖。原因是工作记忆的 KV 表用的是session_id作为主键同一 session 的并发写入会互相覆盖。解决办法是在写入前加一个基于 session_id 的分布式锁或者改用session_id turn_index作为复合主键。提示如果你用的是单机部署直接用本地锁就行不用上分布式锁省得引入额外依赖。6.3 记忆膨胀的治理跑了一周后我发现语义记忆表膨胀得很快从最初的几十条涨到了三千多条。排查后发现是提炼阈值设得太低把很多一次性偏好也写进去了。治理办法有两个一是提高提炼阈值二是加一个定期的记忆合并任务把相似度高的语义记忆合并成一条。我两个都做了表大小稳定在了 400 条左右。6.4 常见问题速查表症状排查第一步常见修复Agent 响应变慢看写入是否同步阻塞改异步写入记忆丢失检查 TTL 配置调长过期时间记忆错乱检查注入格式加类型标注存储增长过快统计各类记忆条数提高提炼阈值召回为空检查索引是否建立重建向量索引7. 我对这套架构的实际体会读源码这件事最大的价值不是学会怎么用而是理解为什么这么设计。TencentDB Agent Memory 的三层记忆划分本质上是在解决“不同时间尺度的信息需要不同的存储和检索策略”这个问题。这个思路可以迁移到很多场景不只是 Agent。Codex 接入的硬冲突说到底是一个状态归属权的问题。无状态客户端和有状态服务端之间的边界在哪里这个边界怎么划直接决定了你的架构复杂度。我现在的做法是把边界划在“最近 5 轮”这个位置再往前的交给记忆系统。这个数字不是拍脑袋定的是我测了 3 轮、5 轮、10 轮之后在延迟和记忆连续性之间找到的平衡点。最后分享一个小技巧调试记忆系统时一定要把 memory_id 打到日志里。Agent 行为异常时顺着 memory_id 反查它的来源和引用链比盲目看对话历史高效得多。我靠这一招定位过好几次“Agent 为什么突然改变行为”的问题每次都能在十分钟内找到根因。
企业数字化 ERP 产品动态
相关推荐
光伏投资智能测算工具iSolarBP的核心功能与应用 1. 光伏投资决策的痛点与解决方案 光伏行业近年来发展迅猛,但投资决策过程中存在诸多痛点。传统的光伏收益测算往往依赖Excel表格,需要手动输入大量参数,计算过程繁琐且容易出错。不同地区的辐照数据、电价政策、组件衰减率等变量相互影响&am… · 2026/11/3 8:34:26
12大AI平台实测:科研场景下的机器学习工具选型指南 1. 项目背景与核心价值最近在指导实验室新生搭建科研环境时,发现2026届学生普遍面临AI工具选择的困境。市面上各类机器学习平台、云服务、开源框架层出不穷,但缺乏针对科研场景的系统性评测。为此,我耗时三个月深度测试了12个主流AI平台&… · 2026/11/3 8:34:26
稀土抑烟剂技术原理与工业应用解析 1. 稀土抑烟剂技术概述在工业生产和高分子材料应用领域,烟雾抑制一直是环保安全的重要课题。稀土抑烟剂作为新一代功能性添加剂,通过稀土元素独特的电子层结构和催化特性,在聚合物燃烧时能有效减少有毒烟雾产生。与传统卤系阻燃剂相比&#x… · 2026/11/3 8:34:26
MAX14431隔离SPI链路设计:PIC18F87J11与嵌入式隔离方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 9:02:48
STM32F745搭配MAX14431实现SPI总线隔离的设计指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 9:02:48
STM32F207ZG与MAX14431的SPI数字隔离设计实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 9:02:48
S7-1500R/H冗余系统中PN IO设备S1与S2模式的核心差异与选型指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 9:02:48
工业SPI隔离通信实战:MAX14431与PIC18F86J11设计调试全解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 9:02:48
磁性直接驱动器:音圈、线性与力矩电机的精密运动控制选型指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 9:01:47
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46