第 6 章 · 上下文压缩 Compaction 与 Context Files
本章目标:理解 pi 为什么需要上下文压缩、自动/手动 compaction 的触发与工作原理,掌握 Context Files(AGENTS.md)的加载层级与 system prompt 定制方法。
6.1 为什么需要压缩
LLM 的上下文窗口是有限的,而编码会话天然"吃"上下文:每读一个文件、每跑一条命令,工具结果都会进入对话历史。一个持续重构半小时的会话,read 和 bash 的输出轻松占掉几十万 token。这带来两个问题:
- 硬限制:超过模型窗口后请求直接失败;
- 软成本:即使没超限,冗长的历史也会推高每次请求的费用和延迟。
pi 的解法是压缩(compaction):把较早的对话总结成一份结构化摘要,只保留最近的消息原文。它还有一套配套机制——Context Files(如 AGENTS.md),让你用极小的固定成本注入项目级指令。
6.2 自动与手动触发
自动压缩的触发条件是一条简单的不等式(来自官方文档 compaction.md):
contextTokens > contextWindow - reserveTokens即"当前上下文 token 数超过 模型窗口 − 预留空间"时立即压缩。reserveTokens 默认 16384,给模型的回复留出空间;你也可以在设置里调整(见下文)。此外,当一轮因超限而失败时,pi 会先压缩再重试(overflow recovery)。
手动触发则是在交互模式里输入 /compact,并可以附加指令来聚焦摘要重点:
# 交互模式下:
/compact # 按默认结构总结全部旧内容
/compact 重点保留数据库迁移相关的决策和未完成的测试清单关闭自动压缩
在 settings.json 中把 compaction.enabled 设为 false 可以关闭自动压缩,但 /compact 手动命令仍然可用。长会话中关掉它很容易撞上窗口上限,一般不建议。
6.3 压缩是怎么工作的
理解原理能帮你预判"哪些信息会被丢掉"。官方流程分五步:
- 找切点:从最新消息向前累计 token,直到达到
keepRecentTokens(默认 20000),这个位置就是切点; - 提取消息:切点之前(到上一次压缩边界为止)的消息全部进入待总结集合;
- 生成摘要:调用 LLM 按固定结构化格式总结,若存在上一份摘要则作为迭代上下文传入;
- 追加条目:把摘要和
firstKeptEntryId写入会话的CompactionEntry; - 重建上下文:下次请求时,LLM 看到的 = system prompt + 摘要 + 切点之后的原文消息。
压缩前后 LLM 视角的变化:
之前: [system] [msg1] [msg2] ... [msg8] ← 全部原文
之后: [system] [summary] [msg7] [msg8] ← 摘要替代了 msg1~6两个值得注意的细节:
- 切点永远不会落在工具结果上——工具结果必须与它的工具调用待在一起,否则会出现"有答案没问题"的断裂上下文;
- 序列化时会截断工具结果:单条工具结果最多保留 2000 字符,超出部分替换为截断标记,防止总结请求本身撑爆预算。
6.4 信息损失的权衡与调参
压缩本质是有损压缩。结构化摘要格式保证了关键信息不丢:
## Goal ← 用户要完成什么
## Constraints & Preferences
## Progress ← Done / In Progress / Blocked
## Key Decisions ← 决策及理由
## Next Steps
## Critical Context
<read-files>...</read-files>
<modified-files>...</modified-files>注意末尾的 <read-files> / <modified-files>:pi 会跨多次压缩累积追踪文件操作,所以即使读过某文件的记录被压缩掉了,"这个文件被动过"的事实仍会传递下去。
参数调优示例——小窗口模型或长任务可以保留更多近况:
// ~/.pi/agent/settings.json 或 <project>/.pi/settings.json
{
"compaction": {
"enabled": true,
"reserveTokens": 16384, // 给回复预留的 token
"keepRecentTokens": 40000 // 多留一些近期原文,减少信息损失
}
}权衡关系很直接:keepRecentTokens 越大,摘要需要覆盖的内容越少、损失越小,但每次请求携带的历史也越贵。另一个相关机制是 /tree 分支导航时的分支总结(branch summarization):切换分支时 pi 会询问是否为被离开的分支生成摘要注入新分支,同样使用上述格式。
6.5 Context Files:AGENTS.md 的加载层级
如果说 compaction 是"动态上下文的减法",Context Files 就是"静态上下文的加法"。pi 启动时按以下顺序加载并拼接 AGENTS.md(或 CLAUDE.md):
~/.pi/agent/AGENTS.md ← 全局个人偏好
+ 父目录链上的 AGENTS.md ← 从 cwd 一路向上走
+ 当前目录的 AGENTS.md ← 项目约定<!-- ~/.pi/agent/AGENTS.md:全局,适合放个人习惯 -->
- 回复使用中文
- 提交信息遵循 Conventional Commits
<!-- ./AGENTS.md:项目根目录,适合放项目约定 -->
- 包管理器用 pnpm,不要用 npm/yarn
- 测试命令:pnpm test;lint 命令:pnpm lint
- src/legacy 下是即将废弃的代码,不要在其中新增功能两个进阶用法:
- 目录级覆盖:某目录存在
AGENTS.override.md时,pi 加载它而跳过该目录的AGENTS.md/CLAUDE.md(其他目录照常拼接)——适合 monorepo 中个别子包有特殊规则的场景; - 禁用开关:
--no-context-files(-nc)可完全关闭加载,做对比实验时有用。
6.6 System Prompt 定制与 monorepo 实践
默认 system prompt 也可以整体替换或追加:
.pi/SYSTEM.md ← 项目级,整体替换默认系统提示词
~/.pi/agent/SYSTEM.md ← 全局版
APPEND_SYSTEM.md ← 同目录放置则只追加、不替换大型 monorepo 的推荐组织方式是"全局薄、项目厚、子包精准":
my-monorepo/
├── AGENTS.md # 通用约定:语言、提交规范、顶层命令
├── apps/
│ └── web/
│ └── AGENTS.md # 前端专属:组件规范、样式方案
├── packages/
│ └── core/
│ ├── AGENTS.md # 核心库专属:公共 API 变更需评审
│ └── AGENTS.override.md # 若此包规则完全独立,覆盖而非叠加
└── .pi/
├── settings.json # 项目级 pi 设置
└── SYSTEM.md # 可选:项目级系统提示词配合第 6.4 节的压缩调参,长任务的工作流就完整了:Context Files 保证"常驻知识"永远在,compaction 保证"历史过程"不撑爆窗口。
本章小结
- 自动压缩条件:
contextTokens > contextWindow - reserveTokens(默认预留 16384);手动用/compact [instructions]; - 压缩保留最近
keepRecentTokens(默认 20000)token 的原文,更早内容总结为固定结构化摘要,文件读写记录跨压缩累积; - 工具结果不会被单独切断,序列化时截断到 2000 字符/条;
- Context Files 按 全局 → 父目录链 → 当前目录 顺序拼接
AGENTS.md,AGENTS.override.md可覆盖所在目录; .pi/SYSTEM.md替换系统提示词,APPEND_SYSTEM.md只追加。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. pi 自动压缩(auto-compaction)的触发条件是什么?
2. 关于压缩的切点(cut point),下列说法正确的是?
3. 某目录同时存在 AGENTS.md 和 AGENTS.override.md,pi 会怎么做?
4. 想把"最近保留原文"的量从默认值提高到 40000 token,应该怎么配置?
🛠️ 动手实践
- 在一个测试项目中把
keepRecentTokens调成 5000 并进行一次超长对话(让 pi 反复读大文件),观察压缩触发的时机与摘要内容。 - 为你的某个真实项目编写
AGENTS.md(至少包含构建/测试/lint 三条命令和两条代码约定),重启 pi 后验证 agent 是否遵守。 - 用
/compact 聚焦保留 API 设计决策做一次带指令的手动压缩,对比有无指令时摘要的差异。
完成以上练习后,进入下一章:Settings 设置与项目信任。