博主头像
Kurfuerst

ワクワク

长期记忆系统的分层实践:TencentDB Agent Memory

最近在做agent memory相关的工作,在memory该如何分层这个问题上纠结了许久,考虑到要让分层有效,还要避免过度设计。就在此时,腾讯开源了TencentDB Agent Memory。

TencentDB Agent Memory分为两个部分:长期记忆与短期记忆。由于我的工作主要与长期记忆相关(短期记忆我理解更接近上下文管理)这里我们只关注TencentDB Agent Memory的长期记忆部分。

这套记忆系统的长期记忆部分主要分成了四层:L0 Conversation -> L1 MemoryRecord -> L2 Scene Block -> L3 Persona

Memory系统的必要性

如果只把所有历史对话切片后写入向量库,早期确实能解决一些问题,用户提到过的事实可以被召回,模型也能在一定程度上延续上下文。

但它很快会暴露几个问题:

第一,原始对话关注点不单一。聊天里有临时请求、工具调用、模型自己的回答,还有很多只在当轮有效的指令。这类信息进入memory后会污染召回结果

第二,单条向量结果碎片化严重。用户在某一场景下的长期偏好需要在多轮对话中总结才能得到。比如用户反复要求“给出调用链”“查询某一看板的数据”“请给出具体的代码”,只保存单条memory难以得到这些规律/偏好。

第三,摘要不可追溯。如果只维护一个全局用户画像,画像会变得很干净,但看不到记忆的底层来源,也看不到过期时间以及是否有新的信息与之冲突,导致出了问题很难进行排查回溯。

第四,上下文预算有限。长期记忆会逐渐增多,不能每轮把所有内容塞进 prompt。需要一种渐进式使用方式——先自动注入最有价值的内容,需要时再下钻。

所以这个项目把长期记忆拆成四层。低层保留证据和可检索事实,高层保留结构、叙事和稳定画像。

memory作用时机

长期记忆由Agent在生命周期中主动进行管理,主要在两个时间点使用memory。

第一是在 prompt 构建前。每轮对话开始前,系统会执行 before_prompt_build,进入 handleBeforeRecall(),把和当前输入相关的长期记忆注入上下文。这里会召回 L1、读取 L3、生成 L2 navigation。

第二是在一轮 Agent 结束后。agent_end 会进入 turn committed 流程,先写入 L0 原始对话,再调度后续的 L1/L2/L3 pipeline。

也就是说,Agent会在当前轮用已有记忆,当前轮结束后又产生新记忆,使长期记忆不断迭代。

从用户侧暴露出来的主动工具主要有三个:

  • tdai_memory_search:搜索 L1 结构化记忆。
  • tdai_conversation_search:搜索 L0 原始对话。
  • read_file:在 Scene Navigation 指引下读取 L2 scene block 全文。

自动召回解决“模型默认应该知道什么”,工具主动搜索解决“当前任务需要更深证据时如何下钻”。两者结合后,能够较好的平衡信息完整性与上下文长度。

四层记忆各自解决什么问题

L0 是原始对话证据层。它保存用户和 assistant 的真实消息,按日期写入 conversations/YYYY-MM-DD.jsonl,同时可以同步到 TCVDB 的 ${database}_l0_conversations collection。L0 的价值不是“好读”,而是“可追溯”:当高层记忆有疑问时,可以回到原始消息。

L1 是结构化事实层。它把 L0 中值得长期保存的信息抽成 MemoryRecord,只保留三类:personaepisodicinstruction。这一步相当于把混杂对话变成可检索、可排序、可去重的事实单元。

