很多 memory 项目把注意力放在跨会话记忆上,TencentDB-Agent-Memory 的特殊之处,是它同时盯住另一个更短、更急的问题: agent 当前这次任务还没结束,工具结果、报错、搜索输出、文件片段已经把上下文顶满了。直接删会丢证据,直接总结又怕把细节压坏。 它的回答是:低层保留原始证据,高层只给模型一张结构图。

场景:代码 agent 连续跑了 8 次测试和 12 次文件搜索

原始上下文:
  assistant tool_use -> tool_result(几千行测试日志)
  assistant tool_use -> tool_result(多段源码)
  assistant tool_use -> tool_result(报错堆栈)
  ...

TencentDB-Agent-Memory 的处理:
  refs/*.md        保存完整工具结果
  offload-*.jsonl  保存 tool_call_id、摘要、score、result_ref
  mmds/*.mmd       把多个工具调用合成任务拓扑节点
  prompt           只保留摘要和 MMD,必要时再按 result_ref 找回原文

这里最重要的是“可展开”。如果模型后来需要确认某个测试失败的原始堆栈,它可以沿着 node_idtool_call_idresult_ref 回到原始文件, 不用赌摘要没有丢掉关键行。 这也是它和普通 summary memory 最大的区别。

阅读契约。 读完这一篇,应该能回答五件事:Context Offload 到底卸载了什么; refs、offload JSONL、MMD 任务画布如何连起来;L0 到 L3 长期记忆怎样生成和召回; Hermes provider 怎样把本地 Gateway 变成可检查、可恢复的 sidecar; 以及它和 Mem0、LangMem、Cognee / Supermemory 分别差在哪里。

证据边界。 本文使用 TencentCloud/TencentDB-Agent-Memory 的 README、插件配置和源码,分析时源码快照为 45e6e80。 仓库同时支持 OpenClaw 与 Hermes 生态;本文关注公开仓库里能直接看到的本地插件、Gateway 和 provider 实现, 不推断后端托管服务的私有逻辑。

一、它解决的是两种不同的上下文压力

1.1 当前任务太厚,长期历史太散

TencentDB-Agent-Memory 的 README 把设计理念归为“记忆分层”和“符号化记忆”。短期任务侧, 底层保存完整工具结果到 refs/*.md,中层写入步骤摘要 jsonl,高层浓缩成带 node_id 的 Mermaid 任务画布;长期用户理解侧,则是 L0 ConversationL1 AtomL2 ScenarioL3 Persona 的语义金字塔。这个总纲可以看 README 的 核心技术说明

这两条线要分开看。短期 Context Offload 处理的是“这轮任务还在跑,但上下文太满了”; 长期 L0-L3 处理的是“下次再见到用户时,系统应该记得什么”。前者更像把工作台上的厚材料搬到带编号的档案柜, 桌面只留下任务地图;后者更像把很多次聊天逐层整理成事实、场景和画像。它们用同一句话串起来: 低层保留证据,高层保留结构。

读这篇时不要把两条线混成一张大图。可以先拆成两个生命周期:工具结果路径回答“当前任务怎样瘦身”; 长期事实路径回答“用户画像怎样沉淀”。

工具结果路径 发生了什么 最后如何使用
触发点 after_tool_call 发现工具输出太长,生成 result_ref 当前 prompt 不再直接携带完整工具日志。
存储记录 原文进入 refs/*.md 和 offload JSONL。 需要核对证据时,按引用恢复原文。
结构摘要 MMD 节点保存任务图、关键结论和引用关系。 模型默认看到摘要和任务结构,而不是厚日志。
长期事实路径 发生了什么 最后如何使用
触发点 多轮对话形成 L0 Conversation。 系统从聊天流里寻找可长期复用的信息。
存储记录 L1 Atom 经过去重与合并,再归入 L2 Scenario 和 L3 Persona。 低层保留事实证据,高层形成场景导航和用户画像。
召回入口 下一轮 prompt 构建前检索相关 atom、scene 和 persona。 稳定画像进入 system context,L1 命中进入当前 user prompt 前缀。

1.2 它靠近运行时插件这一侧

从实现边界看,TencentDB-Agent-Memory 是 OpenClaw 插件。默认本地存储使用 SQLite + sqlite-vec, README 说明启用后会自动完成对话录制、记忆提取、场景归纳、用户画像生成和下一轮召回; 短期 offload 则是独立开关。相关配置在 README 的 快速开始插件 schema 里都能看到。

这个定位决定了它和 Supermemory 这类 context API 不一样。Supermemory 更像外部服务来接收内容、处理连接器、返回 context; TencentDB-Agent-Memory 更靠近本机 agent runtime:它能在 after_tool_callbefore_prompt_buildllm_input 这些时机改写进入模型的消息数组,也能把本地文件和 SQLite 作为可追溯的证据层。

Hermes 入口把这个运行时边界补得更完整。Provider README 直接列出生命周期映射: prefetch(query) 同步打到 POST /recall,拿回可注入的 <memory-context>sync_turn(user, assistant) 后台打到 POST /capture,最多保留 4 个并发 capture;shutdown() 或 session end 会触发 POST /session/end,把未完成的管线工作刷完。也就是说,Hermes 看到的是一个 Python memory provider,真正的记忆管线仍然由本地 Node Gateway 承接。参考 Hermes lifecycle mapping

它还承担了 HTTP 客户端之外的运行保障。provider 里有熔断、capture 背压、Gateway 自动发现和受监督启动; supervisor.py 还会把 Hermes 侧的环境变量镜像给 Gateway,避免 Windows 原生安装要维护两套配置。 子进程日志写入文件,避免 stdout/stderr 管道塞满导致 Gateway 卡死;Windows 上关闭时按进程树终止。 这些细节解释了为什么 memory 系统不能只讲“怎么抽取和召回”:只要它在本地 agent 里运行,Gateway 是否健康、 provider 是否能恢复、capture 是否会堆爆线程,都会直接影响下一轮能不能拿到记忆。源码可看 Gateway 启动与日志处理关闭与 Windows 进程树处理

TencentDB Agent Memory 分层机制图,上方是 context offload,下方是 L0 到 L3 长期记忆
这篇的主线在于两条压力如何共用一个分层策略:短期任务先卸载,长期用户记忆再沉淀。

二、短期记忆:Context Offload 是可恢复的上下文卸载

2.1 先把厚重工具结果搬离模型视野

Offload 的文件层很直接。storage.ts 的注释说明,不同 agent 使用独立目录,同一 agent 共享 mmds/refs/state.json,每个 session 有自己的 offload-<sessionId>.jsonl;路径构造里也能看到默认根目录、refsDirmmdsDir 与 session JSONL 的关系。看 storage.ts 顶部设计说明StorageContext 构造

这一步解决的不只是 token 数。工具结果最麻烦的地方,是信息密度不均匀:有些行只是执行噪音, 有些行却是关键报错、文件位置或设计决策。TencentDB-Agent-Memory 不急着让模型永远背着完整日志走。 它把原文放到外部文件和 JSONL 索引里,让模型当前只看摘要和任务结构。

export interface OffloadEntry {
  timestamp: string;
  node_id: string | null;
  tool_call: string;
  summary: string;
  result_ref: string;
  tool_call_id: string;
  score?: number;
}

export async function writeRefMd(ctx, timestamp, toolName, content) {
  const filename = `${isoToFilename(timestamp)}.md`;
  const filePath = join(ctx.refsDir, filename);
  await writeFile(filePath, header + safeContent, "utf-8");
  return `refs/${filename}`;
}

一条工具结果在这里被拆成两部分:原文进入 refs/*.md,索引进入 OffloadEntrysummary 给模型快速理解,result_ref 给系统恢复细节, score 用来判断后续 L3 压缩能不能用摘要替代原文。

2.2 L1 写步骤摘要,L2 把步骤变成任务拓扑

这里的 Offload L1 指工具结果摘要层,和长期 memory 的 L1 atom 是两套概念。l1-prompt.ts 要求 LLM 把 tool call/result 合成 JSON 数组,每条包含 tool_callsummarytool_call_id、时间戳和 score。prompt 还要求摘要说清“发现了什么关键线索”“做了什么关键动作”“遇到了什么具体报错”。 这可以看 L1 摘要 prompt

L2 再把这些 offload entries 组织成 Mermaid flowchart。l2-prompt.ts 要求模型避免流水账, 用少量节点表达任务状态,合并连续常规动作,保留关键转折和失败路径,并把每个 tool_call_id 映射到一个 node_id。同一个文件里还规定了 writereplace 两种更新方式,以及 4000 字以内的 MMD 控制目标。 参考 L2 MMD 生成规则新 offload entries 的输入格式

const req: L2Request = {
  existingMmd,
  newEntries: batch.map((e) => ({
    tool_call_id: e.tool_call_id,
    tool_call: e.tool_call,
    summary: e.summary,
    timestamp: e.timestamp,
  })),
  recentHistory,
  currentTurn,
  taskLabel,
  mmdPrefix,
};

// L2 返回 node_mapping,把每个 tool_call_id 挂到一个 Mermaid 节点上。

这一步解释了 MMD 图为什么有用。它承担的是把“很多条工具调用”合并成“少量任务节点”的工作。 例如连续查看五个文件,L2 可以合成一个“定位实现边界”的节点;一次测试失败和一次修复,则应该保留成两个有方向关系的节点。 只要 node_mapping 保留,模型就能从任务图下钻到具体工具调用。

工具原文:refs/*.md

保存长日志、报错、搜索结果、文件片段。它不直接塞回模型,但可以按引用路径找回。

步骤索引:offload-*.jsonl

记录每次工具调用的摘要、时间、tool_call_id、引用文件和后续 node_id 映射。

任务画布:mmds/*.mmd

把许多工具调用压成少量任务节点。模型看结构,必要时再沿 node_id 下钻。

2.3 L3 真正改写进入模型的上下文

真正发生“卸载”的地方在进入模型前的消息处理。before-prompt-build.ts 顶部把流程分成三段: 快速重放已经确认的替换和删除、按 token 阈值做 L3 压缩、最后注入 MMD。代码里会读取 offload entries, 把确认可替代的 tool result 换成摘要;如果超过 aggressive 阈值,则删除更老的工具消息,并用历史 MMD 作为补偿; mild 阶段则按分数级联替换。对应源码在 三阶段注释核心处理逻辑

MMD 注入是另一个关键点。mmd-injector.ts 只注入当前活跃的 MMD,历史 MMD 主要在 aggressive 删除后作为丢失上下文的替代。 注入时还会按 mmdMaxTokenRatio 计算预算,并把 MMD 作为带标记的 user message 插到合适位置。 这样模型看到的是“当前任务图”,不再被一串已经滚远的工具日志占满。可以看 MMD 注入策略注入实现

// before_prompt_build 的三段逻辑
// 1. 已确认可替换的 tool_result -> 摘要
if (entry && isToolResultMessage(msg)) {
  replaceWithSummary(msg, entry);
  msg._offloaded = true;
}

// 2. 超过 aggressive 阈值时删除更老工具消息,并注入 history MMD
const result = await aggressiveCompressUntilBelowThreshold(...);

// 3. 注入当前 active MMD
await injectMmdIntoMessages(messages, stateManager, logger, getContextWindow, pluginConfig);
[Offloaded Tool Result | node: 012-N3]
Summary: 测试失败集中在连接池重试逻辑,超时错误可复现
result_ref: refs/2026-07-02T10-21-33.md (read this file for full tool call and raw result)

所以 L3 的动作有轻有重:mild 阶段把工具结果换成摘要;aggressive 阶段会删除更老的工具消息, 再用历史 MMD 补上被删掉的任务结构;最后 active MMD 进入消息数组。模型看到的是“当前任务为什么走到这里”, 避开“过去每一次命令完整输出了什么”的负担。

三、长期记忆:L0 到 L3 是另一条语义金字塔

3.1 L0 保留对话,L1 抽出结构化事实

长期记忆这边,L0 是原始对话记录,L1 才是结构化 memory。l1-extractor.ts 的文件注释直接写出管线: 从 L0 读取近期消息,调用 LLM 做场景切分和 memory extraction,再做批量冲突检测,最后写 L1 JSONL。 实现里还会先通过质量过滤,只把值得提取的消息送给 LLM,并把新消息和少量 background messages 分开。 源码见 L1 extractor 总流程过滤、抽取、展平步骤

const qualifiedMessages = messages.filter((m) => shouldExtractL1(m.content));
const newMessages = qualifiedMessages.slice(-maxNewMessages);
const backgroundMessages = qualifiedMessages.slice(...);

const scenes = await callLlmExtraction({
  newMessages,
  backgroundMessages,
  previousSceneName,
});

// scenes -> memories -> L1 records

这段代码说明 L1 有自己的筛选门槛。L0 可以保留原始对话,但 L1 会先过质量过滤, 再把最近消息和背景消息分开交给 LLM。这样做能减少噪声,也让模型知道当前这一段和上一段场景之间的关系。

Dedup 没有走全量 JSONL 扫描。l1-dedup.ts 注释说,v4 移除了 JSONL Jaccard fallback, 候选召回主要靠向量搜索,降级到 FTS5 BM25,如果两者都不可用就跳过冲突检测直接写入。这个选择很工程化: 避免每次写 memory 都做 O(N) 文件扫描,把“可能冲突的旧记忆”先用检索缩小,再交给 LLM 批量判断 store、skip、update、merge。 参考 L1 dedup 注释三层候选策略

新 L1 memory 写入前:
  1. vector recall 找 top-k 候选旧记忆
  2. 没有向量能力时,用 FTS5 BM25 找关键词候选
  3. 两者都没有时,跳过冲突检测,直接 store
  4. LLM 只比较“新记忆 + 候选池”,批量判断 store / skip / update / merge

3.2 L2 是场景日记,L3 是用户画像

L2 的场景抽取由 SceneExtractor 完成。L1 memory 到这里不再继续堆成列表,系统会让 LLM 在受限的 scene_blocks/ 目录里读写 Markdown 场景文件。文件注释写得很明确:先备份和加载 scene index, 再用 memories 与 scene context 组装 prompt,随后用开启工具的 runner 在沙箱目录里操作场景文件,最后清理软删除、同步索引、更新导航。 看 SceneExtractor 说明

L3 的 PersonaGenerator 则读取发生变化的场景内容,生成或增量更新 persona.md。 它会剥离旧导航、读取 changed scenes,把 prompt 交给允许文件工具的 runner,最后把工程生成的 scene navigation 追加回 persona。 因此高层画像有清楚来源:先有 L2 场景文件,再继续向上抽象。相关逻辑可以看 generateLocalPersona

新版 prompt 还把“输出语言”写成了明确契约。L2 场景抽取会从新记忆里判断主导语言,场景文件名、 Markdown 标题和正文都跟着这个语言走;但 createdupdatedsummary 这些 META 字段以及 [DELETED] 这类系统标记继续保持英文。L3 画像也是同样思路: persona.md 的自然语言内容、标题和叙事段落跟随 changed scenes 的语言,文件名和结构标记保持稳定。 这会直接影响长期记忆是否可用:用户如果一直用英文或日文讨论,场景日记和画像也应该保持同一语言; 下游工程系统则继续依赖稳定的字段和标记。相关 prompt 在 Scene output language contract场景文件命名规则Persona output language contract

3.3 召回时,稳定画像和动态记忆分开注入

召回阶段也能看出它的层次。auto-recall.ts 顶部说明:L1 可以按 keyword、embedding、hybrid 搜索; L3 persona 会注入;L2 scene navigation 也会注入,由 LLM 决定是否进一步读取。真正组装上下文时, L3 persona、L2 navigation 和工具使用说明放在 stable system context,L1 relevant memories 则放到当前 user prompt 前缀。 这样做的好处是:画像和场景导航变化较慢,适合 prompt cache;L1 命中每轮都不同,放在动态区域更合适。 源码见 auto-recall 顶部说明stable/dynamic context 拆分

const stableParts: string[] = [];
if (personaContent) stableParts.push(`<user-persona>...</user-persona>`);
if (sceneNavigation) stableParts.push(`<scene-navigation>...</scene-navigation>`);

let prependContext: string | undefined;
if (memoryLines.length > 0) {
  prependContext = `<relevant-memories>\n${memoryLines.join("\n")}\n</relevant-memories>`;
}

return { appendSystemContext, prependContext };

这段拆分很关键。Persona 和 Scene Navigation 是慢变量,适合放进 system prompt 末尾,给 prompt cache 留机会; L1 检索结果每轮都变,放进 user prompt 前缀更自然。系统按变化频率安排位置,避免把所有记忆塞进同一个段落。

存储层也配合这个策略。sqlite.ts 管理 L1 结构化记忆和 L0 原始对话两层索引:关系表保存 metadata, sqlite-vec 虚表做 cosine similarity;同时还有 FTS5 和中文分词相关逻辑。搜索策略里 hybrid 会优先使用原生 hybrid, 否则并行跑 keyword 与 embedding,再用 RRF 合并。可看 SQLite store 设计注释recall 搜索调度

四、把它放回 Agent Memory 地图

如果按“谁拥有历史,谁组装模型视图”来看,TencentDB-Agent-Memory 应该放在 LangMem 之后、OpenViking 之前。 它和 LangMem 一样靠近 agent runtime,但 LangMem 重点讨论 memory 写入是在 hot path 还是 background manager; TencentDB-Agent-Memory 进一步把当前任务的工具历史也纳入管理,用 Context Offload 解决“还在同一轮任务里就已经太长”的问题。

项目 最核心的问题 TencentDB-Agent-Memory 的区别
Mem0 外置 memory layer 怎样写入、合并、检索长期记忆。 它不只关心跨会话记忆,还直接处理当前任务的上下文卸载。
LangMem / LangGraph 记忆动作放在 hot path 还是 background path。 它把工具日志压缩、MMD 任务图和 token 阈值接入运行时消息处理。
OpenViking 资源、记忆、技能和会话怎样进入统一上下文树。 它更专注当前任务的 tool-log offload 与 L0-L3 用户记忆;OpenViking 再把边界扩到统一寻址和分层检索。
Cognee / Supermemory 记忆如何成为多人、多源、多调用方的平台层。 它偏向本地插件和白盒文件系统,距离外部 context API 更远。

这也是它最值得单独写一篇的原因:很多 memory 讨论默认把“当前上下文”当作已经组装好的输入, 只讨论长期记忆怎么召回。TencentDB-Agent-Memory 把这个输入本身拆开了:哪些内容仍应留在模型视野, 哪些内容可以替换成摘要,哪些内容可以删掉但由 MMD 任务图代表,哪些内容必须能按 node_id 找回。

五、取舍:它把复杂度换成可恢复性

这种设计不轻。它引入了 L1、L1.5、L2、L3,多套文件目录,MMD、JSONL、SQLite、FTS、vector, 还依赖 runtime hook 能拿到足够完整的 messages。好处是证据链清楚:短期任务可以从 MMD 下钻到 JSONL 再到 refs; 长期画像可以从 persona 下钻到 scene,再到 L1 atom 与 L0 conversation。

新增的 disableThinking 配置也应该放在这个取舍里看。L1/L2/L3 抽取和 offload 摘要属于工具型 LLM 调用, 目标是稳定地产生结构化记忆,不一定需要每次都走长推理路径。配置层把 llm.disableThinkingoffload.disableThinking 分开; createNoThinkFetch 再按 vLLM、DeepSeek、DashScope、OpenAI o-series、Anthropic、Kimi、Gemini 等不同后端改写 chat-completion 请求体。它只处理带 messages 数组的聊天请求,embedding 和其他非 chat 请求会原样透传;OpenAI o-series 也只能降到 reasoning_effort: "low",不能完全关闭推理。 这层优化主要服务延迟和成本,不能替代前面的质量过滤、dedup、可恢复链路。源码见 LLM 与 offload 配置disableThinking 策略fetch wrapper 实现

所以它适合长任务、重工具调用、需要白盒追溯的 agent。若只是简单客服问答,接一个 memory API 可能更省事; 如果是代码 agent、数据库 agent、研究 agent,工具日志本身就是任务资产,Context Offload 的价值会变得明显。 它真正提供的是一种可追溯的上下文管理方式:agent 在上下文变厚时仍然知道自己做过什么、证据在哪里、下一步该从哪条线继续。

参考资料