AI 工程

Agent 烧 Token 烧到心痛?上下文管理 + 成本优化实战

你的 Token 都烧在哪了

用 AI Agent 做稍大一点的项目,账单会让你肉痛。

一个典型的场景:

比如一个正常点的项目,仓库里有 50 个文件。

你以为贵的是那句新指令,其实很多时候,贵的是 Agent 开工前先读的那堆上下文。

比如一轮对话中,你新发的指令只有约 200 tokens,Agent 输出 800 tokens,但它为了重新理解项目,又额外读了 15000 tokens 的背景信息。

算下来,总消耗是 15000 + 200 + 800 = 16000 tokens。

你真正新增的那句指令,占比其实只有 1.25%。

也就是说,大部分 Token 不是花在“干活”上,而是花在“先进入状态”上。

这就是上下文管理要解决的问题。

三个核心策略(以及它们的关系)

《马书》(张汉东的 Claude Code 源码分析)用了整整两篇来讲上下文管理和提示词缓存。提炼出来就三个策略:

  1. 渐进式披露 — 控制每次塞进去多少(源头降量)
  2. 上下文压缩 — 控制长会话里留下多少(过程瘦身)
  3. 提示词缓存 — 控制重复的部分怎么不再付费(复用降本)

三者是互补而非替代:渐进式披露让缓存有稳定的"不变区"可缓存,上下文压缩让长会话不至于把缓存冲垮。三管齐下才是完整方案。

策略一:渐进式披露

一句话:别把所有信息一次性塞给 Agent。它用到什么,再给什么。

错误做法:

# CLAUDE.md(2000 行的巨型文件)

## 项目概述

...200 行...

## 架构设计

...300 行...

## API 文档

...500 行...

## 测试规范

...300 行...

## 部署流程

...400 行...

## 编码规范

...300 行...

Agent 每次对话都要吃下这 2000 行。但它当前任务可能只需要其中的 50 行。

为什么错? 大模型的注意力是有限资源。塞的内容越多,关键指令被"稀释"的概率越大,不光费钱,回答质量也会下降(俗称"中间丢失" Lost-in-the-Middle)。

正确做法:主文件只放导航,详情分散到子文件。

# CLAUDE.md(50 行的导航文件)

## 核心规则

- 一次只处理一个 feature
- 每个 feature 完成后运行 verify.sh
- 提交前更新 progress.md

## 需要时再读

- 架构设计 → 读 `docs/architecture.md`
- API 文档 → 读 `docs/api-spec.md`
- 测试规范 → 读 `docs/testing.md`
- 部署流程 → 读 `docs/deploy.md`
- 编码规范 → 读 `docs/coding-style.md`

主文件从 2000 行压到 50 行。 Agent 每次对话只吃 50 行的导航文件,需要哪个文档再单独读。

Token 消耗通常能降一个数量级。

边界提醒:别矫枉过正。如果把"核心规则"也拆得七零八落,Agent 每次都要多轮 tool call 才能拼齐必要上下文,反而增加交互成本。主文件里该留的规则要留住,拆出去的只应是"按需才会用到的详细资料"。

策略二:上下文压缩

Agent 在长会话中会积累大量的对话历史。Claude Code 有一个内置的上下文压缩机制(Context Compaction),在接近 Token 上限时自动压缩早期对话。

但你不能完全依赖自动压缩。主动管理比被动压缩更省 Token,也更可控(自动压缩有时会丢掉你认为重要的细节)。

几个实操技巧:

技巧 1:短会话 > 长会话

一个跑了 2 小时的长会话,后半段的上下文里塞满了前半段的试错记录、错误日志、废弃方案。

不如拆成 4 个 30 分钟的短会话。每个会话干一个 feature,交接笔记只保留关键信息(交接笔记的写法见本系列篇三)。

技巧 2:及时 commit,减少 diff 膨胀

Agent 改了 10 个文件但一直没提交,上下文里就会持续携带这些改动的 diff。

