如果只把 memory 理解成“把历史对话塞回 prompt”,TencentDB-Agent-Memory 会显得很复杂:它有 Context Offload、L0-L3、Gateway、MemoryCore、Memory Hub、Memory Proxy、Skill、Wiki、CodeGraph、ACL 和 Loadout。 换一种读法就顺很多:它一直在回答两个问题:什么值得留下,下一位 Agent 凭什么可以使用。

同一条经验的生命周期:
  1. 当前任务产生厚日志、报错、修复步骤和文档线索
  2. v1 先保存原始证据,并把本轮 prompt 里的厚材料换成可下钻摘要
  3. v1.0 把记忆引擎拆成独立 Gateway,其他 Agent 框架也能接入
  4. v2 把 Chat Memory / Skill / Wiki / CodeGraph 登记成团队资产
  5. Memory Hub 控制 owner、visibility、version、status 和绑定关系
  6. Memory Proxy 在下一轮请求前,只把当前 Agent 有权使用的资产注入上下文

正文沿着上面这条路径读源码:先讲 v1 为什么要做 Context Offload 和 L0-L3;再讲 v1.0 的 Gateway 化解决了什么;最后讲 v2 的团队记忆为什么需要资产、权限和代理注入。

术语 先按这句话理解
Context Offload 把当前任务里的厚工具输出移出 prompt,留下摘要、任务节点和可找回原文的引用。
MMD Mermaid 任务图,用少量节点表达任务进展,并把节点连回具体工具调用。
Memory Hub 团队面板,管理资产的 owner、visibility、status、version 和绑定关系。
Memory Proxy 模型请求代理,在请求发给 LLM 前按 Team、User、Agent、Task 注入资产。
Agent Loadout 当前 Agent 被允许携带的一组资产,以及每个资产进入上下文的方式。

