第 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
└──────────────────────────────────────────────┘
两步拼装:
Memory.compile()(schemas/memory.py:688)负责<memory_blocks>+<tool_usage_rules>+<directories>。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/persona、skills/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.py | get_system_message_from_compiled_memory |
| 记忆元数据块 | letta/prompts/prompt_generator.py | compile_memory_metadata_block |
| 安全模板替换 | letta/prompts/prompt_generator.py | safe_format |
| 编译记忆块 → 文本 | letta/schemas/memory.py | Memory.compile、_render_memory_blocks_standard |
| 按需重建系统消息 | letta/agents/letta_agent_v2.py | _rebuild_memory |
| git 风格记忆树 | letta/schemas/memory.py | _render_memory_blocks_git、_render_memory_filesystem、compile_available_skills |