三层记忆:`memory` 目录的分层与落盘

2026-08-10

「三层记忆」这四个字放在 README 的 Key Features 里很好听——原文把它写成「可检视记忆(L1/L2/L3)」(README.md:195-200)。但要真去读代码,光有这句话是没法下手的:三层各存什么?存到哪个目录?谁在写、谁在读?

好消息是这套东西在 DeepTutor 里落得相当实。分层边界不是靠文档约定的,是靠两条 Literal 类型钉死的;落盘位置有明确的目录常量;两个对模型开放的记忆工具,挂载规则还不一样。下面按文件与行号走一遍。

本文对应的仓库快照是 HEAD 456f9c2,采集日 2026-08-10,deeptutor/__version__.py 里的版本号是 1.5.11。行号会随版本漂,文件名和标识符是稳的。

一、先把三层的边界读出来

模块的自述在 deeptutor/services/memory/__init__.py:1-13:这是一个三层记忆子系统,trace 负责 L1,document 负责 L2 与 L3,另有 opspathsidsstoreconsolidator 五个模块,consolidator 是把 L1 往 L2、L2 往 L3 归并的那一环。

真正把边界定死的是 deeptutor/services/memory/paths.py:47-56 里的两条类型声明:

定义取值
L2Surfacechat / notebook / quiz / kb / book / partner / cowriter(7 个)
L3L3Slotrecent / profile / scope / preferences(4 个)

这两行是本文最值得你自己去看一眼的地方。它意味着 L2 不是「一堆记忆条目」,而是按来源切成七份——聊天一份、笔记本一份、测验一份、知识库一份、书一份、partner 一份、协作写作一份;L3 也不是「更久远的记忆」,而是四个固定槽位,槽位名字本身就说明了它装什么。

CLI 侧的口径与之对得上。deeptutor_cli/memory.py 只有 99 行、注册了 2 个命令,memory show 的 help 自述 L3 是「四个 global docs」、L2 是「七个 surface docs」(memory.py:25)。命令组的 help 文案是 View and manage lightweight memory.deeptutor_cli/main.py:36-46)。

顺带一个容易忽略的默认值:memory show [target] 的 target 默认是 L3,不是 L1(memory.py:21-27)。也就是说你不带参数敲这个命令,看到的是最上面那一层归并结果,不是原始事件。

二、清理命令的作用范围是限定的

另一个命令是 memory clear [target],target 默认 all,带 --force/-fmemory.py:63-70)。

这里的 all 需要照字面读一遍:它只删 memory 根目录下的文件,以及 traceL2L3 三个子目录里的文件(memory.py:81-90)。范围是明确列举的,不是「把这个目录整个端掉」。这两种语义的差别在什么时候会咬人,取决于你的目录里还有什么,本文不做推断——你要做的是在敲这条命令前,先按下一节的路径把目录列一遍,自己确认删的是哪些东西。

这是本机磁盘上的用户数据,包含学习过程中沉淀下来的内容。清理动作不可逆,--force 更是把确认环节跳过了。

三、落盘落在哪:两个都叫 memory 的位置

运行期目录布局写在 deeptutor/services/setup/init.py:109-141init_user_directories() docstring 里,结构是 data/user/{chat_history.db, logs/, settings/{interface.json, main.yaml, agents.yaml}, workspace/{notebook, memory, co-writer, book, chat/...}}

记忆落在 workspace/ 下面,和 notebookco-writerbook 平级。

docker-compose.dev.yml(51 行)的可写挂载列的是三项:data/userdata/memorydata/knowledge_basesdocker-compose.dev.yml:17-40)。把两处并排摆一下:docker-compose.dev.yml:17-40 的可写挂载里出现的是 data/memory,而 services/setup/init.py:109-141 的目录树里记忆位于 data/user/workspace/memory;这两个路径是否指向同一份数据,我们没有核实。

本文只负责把这两个位置指给你,谁对谁错、要不要放在一起理解,都不在我们读到的范围内。

四、反直觉的那一处:写常驻挂载,读要过门控

模型能碰到记忆,靠的是两个内置工具,都定义在 deeptutor/tools/builtin/__init__.pyread_memory(:751)和 write_memory(:782)。入参也不对称——read_memory 无参,write_memory 收四个参数 op / text / target_id / reason