L2 是场景叙事层。它会把 L1 事实整合到 scene_blocks/*.md 中。一个 scene block 通常对应一个主题或场景,比如“工程协作偏好”“职业发展与项目实践”“生活习惯与节奏”。它会包含摘要、热度、核心叙事、演变轨迹和待确认点。

L3 是全局用户画像层。它是单个 persona.md,从多个 L2 场景中提炼跨场景稳定结论,例如长期偏好、沟通协议、决策逻辑和深层洞察。L3 是最适合每轮注入给 Agent 的高层上下文。

这四层是不同抽象层级。

L0 和 L1:提炼长期信号

虽然这篇文章重点不是 L0 到 L1 的抽取细节,但理解 L2/L3 前,需要知道 L1 是什么样的输入。

L1 的基本结构是:

interface MemoryRecord {
  id: string;
  content: string;
  type: "persona" | "episodic" | "instruction";
  priority: number;
  scene_name: string;
  source_message_ids: string[];
  metadata: {
    activity_start_time?: string;
    activity_end_time?: string;
  };
  timestamps: string[];
  createdAt: string;
  updatedAt: string;
  sessionKey: string;
  sessionId: string;
}

例如用户说:

以后回答我项目里的实现问题时,尽量给出具体文件路径和调用链,不要只讲概念。

它可能被抽成一条 instruction

{
  "content": "用户要求 AI 以后回答项目实现问题时尽量给出具体文件路径和调用链,不要只讲概念。",
  "type": "instruction",
  "priority": 90,
  "scene_name": "工程协作偏好",
  "source_message_ids": ["message-id"],
  "metadata": {}
}

L1 的三类记忆不是随便分的。它们对应的是长期记忆里三种不同的使用方式。

persona 记录稳定属性、偏好、技能、价值观和习惯。它的典型触发是“喜欢”“习惯”“经常”“我这个人”。健康、禁忌、核心特质这类强影响信息优先级最高,普通偏好和技能次之,模糊弱信号可以丢弃。

episodic 记录客观发生的动作、决定、计划和结果。它强调“发生过什么”,而不是“用户感觉如何”。如果能根据消息时间推断出活动时间,会写入 metadata.activity_start_timemetadata.activity_end_time,方便后续按时间理解事件。

instruction 记录用户对 AI 的长期行为要求,比如格式偏好、语气控制、回答规则。它的触发词通常是“以后都”“从现在开始”“记住”“必须”。这里还支持一种很高优先级的严格规则:当某条指令是全局死命令时,priority 可以使用特殊高约束值,表示它比普通偏好更像协议。

这三类的边界让 L1 在召回时更容易被模型理解:同样是“用户要求给调用链”,作为 instruction 被召回时,模型知道它不是一个历史事件,而是未来回答应该遵守的行为规则。

L1 会写入本地 records/YYYY-MM-DD.jsonl,也会写入 ${database}_l1_memories。其中本地 JSONL 是 append-only 的,便于恢复和审计;TCVDB/VectorStore 则用于召回和去重。

这里有一个重要设计:L1 会去重、更新、合并。系统会先检索相似旧记忆,再让 LLM 判断是 storeskipupdate 还是 merge。如果是 update/merge,旧记录会从 VectorStore/TCVDB 中删除,新记录再写入。JSONL 文件仍然保留追加历史,但在线检索面以最新结果为准。

从 L1 到 L2:把事实变成场景

如果 L1 已经是结构化事实,为什么还要 L2?

因为事实本身是。长期记忆真正有价值的地方,往往来自事实之间的关系:一个偏好如何逐渐稳定,一个决定如何影响后续行动,一个主题如何从兴趣变成长期工作流。

L2 的目标就是把碎片事实整合成场景叙事。

L2 runner 每次运行时,不会全量读取所有 L1,而是按 cursor 增量读取。优先从 VectorStore/TCVDB 查询某个 session 下 updatedAfter 之后的 L1;如果远程存储不可用,则回退到本地 JSONL。

传给 L2 的输入会被压缩成很小的结构:

interface L2InputMemory {
  content: string;
  created_at: string;
  id?: string;
}

也就是说,L2 只关心“事实内容、创建时间、证据 id”。它不需要把这些事实融入到场景文件中。

整体流程大致是:

读取增量 L1
-> 读取 scene_index.json
-> 根据已有 scene 构造摘要和可读文件清单
-> 构造 L2 prompt
-> 让 LLM 在 scene_blocks/ 沙箱里读写 Markdown 文件
-> 工程侧清理 [DELETED] 文件
-> 重建 scene_index.json
-> 同步 L2 profile 到 TCVDB

这里的 LLM 不是普通“返回一段文本”的调用,而是工具型 LLM。它的工作目录被限制在 scene_blocks/,只能操作 .md 场景文件,看不到 .metadata/checkpointpersona.md 等系统文件。这个沙箱边界很关键:L2 的生成具有一定自治能力,但不能越权修改系统状态。

L2 prompt 里对它的定位很明确:它是 Scene Diaries,不是记忆清单。它要做的是 Create、Integrate、Rewrite,也就是创建新场景、整合进已有场景、重写场景叙事。禁止把 L1 逐条追加成列表,因为那样只是换了个文件保存碎片,不能形成长期上下文。

L2 prompt 会要求模型优先做 UPDATE,其次 MERGE,最后才是 CREATE。

这背后的工程考虑很现实:如果每来一批 L1 都新建一个 scene,L2 很快会失控。长期记忆必须“浓缩”,而不是无限增长。

具体策略是:

  • 能融入已有场景,就读取旧 scene,整体重写或局部编辑。
  • 主题重叠、叙事弧线相同,或者 scene 数量接近上限时,主动合并。
  • 只有新主题确实无法融入现有 scene,才创建新文件。
  • 达到 maxScenes时,必须先 merge,不能继续 create。
  • 合并后的旧文件必须写成 [DELETED],工程侧随后真正删除。

一个 L2 scene block 大概长这样:

-----META-START-----
created: 2026-05-15T10:00:00.000Z
updated: 2026-05-15T10:10:00.000Z
summary: 用户偏好工程实现解释包含调用链、具体文件和真实依据
heat: 3
-----META-END-----

## 用户核心特征

用户在工程问题中明显偏好证据驱动的解释方式,希望回答能从真实入口、调用链和边界条件展开,而不是停留在概念层面。

## 用户偏好
- 回答项目实现问题时给出具体文件和调用链
- 更信任基于当前代码路径的解释

## 核心叙事
用户多次在项目分析任务中要求先阅读真实代码,再说明实现链路。这说明他关注的不只是结论,而是结论能否被源码证据支撑。随着讨论深入,这一偏好从“希望给路径”扩展为“希望说明入口、状态流转、边界条件和维护机制”。

## 待确认/矛盾点
- 暂无

注意,L2 不是简单把 L1 追加成列表。它会重写正文,把新记忆和旧内容融合起来。如果新旧信息冲突,不直接覆盖,而是写入“演变轨迹”或“待确认/矛盾点”。

这也是 L2 和向量库摘要最大的区别:它保留了叙事结构,也保留了未来继续演化的位置。

从 L2 到 L3:把场景变成画像

L3 不是直接从 L1 生成的,而是从 L2 scene blocks 生成的。

这点很重要。L1 是事实,L2 是场景,L3 才是画像。如果直接从 L1 生成 persona,很容易得到一堆没有层次的偏好列表;而从 L2 生成,模型看到的是已经整理过的主题叙事,更容易提炼出跨场景稳定模式。

L3 的生成由 PersonaTrigger 控制,不是每次 L2 更新都立刻运行。触发条件包括:

  • L2 显式发出 PERSONA_UPDATE_REQUEST
  • 已经有 scene,但还没有 persona,冷启动首次生成。
  • persona.md 正文丢失,需要恢复。
  • 第一次 scene block 提取完成。
  • 新记忆数达到 persona.triggerEveryN

生成时,PersonaGenerator 会读取 checkpoint、现有 persona.mdscene_index.json,再找出自上次 persona 更新时间之后发生变化的 scene blocks。只有这些变化的场景会被重点塞进 prompt。

核心流程可以概括为:

读取现有 persona,并剥离末尾 Scene Navigation
-> 读取 scene_index.json
-> 找出 updated > last_persona_time 的 scene blocks
-> 读取这些 scene 的完整 Markdown
-> 判断 first / incremental 模式
-> 构造 persona prompt
-> LLM 只允许写 persona.md
-> 工程侧重新读取 persona.md
-> 剥离模型可能误写的 navigation
-> 追加工程生成的 Scene Navigation
-> 同步 L3 profile 到 TCVDB
-> 更新 checkpoint

L3 prompt 使用的是“四层深度扫描”模型:

关注点价值
基础锚点确凿事实、当前状态提供基础上下文
兴趣图谱用户投入注意力的事物支持闲聊和生活化理解
交互协议沟通习惯、工作流偏好、雷区指导 Agent 如何回答
认知内核决策逻辑、矛盾点、驱动力支持更深层判断

最终 persona.md 更像一份“用户叙事画像”,而不是字段表。它会记录用户的长期偏好、沟通策略、决策逻辑和一些跨场景洞察。

例如:

# User Narrative Profile

> **Archetype**: 一位强调证据链和真实实现路径的工程型使用者。

> **长期偏好**
- 解释项目实现时,应优先给出入口、调用链和关键文件。
- 不喜欢只给概念性总结,更看重当前代码中的真实行为。

L3 会被自动注入到后续对话中,所以它需要足够短、足够稳定、足够高信噪比。项目里对它有大约 2000 字符的压缩约束,也是为了避免 persona 自身变成新的上下文负担。

L2 和 L3 会不会重复

会有信息重叠,但它们不应该是同一种内容。

更准确地说:语义上允许少量重复,因为 L3 本来就是 L2 的摘要和抽象;存储和职责上不应该冲突,因为两者的读写路径、粒度和使用方式不同。

L2 是“主题档案”。它可以保留更多过程、局部背景、矛盾点和演变轨迹。一个用户可以有多个 L2,每个 L2 对应一个较稳定的场景。

L3 是“全局画像”。它只保留跨场景稳定、对未来交互有持续价值的信息。它不应该保存每个事件的细节,也不应该重复所有 scene 的 summary。

可以用一个简单标准区分:

如果这条信息只在某个主题下有用,进入 L2。
如果这条信息会影响 Agent 以后几乎所有回答方式,进入 L3。

记忆如何被用起来

长期记忆如果只会存,不会用,就只是日志系统。

这个项目的自动召回发生在每轮 prompt 构建前,核心注入三类内容:

1. 直接召回相关 L1 memories
2. 注入 L3 persona 正文
3. 注入 L2 scene navigation

L1 召回使用当前用户输入作为 query,策略由 recall.strategy 控制,可以是 keywordembeddinghybrid。如果后端是 TCVDB 且支持 native hybrid search,就使用服务端 dense + 可选 sparse + RRF 的混合检索。

注意,自动召回不会按 persona/episodic/instruction 三类分别召回,。type 只是召回结果上的标签,帮助模型理解这条记忆的性质。

L2 则不会全文注入。系统只注入 Scene Navigation,大致包含每个 scene 的路径、heat、更新时间和 summary。模型如果觉得某个场景和当前任务相关,可以再调用 read_file 读取对应 scene block。这是一种渐进式披露:

默认给 Agent 全局画像和场景目录
需要细节时再打开具体场景
再需要证据时可以搜索 L1 或 L0

L3 的使用方式更直接。只要 persona.md 存在且正文非空,就整体注入。因为 L3 已经是压缩后的稳定画像,适合成为每轮对话的长期背景。

在实际 prompt 注入里,L2 navigation 更像一张目录。它提供 scene 的路径、热度和摘要。对于本地运行的 Agent,navigation 里的路径可以是绝对路径,方便直接用 read_file 下钻;在面向外部读者解释时,可以把它理解为“可展开的场景索引”。

这种方式避免了长期记忆占用大量上下文的问题。

整合与遗忘:长期记忆必须会变

用户偏好会变化,旧信息会变得不重要,不同记忆之间可能冲突。如果系统只会追加,最后一定会变成一堆过期事实。

这个项目里有几类维护机制。

L1 通过去重决策维护。新记忆进来后,会和相似旧记忆比较。如果旧记忆更好就 skip,如果新记忆更具体就 update,如果两边互补就 merge。update/merge 会删除检索库里的旧 id,再写入新记录。

L2 通过场景合并和重写维护。它默认 UPDATE,接近 maxScenes 时主动 MERGE,达到上限时必须先合并。旧 scene 通过 [DELETED] 软删除,再由工程侧清理。

L3 通过摘要压缩维护。它不会保留所有细节,而是不断把变化的 L2 场景重新压缩进 persona.md。过时、不重要、低价值的细节会在重写中自然消失。

TTL 清理只作用于 L0/L1,由 capture.l0l1RetentionDays 控制。默认值是 0,也就是不清理。系统没有独立的“记忆衰减分数”,也没有按时间自动删除 L2/L3 的机制。

这是一种比较保守的设计:低层证据默认保留,高层画像通过压缩和合并控制规模。

TCVDB 在这里扮演什么角色

这个系统采用的是“本地文件 + TCVDB”的混合模式。

本地文件的价值是可读、可恢复、可调试。conversations/records/ 保留原始证据和追加历史;scene_blocks/persona.md 可以直接给人看,也方便排查 LLM 生成质量。

TCVDB 的价值是检索和同步。L0/L1 写入各自的 collection,用于搜索、召回、去重候选;L2/L3 写入 ${database}_profiles,通过 type = "l2"type = "l3" 区分,用于跨环境同步。

总结

这套长期记忆设计代理复杂度,但是总体提升了memory的可用性以及可控性。

L2/L3 都需要 LLM 参与生成,还需要文件沙箱、checkpoint、profile 同步和清理逻辑。

但它解决了长期记忆里最关键的几个工程问题:

  • 有证据层,不会只剩不可追溯摘要。
  • 有结构化事实层,能召回、去重、更新。
  • 有场景层,能把碎片事实组织成主题叙事。
  • 有画像层,能把长期稳定模式注入日常对话。
  • 有渐进式召回,不把所有记忆一次性塞进上下文。
  • 有合并和压缩机制,避免长期记忆无限膨胀。

如果把 Agent 的长期记忆看作一个“第二大脑”,那 L0 是原始笔记,L1 是卡片,L2 是主题档案,L3 是个人画像和交互手册。这些层之间有清晰的边界、生成方式和召回方式,这让这个memory系统真正可用。

长期记忆系统的分层实践:TencentDB Agent Memory
http://blog.kurfuerst.online/index.php/archives/47/
本文作者 Großer Kurfürst
发布时间 2026-04-16
许可协议 CC BY-NC-SA 4.0
发表新评论