跳到主要内容

第 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.pyLettaCoreToolExecutor.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:832prompts/prompt_generator.py:84-85)。这样模型搜归档时知道有哪些标签可用——又一个「把外部状态的目录暴露给模型」的设计。

1.5 小结:三层为什么这样分

  • Core = 高频、必看、量小 → 放上下文,可自编辑。
  • Recall = 全量对话、低频回看 → 放库,文本+语义搜。
  • Archival = 提炼后的长期知识 → 放库,向量+标签搜。

三层都通过工具让模型按需调动——模型不是被动接受一个被截断的上下文,而是主动管理自己的记忆。下一章看驱动这一切的循环。

→ 继续 02-agent-loop.md

代码地图

主题文件符号
记忆块数据结构letta/schemas/block.pyBaseBlockBlock
记忆容器 + 渲染letta/schemas/memory.pyMemorycompile_render_memory_blocks_standard_render_memory_blocks_line_numbered
默认 human/personaletta/schemas/memory.pyChatMemoryBasicBlockMemory
core 编辑工具letta/functions/function_sets/base.pycore_memory_appendcore_memory_replacememory_replacememory_insertmemory_rethinkmemory_apply_patch
recall 搜索letta/functions/function_sets/base.pyconversation_search
archival 工具letta/functions/function_sets/base.pyarchival_memory_insertarchival_memory_search
archival 持久化/嵌入/标签letta/services/passage_manager.pyPassageManagercreate_agent_passage_async_create_tags_for_passage