每完成一个逻辑单元就 commit 一次。commit 之后,工作区 diff 归零,上下文瘦身。

技巧 3:错误日志不要全贴

Agent 报错了,你把 200 行的堆栈信息全贴进去?

实际有用的通常只有前 5 行和最后 5 行(异常类型 + 抛出位置)。把关键错误信息和文件定位贴进去就够了。

策略三:提示词缓存

这是隐藏的成本大杀器。《马书》第四篇专门讲了这个。

原理很简单:API 请求中不变的部分(系统提示词、指令文件内容),在第一次请求后会被缓存。后续请求只为变化的部分付费。

据 Anthropic 官方文档,提示词缓存可以最高降低约 90% 的输入 Token 成本,实际节省幅度取决于缓存命中率,和你的交互模式强相关)。

怎么用好它?

关键:把不变的内容放前面,变化的内容放后面。

┌─────────────────────────────────────┐
│ 系统提示词(不变)                      │ ← 被缓存,只付一次钱
│ CLAUDE.md 内容(很少变)                │ ← 被缓存
│ 工具定义(不变)                        │ ← 被缓存
├─────────────────────────────────────┤
│ 对话历史(每轮变化)                     │ ← 正常计费
│ 当前用户输入(每轮变化)                  │ ← 正常计费
└─────────────────────────────────────┘

不变的部分越多、越靠前,缓存命中率越高,省的钱越多。

实操建议:

1. CLAUDE.md 的内容尽量稳定。 不要频繁改动指令文件。每次改动都会导致缓存失效,下一次请求需要按原价重建缓存。

2. 缓存有 TTL(生存期)。 Anthropic 默认缓存 TTL 为 5 分钟(近期也提供了 1 小时的长 TTL 选项,具体以官方文档为准)。超过 TTL 没有新请求,缓存就过期了。如果你中间思考了 10 分钟,下一次请求就会重新付全价。

3. 短会话多轮快打 > 长间隔单轮。 在缓存窗口内密集交互,比每隔半小时发一条消息省钱得多。需要长时间思考时,先离开 Agent,想清楚再一次性发指令。

异常处理:怎么知道缓存没命中?

  • Claude API 的响应里会返回 cache_read_input_tokenscache_creation_input_tokens读大于 0 就是命中,创建大于 0 就是这次在重建缓存。
  • 如果你发现某一轮请求账单异常高,第一反应检查这两个字段,多半是缓存被下面这几件事打破了:改了 CLAUDE.md、改了系统提示词顺序、距离上次请求超过 TTL。
  • 降级策略:缓存过期不是故障,只是"这一次变贵了"。不需要恢复,下一次密集交互时缓存会自动重建。真正要避免的是反复改 CLAUDE.md 导致每次都在重建。

动手:三个模板优化你的上下文

下面三个模板分别对应上面三个策略,可以按需取用:

模板对应策略解决的问题
模板一:项目文档结构策略一(渐进式披露)主文件太大,Agent 每次全量吃
模板二:Token 预算规划表策略二(上下文压缩)会话失控,不知道该多长、塞多少
模板三:上下文生存清单策略三 + 综合落地Agent 自己不知道该节约,你要写进 AGENTS.md

模板一:项目文档结构模板(对应策略一)

用途:按渐进式披露原则重新组织你的项目文档,给 Agent 减负。

项目根目录/
│
├── CLAUDE.md                 ← 50 行以内的导航文件
│
├── docs/
│   ├── architecture.md       ← 架构设计(Agent 需要时再读)
│   ├── api-spec.md           ← API 规范(Agent 需要时再读)
│   ├── coding-style.md       ← 编码规范(Agent 需要时再读)
│   ├── testing.md            ← 测试规范(Agent 需要时再读)
│   └── deploy.md             ← 部署流程(Agent 需要时再读)
│
├── contracts/                ← Sprint Contract(按 feature 拆分)
│   ├── feat-001.contract.json
│   └── feat-002.contract.json
│
├── AGENTS.md                 ← Agent 操作手册
├── feature_list.json         ← 功能清单
├── progress.md        ← 进度追踪
├── init.sh                   ← 环境初始化
└── verify.sh                 ← 验证管线