真正反直觉的是它们的挂载规则,写在 deeptutor/agents/_shared/tool_composition.py 里:

  • write_memory 属于恒定挂载(always-on)的 5 个工具之一,另外四个是 web_fetchgithubask_usercrontool_composition.py:190-192)。
  • read_memory 属于条件挂载,条件是 has_memory;这张条件挂载表共 10 条,同表里还有 rag/kb_files←has_kbread_source←has_sourceslist_notebook/write_note←has_notebooks 等(tool_composition.py:46-57)。

按直觉,读是安全操作、写才该受限,这里恰好相反:写永远在工具面上,读反而要看用户有没有记忆。

这个顺序对着冷启动场景看就顺了:一个全新用户没有任何记忆文档,read_memory 挂上去也读不到东西,而 write_memory 必须常驻,否则第一条记忆永远写不进来。至于门控本身,user_has_memory()user_has_notebooks() 两个函数都是 fail closed 的,异常时返回 Falsetool_composition.py:215-249)。翻译成排查语言就是:门控出错的表现不是报错,而是 read_memory 悄悄没挂上。你看到的现象会是「它明明有记忆却不去读」,而不是一条异常。

同一个文件里还有两个更强的开关:forced 绕过所有门控直接追加工具,suppressed 则不论工具怎么进来都把它移除(tool_composition.py:149-155196-201)。partner runtime 就是用 suppressed 把 chat 的记忆工具换掉的。所以「为什么这个场景下模型没有记忆工具」这个问题,答案可能在门控,也可能在 suppressed,得两处都看。

记忆还会占上下文预算:deeptutor/agents/chat/context_budget.py:33-51 把 PromptBlock 映射成 13 个展示段,memory 是其中之一,和 system_promptpersona_styletool_manifestsourcesskills 等并列。这两个工具的提示词 hint 也各自有文件——hints/{en,zh}/ 下各 19 个 YAML,文件名与工具名一一对应,read_memorywrite_memory 都在其中。

五、6332 行是怎么摆的:归并被拆成四个 mode

deeptutor/services/memory/ 共 28 个文件、6332 行 Python。放在 services 的 26 个子领域里排第四,前面是 llm(9263)、rag(7539)、session(6677)。

consolidator/ 下有四个 mode(update/audit/dedup/merge),这四个 mode 文件合计约 1645 行,另配 14 个 YAML 提示词;memory 目录内部各子目录的行数分布我们没有统计,所以这里不排「哪一块最厚」。单看文件的话,consolidator/modes/update.py 有 706 行,是整个 services 层行数 Top 15 表里的最后一名。

从目录结构本身能看出来的只有一件事:从 L1 往上归并的那一步被拆成了四个独立的 mode,去重、合并、审计各占一个。四个 mode 同目录下另有 14 个 YAML 提示词文件;这些提示词具体怎么参与归并、四个 mode 是否都要调用模型,我们没有读提示词内容,不下结论。

必须说清边界:这四个 mode 与 14 个提示词,我们只看了文件名与目录结构,没有读提示词内容,也没有核实 L1→L2→L3 的具体算法。所以本文不描述”它是怎么决定要不要合并两条记忆的”,那一层我们没有依据。

六、v1 已经删掉了:唯一入口是 get_memory_store

memory/__init__.py:11-12 有一句原样写在 docstring 里的话:v1 那个两文件版的 MemoryService 已经没了,所有调用方都走 get_memory_store

这句话对读源码的人有直接价值:你在网上或旧文里看到的 MemoryService 用法,在这个快照里已经不成立,别照着找。

与之配套的还有一处迁移动作。API 服务的 lifespan 启动序列的最后一项,是迁移 v1 memory、并把 legacy 的 tutorbot memory surface 改名为 partnerdeeptutor/api/main.py:114-170)。回头看第一节那张 Surface 表里的 partner,它的历史名字就是 tutorbot

七、谁在用它

三个可核查的数字,摆在一起看比单看有意义:

  • deeptutor/api/ 层引用 deeptutor.services.memory23 处,在 api 引用最多的 services 子模块里并列第二(config 34、rag 23、memory 23)。
  • agents + core + tools 三层合计引用 memory 8 处
  • REST 侧 /api/v1/memory27 条路由。

