主题
13.6 外置化:把上下文搬出窗口
上下文窗口不是硬盘,是工作台。工作台上只该放正在用的东西。
读完你能做什么:用文件系统、子代理、脚本、笔记四种载体把信息移出上下文窗口,并算清每种外置手段的压缩比与它自身的开销。
一、核心原则
Anthropic 的表述是:好的上下文工程,是找到能达成目标的最小高信号 token 集合。
「最小」的实现路径只有两条:不放进去,或者放进去后再拿出来。前者永远比后者便宜(见 13.3:拿出来的手段都有代价)。
所以外置化的判断顺序是:
text
这段信息现在这一步用得上吗?
├─ 用得上 → 放进上下文
└─ 用不上 → 它在窗口之外能被找到吗?
├─ 能 → 留一个指针(文件路径、查询语句、URL)
└─ 不能 → 先把它变成能被找到的形态,再留指针「留指针」这件事本身还有额外收益:路径与命名带有语义。tests/test_utils.py 和 src/core_logic/test_utils.py 对模型意味着不同的东西。目录层级、命名规范、时间戳都是免费的元数据。
二、四种外置载体
| 载体 | 存什么 | 取回方式 | 压缩比 | 自身开销 |
|---|---|---|---|---|
| 文件系统 | 原始资料、中间产物、索引 | Read / Grep / Glob | 取决于读多少 | 近乎零 |
| 子代理 | 探索过程与中间结论 | 返回摘要 | 20~50× | 子代理的启动上下文 + 无缓存首轮 |
| 脚本 | 确定性计算与转换 | stdout | 高,且确定 | 零(代码不进上下文) |
| 结构化笔记 | 跨压缩、跨会话的状态 | 主动读回 | 高 | 写入时的少量 token |
三、文件系统:即时检索(just-in-time)
预先把所有可能相关的资料塞进上下文,是检索时代的思路。Agent 时代的做法是:保留轻量标识符,运行时按需加载。
Claude Code 本身就是混合模式的示范:CLAUDE.md 无条件前置加载(少量、高价值、每次都用),而 glob、grep、head、tail 让它在需要时才把具体内容拉进来。
3.1 用命令行做窗口外的数据处理
处理大文件时,Bash 管道是最省上下文的工具:
bash
# ❌ 把 20MB 日志读进上下文
cat app.log
# ✅ 先看形状
wc -l app.log && head -5 app.log
# ✅ 再定向取
grep -n 'ERROR' app.log | tail -50
# ✅ 大 CSV:先看结构,再聚合,全程不进上下文
head -1 data.csv
awk -F, 'NR>1 {sum[$3] += $5} END {for (k in sum) print k, sum[k]}' data.csv原则:让计算发生在窗口之外,只把结论带进来。
3.2 建索引,而不是读全文
对于反复要查的大型资料,先生成一次索引,之后每次只读索引:
bash
# 一次性:为每份文档生成结构化摘要,落盘
for f in docs/*.md; do
echo "## $f" >> .index/summary.md
head -20 "$f" | sed 's/^/ /' >> .index/summary.md
echo "" >> .index/summary.md
done之后的会话只读 .index/summary.md(几千 token),定位到具体文件后再精读那一份。
详细战术见 03.3 长文档处理战术。
四、子代理:最高压缩比的手段
子代理在自己的上下文窗口里工作,可能消耗几万 token 做探索,最终只返回一两千 token 的结论。
4.1 压缩比的量化
一个真实形状的任务:「在 200 个文件里找出哪些实现了缓存失效逻辑」。
| 做法 | 主会话新增 token | 相对成本 |
|---|---|---|
| 主会话直接 Grep + 逐个 Read 15 个候选文件 | ≈ 22,000 | 100% |
| 主会话 Grep,只读最像的 3 个 | ≈ 5,000 | 23% |
| 派子代理探索,返回带文件引用的结论 | ≈ 600 | 3% |
主会话侧压缩到 1/35。但账不能只算这一边。
4.2 子代理自己的开销
| 开销项 | 说明 |
|---|---|
| 启动上下文 | 自己的系统提示词、工具集,通常还有 CLAUDE.md |
| 首轮无缓存 | 子代理从零建缓存,首次调用没有命中 |
| TTL 更短 | 子代理用 5 分钟 TTL,即使你在订阅计划上 |
| 模型单价 | 若不指定,可能沿用主会话的贵模型 |
所以任务规模必须大于子代理的启动开销,否则是负收益。经验阈值:预计要读超过 5,000 token 的原始资料,才值得派子代理。
4.3 三个降低子代理开销的配置
markdown
---
name: scanner
description: 在大型代码库中快速定位相关文件。用于需要广泛搜索但不需要深度分析的场景。
model: haiku
tools: Glob, Grep, Read
---
你只做定位,不做分析。返回:文件路径 + 匹配行号 + 一句话说明。
不要粘贴大段代码。不要给改进建议。- 指定便宜模型。 扫描定位类任务用 Haiku,成本降到 Opus 的约 1/25。
- 收窄工具集。 工具定义也是子代理的常驻成本。
- 在提示词里限定返回格式。 子代理返回多少,主会话就付多少。
内置的 Explore 与 Plan 代理会跳过 CLAUDE.md 与 git status,启动上下文更小,适合纯探索。
4.4 什么时候不该用子代理
| 场景 | 原因 |
|---|---|
| 任务需要完整的对话上下文 | 子代理看不到你们之前聊了什么。这种情况用 fork |
| 任务很小(读一两个文件) | 启动开销大于收益 |
| 需要多轮交互式澄清 | 子代理是一次性的 |
| 代理团队(agent teams)滥用 | 官方数据:计划模式下的代理团队约消耗标准会话 7 倍 token |
五、结构化笔记:跨越压缩边界的状态
压缩和 /clear 会带走对话历史,但带不走磁盘上的文件。所以长任务的状态应该写在文件里,而不是指望它活过压缩。
markdown
# 对账模块重构 · 进度
## 已完成
- [x] 抽出 `ReconcileEngine` 接口
- [x] 迁移支付宝渠道(`src/channels/alipay.ts`)
## 进行中
- [ ] 迁移微信渠道 —— 卡在 `WxPayAdapter` 的异步回调签名不兼容
## 关键约定(不要违反)
- 所有金额用 `Decimal`,禁止 `number`
- 渠道适配器不得直接读数据库,必须走 `RepositoryPort`
## 已知坑
- `tests/fixtures/wx.json` 里的 mock 数据是旧格式,改之前先跑 `npm run gen:fixtures`配套习惯:
text
> 继续之前,先读 PROGRESS.md
> 这一阶段做完后,把结论追加到 PROGRESS.md 再继续这份文件的三个作用,每一个都独立成立:
- 跨压缩持久化:压缩后读一次就恢复状态。
- 跨会话持久化:
/clear后新会话读一次就接上。 - 降低压缩损失:关键约定在文件里有权威版本,摘要丢了也不致命。
Auto memory(MEMORY.md)是这个模式的内置版本,压缩后会从磁盘重新注入。API 侧有对应的 memory 工具,可以和 context editing 联动:上下文接近清除阈值时,模型会收到提示,先把重要信息写进记忆文件,再让旧的工具结果被清掉。
六、把它们串起来:一次长任务的标准形状
text
阶段 0 准备
└─ 写 PROGRESS.md,列出目标、约束、验收标准
阶段 1 勘察(子代理)
└─ 派 Haiku 子代理扫描,返回文件清单 + 一句话说明
主会话新增 ≈ 0.6k
阶段 2 制定方案(主会话,计划模式)
└─ 只读子代理点名的 3~5 个文件
把方案写进 PROGRESS.md
阶段 3 执行(主会话)
├─ 每完成一个子任务,更新 PROGRESS.md
├─ 测试输出经 Hook 过滤,只回失败部分
└─ 到任务边界时 /compact,并指定保留重点
阶段 4 换任务
└─ /clear(零成本),下一个任务从 PROGRESS.md 起步每一步都在做同一件事:让窗口里只有当前这一步需要的东西。