第 1 章 · 三层记忆
本章讲清 Letta 记忆系统的三层:core(常驻、可自编辑)、recall(历史消息)、archival(长期知识)。这是理解 Letta 的地基。
1.1 Core memory:可被 agent 自己改写的「常驻内存」
它要解决的小问题: 怎么让 agent 始终记得「最关键的几条事实」(你是谁、它是谁),而不依赖搜索?
思路: 把这几条事实做成记忆块(memory block),直接嵌进系统提示——模型每一步都看得到,根本不用搜。代价是:有字符上限,装不下太多。
记忆块长什么样
一个块就是一条带元数据的文本。核心字段(letta/schemas/block.py:18-39):
| 字段 | 含义 |
|---|---|
label | 块的名字,如 human / persona,会变成系统提示里的 XML 标签 |
value | 实际内容(就是文本) |
limit | 字符上限,默认 CORE_MEMORY_BLOCK_CHAR_LIMIT = 100000(constants.py:435) |
description | 这个块「该如何影响 agent 行为」的说明 |
read_only | 是否只读(agent 不能改) |
两个默认块来自 ChatMemory(letta/schemas/memory.py:840-854):persona(agent 的人设)和 human(关于用户的事实)。
它怎么被渲染进上下文
Memory.compile() 把所有块拼成一段 XML 喂进系统提示(letta/schemas/memory.py:688)。标准渲染逻辑在 _render_memory_blocks_standard(memory.py:143),产出大致是:
<memory_blocks>
The following memory blocks are currently engaged in your core memory unit:
<human>
<description>
关于用户的事实
</description>
<metadata>
- chars_current=42
- chars_limit=100000
</metadata>
<value>
Name: Timber. 养了一条狗。
</value>
</human>
...
</memory_blocks>
为什么连 chars_current / chars_limit 都告诉模型? 因为块是模型自己改的——它得知道还剩多少空间,才知道该不该精简。这是个贴心的设计细节(memory.py:164-165)。
巧妙处:给 Anthropic 模型的「行号视图」
对某些 agent 类型 + Anthropic 模型,渲染会切换成带行号的版本 _render_memory_blocks_line_numbered(memory.py:175),每行前面加 1→ 2→ …。判断逻辑在 compile() 里(memory.py:696-702):
# 示意,非源码:只有「特定 agent 类型 + Anthropic 模型」才用行号
is_line_numbered = is_line_numbered_agent_type and is_anthropic
行号是给模型编辑时定位用的——但有个坑:模型可能手贱把 1→ 当成内容写进编辑参数里。所以编辑工具会主动检测并拒绝带行号前缀的输入(见下文 memory_replace)。
1.2 agent 怎么自己改 core memory
这是 MemGPT 的标志性能力:agent 用工具改自己的记忆。工具定义在 letta/functions/function_sets/base.py。
最经典的两个:append 和 replace
core_memory_append(base.py:246)和 core_memory_replace(base.py:263)——逻辑朴素到一句话:取出块的 value、改字符串、写回。
# 真实源码节选,letta/functions/function_sets/base.py:263 core_memory_replace
current_value = str(agent_state.memory.get_block(label).value)
if old_content not in current_value:
raise ValueError(f"Old content '{old_content}' not found ...")
new_value = current_value.replace(str(old_content), str(new_content))
agent_state.memory.update_block_value(label=label, value=new_value)
上面这段就是「替换记忆里某句话」的全部实现:找到旧串、replace、写回块。删除信息就把 new_content 传空串。
更稳的版本:memory_replace(防误用)
memory_replace(base.py:311)是 replace 的加固版,借鉴了 Anthropic computer-use 的文件编辑工具。它多做了几件事(base.py:343-373):
- 拒绝行号前缀。 如果
old_string里含Line 12:这种或行号警告语,直接报错——防止模型把视图用的行号当内容(base.py:345-352)。 - 要求唯一匹配。 如果
old_string在块里出现了 0 次或多次,报错并告诉你它在哪几行,逼模型给出能唯一定位的片段(base.py:363-373)。
# 真实源码节选,base.py:363 —— 要求 old_string 唯一,否则拒绝
occurences = current_value.count(old_string)
if occurences == 0:
raise ValueError(f"... did not appear verbatim ...")
elif occurences > 1:
lines = [idx + 1 for idx, line in enumerate(...) if old_string in line]
raise ValueError(f"Multiple occurrences ... in lines {lines}. Please ensure it is unique.")
重点看:这和代码编辑工具里的「精确匹配 + 唯一性」是同一招——把 LLM 容易出错的「模糊定位」逼成「精确定位」,失败就给可操作的报错。
还有一组更高层的编辑工具
同文件里还提供了 memory_insert(按行插入,base.py:391)、memory_rethink(整块重写,base.py:488)、memory_apply_patch(unified-diff 风格补丁,可跨块增删改,base.py:453)。它们覆盖从「改一行」到「重组整块」的不同粒度。memory(base.py:10)是个统一的多子命令入口(create/str_replace/insert/delete/rename),实际由服务端 executor 实现(base.py 里函数体是 raise NotImplementedError,真正逻辑在 services/tool_executor/core_tool_executor.py 的 LettaCoreToolExecutor.execute(:29)——它按工具名把 memory_replace(:346)、memory_insert(:683)、core_memory_append(:319)等分派到各自实现)。
1.3 Recall memory:数据库里的全部历史
它是什么: 这个 agent 收发过的所有消息,持久化在数据库。上下文里只放最近一段;更早的要搜。
怎么搜: conversation_search(base.py:87)。它支持按文本 + 语义混合搜,还能按角色(assistant/user/tool)和日期范围过滤。底层就是调 message_manager.list_messages_for_agent(... query_text=query ...)(base.py:142)。
# 真实源码节选,base.py:142 —— conversation_search 的核心一步
messages = self.message_manager.list_messages_for_agent(
agent_id=self.agent_state.id, actor=self.user,
query_text=query, roles=roles, limit=limit,
)
系统提示里会用一行元数据告诉模型「recall 里还有多少历史消息」,提示它可以去搜(prompts/prompt_generator.py:74)。
1.4 Archival memory:带向量的长期知识库
它要解决的小问题: core memory 有字符上限,装不下大量知识;recall 是「对话流水」,不适合存「我想永久记住的提炼后的事实」。archival 就是后者的家。
思路: agent 用 archival_memory_insert 主动写入自包含的事实/摘要,带上标签;系统给它算向量嵌入存进数据库。要用时 archival_memory_search 按语义相似度 + 标签搜回来。
工具的设计很 「教学化」——docstring 里直接写明 best practice(base.py:164-189):存自包含的事实而非对话碎片、加描述性标签、用于会议纪要/项目进展/总结。
写入路径里的两个实现细节
看 passage_manager.py(passage = 归档记忆的一条):
- 向量维度对齐。 写 Postgres 向量库时,嵌入会被补零 pad 到
MAX_EMBEDDING_DIM = 4096(constants.py:93),因为 pgvector 列是定长的;Turbopuffer/Pinecone 则不 pad(passage_manager.py:148-159)。 - 标签双存。 标签既存在 passage 的 JSON 列里,又写进一张专门的标签关联表(
_create_tags_for_passage,passage_manager.py:49),后者是为了「按标签高效过滤」。这是典型的「冗余换查询性能」。
标签会反馈进系统提示
重建系统提示时,Letta 会查出该归档库里所有 unique 标签,塞进 <memory_metadata> 的「Available archival memory tags」一行(agents/letta_agent_v2.py:832、prompts/prompt_generator.py:84-85)。这样模型搜归档时知道有哪些标签可用——又一个「把外部状态的目录暴露给模型」的设计。
1.5 小结:三层为什么这样分
- Core = 高频、必看、量小 → 放上下文,可自编辑。
- Recall = 全量对话、低频回看 → 放库,文本+语义搜。
- Archival = 提炼后的长期知识 → 放库,向量+标签搜。
三层都通过工具让模型按需调动——模型不是被动接受一个被截断的上下文,而是主动管理自己的记忆。下一章看驱动这一切的循环。
→ 继续 02-agent-loop.md
代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 记忆块数据结构 | letta/schemas/block.py | BaseBlock、Block |
| 记忆容器 + 渲染 | letta/schemas/memory.py | Memory、compile、_render_memory_blocks_standard、_render_memory_blocks_line_numbered |
| 默认 human/persona | letta/schemas/memory.py | ChatMemory、BasicBlockMemory |
| core 编辑工具 | letta/functions/function_sets/base.py | core_memory_append、core_memory_replace、memory_replace、memory_insert、memory_rethink、memory_apply_patch |
| recall 搜索 | letta/functions/function_sets/base.py | conversation_search |
| archival 工具 | letta/functions/function_sets/base.py | archival_memory_insert、archival_memory_search |
| archival 持久化/嵌入/标签 | letta/services/passage_manager.py | PassageManager、create_agent_passage_async、_create_tags_for_passage |