前端路由也给了它相当的位置:(utility)/memory 下共 8 个页面路径——/memory/memory/graph/memory/l1/memory/l2/memory/l2/[surface]/memory/l3/memory/l3/[slot]/memory/resolve;另外 settings 的 26 个子页里也有一个 memory(连 /settings 自身共 27 页)。注意 /memory/l2/[surface]/memory/l3/[slot] 这两个动态段,正好对应第一节那两条 Literal 的取值。

这三组数字合起来说明的是:记忆子系统主要由 API 层与前端驱动,agent 侧只通过那两个工具接触它。排查时的方向也就清楚了——「记忆没写进去」优先看工具与门控,「记忆页显示不对」优先看 API 路由那 27 条。

八、「memory」这个词在这个仓库里指三种东西

这是搜代码时最容易串味的一处,单独拎出来说:

  1. 记忆子系统deeptutor/services/memory/,就是本文讲的这个。
  2. 进程内存deeptutor/runtime/memory_probe.py(343 行)观测进程树的常驻内存供状态展示,memory_reclaim.py(73 行)在高分配任务之后做尽力而为的内存回收。这两个和 L1/L2/L3 毫无关系。
  3. 调用点上的内存回收:BookEngine 的后台 worker 在队列空时会注销 runtime 并调用 schedule_memory_reclaim()bookengine.py:833-852)——名字里有 memory,做的是第 2 类的事。

所以在这个仓库里 grep -rn memory 会同时命中三类东西。要找记忆子系统,直接按 services.memoryget_memory_storeread_memorywrite_memoryL3SlotSurface 这些标识符搜,比搜 memory 干净得多。

九、你可以照着核的六步

  1. 打开 deeptutor/services/memory/paths.py:47-56,把 Surface 的 7 个值和 L3Slot 的 4 个值抄下来——后面所有路径和路由的动态段都从这两条来。
  2. 打开 deeptutor/services/memory/__init__.py:1-13,确认七个模块的分工,以及第 11-12 行那句 v1 已移除的说明。
  3. 打开 deeptutor/agents/_shared/tool_composition.py,先在 :190-192 确认 write_memory 在 always-on 的 5 个里,再在 :46-57 确认 read_memory 挂在 has_memory 上,最后看 :215-249 的 fail closed。
  4. 分别打开两处与落盘相关的位置:deeptutor/services/setup/init.py:109-141 的目录树、docker-compose.dev.yml:17-40 的 volumes,看清各自写的是哪个路径。
  5. deeptutor memory show 之前先意识到 target 默认是 L3;敲 clear 之前先按 memory.py:81-90 核清删除范围。上述命令语义均来自源码阅读,以官方文档与 --help 的实际输出为准。
  6. 想看归并逻辑,去 deeptutor/services/memory/consolidator/modes/ 下的四个文件与同目录的 YAML 提示词——那部分我们没读,需要你自己看。

最后交代边界:本文只读了 memory 目录的模块 docstring、paths.py 的类型声明、CLI 的命令定义与工具挂载表,store.pyops.pydocument.pytrace.py 的实现体与 consolidator 的算法都没有读;/api/v1/memory 那 27 条路由只统计了数量,没有逐条核实语义。凡是涉及”归并出来的内容质量如何""记忆对学习过程有什么效果”的部分,本文一律不下结论——那既超出我们读到的范围,也不是源码能回答的问题。文中出现的槽位划分、目录布局与挂载规则,都是这个快照里的实现选择,不是经过验证的通用结论。


本文依据 DeepTutor 官方仓库(github.com/HKUDS/DeepTutor)的 README、AGENTS.mdpyproject.tomldeeptutor/ 下的源码整理,核对日 2026-08-10,对应仓库快照 456f9c2(版本 1.5.11)。 本文内容为仓库源码与文档口径,我们没有安装、部署或运行过该项目,也没有调用过其中任何一个模型 API, 因此不涉及生成质量、响应速度与教学效果的任何描述。 参数与默认值随版本变动,请以仓库最新代码与 --help 的实际输出为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。