第 4 章 · 压缩、巧妙之处、边界
本章讲上下文满了怎么办(compaction),然后提炼整套设计可借鉴的精华、它的边界,以及和兄弟项目的取舍对比。
4.1 Compaction:上下文版的「换页」
它要解决的小问题: 对话越来越长,迟早塞不进上下文窗口。直接截断会丢信息;Letta 的做法是把旧消息摘要成一条「summary 消息」,用少量 token 保住要点。
什么时候触发
两条触发路径:
- 被动(最常见)。 调模型时真的抛了
ContextWindowExceededError,在_step里捕获,压缩后重试最多若干次(agents/letta_agent_v3.py:1218-1247)。 - 阈值。
get_compaction_trigger_threshold(services/summarizer/thresholds.py:27)按模型给阈值:GPT-5 家族在上下文窗口的 90% 就主动压缩(SUMMARIZATION_TRIGGER_MULTIPLIER = 0.9,constants.py:83),其他模型到 100% 才压。注释解释:观察到 GPT-5 在输入接近 272k 时会撞 max_output_tokens,所以提前压(thresholds.py:34-39)。
# 真实源码节选,thresholds.py:41
return int(llm_config.context_window * SUMMARIZATION_TRIGGER_MULTIPLIER)
怎么压
入口 compact_messages(services/summarizer/compact.py:135),返回一个 CompactResult(摘要消息 + 被压掉的消息 + 摘要文本 + 新 token 估计,compact.py:32-39)。它支持多种模式(compact.py:189-219):
| 模式 | 思路 |
|---|---|
self_compact_all | 让 agent 自己(用自己的模型)总结全部消息 |
self_compact_sliding_window | 滑动窗口式自我总结(上面失败时的兜底) |
summarize_all / sliding window | 用一个独立的 summarizer 模型总结 |
巧妙处:压缩路由到便宜模型
压缩用哪个模型很讲究(build_summarizer_llm_config,compact.py:42):
- auto 模式的 agent,压缩特意路由到 Haiku 4.5(便宜、适合总结),Haiku 不可用再退到
zai/glm-5(compact.py:65-77)。 - 没指定就按 provider 取轻量默认模型(
compact.py:79-84)。
直觉:总结是个「低智力、高频次」的活,没必要烧主力大模型的钱——这是很实际的成本工程。
4.2 巧妙之处(可借鉴的技术)
- 把上下文管理「工具化」。 最核心的一招:不靠系统在背后偷偷截断,而是把
archival_memory_search、memory_replace等做成工具,让模型自己决定何时存、何时取、何时整理(functions/function_sets/base.py)。上下文管理从「被动截断」变成「主动操作」。 - 把外部存储的「目录」喂回提示。
<memory_metadata>告诉模型「recall 还有 N 条、archival 有这些标签」(prompt_generator.py:69-88),模型才知道值得去搜——否则它不知道外面有什么。 - 编辑工具借鉴代码编辑的「精确匹配 + 唯一性」。
memory_replace要求old_string唯一、拒绝行号前缀(base.py:343-373),把 LLM 易错的模糊定位逼成精确定位。 - 系统提示按需重建。 只在记忆真变了才重编译 message[0],动态元数据不计入变化判断(
letta_agent_v2.py:764-766,853-860)——省掉大量无谓重建。 - 压缩路由到便宜模型 + 按模型设触发阈值(
compact.py:65-77、thresholds.py:27)。 - 工具返回动态截断,防单个工具结果吃掉上下文(
letta_agent_v3.py:143-153)。
4.3 边界与局限(诚实)
- 重度依赖持久化层。 三层记忆里 recall/archival 都在数据库;archival 还要嵌入向量(pgvector / Turbopuffer / Pinecone)。这不是一个纯内存库,要跑起来得有数据库 + 嵌入服务,部署比「拼 prompt 的 agent 库」重。
- 多代实现并存。 仓库里同时有
LettaAgentV2/V3、多种AgentType、多套系统提示(memgpt/memgpt_v2/letta_v1/react/voice…)。这是活跃演进的产物,读代码时要认清「当前主力是 v3 / letta_v1_agent」(agent_loop.py:20-43),别被旧路径带偏。 - 摘要必然有损。 compaction 把旧消息换成摘要,细节会丢;靠 recall 搜索找回原文,但前提是模型想起来要搜。
- 行号编辑的脆弱性。 行号视图给 Anthropic 模型用,但需要工具层主动拦截「模型把行号写进编辑参数」的错误(
base.py:345-352)——说明这条路径本身是易错的,靠防御性校验兜着。 - 本库未深读的部分(见 frontmatter uncertainties): 各 provider client 的具体适配、归档 hybrid 搜索的完整 SQL、sleeptime 多 agent 的内部 step 逻辑。
4.4 横向对比(同 shelf 兄弟)
Letta 在「memory」这个关切上的取舍,和其他做记忆/agent 的项目可对照:
| 维度 | Letta (MemGPT) | 典型「prompt-stuffing」agent | 典型纯向量 RAG 记忆 |
|---|---|---|---|
| 常驻记忆 | 可被 agent 自编辑的记忆块,渲染进系统提示 | 把全部历史塞进上下文,满了截断 | 一般没有「常驻、可编辑」概念 |
| 长期记忆 | archival(向量 + 标签)+ recall(全量消息) | 无,或简单滚动窗口 | 向量库检索 |
| 谁管理上下文 | 模型自己(用工具) | 框架被动截断 | 检索器(retriever),模型被动接收 |
| 上下文超限 | compaction 摘要 + 可路由便宜模型 | 截断/报错 | 不适用(每次重新检索) |
一句话定位:Letta 把 RAG 的「外部记忆」和 agent 的「工具调用」缝合成一个统一系统,并让模型成为自己上下文的管理者——这正是 MemGPT 论文「LLM as OS」隐喻的工程落地。
4.5 总代码地图(导航索引)
| 主题 | 文件 | 符号 |
|---|---|---|
| 压缩入口 | letta/services/summarizer/compact.py | compact_messages、CompactResult |
| 压缩模型选择 | letta/services/summarizer/compact.py | build_summarizer_llm_config |
| 触发阈值 | letta/services/summarizer/thresholds.py | get_compaction_trigger_threshold、is_gpt5_model_family |
| 上下文超限重试压缩 | letta/agents/letta_agent_v3.py | _step(ContextWindowExceededError 分支) |
| 工具返回截断 | letta/agents/letta_agent_v3.py | _compute_tool_return_truncation_chars |
| 关键常量 | letta/constants.py | DEFAULT_MAX_STEPS、SUMMARIZATION_TRIGGER_MULTIPLIER、CORE_MEMORY_BLOCK_CHAR_LIMIT、MAX_EMBEDDING_DIM |
| 上下文窗口概览 schema | letta/schemas/memory.py | ContextWindowOverview |
回到 index.md · 上一章 03-system-prompt-assembly.md