跳到主要内容

第 3 章 · 系统提示的组装

本章讲 core memory「常驻上下文」这件事到底怎么落地:系统提示是哪几块拼出来的,放在消息列表哪里,以及一个关键性能优化——只在内容变化时才重建。

3.1 系统提示 = 消息列表的第 0 条

Letta 把整个对话表示成一个消息列表,第 0 条永远是系统消息(in_context_messages[0])。这条系统消息里就嵌着 core memory。所以「记忆常驻上下文」在工程上就是:记忆被写进了 message[0] 的文本

这也解释了「改记忆」的实现:agent 调编辑工具改了某个块的 value 后,系统会重新生成 message[0] 的文本并更新它(agents/letta_agent_v2.py:887-890)。

3.2 三层拼装:基座 + 记忆 + 元数据

系统提示的文本由 PromptGenerator.get_system_message_from_compiled_memory() 拼出(prompts/prompt_generator.py:107)。结构是:

┌──────────────────────────────────────────────┐
│ 基座系统提示(letta_v1 的 PROMPT 等) │ ← 固定指令:你是谁、怎么用记忆/文件系统
│ ……里面有一个占位符 {CORE_MEMORY} …… │
├──────────────────────────────────────────────┤
│ 占位符被替换成: │
│ <memory_blocks> … core memory 各块 … │ ← 来自 Memory.compile()
│ <tool_usage_rules> …(若有 tool rules)… │
│ <directories> …(若挂了文件/数据源)… │
│ + \n\n + │
│ <memory_metadata> …(消息计数/归档计数/标签)… │ ← 来自 compile_memory_metadata_block
└──────────────────────────────────────────────┘

两步拼装:

  1. Memory.compile()(schemas/memory.py:688)负责 <memory_blocks> + <tool_usage_rules> + <directories>
  2. compile_memory_metadata_block()(prompt_generator.py:26)负责 <memory_metadata>,把「外部记忆的目录信息」告诉模型。

两者拼成 full_memory_string,再替换进基座提示里的 {CORE_MEMORY} 占位符(prompt_generator.py:149-169):

# 真实源码节选,prompt_generator.py:149
full_memory_string = memory_with_sources + "\n\n" + memory_metadata_string
variables[IN_CONTEXT_MEMORY_KEYWORD] = full_memory_string
# ...
formatted_prompt = system_prompt.replace(memory_variable_string, full_memory_string)

贴心处: 如果基座提示里没有这个占位符,代码会自动把记忆追加到末尾(prompt_generator.py:158-162),保证记忆一定被注入。

3.3 memory_metadata:把「磁盘目录」告诉模型

<memory_metadata> 这一段是 Letta「让模型知道外部还有什么」的关键(prompt_generator.py:69-88)。它列出:

  • agent_id / conversation_id;
  • 系统提示上次重编译的时间;
  • recall 里还有多少条历史消息(previous_message_count);
  • archival 里存了多少条长期记忆;
  • archival 有哪些可用标签
<memory_metadata>
- AGENT_ID: agent-123
- System prompt last recompiled: 2024-01-15 09:00 AM PST
- 42 previous messages between you and the user are stored in recall memory
- 156 total memories you created are stored in archival memory (use tools to access them)
- Available archival memory tags: project_x, meeting_notes, research
</memory_metadata>

直觉:这就像在 RAM 里放一张「磁盘上有哪些文件」的目录卡片——模型看到「归档里有 156 条、标签有 project_x」,就知道值得去 archival_memory_search

3.4 关键优化:只在「真的变了」时才重建

问题: 每步都重编译系统提示很贵(要查库、拼字符串)。但很多步里记忆根本没变。

解法: _rebuild_memory()(agents/letta_agent_v2.py:813)先编译出当前应有的记忆串,然后和 message[0] 现有文本比对,没变就直接跳过(letta_agent_v2.py:853-860):

# 真实源码节选,letta_agent_v2.py:854 —— 没变就不重建
system_prompt_changed = agent_state.system not in curr_system_message_text
memory_changed = curr_memory_str not in curr_system_message_text
if (not force) and (not system_prompt_changed) and (not memory_changed):
# 记忆/来源/系统提示都没变,跳过重建
return in_context_messages

注意它故意不把动态元数据(消息计数等)纳入「是否变化」的判断——否则每收发一条消息计数就变、永远在重建。注释里说得很清楚(letta_agent_v2.py:764-766):「只在 memory/tool-rules/directories 变化时重建,避免因动态元数据每步重建」。

确实需要重建时,才查 num_messages / num_archival_memories、生成新文本、更新 message[0] 这条数据库记录(letta_agent_v2.py:862-890)。

3.5 进阶:git 风格的记忆文件系统

git_enabled 的 agent,记忆块的 label 是路径式的(如 system/personaskills/foo/SKILL),渲染成一个类似目录树的结构,而不是平铺的 <memory_blocks>。逻辑在 _render_memory_blocks_git(schemas/memory.py:205)和 _render_memory_filesystem(memory.py:351),后者用 ├── └── │ 画出 tree 命令风格的目录树。这让 agent 能像管理文件系统一样管理记忆(配合「skills」「external projection」等)。这是较新的、面向「Letta Code」终端 agent 的能力,本章只点到为止。

→ 继续 04-compaction-and-internals.md

代码地图

主题文件符号
拼系统提示文本letta/prompts/prompt_generator.pyget_system_message_from_compiled_memory
记忆元数据块letta/prompts/prompt_generator.pycompile_memory_metadata_block
安全模板替换letta/prompts/prompt_generator.pysafe_format
编译记忆块 → 文本letta/schemas/memory.pyMemory.compile_render_memory_blocks_standard
按需重建系统消息letta/agents/letta_agent_v2.py_rebuild_memory
git 风格记忆树letta/schemas/memory.py_render_memory_blocks_git_render_memory_filesystemcompile_available_skills