CLAUDE.md 模板

# 项目名称

## 核心规则

1. 一次只处理一个 pending feature
2. 编码完成后运行 `./verify.sh`
3. 提交前更新 `progress.md`
4. 不修改已完成 feature 的代码

## 技术栈

- 语言:TypeScript 5.x(strict mode)
- 框架:Express 5
- 数据库:PostgreSQL 16
- 测试:Jest + Playwright

## 目录结构

- `src/` — 业务代码
- `tests/` — 测试文件
- `docs/` — 详细文档(按需读取)
- `contracts/` — Sprint Contract

## 按需读取

当你需要以下信息时,读对应文件:

- 架构和数据流 → `docs/architecture.md`
- API 接口规范 → `docs/api-spec.md`
- 编码风格要求 → `docs/coding-style.md`
- 测试编写规范 → `docs/testing.md`

## 当前任务`feature_list.json``progress.md` 获取。

40 行。 Agent 每次对话只需要吃这 40 行,就知道项目全貌和去哪找详情。

旧项目怎么迁移? 不用一次性重写。分三步:

  1. 冻结:先把现有 CLAUDE.md 原封不动备份成 docs/CLAUDE.legacy.md,避免改坏。
  2. 抽取:把"架构/API/测试/部署"等大段内容按主题剪到 docs/ 下对应子文件。
  3. 瘦身:CLAUDE.md 里每段内容换成一行"→ 读 docs/xxx.md"。跑一个 feature 试试,看 Agent 会不会自己去取子文件;不会就在导航里写得更明确。

团队协作注意:CLAUDE.md / docs/ 这些文件本质是项目源代码的一部分,应纳入 git 管理、走 code review。团队切忌每人一个本地魔改版——这会让你看到的"Agent 行为"和同事看到的完全不同,排查问题时非常痛苦。

模板二:Token 预算规划表(对应策略二)

用途:根据项目规模,规划每个会话的 Token 预算和会话策略。

项目规模文件数建议 CLAUDE.md 长度每会话 feature 数建议会话时长预估单次会话 Token
小型项目<2030-50 行1-2 个30-60 分钟20K-50K
中型项目20-10040-60 行1 个30-45 分钟50K-100K
大型项目100+50-80 行1 个20-30 分钟80K-150K

表中数字是经验值,用来给你一个起点,不是硬性上限。实际请按你项目的真实消耗校准。

几个关键经验

CLAUDE.md 通常不建议超过 100 行。 超过了就该考虑拆分到 docs/ 目录。这不是硬规则,但超过 100 行之后缓存命中带来的边际收益会被 CLAUDE.md 本身的编辑频率抵消。

大项目的会话要更短。 项目越大,每轮对话积累的上下文越多。20-30 分钟做一个 feature,提交,交接,开新会话。比跑 2 小时最后上下文爆掉强太多。

缓存窗口内密集操作。 两次操作间隔控制在 TTL 以内(默认 5 分钟),让提示词缓存持续命中。需要思考就先暂停 Agent,想清楚了再一次性发指令。

模板三:上下文生存清单(综合落地)

用途:贴在 AGENTS.md 里,让 Agent 自己也注意 Token 消耗。

## 上下文管理规则

### 读文件规则

- 只读当前任务需要的文件
- 不要一次性读取整个 src/ 目录
- 读大文件时只读需要的部分(指定行号范围)
- 读过的文件内容不需要在回复中重复

### 输出规则

- 回复只包含改动的代码,不要输出整个文件
- 用 diff 格式展示改动,而不是输出完整文件内容
- 解释简洁,不要长篇大论复述已有代码