读完后应该能复述。 v1 解决的是单个 Agent 的长任务压力;v1.0 解决的是记忆能力被宿主插件绑住的问题; v2 解决的是多人、多 Agent 之间如何复用经验又不泄露私有资产的问题。文档里的 /v3/* 是接口版本,不等于“产品 v3”。

本文依据。 v1 侧使用 v1.0.1 与 v1.0.0/v1.0.1 changelog; v2 发布侧使用 v2.0.0 release notes 与 v2.0.1 release notes; 当前团队分支实现快照是 0468a2a。 feat/server 更接近 v1.0.x 的 Gateway 化主线;feat/server_team 才是 v2 Team Memory 主线。

一、先把版本关系理顺

1.1 v1 先处理单个 Agent 的上下文压力

代码 Agent 的上下文会被两类东西撑大。一类是当前任务里的工具输出:测试日志、搜索结果、文件片段、报错堆栈。 这些内容很厚,却经常只需要在关键时刻回看原文。另一类是跨会话信息:用户偏好、项目决策、稳定约束。 v1 的核心设计就是把两类信息分层处理:当前任务的厚材料先卸载,长期信息再逐层提炼。

Context Offload 的文件层很直白。不同 Agent 有独立目录,同一个 Agent 共享 mmds/、refs/、state.json,每个 session 有自己的 offload-<sessionId>.jsonl。源码注释把这个隔离关系写在 storage.ts 顶部;路径构造在 createStorageContext。

export interface StorageContext {
  readonly dataRoot: string;
  readonly dataDir: string;
  readonly refsDir: string;
  readonly mmdsDir: string;
  readonly offloadJsonl: string;
  readonly stateFile: string;
  readonly agentName: string;
  readonly sessionId: string;
}

一条工具结果不会直接消失。原文进入 refs/*.md,索引和摘要进入 JSONL,多个工具步骤再合成 MMD 任务图。模型平时读摘要和任务图,真要核对证据时再沿 result_ref 或 node_id 回到原文。这比单向 summary 更适合代码任务,因为“哪一行测试失败”经常比“测试失败了”更重要。

1.2 真正的卸载发生在进入模型前

v1 的 offload 写完文件后,还会在下一次模型调用前改写消息数组。before_prompt_build 的注释把流程分成三段:先快速重放已经确认的替换和删除;如果 token 仍然超阈值,就跑完整 L3 压缩; 最后把 MMD 注入消息。对应代码在 hook 顶部说明 和 fast-path 替换逻辑。

进入模型前:
  1. 已确认可替换的 tool_result → 摘要
  2. 超过 aggressive 阈值的旧工具消息 → 删除
  3. 被删掉的任务过程 → 用 history MMD 补回结构
  4. 当前 active MMD → 注入本轮消息

这一步让“省 token”和“可追溯”同时成立。模型不必每轮携带完整工具日志,但任务图仍然告诉它自己做过哪些关键动作。 如果后面发现摘要不够,系统还保留了下钻路径。

1.3 L0-L3 处理长期记忆,不处理团队共享

v1 的长期记忆同样按层组织:L0 是原始对话,L1 是可检索事实,L2 是场景,L3 是 persona 或核心画像。 召回时,代码会把 L3 persona、L2 scene navigation 和工具说明放进相对稳定的 system 区域,把每轮都变化的 L1 命中放进 user prompt 前缀。这个拆分写在 auto-recall 说明 和 上下文组装代码。

// stable: persona / scene navigation / memory tools guide
const appendSystemContext = stableParts.length > 0
  ? stableParts.join("\n\n")
  : undefined;

// dynamic: L1 relevant memories for this turn
return { prependContext, appendSystemContext };

这套设计已经能让一个 Agent 更懂用户和任务,但它还没有回答团队问题。谁拥有这条 Skill? team admin 能不能看别人的私有记忆?Reviewer Agent 应该自动带上哪些 Wiki 和 CodeGraph? 这些问题已经越过 L0-L3 的范围,于是 v2 把“记忆记录”推进成“记忆资产”。

二、v1.0 Gateway 先把能力从插件里拆出来

2.1 如果记忆只活在一个插件里,别的 Agent 很难复用

v1.0.0 的 changelog 把变化说得很清楚:项目从 OpenClaw 专属插件演进成面向所有 Agent 的通用记忆服务, 记忆引擎拆成独立 Gateway 服务进程,提供 v2 HTTP API、TypeScript SDK、Python SDK,并继续保留 OpenClaw 与 Hermes 适配。见 v1.0.0 release notes。

阶段 主要变化 解决的问题 下一步为什么需要
v0.x / v1 本地插件 Context Offload、L0-L3、SQLite / 文件存储、OpenClaw / Hermes 入口。 单个 Agent 的长任务不被工具日志压垮,跨会话能召回用户信息。 能力仍贴着宿主插件,其他 Agent 框架要复用时需要标准服务入口。
v1.0 Gateway 独立服务进程、v2 HTTP API、SDK、Docker、观测指标。 记忆能力脱离单个宿主插件,其他 Agent 框架也能通过 API 接入。 API 能共享能力,但还缺 owner、ACL、审核状态和 Agent Loadout 这些团队规则。
v2 Team Memory MemoryCore、Memory Hub、Memory Proxy、四类资产、ACL、Agent Loadout。 团队内多人、多 Agent 共享经验,同时保留 owner、权限和绑定边界。 后续重点转向客户端覆盖、冷启动、会话内命令和写入侧控制,v2.0.1 正在补这些体验。

所以 feat/server 这条线更像“把本地记忆服务化”。它的关键价值在于把 OpenClaw 插件里的 memory pipeline 抽成可被外部调用的服务。v2 的团队资产、Hub 和 Proxy,则在 feat/server_team 这条线继续展开。

2.2 团队协作还需要资产边界

一个 API 可以让多个客户端访问同一套能力,但团队协作还需要更多约束。比如,某位工程师在排障中生成的 Skill 是否默认全队可见?一个 Agent 的 user persona 能不能给另一个 Agent 读取?Wiki 和 CodeGraph 应该按项目、团队还是 Agent 绑定? 这些已经进入资产管理范畴,单纯的记忆 CRUD 没法给出稳定答案。

v2 的设计从这里接上:把可复用经验统一登记为资产,再让 Memory Hub 管理归属和可见性,让 Memory Proxy 在请求进入模型前按当前 session 的 Team、User、Agent、Task 装配上下文。

三、v2 把“记忆”升级成团队资产

3.1 四类资产覆盖经验、文档、代码和对话

v2.0.0 的 release notes 给出新的产品定位:让 Agent 的经验、文档、代码沉淀成可复用资产,让下一位 Agent 直接读档。 四类资产分别是 Chat Memory、Skill、Wiki、CodeGraph,见 v2.0.0 的资产说明。 这一步把“memory”的范围从聊天事实扩展到可复用工作资产。

资产 来自哪里 下一位 Agent 怎么用
Chat Memory 对话中的偏好、事实、决策和历史事件。 恢复用户、任务和团队的长期语境。
Skill 跑通过的任务步骤、触发条件、验证规则和资源文件。 遇到相似任务时直接按 SOP 执行或参考。
Wiki 产品文档、设计方案、运维手册等材料。 先读结构化页面和链接关系,少走文档迷宫。
CodeGraph 仓库符号、文件、调用关系和影响路径。 改代码前先看 callers、callees 和 impact analysis。

用前面的排障例子来看,测试日志和聊天过程会进入 Chat Memory;稳定的上线检查步骤可以沉淀成 Skill; 项目 README、架构文档和运维手册进入 Wiki;代码仓库的符号与调用关系进入 CodeGraph。 这些东西来源不同,但 v2 把它们放进同一个资产框架里管理。

3.2 资产记录先写清 owner、状态和可见性

团队记忆最怕含糊:看起来是共享经验,实际可能把个人对话、未审核步骤或过期文档塞给了不该看的 Agent。 v2 的元数据类型把这个边界落到字段上。AssetEntity 记录 team_id、asset_type、owner_user_id、version、 visibility、status、content_ref 等字段; FixedAssetBindingEntity 再把资产绑定到某个 Agent,并记录注入方式和优先级。源码见 资产类型和注入模式 与 资产及固定绑定实体。

export type AssetType = "skill" | "llm_wiki" | "code_graph" | "chat_memory";
export type AssetVisibility = "private" | "team" | "restricted" | "agent" | "task";
export type InjectionMode = "direct" | "summary" | "tool" | "reference";

export interface AssetEntity {
  asset_id: string;
  team_id: string;
  asset_type: AssetType;
  owner_user_id: string;
  version: number;
  visibility: AssetVisibility;
  status: AssetStatus;
  content_ref?: string | null;
}

export interface FixedAssetBindingEntity {
  agent_id: string;
  asset_id: string;
  injection_mode: InjectionMode;
  priority: number;
}
排障 Skill 的一次资产化:
  1. Coder Agent 跑通连接池超时排障流程
  2. 系统生成 Skill,owner_user_id 指向创建者,status 先是 draft / candidate
  3. 人工审核后 status 变成 approved,必要时 version 增加
  4. owner 把 visibility 调成 team,或用 restricted + ACL 精确授权
  5. Release Agent 通过 FixedAssetBinding 绑定这条 Skill
  6. 下一轮请求里,Proxy 按权限和 Loadout 决定是否注入这条 Skill

这段类型说明了 v2 的责任边界:资产先有归属和版本,再谈能不能被 Agent 带进上下文。 例如,一个 Skill 可以先是 draft 或 candidate,审核后再变成团队可见; 一个私有 Chat Memory 仍然属于 owner,不能因为同属一个 Team 就自动暴露。

3.3 权限判断比“同一个团队”更细

权限规则在 permission-checker.ts 里以纯函数实现。它的顺序是:资源是否存在,当前用户是不是 owner, 是否为团队成员,visibility 是否允许,再看角色默认权限和显式 ACL。源码注释和实现见 设计要点 与 checkPermission。

一次 read 判断:
  asset 不存在或 archived          → 拒绝
  user 是 owner                    → 放行
  user 不是 active team member      → 拒绝
  visibility = private 且非 owner   → 拒绝
  visibility = restricted           → 查 user / role / agent ACL
  role 默认权限覆盖本动作           → 放行
  显式 ACL 命中                     → 放行
  其余情况                          → 拒绝

这里有一个容易忽略的细节:private 的语义很严格,团队管理员也不能直接读取别人的 private asset; 如果确实要共享,需要 owner 主动调整 visibility 或授权。绑定到 Agent 时也有单独规则: team 和 agent 可在同 team 内绑定,private 需要同 owner, task 和 restricted 不能直接做固定绑定。见 canBindAsset。

四、Memory Proxy 把团队资产送进不同 Agent

4.1 多客户端接入集中到 Proxy

v2.0.0 里,Memory Proxy 支持 Anthropic 和 OpenAI 两类协议入口;v2.0.1 又增加 OpenCode、DeepSeek Harness、 Codex CLI、WorkBuddy 等客户端接入,见 v2.0.0 Proxy 说明 与 v2.0.1 客户端列表。 README_CN 也明确写着:一套 Proxy,协议不变,把 Agent 的 base URL 指向 Proxy 即可,见 多 Agent 接入说明。

首轮请求时,Proxy 会让用户选择 Team、Agent、Task,并把这个绑定保存下来。之后每轮请求进入模型前, Proxy 根据当前绑定取该 Agent 的 loadout:哪些 Chat Memory、Skill、Wiki、CodeGraph 有权使用,哪些应该直接注入, 哪些只作为工具或引用出现。

4.2 注入管线把协议消息转成统一上下文

InjectionPipeline 的文件注释给了最短路径:raw body → Adapter.parse() → AgentContext → execute hooks → Adapter.serialize() → modified body。实现里先按协议选择 adapter,再解析出统一的 AgentContext,随后按注入点执行 hook,最后序列化回原协议。见 pipeline 注释 与 process 主流程。

Proxy 每轮请求:
  1. 用 URL path 或 system prompt 识别 agent profile
  2. adapter.parse(body) 得到 AgentContext
  3. 依次跑 system.prefix / system.before_tools / system.after_tools / user.* 等 hooks
  4. hooks 返回 Chat Memory、Skill、Wiki、CodeGraph 等 ContextBlock
  5. 优先按 agent profile 的 anchor 精确落位,失败则按注入点追加
  6. adapter.serialize(ctx) 还原成上游 LLM 能识别的请求
一个极简请求形状:
  注入前
    system: Agent 的基础规则
    user: 帮我修连接池超时

  注入后
    system: Agent 的基础规则
            <session_context>当前 Team / Agent / Task</session_context>
            <available_skills>发布检查 Skill 摘要或全文</available_skills>
            <user_memory>相关 Chat Memory</user_memory>
            <knowledge>Wiki / CodeGraph 的可读摘要或引用</knowledge>
    user: 帮我修连接池超时

这里的 InjectionMode 决定资产进入上下文的姿势:direct 和 summary 更偏向直接放文本;tool 和 reference 更偏向告诉 Agent 有哪些工具或引用可按需读取。这样同一条资产可以被“直接带上”,也可以只作为可下钻线索出现。

注入可以避免每轮从头计算。hook 可以声明 cacheStrategy:none 每次执行, session_init 读取预热缓存,hybrid 合并缓存和本轮新结果。相关逻辑在 hook 执行顺序 和 cacheStrategy 分支。 如果 hook 声明了 anchor,pipeline 会优先把内容放进该 Agent 的语义槽位;槽位解析失败时,才退回通用注入点,见 applyInjection。

4.3 Codex 路径展示了 Proxy 怎样适配新协议

v2.0.1 增加 Codex CLI 接入后,代码里有一条独立的 Responses API handler。 codexHandler.ts 顶部说明它处理 POST /v1/responses,并把 /responses/compact、/memories/trace_summarize、/realtime/calls 这类辅助请求作为 passthrough。主请求才走 session-init、asset injection 和转发。见 Codex handler 注释 与 auxiliary 判断。

Codex 的注入实现也很有代表性。它先构造一个 synthetic OpenAI body,让现有 injection pipeline 处理; pipeline 把资产块追加到 system message;Codex handler 再把这些文本取出,包进 <tdai_injections> 放回 Responses API 的请求结构。这样可以复用原来的 hook、cache、prewarm 和 injector,不必为 Codex 再写一整套资产注入系统。 对应代码见 Codex asset injection。

五、写入侧也开始有团队边界

读侧的 ACL 和 Loadout 防止没权限的资产被注入;写侧则要防止噪声、临时上下文和高风险任务结果变成团队资产。 所以下面两个机制仍然属于 v2 的团队边界:一个清洗写入 L0 的用户问题,一个给 extraction 加总开关和白名单。

5.1 L0 写入只保留真正的用户问题

团队记忆不只要“读得准”,也要“写得干净”。最新团队分支里,tdai/recorder.ts 的注释解释了一个现实问题: CodeBuddy、Claude Code、DSH 等 coding agent 的 user message 往往混入大量 harness 上下文,例如 <additional_data>、系统提醒、运行时快照。把整条 user message 原样写进 L0,会让记忆库被每轮都变化的噪声污染。 这段逻辑只抽取最后一条 user 消息里的真实问题,再写入 L0,见 写入动机 和 recordTdaiTurn。

export function extractLatestUserMessage(messages: unknown[]): TdaiMessage | null {
  for (let i = messages.length - 1; i >= 0; i--) {
    const msg = messages[i] as Record<string, unknown>;
    if (msg?.role !== "user") continue;
    const content = extractUserQueryText(extractContentText(msg.content));
    if (content.trim()) return { role: "user", content };
  }
  return null;
}

这个细节很重要。Memory 系统如果把运行时塞进来的噪声都当成用户事实,后续检索再聪明也会越查越脏。 写入侧先做清洗,才有资格谈召回质量。

5.2 读和写可以分开开关

Proxy 的注入管线解决读侧:每轮给模型放什么上下文。写侧后来补了一个更小的 extraction-gate: 配置里可以关闭所有 extraction,也可以只允许 skill 或 tdai-memory 这类指定写入。 缺省时保持历史行为,配置缺失或字段写错不会悄悄把写入关掉。源码见 设计说明 与 判断函数。

这让生产系统多了一个实用模式:可以继续注入团队资产帮助 Agent 工作,但临时关闭写入,避免压测、迁移、演示或高风险任务把临时内容写进团队记忆。

5.3 回答用户和抽取记忆可以使用不同模型

写入开关决定“是否整理”,实例上游配置再决定“交给谁整理”。例如,团队可以让 Codex 的主对话继续使用原有模型, 把内部记忆抽取交给另一条统一配置的模型服务。配置按 conversation 与 extraction 分开保存; 查找时优先匹配指定 Agent 来源与类型,再退回同类型的 default。 Codex 主请求读取前者, 内部 system-user 的抽取转发路径读取后者。这里区分的是请求用途,团队资产的访问权限仍由前面的 ACL 与绑定决定。

配置模式实例配置改变什么抽取请求能否使用
official不覆盖原有上游选择。可以。
custom_unified使用实例指定的地址和统一 API key;主请求可按配置替换 model。可以。
custom_passthrough改变上游地址,在这个覆盖步骤保留已有鉴权头。不可以,保存配置时拒绝。

因此,修改抽取配置后,不能只看下一次用户回答用了哪个模型来判断是否生效。 Proxy 按实例缓存配置 5 分钟,读取不会延长到期时间;刷新失败时继续使用最后一次成功值,首次获取失败则不做实例覆盖。 这意味着配置更新存在可见延迟,服务故障时还可能继续使用旧模型。排查应区分主对话和内部抽取请求,并检查实际转发目标。 这些行为分别来自配置缓存与选择函数以及保存时的模式校验。

六、接口 v3 是 v2 产品里的隔离契约

容易混淆的一点是:v2 产品线里出现了 /v3/* 接口。MemoryCore 的接口文档写明,v3 是 RPC 风格, 数据面接口通过 body 或 header 传入 team_id、agent_id、user_id、task_id, 并强制 team + agent + user 三元组隔离;接口目录覆盖 L0-L3、Skill、Knowledge、Chat-Memory、Memory-Prompt、Generation-Log、Meta 等模块。 见 鉴权与隔离字段 和 接口目录与 conversation/add。

所以这里的“v3”要按接口代际读。更精确的产品演进是:v1 本地记忆 → v1.0 Gateway 服务化 → v2 团队资产; API v3 是 v2 系统内部承接身份隔离和模块边界的接口代际。

七、放回 Agent Memory 系列看

TencentDB-Agent-Memory 在这个系列里最值得记住的地方,是它同时覆盖了两条路径。 第一条是运行时上下文:工具日志怎样从 prompt 里移出去,摘要和 MMD 怎样代表任务进展,原始证据怎样找回。 第二条是团队资产:一次任务里产出的聊天记忆、Skill、Wiki、CodeGraph,怎样有 owner、visibility、status 和 Agent 绑定。

项目 系列里的主问题 TencentDB-Agent-Memory 的位置
Mem0 长期记忆如何抽取、去重、排序和召回。 TencentDB 也做长期记忆,但更强调工具日志、团队资产和 Proxy 注入。
LangMem / LangGraph 记忆写入放在 hot path 还是后台整理。 TencentDB 把写入、注入和当前任务 offload 都接到 Agent 运行时旁边。
OpenViking Memory、Resource、Skill 如何进入统一上下文树。 TencentDB 用资产和 loadout 管共享;OpenViking 更像统一资源文件系统。
Cognee / Supermemory 知识处理平台与 context API 怎样服务多个调用方。 TencentDB 更靠近 coding agent 的请求代理、权限装配和团队协作场景。

如果只看 v1,它像一个很工程化的本地 memory 插件:把工具输出和长期画像都做成可追溯层级。 看到 v2 后,重点就变了:它试图把 Agent 做过的事,变成团队可以审核、授权、装配和复用的资产。 这也是它比单纯 vector memory 更重的原因。这份重量来自团队协作必须回答的归属、权限、版本、绑定和注入位置。

参考资料