很多 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_id、tool_call_id 和 result_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 Conversation、L1 Atom、L2 Scenario、L3 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_call、before_prompt_build、
llm_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 进程树处理。
二、短期记忆:Context Offload 是可恢复的上下文卸载
2.1 先把厚重工具结果搬离模型视野
Offload 的文件层很直接。storage.ts 的注释说明,不同 agent 使用独立目录,同一 agent 共享
mmds/、refs/ 和 state.json,每个 session 有自己的
offload-<sessionId>.jsonl;路径构造里也能看到默认根目录、refsDir、
mmdsDir 与 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,索引进入 OffloadEntry。
summary 给模型快速理解,result_ref 给系统恢复细节,
score 用来判断后续 L3 压缩能不能用摘要替代原文。
2.2 L1 写步骤摘要,L2 把步骤变成任务拓扑
这里的 Offload L1 指工具结果摘要层,和长期 memory 的 L1 atom 是两套概念。l1-prompt.ts 要求 LLM 把 tool call/result
合成 JSON 数组,每条包含 tool_call、summary、tool_call_id、时间戳和
score。prompt 还要求摘要说清“发现了什么关键线索”“做了什么关键动作”“遇到了什么具体报错”。
这可以看 L1 摘要 prompt。
L2 再把这些 offload entries 组织成 Mermaid flowchart。l2-prompt.ts 要求模型避免流水账,
用少量节点表达任务状态,合并连续常规动作,保留关键转折和失败路径,并把每个
tool_call_id 映射到一个 node_id。同一个文件里还规定了
write 和 replace 两种更新方式,以及 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 标题和正文都跟着这个语言走;但 created、updated、summary
这些 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.disableThinking 和 offload.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 在上下文变厚时仍然知道自己做过什么、证据在哪里、下一步该从哪条线继续。
参考资料
- TencentCloud/TencentDB-Agent-Memory
- README_CN:记忆分层、短期卸载、L0-L3 与可恢复链路
- OpenClaw plugin schema:独立 LLM 与 Context Offload 配置
- Hermes provider:生命周期映射、熔断、背压与自动发现
- Hermes supervisor:Gateway 环境变量、日志和启动
- Context Offload storage paths
- Offload L1 summarization prompt
- Offload L2 MMD prompt
- before_prompt_build L3 compression
- Long-term L1 extraction pipeline
- Scene prompt:输出语言与文件命名契约
- disableThinking fetch wrapper
- Auto-recall search and context assembly