### 错误处理

- 报错时只贴关键的错误信息和文件定位
- 不要贴完整的堆栈信息(除非关键信息在最底部)
- 修复后只展示改动部分

### 会话管理

- 每完成一个 feature 就考虑结束会话,开新会话继续
- 会话中如果累积了大量试错记录,主动压缩或开新会话
- 结束会话前必须写交接笔记

省钱效果到底有多大

声明在前:下面是一组典型中型项目(约 50 个文件)的经验估算,不是 benchmark。你的实际数字会因模型定价、使用强度、缓存命中率而浮动。

优化前

  • CLAUDE.md:约 800 行 ≈ 2400 Token
  • 每轮对话都全量加载上下文
  • 两次操作间隔 10-15 分钟,缓存经常失效
  • 单次会话跑 2 小时,累计 200K+ Token
  • 一个 feature 的成本:通常在 $3-5 区间

优化后

  • CLAUDE.md:约 50 行 ≈ 150 Token(主文件本身压缩约 94%)
  • 子文档按需加载,每次只多吃 200-500 Token
  • 缓存窗口内密集操作,命中率可达 80% 左右
  • 短会话 30 分钟,累计 40K-60K Token
  • 一个 feature 的成本:通常降到 $0.5-1

综合下来通常可省 70-80% 的 Token 成本。 不是靠换更便宜的模型,是靠更好的上下文管理。

别省过头:过度压缩会让 Agent 缺乏必要背景而反复发问、反复读文件,反而更贵、更慢。判断标准很简单:如果你发现 Agent 频繁主动请求 docs/ 下的同一个文件、或同一会话里问相同的问题,说明那部分信息压错了地方——该放回 CLAUDE.md 主文件里。

全系列回顾

三篇写完了。回头看整个系列:

解决什么问题核心模板
篇一Agent 乱跑4 个基础 harness 文件
篇二Agent 自说自话Sprint Contract + verify.sh + 验收清单
篇三Agent 失忆Initializer/Coding 提示词 + init.sh + 交接笔记
本篇Agent 烧钱文档结构模板 + Token 预算表 + 上下文生存清单

从"裸模型"到"完整 Harness",一共就这些文件。

不需要学新框架,不需要引入新依赖,不需要改代码架构。 就是在项目里加几个配置文件和脚本,Agent 的产出稳定性和成本效率就能有明显提升。

写在最后

Harness Engineering 说到底就三件事:

1. 告诉 Agent 怎么干(指令 + 合同)

2. 验证 Agent 干没干好(测试 + 验收)

3. 让 Agent 高效地干(上下文 + 缓存)

模型在快速迭代,但围绕模型的工程系统不会过时。

不管未来用 Claude 还是 GPT 还是别的什么模型,Harness 的思路是通用的:约束行为、验证产出、管理状态。不同模型的具体缓存实现、上下文窗口、计费方式会有差异,但"导航而非堆砌、主动而非被动、稳定而非频繁改动"这三条底层原则基本通用。


延伸阅读

  • 上下文管理深度:《马书》第三篇 — 自动压缩、微压缩、Token 预算策略的完整技术分析
  • 提示词缓存:《马书》第四篇 — 缓存架构、断点设计、缓存中断检测
  • 在线阅读harness-engineering-from-cc-to-ai-coding
  • 实战课程learn-harness-engineering(GitHub: walkinglabs/learn-harness-engineering)

系列导航

  • 篇一:Harness 不难啊,加这 4 个文件足够了
  • 篇二:Agent 说"搞定了"你就信?三个模板让它说到做到
  • 篇三:跑了 3 轮 Agent,每轮都从头来?两阶段工作流搞定多会话续接

如果你觉得这篇文章有帮助,欢迎:

  • 点个 Star
  • 在评论区分享你的 Token 成本控制心得
  • 关注「解药不甜SM」 公众号,一起交流 AI 实战经验,免费获取更多学习资料