怎么优化 Coding Agent 的成本:从 Prompt Cache 到 Context Engineering 的调研笔记
本文写于 2026 年 5 月。内容都是调研结论,还没有在项目中实际落地。这个领域变化极快,读的时候请注意时效性。
全文约 20,000 字,阅读大约需要 30-40 分钟。
TL;DR
- Coding Agent 的成本大头是 input token(占 token 总量的 93-99%),因为每轮 API 调用都重发完整上下文
- 最有效的省钱手段是 Prompt Cache(命中 cache 的 input 便宜 10 倍),做好缓存纪律(固定前缀顺序、清理多余 MCP)一次配置就能持续受益
- Context Engineering 四个方向:Select(让正确的代码进来)、Compress(压缩已有上下文)、Isolate(拆任务、换模型)、Write(把信息存到文件系统)
- 模型分级(Haiku 做探索、Sonnet 做实现、Opus 做架构)可以在 token 总量不变的情况下把成本降几倍
- 写到文件的外置记忆(SPEC.md、planning-with-files、session handoff)可以让工作跨 session 延续,但维护成本是一个 open problem(代码变了文档没跟着变)
- 用我今天的真实 session token 数据(Input 1.62M, Output 128.1K, Opus 4.6)做粗算:从什么都不做的 $11.30 到假设全部优化措施到位后的 $4-5。这是基于合理假设的估算(70% cache 命中率等),不是实测结果
起因
我们团队在使用Github Copilot作为开发的工具,前段时间Github Copilot开始宣布从6月份开始将转化为usage based billing,我大概估算了一下团队的用量,可能要翻个三倍的费用,于是把目前市场上的coding agent成本优化的开源方案进行了一轮调研,发布出来希望给大家进行参考。我个人一直在用claude code,团队则是在用opencode,所以主要是以这两个为主。
定价表
要省钱得先知道钱怎么收的。以下是截至 2026 年 5 月 Anthropic、OpenAI、GitHub 各家官方定价页面的数字。
Claude 和 GPT 的定价对比
| 模型 | Input ($/MTok) | Output ($/MTok) | Cached Input | Cache Write (5min) |
|---|---|---|---|---|
| Claude Opus 4.6 (1M) | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Sonnet 4.6 | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 |
| GPT-5.5 | $5.00 | $30.00 | $0.50 | — (自动) |
| GPT-5.4 | $2.50 | $15.00 | $0.25 | — |
| GPT-5.4 mini | $0.75 | $4.50 | $0.075 | — |
GitHub Copilot AI Credits
新计费方式用 AI Credits 计量,1 AI Credit = $0.01 USD,消耗依据是每次请求的 input tokens、output tokens、cached tokens 以及你用的模型单价。
| Plan | 月费 | 含 AI Credits |
|---|---|---|
| Pro | $10 | $10 (1,000 credits) |
| Pro+ | $39 | $39 (3,900 credits) |
| Business | $19/user | $19/user |
| Enterprise | $39/user | $39/user |
从这个表里就可以看出来,Cached Input 是普通 Input 的十分之一。Opus 4.6 的 input $5/MTok,命中缓存只要 $0.50/MTok,80% 命中率的话 input 成本直接打二折。Output 比 Input 贵五倍,Opus 4.6 是 $25 vs $5,GPT-5.5 更极端,$30 vs $5。Reasoning/Thinking tokens 也按 output 单价算,模型”想”得越多花得越多。
优化优先级很清楚:先让 input 命中缓存(成本差 10 倍),再减少不必要的 input,最后控制 output。
于是我所调研的第一个问题就是Prompt Cache 到底怎么回事?为什么它能让 input 便宜十倍? 为了解决这个问题,需要先简单了解一下大模型的工作流程
大模型处理一次请求的完整流程
flowchart TD
A["📝 输入文本"] --> B["① Tokenization\n文本切成 token(子词单元)"]
B --> C["② Embedding\n每个 token 映射成高维向量"]
subgraph prefill["Prefill 阶段(一次性处理全部 input,最贵)"]
C --> D["③ Transformer Layers × N 层\n每层做 Self-Attention + Feed-Forward Network\n计算量 O(n²)"]
end
subgraph decode["Decode 阶段(逐个生成 output,复用 KV Cache)"]
D --> E["④ 预测下一个 token 的概率分布"]
E --> F["⑤ 采样,选一个 token 输出"]
F --> G{生成结束?}
G -->|"否"| H["新 token 加入序列\n复用已有 KV Cache\n只算新 token 的 attention"]
H --> E
end
G -->|"是"| I["🏁 完整回复"]
style prefill fill:#fff0f0,stroke:#cc0000
style decode fill:#f0f7ff,stroke:#0066cc
style D fill:#ff6b6b,color:#fff,stroke:#cc0000
Prompt Cache 跳过的就是红色的 Prefill 阶段。如果这次请求的 input 前缀和上次一样,直接复用上次的计算结果,省掉 O(n²) 的开销。
上面这张流程图是一个总览,下面每一步拆开看看钱到底花在哪。
Tokenization
模型不认识”文字”。发给 API 的每一段代码、每一句话,首先会被切成 token(子词单元)。
tokens = tokenizer("帮我修一下 billing 里的 bug")
# → [帮, 我, 修一下, billing, 里的, bug]
# → [24891, 1588, 78432, 53012, 9876, 31205]切分用的算法通常是 BPE(Byte Pair Encoding)。原理:从单个字节开始,统计语料里哪些字节对最常相邻出现,把它们合并成一个新的 token,反复合并,最终建出一个几万到十几万条目的词表。高频词(“the”)整个是一个 token,低频词会被拆成多个子词。
flowchart LR
A["'帮我修一下 billing 里的 bug'"] --> B["Tokenizer\n(BPE 算法)"]
B --> C["[24891, 1588, 78432, 53012, 9876, 31205]"]
C --> D["6 个 token"]
style B fill:#4ecdc4,color:#fff,stroke:#2ba89e
切的效果大概是这样:
| 输入文本 | 切出的 tokens | token 数 |
|---|---|---|
帮我修一下 billing 里的 bug | 帮 + 我 + 修一下 + billing + 里的 + bug | 6 |
你好世界 | 你好 + 世界 | 2 |
calculateUsage | calculate + Usage | 2 |
const x = 42; | const + x + = + 42 + ; | 5 |
不可变数据结构 | 不可 + 变 + 数据 + 结构 | 4 |
定价表里的 “per million tokens” 就是这个 token。OpenAI 的 Tokenizer 工具 和 Anthropic 的 token 计数 API 都能看到代码实际有多少 token。
这里有个和钱直接相关的细节。不同模型的 tokenizer 不一样,同样的文本在不同模型上切出的 token 数量可能差不少。Opus 4.7 换了新的 tokenizer,Anthropic 官方说法是”改进了模型处理文本的方式,代价是同样的 input 可能映射出更多 token,大约 1.0-1.35 倍”。Simon Willison 做了实测,同一段 system prompt 在 Opus 4.7 上产生 7,335 个 token,在 4.6 上只有 5,039 个,涨了 46%。per-token 价格没变,但同样的文本要花更多 token,实际账单涨了 12-27%。对比不同模型成本时这种”隐性涨价”容易被忽略。
Embedding
token 到这一步只是一个编号,比如词表里的第 53,012 个条目。一个整数没有语义信息,模型没法直接拿它做计算。
Embedding 做的事情是给每个 token 分配一个高维向量。具体来说就是查一张大表(词表大小 × 向量维度的矩阵),拿 token 编号当行号,取出对应的一行浮点数。
flowchart LR
A["token #53012\n'billing'"] --> B["查 Embedding 表\n(词表大小 × 4096)"]
B --> C["[0.12, -0.85, 0.33, ..., 0.07]\n4096 维向量"]
D["token #31205\n'bug'"] --> B
style B fill:#9b59b6,color:#fff,stroke:#7d3c98
可以把它想成给每个词在一个 4096 维的空间里安排一个坐标。语义相近的词坐标靠近:“dog” 和 “puppy” 的向量距离近,“dog” 和 “spreadsheet” 的向量距离远。这一步计算量不大,就是一次表查找,速度很快。
不过 embedding 有一个局限:它给每个词分配的向量是固定的,不看上下文。“bank” 在 “river bank” 和 “bank account” 里拿到的是同一个向量,区分不了”河岸”和”银行”。
这个问题要到下一步才能解决。
Transformer 和 Self-Attention
Transformer 是 2017 年 Google 在论文 “Attention Is All You Need” 里提出的模型架构。今天几乎所有大语言模型(GPT、Claude、Gemini、LLaMA)都基于它。
在它之前,处理序列数据主要靠 RNN(循环神经网络)。RNN 有两个问题:一是必须一个词一个词顺序处理,长序列跑得很慢,没法利用 GPU 的并行能力;二是前面的信息传到后面会逐渐衰减,一段 2000 词的文本,开头的内容到结尾已经模糊了。Transformer 用 Self-Attention 机制替代了顺序处理,让每个 token 可以同时看到序列里的所有其他 token,既能并行,又不会因为距离远就丢信息。
回到 “bank” 的例子。Embedding 给 “bank” 的向量是固定的,没法区分两种含义。Self-Attention 的做法是让 “bank” 去看同一句话里的其他词:如果旁边是 “river”,它的表示就会往”河岸”方向移动;如果旁边是 “deposit”,就往”银行”方向移动。
图:Self-Attention 的矩阵计算——Query × Key 得到注意力分数,再和 Value 相乘(来源:Jay Alammar, The Illustrated Transformer)
具体机制用一个简化的数字例子说明。假设序列只有 3 个 token:I、went、bank,每个 token 的 embedding 是 2 维向量(实际是 4096 维,这里简化)。
每个 token 的向量经过三组权重矩阵 W_Q、W_K、W_V,生成三个新向量:
- Query(Q):“我在找什么信息?”
- Key(K):“我能提供什么信息?”
- Value(V):“我的实际内容。”
Q K V
I [1.2, 0.3] [0.5, 1.1] [0.8, 0.2]
went [0.7, 0.9] [1.0, 0.4] [0.3, 0.7]
bank [1.1, 0.6] [0.8, 0.9] [0.6, 0.5]对 bank 来说,用它的 Q 和所有 token 的 K 做点积,算出注意力分数:
bank_Q · I_K = 1.1×0.5 + 0.6×1.1 = 1.21
bank_Q · went_K = 1.1×1.0 + 0.6×0.4 = 1.34
bank_Q · bank_K = 1.1×0.8 + 0.6×0.9 = 1.42经过 softmax 归一化后变成概率分布(和为 1),再用这些分数对 V 做加权求和,得到 bank 在这句话里的新表示。
flowchart TD
subgraph attn["Attention 权重示例:'I went to the bank to deposit money'"]
direction LR
B["bank"] -->|"0.35"| D["deposit"]
B -->|"0.28"| M["money"]
B -->|"0.15"| W["went"]
B -->|"0.12"| T["to"]
B -->|"0.10"| I["I"]
end
style B fill:#ff6b6b,color:#fff
style D fill:#ffd93d,color:#333
style M fill:#ffd93d,color:#333
在完整句子中,“bank” 对 “deposit” 和 “money” 的注意力最高,它的表示向”银行”方向移动。如果句子换成 “I sat on the bank by the river”,权重分布会完全不同。
上下文越长越贵的根本原因就在这里。n 个 token 的上下文,每个 token 都要和其他所有 token 算关系,一共 n x n 个点积运算。10 万 token 的上下文意味着每一层都有 100 亿次 attention 计算。不是线性增长,是平方增长,O(n^2)。
flowchart TD
subgraph layer["单层 Transformer Layer(重复 N 次)"]
direction TB
T["输入:每个 token 的向量表示"] --> Q["生成 Query 向量\n「我在找什么?」"]
T --> K["生成 Key 向量\n「我能提供什么?」"]
T --> V["生成 Value 向量\n「我的实际内容」"]
Q --> S["Query × 所有 Key\n→ 注意力分数矩阵\n(n × n,这就是 O(n²) 的来源)"]
K --> S
S --> W["分数 × Value 加权求和\n→ 每个 token 的新表示"]
V --> W
W --> F["Feed-Forward Network\n非线性变换(计算量固定)"]
F --> O["输出:更新后的 token 向量"]
end
style S fill:#ff6b6b,color:#fff,stroke:#cc0000
style layer fill:#f9f9f9,stroke:#ccc
红色部分是成本的核心:n 个 token 之间两两计算关联,上下文越长这个矩阵越大。
现代大语言模型有几十到上百层这样的 Transformer layer,每一层做两件事:Self-Attention 加上 Feed-Forward Network(对 attention 结果做非线性变换,计算量固定,不随上下文长度变)。
如果想更直观地理解这个过程,Georgia Tech 的交互式 Transformer Explainer 可以实时看到每一步的计算。
Prefill:一次性处理全部 input
从 Tokenization 到 Embedding 到 Transformer 的 N 层 Self-Attention,这一整套流程跑在你发过来的全部 input tokens 上,一次性并行处理。这个阶段叫 Prefill。
Prefill 是整个推理过程中计算量最大的部分。你发了 60k token 的上下文,每一层 Transformer 都要算 60k × 60k 的 attention 矩阵。定价表里 input token 的费用主要就花在这里。
Prefill 还有一个重要的副产品:在算 Self-Attention 的过程中,每个 input token 的 Key 和 Value 向量都已经计算出来了。这些 K/V 向量会被保存在 GPU 显存里,形成初始的 KV Cache。后面 Decode 阶段生成新 token 时就不用重新算这些旧 token 的 K/V 了,直接从 Cache 里取。
换句话说,KV Cache 在 Prefill 阶段建立,在 Decode 阶段使用和增长。
这里有个有意思的想法:既然 Prefill 做的事情(Tokenization → Embedding → Transformer 各层)对所有基于同一架构的模型来说流程是类似的,那能不能在不同模型之间共享 Prefill 的计算结果?比如同一段 system prompt,Prefill 一次,K/V Cache 给多个模型用?
学术界已经在探索这个方向。2026 年初的一篇论文 PrefillShare 提出了跨模型共享 Prefill 模块的方案:把模型拆成 Prefill 和 Decode 两部分,冻结 Prefill 模块,只微调 Decode 模块,这样多个任务专用模型就可以共享同一个 Prefill 的 KV Cache。实测在多模型 Agent 工作负载下,p95 延迟降了 4.5 倍,吞吐提升 3.9 倍。工业界也在朝这个方向走,vLLM、SGLang、NVIDIA Dynamo 都在做 Prefill-Decode 分离架构,把 Prefill 和 Decode 拆成独立的服务分别部署和扩缩容。这些目前还是比较前沿的基础设施层面的工作,普通 API 用户暂时碰不到,但它说明 Prefill 的计算复用空间比我们想象的大。
接下来进入生成阶段。
Decode
Prefill 跑完后,最后一层输出一个概率分布:“下一个 token 最可能是什么?“根据采样策略选一个 token,然后把这个 token 加入序列,重新走一遍所有层,生成再下一个 token。如此循环,直到模型输出结束标记。
这个逐 token 生成的过程叫 Decode。每生成一个新 token,理论上需要让它和前面所有 token 重新做 attention,也就是对所有 token 重算 Key 和 Value 向量。但前面 token 的 K/V 在上一步已经算过了,没有变化,全部重算很浪费。
这就是为什么上一节里我们把之前的计算结果给保存在GPU里,把每个 token 算过的 Key 和 Value 向量缓存起来,生成下一个 token 时直接复用,只需要对新 token 算一组新的 Q/K/V。用刚才的矩阵例子说明这个过程:
Prefill 阶段,一次性处理 3 个 input token,K/V 矩阵建好:
K cache: V cache:
[I_K ] [0.5, 1.1] [I_V ] [0.8, 0.2]
[went_K] [1.0, 0.4] [went_V] [0.3, 0.7]
[bank_K] [0.8, 0.9] [bank_V] [0.6, 0.5]
Decode 第 1 步,模型生成 "account":
只算 account 自己的 Q/K/V,然后把 K/V 追加到缓存:
K cache: V cache:
[I_K ] [I_V ]
[went_K ] [went_V ]
[bank_K ] [bank_V ]
[account_K] ← 新增 [account_V] ← 新增
account 的 Q 和缓存里全部 4 个 K 做点积 → 注意力分数 → 加权求和 V
Decode 第 2 步,模型生成 "balance":
同样只算 balance 自己的 K/V,追加一行,Q 和 5 个 K 做点积。每步只增加一行,不重算旧的。这是同一次请求内部的加速机制,推理引擎自动处理。
图:KV Cache 随生成的 token 逐个累积——Token 1 创建 [K1,V1],Token 2 追加 [K2,V2],后续 token 只需计算自己的 K/V 并追加(来源:Hugging Face KV Caching Explained)
Prefill vs Decode:钱花在哪
| 阶段 | 做什么 | 计算量 | 对应费用 |
|---|---|---|---|
| Prefill | 一次性处理全部 input tokens | O(n^2),n = input 长度 | Input token 费用(Opus 4.6: $5/MTok) |
| Decode | 逐个生成 output tokens | 每个新 token O(n) | Output token 费用(Opus 4.6: $25/MTok) |
Prefill 贵是因为它要一次性处理整个 prompt。发了 60k token 的上下文,每一层都得计算 60k x 60k 的 attention 矩阵。Output 单价更高($25 vs $5)则是因为 Decode 阶段 GPU 利用率低,每次只生成一个 token,没法像 Prefill 那样大规模并行,provider 把 output 单价设高来覆盖这部分硬件闲置成本。
那么问题来了:如果这次请求和上次请求的 input 前半段一模一样(system prompt、工具定义、项目规则都没变),是不是可以跳过那段的 Prefill 计算?
可以。这就是 Prompt Cache。
各种 Cache 的区别
做开发的人每天都在和各种 Cache 打交道,但 LLM 语境下的 “Cache” 和我们平时说的不完全一样。我也是看完了大模型的工作原理之后才把它们理清楚。
开发者熟悉的 Cache
| Cache | 在哪 | 缓存什么 |
|---|---|---|
| 浏览器缓存 | 客户端 | CSS/JS/图片;Cache-Control 头控制 |
| CDN 缓存 | 边缘节点 | HTTP 响应 |
| Redis / Memcached | 应用服务器 | 数据库查询结果、计算结果 |
| 数据库 Buffer Pool | 数据库内部 | 热数据页 |
这些 Cache 有个共同点:缓存的是”结果”。输入一样,直接返回之前的结果,跳过计算。
LLM 世界里的 Cache
LLM 推理有两种 Cache 对我们很重要,它们缓存的是中间计算状态,不是最终答案。
KV Cache 是同一次请求内部的加速。生成第 101 个 token 时,前 100 个 token 的 K/V 已经算过了,直接复用。你可以把它理解成写文档时的”短期记忆”,前面写过的内容不用从头重读。这个你控制不了,推理引擎自动处理。
Prompt Cache(也叫 Prefix Cache)是跨请求复用 Prefill 计算。如果你这次请求的 input 前半段和上次请求一样,就跳过那段前缀的 Prefill,直接复用上次算好的 K/V。操作手册的前 50 页你昨天已经读过了,今天只需要从第 51 页开始读。Cached Input 能便宜十倍的原因就在这里,跳过了最贵的 Prefill 计算。
还有一种 Semantic Cache,应用层中间件,如果新问题和之前某个问题”语义上足够相似”就直接返回旧答案,连 API 都不调。这更像 Redis,缓存的是最终结果。对 FAQ、客服有用,对 Coding Agent 不适合,因为代码一直在变,“语义相似的问题”在不同代码版本下答案完全不同。
| Cache | 缓存什么 | 跨请求? | 你能控制? | 对账单的影响 |
|---|---|---|---|---|
| KV Cache | 当前请求已算的 K/V | 不能 | 不能 | 透明(推理加速) |
| Prompt Cache | 相同前缀的 Prefill K/V | 能 | 部分 | 直接决定 cached input 费率 |
| Semantic Cache | 语义相似问题的旧答案 | 能 | 完全 | 跳过整个 API 调用 |
Prompt Cache 的核心规则:前缀必须完全一致
从第一个 token 开始逐个比对。一旦某个位置出现不同,从那个位置往后全部算 cache miss。
请求 A: [system prompt][tools][rules][history A][user message A]
请求 B: [system prompt][tools][rules][history B][user message B]
↑ 这段相同 → cache hit ↑ 从这里不同 → miss不同 provider 的机制有差异:Anthropic 可以显式设置 cache_control breakpoint 和 TTL(5 分钟 / 1 小时),第一次 cache write 比普通 input 贵 1.25x-2x,之后 read 只要 0.1x。OpenAI 是自动检测相同前缀,cached input 0.1x,没有额外 cache write 费用。GitHub Copilot 走底层模型的 cache 机制,用户看到的是 AI Credits 里 cached input 更便宜。
什么东西会影响 Cache
既然前缀必须完全一致,那么只要你更改了你的前面的内容,就会影响Cache的命中,根据社区总结有:
| Cache 杀手 | 为什么 | 怎么修 |
|---|---|---|
| system prompt 里放时间戳 | 每秒都变 | 时间戳移到消息末尾 |
| MCP 工具注册顺序随机 | 工具列表每次不同 | 固定注册顺序 |
| 随机 few-shot 示例 | 示例排列变化 | 固定示例集和顺序 |
| session ID 嵌在前缀 | 每个会话不同 | 移到后缀 |
| 频繁 compact/summarize | 摘要每次不同 → 新前缀 | 尽量延迟 compact |
| 不用的 MCP 工具没清理 | 白白占前缀空间 | 定期清理 |
一个很直观的例子:我的 Claude Code 环境注册了 40+ MCP 工具,光工具 schema 就大约 8-10k tokens。如果这些 schema 顺序每次不同,后面 50k tokens 再相似也 miss。反过来,顺序固定的话,这 8-10k tokens 就是稳稳命中缓存的前缀,每次只花 0.1x 的价格。
这个认识改变了我的优化思路。减少 system prompt 当然好,但更有效的是让它稳定,一直命中缓存。
社区里有个工具专门解决 cache 对齐的问题:claude-code-cache-fix(~1,800 stars, MIT),它发现 Claude Code 的 --resume session 会导致 cache regression(微压缩时嵌入的时间戳每次不同,导致前缀字节变化),通过本地代理把 sentinel 归一化成固定格式,cache hit rate 从 82.3% 提升到 95.5%。
如果你用 Claude Code 接第三方本地模型,还有一个更直接的 cache 杀手。Claude Code 会在每次请求开头加一个 attribution header,这个 header 每次都变。对 Anthropic 自己的 API 来说这不影响什么,但对本地推理服务器来说,前缀变了意味着 KV Cache 全部作废,每次都得从头 Prefill。实测数据展示:Qwen2.5-Coder 14B 在消费级 GPU 上,6k token 上下文的首 token 延迟从 1.8 秒飙到 24 秒,cache 命中率直接归零,吞吐降了约 90%。修复方法是在 ~/.claude/settings.json 的 env 里加一行 "CLAUDE_CODE_ATTRIBUTION_HEADER": "0"(注意 shell 的 export 不生效,必须写在 settings 文件里)。
从 Cache 到 Context Engineering
从前面的内容可以感觉到,Prompt Cache 这一层我们能做的事情比较有限:固定前缀顺序、清理多余 MCP、关掉 attribution header,做完这几件事 cache 命中率就已经很高了。社区里的开源工具绝大多数也不是在 cache 层面做文章,它们集中在另一个层面:怎么减少进入上下文的内容总量, 以及怎么让模型在少说话的情况下输出足够多的代码。
2025 年年中,Context Engineering 这个词开始频繁出现。Anthropic 发了一篇工程博客 “Effective context engineering for AI agents” 做了系统性阐述,核心观点是把上下文窗口当稀缺资源管理,按需加载,不要预加载。LangChain 的 Lance Martin 把具体操作归纳成四类:Select(拉进相关信息)、Compress(减少 token)、Isolate(拆到独立窗口)、Write(存到窗口之外)。后面按这个框架展开。
Select:让正确的信息进来
这是 Context Engineering 里最关键也最复杂的一步。代码库可能有几十万行,Agent 这一轮可能只需要看 500 行,怎么找到那 500 行?
市面上有两种解决方案,第一种是以Cursor为代表的 Indexing,第二种则是以Claude Code为代表的Grep
Codebase Indexing
Indexing 的思路是提前给代码库建一个可查询的索引,让 Agent 一步到位。我调研了目前市面上的方案,按原理分几类。
Embedding 向量索引
把代码切成 chunks,生成语义向量,存进向量数据库,查询时通过语义相似度检索相关代码段。
| 工具 | Stars | License | 特点 |
|---|---|---|---|
| Claude Context (Zilliz) | 11.3k | MIT | AST chunking + Milvus 向量库 + hybrid BM25+向量检索;支持 OpenAI/VoyageAI/Ollama embedding |
| CocoIndex Code | 1.7k | Apache-2.0 | 轻量级;AST-aware chunks + 本地 SentenceTransformers;增量索引 |
| SeaGOAT | 1.3k | MIT | 完全本地;ChromaDB + ripgrep 混合搜索;不调云 API |
| claude-context-local | 229 | GPL-3.0 | Claude Context 的纯本地版;EmbeddingGemma + FAISS;零 API 成本 |
如果项目对数据安全敏感、不想把代码发到云端,SeaGOAT 或 claude-context-local 是更稳妥的选择。需要多仓库多语言支持的话 Claude Context 功能最全。
Cursor、Augment 的 Index 功能也是这个原理。但是既然用Embedding做索引,那么Embedding的问题就都会有,比如说
- 同义词 / 内部术语问题:你搜 “billing”,代码里可能叫 “subscription”、“metering”、“invoice”。Embedding 模型在通用语料上训练,不一定理解你公司内部的词表。搜 “login” 可能找不到叫
SessionCreation的函数。 - 概率性结果:语义搜索返回的是”最相似”的结果,不是”最正确”的。高 confidence 的错误答案比搜不到更危险,因为 Agent 会按搜到的结果继续走下去。
AST / 知识图谱
这类工具用 Tree-sitter 解析代码的 AST(抽象语法树),提取函数、类、调用关系、继承关系,建成图谱。
| 工具 | Stars | License | 特点 |
|---|---|---|---|
| Graphify | 48.6k | MIT | Tree-sitter (20 langs) + LLM 语义提取 → 多模态知识图谱;NetworkX + Leiden 聚类 |
| GitNexus | 37.9k | MIT | Tree-sitter → KuzuDB 图数据库;完全 WebAssembly 客户端运行;Graph RAG Agent |
| code-review-graph | 15.6k | MIT | 增量变更检测 → blast radius 分析;声称 6.8x fewer tokens on reviews, 49x on daily coding |
| CodeGraph | 2.2k | MIT | Tree-sitter → SQLite + FTS5;Claude Code MCP;文件系统 watch 增量同步 |
| Aider Repo Map | 41.6k | Apache-2.0 | Tree-sitter 符号提取 → 有向图 → PageRank 排序;在 token 预算内二分搜索最重要的定义 |
如果已经在用 Claude Code,CodeGraph 通过 MCP 直接接入,上手最快。如果需要跨语言的大规模代码理解,Graphify 覆盖的语言最多。Aider 的方案适合想完全本地运行、不依赖外部服务的场景。
Aider 的做法值得展开说一下。它不用向量数据库,不用 embedding,用 PageRank 对代码符号排重要度。当前聊天里提到的文件权重 50x,活跃标识符权重 10x。然后在 token 预算内(默认 1024 tokens)用二分搜索选出排名最高的定义,渲染成简洁的树状结构给模型看。完全本地、每条消息重新计算、结果确定性。
Grep:Claude Code 为什么不用 Index
讲完 Index 和 AST 图谱,一个自然的问题是:Claude Code 为什么不做 codebase indexing?
答案是它试过,放弃了。Claude Code 早期版本用过 RAG + 本地向量数据库,但团队发现 agentic search(grep + glob + read)的表现持续优于 RAG。Boris Cherny(Claude Code 创始工程师)在多个场合解释过原因。2026 年 Amazon Science 的一篇论文也给出了佐证:关键词搜索通过 agentic tool use 可以达到 RAG 90% 以上的效果,不需要向量数据库。
grep 的优势在几个地方:
- 零配置:不需要索引、不需要 embedding 模型、不需要向量库。clone 下来直接能搜。
- 极致的新鲜度:Agent 刚写完的代码,下一秒 grep 就能搜到。Index 有 5-10 分钟的延迟,stale index 会把 Agent 带到错误方向。
- 全格式覆盖:YAML、Dockerfile、.env、Markdown、配置文件,grep 都能搜。Index 和 LSP 通常只覆盖代码文件。
- 故障模式温和:grep 最差的情况是返回太多结果(噪音),Agent 可以过滤。Index 或 LSP 如果配置错误或缺依赖,可能直接返回空结果(Agent 以为没有相关代码),这种 false negative 比 false positive 危险得多。
grep 的代价也很明确:
- Token 消耗高:搜 “billing” 返回 200 行,大部分是噪音,全部进入上下文。几轮下来上下文里塞满了无关代码。
- 同义词无能:搜 “billing” 找不到叫 “subscription” 的代码,这一点和 embedding 一样有问题,只是失败方式不同。
- 大仓库慢:Cursor 的工程团队报告过 ripgrep 调用在大 monorepo 里超过 15 秒。
到这里可以看到一个清晰的层次:Embedding Index 做的是”找可能相关的代码”,AST 图谱做的是”理解代码之间的结构关系”,grep 做的是”按关键词现场搜”。三者各有优劣,但都有一个共同的精度问题:grep 返回 200 个文本匹配,其中只有 3 个是真正的函数调用点,其余是注释、字符串、测试里的碰巧同名。Embedding 返回”语义最相似”的结果,可能是错的。AST 图谱理解结构,但覆盖不了动态语言的反射和运行时行为。
有没有一种方式能直接用编译器的理解来回答”这个函数在哪被调用了”?有,这就是 LSP。
LSP:给 Agent 编译器的眼睛
LSP(Language Server Protocol)是微软 2016 年为 VS Code 设计的协议。在它之前,每个 IDE 都要自己实现每种语言的智能功能(补全、跳转、重构),工作量巨大。LSP 的思路是把”语言智能”抽成独立的后台服务(Language Server),IDE 通过标准化协议和它通信。TypeScript 有 tsserver,Rust 有 rust-analyzer,Python 有 pyright,Go 有 gopls。任何支持 LSP 协议的编辑器都能获得这些能力。
对 AI Agent 来说,LSP 提供的是编译器级别的精确导航。grep “calculateUsage” 做的是纯文本匹配,它不知道一个字符串是函数名、变量名、注释还是字符串字面量。LSP 的 find-references 通过类型系统和语法分析,只返回语义层面的真实调用点。go-to-definition 一步跳到函数定义,不需要在 200 个 grep 结果里翻。hover 直接给出类型签名,不用读整个文件。
社区做过一个可复现的 benchmark,跨 5 个代码库、13 个任务、3 种语言测试,结果是 LSP 比 grep 节省 5-34x tokens。节省幅度和代码库大小正相关:15k 行的项目省 5 倍,319k 行的项目省 34 倍。原因很直观:grep 的结果量随代码库线性增长,LSP 返回的引用数只和实际调用点有关,跟代码库大小无关。
| 方法 | 过程 | Token 消耗 |
|---|---|---|
| grep | grep calculateUsage → 200+ 结果 → 逐个 read → 5 轮后找到 | ~12,000 |
| LSP find-references | 一步返回 3 个精确调用点 | ~500 |
各 AI coding 工具对 LSP 的支持差别很大。OpenCode 把 LSP 当作核心架构,内置 30+ 预配置语言服务器,打开对应文件类型时自动下载安装,Agent 直接拿到 diagnostics、hover、go-to-definition、find-references、call hierarchy。Claude Code 也有内置 LSP 支持(11 种语言,需设置 ENABLE_LSP_TOOL=1),但实际触发率不高,社区有人反馈 “I haven’t come across a case where it has used the LSP yet”。如果想在 Claude Code 里强制用 LSP,可以装 cclsp(637 stars, MIT)做 MCP bridge,再配合 enforcement-kit(294 stars, MIT)用 hooks 拦截 grep 调用。
LSP 的问题也很明确:
- 资源占用大:语言服务器常驻内存。TypeScript 项目的 tsserver 可以吃 2-4GB 内存,大项目的 rust-analyzer 初始化要 30-60 秒。OpenCode 内置几十个语言服务器全开的话,机器明显变卡。这也就是为啥Opencode通常特别卡的原因之一.。
- 动态语言精度差:Python 的
getattr、Ruby 的method_missing、JavaScript 的动态 import 对 LSP 不可见。LSP 依赖静态分析,看不到运行时行为。 - 非代码文件覆盖不了:YAML、Dockerfile、Markdown、配置文件,LSP 帮不上。grep 反而在这些场景更有用。
- 为编辑器设计,不是为 Agent 设计:LSP 的交互模型是基于光标位置的一次一个操作,Agent 需要的是批量跨文件查询。这个 mismatch 目前没有好的解决方案。
- Agent 不主动用:如果不强制,Agent 会跳过 LSP 直接 grep,因为 grep 调用更简单。enforcement-kit 这类 hook 就是为了解决这个问题。
我的初步判断:静态类型语言(TypeScript、Rust、Go、Java)应该开 LSP,投入产出比明确,特别是 OpenCode 这种开箱即用的环境。动态语言项目或者小型项目,好的 CLAUDE.md 加 grep 可能更务实。
业务代码地图:最可靠的上下文
比任何 Index 都可靠的方法是把人类的知识直接写成 Agent 能读的文档。
CLAUDE.md / AGENTS.md / .cursorrules 做的是同一件事——告诉 Agent “这个项目的入口在哪,业务概念叫什么,常见坑在哪,更多细节去哪找”。Anthropic 在他们的博客里把这叫做 “always-loaded context”。
好的 CLAUDE.md 是路标,不是百科全书。它应该回答的是 “Agent 接到一个任务后最先需要知道什么”,不是把所有模块的文档塞进去。
## Architecture
- 入口:src/api/routes.ts
- 业务逻辑:src/services/
- 计费相关:src/billing/(不是 src/payments/)
## Domain Glossary
- plan = 用户侧套餐,subscription = 账务侧订阅
- metering = 用量采集(≠ billing)
- usage_event ≠ usage_summary(原始事件 vs 聚合)
## Common Pitfalls
- 不要改 src/generated/ 下的文件
- billing 单测需要 mock clock
- 用 npm run test:unit,不要用 npm test(后者跑全量 e2e)为什么这比 Index 稳?因为它是确定性的。Agent 读到 “billing 在 src/billing/” 就不用 grep 整个仓库。省掉的不是一点 token,是 3-5 轮无效探索。而且它不会 stale(只要有人维护)、不需要 embedding、不需要外部服务。
社区在这个方向上已经形成了一些共识。影响最大的是 Karpathy 的 CLAUDE.md 规范(142k+ stars)。Karpathy 观察到 AI Agent 写代码时有四个反复出现的毛病:默默做了错误假设不验证、把简单问题过度抽象、改了不该改的代码、不定义验证标准就开始写。这四条规则把 Agent 的错误率从 41% 降到了 11%。
社区后来在此基础上做了多轮改进。Mnilax 在 30 个代码库上测试了六周后,又加了 8 条规则,覆盖 agent fights、hook cascades、skill loading conflicts、multi-step workflow drift 等原版没涉及的问题,错误率进一步降到 3%。他的一个关键观点是:prompt 指令是建议,hooks 才是强制执行。能用代码强制的规则就不要靠 prompt,期望 80% 的 prompt 合规 + 20% 的 hooks 兜底,是比较现实的工作方式。
关于 CLAUDE.md 的写法,有一条重要的成本约束:Anthropic 建议控制在 200 行以内。200 行大约 1,500-2,000 tokens,注入到每一次请求的前缀里。膨胀到 500 行就是每轮 5,000 token 的税。很多团队会写 Agent 本来就知道的东西(“请用 TypeScript 写代码”),花了 token 但什么都没改变。只写 Agent 没法从代码推断出来的信息:架构决策、业务术语、团队约定、已知的坑。
如果不想从零写,社区也有自动生成的方案。Caliber 可以扫描项目结构、依赖和代码模式,自动生成 CLAUDE.md(同时生成 .cursorrules 和 MCP server 配置)。Claude Code 内置也有一个 /init 命令可以根据代码库生成初始的 CLAUDE.md。不过自动生成的结果通常需要手动裁剪,因为生成器倾向于把所有能发现的东西都写进去,很容易超过 200 行的预算。
Skill/MCP Triage
前面讲的 Index、Grep、LSP 解决的是”怎么找到正确的代码”。但 Coding Agent 的上下文里不只有代码,还有大量 MCP 工具定义和 Skill 指令。我算了一下自己的 Claude Code 环境,注册了 40 个 MCP 工具,光 schema 就占了大约 8-10k tokens,每次请求都在为它们付钱。
这里要解决两个问题:一个是怎么像 Skill 一样对 MCP 做渐进式披露(或者干脆去掉 MCP,用 CLI 替代,就没有 schema 吃 context 的问题了);另一个是当 Skill 也越来越多的时候,怎么让 Skill 本身也做一遍渐进式披露。
MCP 工具的 Triage
Claude Code 内置了 ToolSearch 机制,自动启用,不需要手动配置。Claude Code 会计算所有可 defer 的工具定义的总 token 数,超过 context 窗口 10% 就自动开启 deferred loading。不开的时候,147 个工具定义大约 90k tokens 注入每次请求。开了之后只保留 ~25 个核心内置工具加一个 ToolSearch 工具,大约 15k tokens,减少 95%。Agent 需要某个被 defer 的工具时,先调 ToolSearch(BM25 或 regex 搜索工具名称和描述),拿回 3-5 个候选,自动展开成完整定义,再调用。
ToolSearch 的代价是多了一轮调用(先搜再用),搜索质量依赖工具的名称和描述写得好不好。社区有人反馈内置工具被错误 defer 导致功能退化。
OpenCode 在 MCP 层面也想做类似的事。PR #12520 实现了 mcp_search 做按需发现,目标是把 7+ MCP servers 的 67k tokens 启动成本降到 ~350 tokens。截至写作时还没合入。
社区围绕 MCP triage 做了不少方案,按原理可以分成几类。Schema 压缩是把工具描述本身缩短;懒加载是用 2-3 个 meta-tools 替代全部工具,按需获取完整 schema;Code Mode 更激进,只暴露一两个执行工具,Agent 写代码来调用后端。
| 工具 | Stars | License | 原理 | 特点 |
|---|---|---|---|---|
| IBM ContextForge | 3,747 | Apache-2.0 | Gateway + 压缩 | MCP/A2A/REST 统一网关;TOON 格式压缩工具输出 |
| Headroom | 1,924 | Apache-2.0 | 输出压缩 | 压缩工具返回值(60-95%),不压缩 schema 本身;可逆 |
| 1MCP | 440 | Apache-2.0 | 懒加载 | instructions → inspect → run 三步渐进发现 |
| One MCP | 392 | MIT | 代理聚合 | 多 MCP server 合成单一端点;Service Groups 导出为 Skills |
| mcp-server-code-execution-mode | 333 | GPL-3.0 | Code Mode | 只暴露 run_python(~200 tokens);Agent 写 Python 调用后端工具 |
| MCPProxy | 227 | MIT | 懒加载 | retrieve_tools 语义搜索;~99% token 减少;内置安全隔离 |
| mcp-funnel | 153 | MIT | 过滤 + 懒加载 | hideTools 白名单/黑名单(glob);可选 discover_* meta-tools |
| Forgemax | 149 | Custom | Code Mode | 把 N 个 server × M 个工具压成 2 个工具(search + execute,~1,100 tokens 恒定);V8 沙盒执行 |
Skill 的 Triage
Skill(.md 格式的指令文件)也有 token 膨胀的问题。Anthropic 官方的 Agent Skills 规范定义了三级渐进式披露:Level 1 只在 system prompt 放名称+描述(~50-100 tokens/skill),Level 2 通过 Skill tool 按需加载全文,Level 3 按需加载 resources。各大coding agent 已经内置了这个机制,skill 全文不会预加载。到这一步,问题看起来解决了。
实际装多了就发现没有。
问题出在 metadata 本身。虽然 skill 全文不预加载,但每个 skill 的名称和描述仍然注入 system prompt。社区有人深挖了这个”发现天花板”:Claude Code 给 available_skills 分配的 token 预算有上限(据逆向分析,大约是 context 的 1%,最少 8,000 字符),每个 skill 描述截断到最多 250 字符。8,000 ÷ 250 = 最多约 32 个 skill 能被有效匹配。超过这个数,描述太短,Agent 匹配不上直接忽略。有人装了 63 个 skill,只有 42 个出现在 session 里,其余被预算截掉。Google Workspace CLI 装了 95 个 skill,每轮光 metadata 就吃掉 2,007 tokens。
而且 skill 装多了不只是浪费 token,还会让 Agent 变差。Vercel 的评估发现 Agent 有 56% 的时间忽略了相关的已安装 skill,装了 skill 反而比 baseline 更差(测试通过率 58% vs 63%),未被使用的 skill 在 context 里引入了噪音。Perplexity 在他们的 skill 设计指南里也提到了类似的判断:“每加一个 Skill,都有可能让其他所有 Skill 变差。” 他们没有给出实验数据,但解释了机制:每个新 skill 的描述在路由时和已有 skill 竞争模型的注意力,可能导致错误激活。他们还发现单个 skill 内部的组织方式也很关键:把美国税法全部 1,945 节塞进一个文件夹(一个 skill),模型的表现比不装这个 skill 还差,必须按逻辑分层组织(多级目录 + 快速参考指南 + 自定义搜索工具)才能让模型有效利用。
目前社区对这个天花板有三种应对思路:
控制数量。skill 控制在 ~30 个以内,超过就发现率下降。装了大量 skill 不确定哪些在用的话,Token Optimizer(1,051 stars, PolyForm NC)可以审计实际使用情况,清理从未被触发的 skill 和 hook,有人通过审计 + 清理做到了 44% 的 overhead 削减。
绕过原生索引,用外部搜索替代。那篇发现天花板的博客作者做了一个叫 PreBrief 的本地 daemon:用小模型把所有 skill embed 成向量,用户输入时语义搜索,只把匹配到的 skill 注入 prompt,同时把原始 skill 移到 disabled 目录绕过原生索引。OpenCode 这边,opencode-triage(MIT)用 hooks 隐藏全部 skill,暴露一个 triage({ query }) 做关键词匹配路由(带 IDF 权重和拼写纠正),19 个 skill 从 1,226 tokens 降到 59 tokens(95%)。
根本不预装,需要时再发现。Google Cloud 的一位工程师做了一个 Dynamic Skills Agent,完全不预装 skill,Agent 需要某个能力时通过 searchAgentSkills 从受信的 GitHub 仓库按需搜索和安装。metadata 开销为零,代价是首次使用时多一轮搜索和安装延迟。
三种方向都还在实验阶段。目前最务实的做法仍然是控制 skill 数量 + 定期审计清理。
CLI 替代 MCP:一个正在发生的趋势
还有一个更激进的思路:干脆不用 MCP,用 CLI 命令替代。这样根本就没有 schema 税的问题。
Scalekit 做了一个 benchmark,同样的 GitHub 操作,gh CLI 需要 1,365 tokens,GitHub MCP server 需要 44,026 tokens。32 倍。原因是 GitHub MCP server 暴露了 43 个工具,每次请求这 43 个工具的完整 schema 都在 context 里,即使你只用了 1 个。CLI 没有 schema 税,Agent 从训练数据里就知道 gh 命令怎么用,一次搞定。按 10,000 次/月算,CLI 大约 $3.20,MCP 大约 $55.20。
Google 的做法最能说明趋势。2026 年 3 月他们发布了 Google Workspace CLI(gws),覆盖 Drive、Gmail、Calendar、Sheets、Docs 全部 Workspace API。同一时期他们写好了 1,151 行 MCP server 代码,两天后全部删除,改为发布 CLI。
MCP 的价值在治理能力:per-user OAuth、工具级别的权限边界、结构化的审计日志。但是在日常开发中,这些命令行的权限都来自于开发者本人的权限,只要不要不小心全部rm -rf,更加精细的权限控制属于过度工程化了
我也倾向于能用CLI就不用MCP, Github, Jira, Azure DevOps, Supabase, AWS, Google Workspace 都有官方的或者第三方的CLI工具。如果真的没有CLI,那么可以尝试把官方的API doc搞到本地,在 Claude/Agents.md里写明当想使用这个工具的的时候直接去发送Curl命令。最后真的没办法了,再考虑MCP。
Compress:减少已有上下文的 token 数
上下文已经膨胀了怎么办?压缩。但是这里有一个核心矛盾:压缩和缓存是对立的。压缩要修改历史才能缩短,缓存要求历史不变才能命中。除此之外,根据我个人的感觉和团队同事的反馈,当context占用达到80%的时候,Agent就已经开始说胡话了。所以最好的方式必然不是等到context全满了再去压缩。策略上,预防比压缩重要得多。
预防类工具(不破坏 Cache)
这类工具在内容进入上下文之前就压缩掉,不动已有历史。
| 工具 | Stars | License | 压缩什么 | 原理 | 压缩率 |
|---|---|---|---|---|---|
| RTK | 49.2k | Apache-2.0 | CLI 命令输出 | Rust 二进制拦截 Bash,语义改写输出 | 80%(session 总计) |
| Context Mode | ~15k | ELv2 | 工具执行结果 | sandbox 执行,原始数据不进 context,SQLite + FTS5 存储 | 98%(声称) |
| lean-ctx | 1.7k | Apache-2.0 | CLI 输出 + 文件读取 + MCP | Shell hook + MCP server + Tree-sitter AST | 60-95%(cached re-read ~13 tokens) |
| Headroom | 1.8k | Apache-2.0 | 所有进入 context 的内容 | library/proxy/MCP,CacheAligner 对齐前缀,可逆压缩 | 87%(10,144→1,260 tokens) |
它们的覆盖范围不同,这是选择的关键:RTK 只管 Bash 输出,碰不到 Read/Grep/Glob 和 MCP 返回值;Context Mode 管工具执行结果但拦不了 MCP 的 JSON-RPC 响应;lean-ctx 覆盖最广(Shell + Read + MCP 三个通道);Headroom 是唯一一个主动考虑 cache 对齐的。
RTK:CLI 输出压缩
RTK (Rust Token Killer)(GitHub, 49.2k stars, Apache-2.0)是目前这个领域用得最多的工具。它是一个 Rust 写的 CLI 代理,拦截 shell 命令输出,在进入上下文之前做语义改写,不是 gzip 那种通用压缩,是理解命令语义后精简。50 个相同的 ESLint 错误变成一行 “ESLint: 50 errors in src/components/ (no-unused-vars)“,测试输出只保留失败项和摘要。
安装很简单:rtk init -g 一行命令,装完后所有 Bash 调用透明经过 RTK。
RTK 的 README 里给了一个 30 分钟 session 的 benchmark,按命令类型拆开:
| 命令类型 | 压缩率 |
|---|---|
| 目录列表 (ls/find) | 80% |
| 文件读取 (cat/head) | 70% |
| 搜索 (grep/rg) | 80% |
| Git (status/log/diff) | 92% |
| 测试 (npm test/pytest) | 90% |
| Session 总计 | 80% (~118k → ~24k tokens) |
Git 和测试的压缩率最高,因为它们的输出最重复——100 行 diff 里可能只有 5 行是你关心的变更,200 行测试输出里关键信息可能就最后三行 “3 failed, 47 passed”。
但 RTK 也有明确的局限:它只压缩当前轮的命令输出,碰不到已有历史。在一个 50 轮 / 150k token 的 session 里实际只省约 1.6%。它也无法压缩 Claude Code 内置 Read/Grep/Glob 的输出(这些不走 Bash hook)。
Context Mode:sandbox 执行
Context Mode(GitHub, ~15k stars, ELv2)走了一条完全不同的路。它的核心思路是:原始数据从来不进入上下文窗口。
它提供 6 个 MCP sandbox tools 替代 Agent 原本直接用的工具(ctx_execute、ctx_batch_execute、ctx_execute_file、ctx_index、ctx_search、ctx_fetch_and_index)。数据流:
- 工具在隔离子进程里执行
- 原始输出全量写入本地 SQLite(带 FTS5 全文索引)
- 返回给 LLM 的只有 1-2 行摘要(命令状态、输出行数、关键信息)
- 需要详情时 Agent 调
ctx_search用 BM25 按需检索
具体效果:Playwright 页面快照 56KB → 5.4KB,700KB 文件读取 → 3.6KB。
还有一个 PreCompact Hook:/compact 触发之前自动把关键状态(活跃文件、执行结果、工作进展)存入 SQLite 做恢复快照,新 session 启动时自动注入,Agent 可以断点续跑。
限制:
- ELv2 license(允许内部使用,禁止做托管服务,不符合 OSI 开源定义)
- 无法拦截 MCP tool 响应(JSON-RPC 直达模型,Context Mode 没有中间层截获)
- 3 个月 136 个版本,社区有人质疑稳定性
- HN 首页 570 分,关注度高但落地反馈还不多
lean-ctx:全通道压缩
lean-ctx(GitHub, 1.7k stars, Apache-2.0)和 RTK 最关键的区别在覆盖面。RTK 只管 Bash 输出,lean-ctx 同时覆盖三个通道:
- Shell hook 压缩命令输出(和 RTK 类似)
- MCP server 压缩文件读取和代码分析(RTK 碰不到)
- Tree-sitter AST(21 种语言)理解代码结构后做语义级压缩
文件读取方面,lean-ctx 提供 10 种 read mode:
full:完整内容map:只返回文件结构概览signatures:只提取函数/类签名diff:只看变更部分- 等等
首次读取压缩 60-95%。cached re-reads 是个有意思的设计:同一个文件 Agent 在 session 里再次读取时,lean-ctx 检测到文件没变,直接返回 ~13 tokens 的短消息(“你之前读过了,没变”),避免重复灌入几千 tokens。
整体提供 59 个工具、95+ 种压缩模式。还有一个 Property Graph 功能:在 AST 之上构建代码图谱(追踪 imports/calls/exports/type_ref),查询时混合搜索(BM25 + embeddings + graph proximity)。
Headroom:cache-aware 压缩
Headroom(GitHub, 1.8k stars, Apache-2.0)特别关注压缩和 Prompt Cache 之间的协调。
核心特性:
- CacheAligner:压缩完成后主动调整结果的结构和排列,让压缩后的内容尽量和上次请求的前缀对齐,提高 cache 命中率
- CCR(可逆压缩):压缩过的内容如果模型后来需要看原始数据,可以调
headroom_retrieve拿回来。压缩是可撤销的 - Kompress-base:用 Hugging Face 上的压缩模型做文本压缩,benchmark 10,144 tokens → 1,260 tokens(87%),答案质量没有可测量的下降
安装复杂度对比
这四个工具的上手成本差别很大:
| 工具 | 安装方式 | 复杂度 |
|---|---|---|
| RTK | rtk init -g 一行命令,自动注册 hook | 最简单 |
| Context Mode | Claude Code marketplace 一键安装,或 npm install -g context-mode | 简单,但需要了解 6 个 sandbox tools 的用法 |
| lean-ctx | brew install lean-ctx 或 cargo install lean-ctx,然后 lean-ctx setup 自动检测 AI 工具并配置 | 中等,setup 会自动配置但初次需要理解 read modes |
| Headroom | pip install headroom-ai[all],然后按部署方式配置(library 模式最简单,proxy/MCP 需要额外配置) | 最复杂,Python 环境 + 配置项多,适合愿意折腾的团队 |
RTK 和 Context Mode 属于”装完不用管”的类型。lean-ctx 需要花一点时间理解它的 read modes 才能发挥最大效果。Headroom 的 CacheAligner 和可逆压缩功能很强,但配置和调试成本也最高,更适合有运维能力的团队或者需要集成到自己 pipeline 里的场景。
选择建议:如果团队主要在 CLI 里跑命令(build、test、lint),RTK 立竿见影。如果 Agent 大量做文件读取和 MCP 调用,lean-ctx 覆盖面更广。Headroom 适合对 cache 命中率有极致要求、或者需要把压缩集成到自己 pipeline 里的情况。
压缩类工具(会破坏 Cache)
预防做得再好,session 跑久了上下文还是会涨。我自己的体感是 context 占到 70-80% 的时候模型就开始走神了,等不到 /compact 自动触发的 95%。社区目前的主要方向是 Dynamic Compression,不是一刀切地摘要重来,是在 session 进行中选择性地缩减已有内容。
| 工具 | Stars | License | 压缩粒度 | 原理 |
|---|---|---|---|---|
| DCP | 2.8k | AGPL-3.0 | 选择性剪枝已完成的工具调用 | 自动去重 + LLM 语义压缩,改 API payload,原始 session 不动 |
| Cozempic | 300 | MIT | 分层递进清理(3 级) | 直接改 session JSONL 文件,删冗余数据 |
| Magic Context | 636 | MIT | 后台异步压缩 | historian 模型后台压缩,Compartments + Facts 持久化 |
这四个工具的核心区别是”压缩发生在哪里”和”粒度有多细”。DCP 拦截 API payload,在发送请求之前把旧的工具调用替换掉,原始 session 文件完好无损可以回溯。/compact 是最粗暴的方案,整个上下文压成一段摘要重新开始,cache 完全归零。Cozempic 直接改 session 文件本身,把积累的垃圾数据物理删除。Magic Context 在后台异步跑,不打断主 Agent,还会算一笔账判断 cache 节省够不够抵消 cold start 成本。如果你想在 session 中途精细控制,选 DCP;如果 session 已经膨胀到不可救药了,/compact 或 Cozempic 做一次清理重来;如果不想操心,Magic Context 自动在后台处理。
DCP:选择性剪枝
DCP(GitHub, 2.8k stars, AGPL-3.0)是 OpenCode 插件,在 session 进行中选择性地缩减已完成的工具调用,不是全量摘要。claude-dcp(MIT)是它的 Claude Code 移植版。
- 做两件事:自动去重(零 LLM 成本)和语义压缩(LLM 驱动)
- 自动去重:Agent 在一个 session 里读了同一个文件 3 次,DCP 只保留最后一次输出,前两次替换成 placeholder。对重复的
git status、URL fetch 也一样 - 语义压缩:DCP 暴露一个
compress工具,Agent 自己判断某段对话已经完成(比如一个 bug 修好了,排查过程的 20 轮工具调用不再需要),调用 compress 把那段替换成技术摘要 - 原始 session 文件不改,只在发给 API 的 payload 里做替换,日志完整保留可以回溯
- 配置:
maxContextLimit默认 100k tokens(超过触发压缩提醒),minContextLimit默认 50k tokens,可以改成百分比如 “80%“。我个人觉得这个配置还是挺合适的,对于大部分的200k上下文的模型,100k处于一个用了一半的甜品点。 - shadow copy 机制让 cache hit rate 保持在约 85%(不用 DCP 时 90%)
- 限制:AGPL-3.0 license 商业使用需要注意合规;语义压缩需要额外 LLM 调用,比重新 Prefill 便宜但不是零成本;压缩质量取决于 Agent 对”这段对话是否已完成”的判断,压错了会丢信息
Cozempic:session 文件物理清理
Cozempic(GitHub, 300 stars, MIT)直接操作 Claude Code 的 session JSONL 文件,删掉里面积累的垃圾数据。
- 和 DCP 的区别:DCP 改 API payload(运行时拦截),Cozempic 改文件本身(持久化清理)
- 典型场景:改了一个文件 50 次,每次 tool result 记录完整 diff,累积 2MB,但 Agent 根本不需要旧 diff(重新读文件就行)。Cozempic 只保留最近几个 diff,旧的压成一行摘要或直接删掉
- 18 种清理策略分三级:
- Gentle:删已经被 /compact 摘要过但原文还留着的消息、去重文件读取、剥离 metadata
- Standard:truncate 过长工具输出、清理过期旧结果、去重 system reminder
- Aggressive:删旧的 base64 图片(只保留最近 20%)、折叠重复的 error-retry 循环、截断超过 32KB 的大块
- 节省约 23-45%
- 默认 dry-run 模式(只报告不修改),加
--execute才真改,改之前自动备份
Magic Context:后台异步压缩
Magic Context(GitHub, 636 stars, MIT)是 OpenCode 插件,没有Claude Code插件,在后台异步压缩,主 Agent 完全不用等。
- 后台跑一个独立的 historian 模型,异步处理旧对话。主 Agent 不需要等压缩完成,也不会被打断
- 压缩结果存成 Compartments(按时间线组织的摘要块),新一轮请求时注入上下文
- 从对话里提取 Facts(决策、约束、偏好)单独持久化。Facts 在压缩时不会丢失,部分解决了 /compact 的错误固化问题
- cache-aware 调度:计算 cache 省下的钱和 cold start 成本,只在压缩收益为正的时候才触发
- Facts 跨 session 保留,新 session 启动时自动注入
安装复杂度对比
| 工具 | 安装命令 | 备注 |
|---|---|---|
| DCP | npm i @tarquinen/opencode-dcp | OpenCode 插件,装完即用 |
| claude-dcp | git clone + 配置三个 hooks | Claude Code 移植版,需要手动配 PreCompact、PreToolUse、UserPromptSubmit |
| Cozempic | pip install cozempic && cozempic init | 需要 Python 环境,默认 dry-run 安全模式 |
| Magic Context | 在 opencode.json 里加 "plugin": ["@cortexkit/opencode-magic-context"] | OpenCode 专用;需要禁用内置 compaction,额外配置 historian 模型 |
补充:API 层面的 context editing
如果你在用 API 开发自己的 Agent,Anthropic 的 context editing 提供了服务端的上下文管理能力。clear_tool_uses 策略可以自动按时间清理旧 tool result,支持配置触发阈值、保留最近 N 次调用、排除特定工具。OpenAI 也有类似的 compact endpoint。这些对 Claude Code / ChatGPT 用户是透明的,自己构建 Agent 的话可以直接在 API 层面做更精细的控制。
Isolate:拆任务、换模型
前面的 Select 和 Compress 都是装工具的事,改完配置就能省。Isolate 和接下来的 Write 不一样,它们是使用习惯的改变。
Isolate 的核心思路是把一个大任务拆成子任务,在独立的上下文窗口里执行,完成后只把结论交回主 Agent。省钱的杠杆有两个:一个是模型分级(便宜模型做简单活),一个是上下文隔离(子 Agent 不带不需要的历史)。Token 总量可能不变甚至更多,但成本低了。
模型分级与上下文隔离
回头看一眼定价表。Haiku $1/$5,Sonnet $3/$15,Opus $15/$75(per MTok input/output)。同一件事让 Haiku 做和让 Opus 做,价格差 15 倍。问题是哪些事可以让 Haiku 做。
Claude Code 的做法是它的内置 Explore agent 默认跑在 Haiku 上,只读不写,专门用来在代码库里找东西。另外 Explore 和 Plan agent 会跳过 CLAUDE.md 和父会话的 git status,进一步减少上下文开销。根据 Claude Code 用户社区的统计,36.5% 的 API 调用跑在 Haiku 上,但这部分只占总成本的 2.3%($8/$354)。便宜模型吃掉了超过三分之一的调用量,花的钱几乎可以忽略。
模型分级之外,sub-agent 还通过上下文隔离的方式减少总花销。Anthropic 官方文档里举了一个例子:Explore agent 在自己的上下文里读了 6,100 tokens 的文件(session.ts 2,200 + timeouts.ts 800 + config/*.ts 3,100),回到主 Agent 时只交了 420 tokens 的结论。93% 的中间过程被丢掉了,主 Agent 的上下文保持精简。除此之外,对于特定的任务也可以通过修改sub agent的agents.md,只注入相关的上下文,例如在对SQL和数据库调优的时候只需要输入相关的业务约束和技术知识就可以了,可能并不需要知道整个后端代码是怎么写的,这个时候就可以有一个单独的优化agent来做这件事情。
社区在这个方向上做了更系统的方案。目前比较活跃的有三个:oh-my-claudecode(Claude Code 插件)、oh-my-openagent(原名 oh-my-opencode,OpenCode 插件)、oh-my-opencode-slim(oh-my-openagent 的精简版,OpenCode 插件)。三者来自不同作者,互相独立。
| oh-my-claudecode | oh-my-openagent | oh-my-opencode-slim | |
|---|---|---|---|
| Stars(调研时数据) | ~34.6k | ~59k | ~4.6k |
| License | MIT | Sustainable Use License v1.0(非 OSI,禁止商业再分发) | MIT |
| 目标平台 | Claude Code | OpenCode(+ Codex/Pi 适配中) | OpenCode |
| Agent 数量 | 19 个 + 层级变体 | 7 个核心 agent + 团队专家 | 7-8 个 |
| 模型路由方式 | Haiku→简单,Opus→复杂;支持跨 CLI 路由(Codex、Gemini) | 按任务类别自动路由(quick/deep/visual/ultrabrain) | 每个 agent 在 JSON 配置里显式指定模型 + variant |
License 的差异值得注意。oh-my-claudecode 和 slim 都是 MIT,可以随便用。oh-my-openagent 用的是自定义的 Sustainable Use License v1.0,允许个人和公司内部使用,但禁止基于它做商业产品或 SaaS 再分发。如果是个人或团队内部用不受影响,想二次开发做商业产品的话 slim 的 MIT 协议更安全。
三者的配置方式不同,但分级逻辑高度一致。从三个工具的实际配置里可以归纳出三层模型分级,以及每层对应的任务类型。
第一层:便宜/快模型做跑腿活
典型模型:Haiku($1/$5)、GPT-5.4-mini($0.75/$4.50)、Gemini Flash、DeepSeek Flash。
这层的任务有一个共同特点:结果是确定性的或者出错成本很低。具体包括:
- 文件搜索和代码导航。在代码库里找某个函数定义、grep 一个变量名、列出某个目录的文件结构。Claude Code 内置的 Explore agent 默认就用 Haiku 做这件事。oh-my-opencode-slim 把它叫 Explorer(默认 GPT-5.4-mini),oh-my-openagent 叫 Librarian(默认 Gemini Flash)。这类任务本质上是模式匹配,不需要推理能力,模型只要能正确调用 grep/glob/read 工具就行。
- 单文件的小修改。格式化、import 排序、把一个变量名批量替换、补一个缺少的 return 语句。改动范围小、上下文少,便宜模型出错了改回来也不费什么。oh-my-opencode-slim 专门设了一个 Fixer agent(默认 GPT-5.4-mini)做这种事。
- 文档检索。去读外部文档、查 API 用法、搜索 Stack Overflow。oh-my-opencode-slim 的 Librarian(GPT-5.4-mini)和 oh-my-openagent 的 Librarian 都是做这个的。
这层的模型花费几乎可以忽略。前面提到的数据:36.5% 的 API 调用跑在 Haiku 上,只占总成本的 2.3%。
第二层:中档模型做实现
典型模型:Sonnet($3/$15)、GPT-5.4($2.50/$15)、Kimi K2.6。
这层的任务需要理解代码的语义,但不涉及跨模块的架构决策:
- 写测试。给一个函数写单元测试,需要理解函数签名、输入输出、边界条件,但不需要理解整个系统的设计。
- 修简单 bug。错误信息明确、定位清楚的 bug。比如一个 null pointer、一个 off-by-one、一个缺少的 error handling。修复通常局限在一两个文件里。
- 标准实现。按照已有的 pattern 写一个新的 CRUD endpoint、加一个新的表单字段、实现一个明确定义好接口的功能。不需要做设计决策,照着现有代码的风格写就行。
oh-my-claudecode 称这层为 Tier 2,oh-my-openagent 的 quick 类别路由到这里。Augment Code 的建议也是实现层用 Sonnet(他们引用了 SWE-bench Verified 79.6% 的成绩)。
第三层:强模型做决策
典型模型:Opus($15/$75)、GPT-5.5($5/$30)、Claude Opus 4.7 max variant。
这层的任务如果做错了,代价很高:
- 架构设计。决定一个新 service 的技术栈、设计跨模块的数据流、选择 message queue 还是 REST 做服务间通信。这类决策影响后面几个月的开发,做错了返工成本远超模型费用。
- 跨模块重构。改一个 billing 模块的计费逻辑,需要理解上游的 metering 怎么采集、下游的 invoice 怎么生成、中间的 subscription 状态机怎么转。上下文跨多个 service,推理链不能断。
- 安全审查。检查认证流程有没有漏洞、权限控制是否正确、有没有 SQL 注入或 XSS。这类任务对精确度的要求极高,一个 false negative(没发现的漏洞)比 false positive(误报)危险得多。
Augment Code 的 Model Routing Guide 从另一个角度佐证了同样的分级逻辑:协调用 Opus,实现用 Sonnet(SWE-bench Verified 79.6%),文件导航用 Haiku,代码审查用 GPT-5.2。他们的数据是 model routing 可以削减 40-60% 成本。一个 120 人的团队,月度模型支出 $52,000,4-6 周内通常看到 20-35% 的下降,不需要改变开发者行为。Augment 后来发布了 Prism 来自动化这个过程。
什么时候不该拆
拆 Agent 不是免费的。拆错了不仅不省钱,还会降低质量。
前面讲过 sub-agent 和主 agent 之间的上下文是隔离的。这个隔离是省钱的前提,但也带来一个代价:如果多个 sub-agent 之间需要共享业务上下文,目前没有好的方式。虽然 Google 已经发布了 A2A(Agent-to-Agent)协议,但在实际的 coding agent 场景里,我还没有看到一个 agent team 能做到像人类开发者团队那样——一个 agent 直接问另一个 agent”你那边的业务上下文是什么”,然后拿到精准的回答。
现在的实现都比较粗暴。每个 sub-agent 要了解业务上下文的时候,都是自己去代码库里重新探索一遍。agent A 读了 billing 模块的 3,000 行代码搞清楚了计费逻辑,agent B 如果也需要这个信息,它不会去问 agent A,而是再读一遍同样的 3,000 行。3 个 agent 就是 3 倍的探索成本。UIUC 的论文在 7 个数据集和 6 个模型上实测,multi-agent 系统消耗比 single-agent 多 4-220 倍的 tokens,即使优化后仍需要 2-12 倍。
我个人判断要不要拆的标准大概如下:
| 适合拆 | 不适合拆 |
|---|---|
| 在独立目录里探索代码(结论可以几百 token 总结) | 跨 service 的因果推理(拆开会丢推理链) |
| 跑测试、lint、格式化(结果是 pass/fail) | 需要反复协商的设计决策(协调开销 > 节省) |
| 并行处理互不依赖的文件 | 上下文紧密耦合的重构(每个 agent 都要重新理解全貌) |
一个经验法则:如果你能用一句话给 sub-agent 下指令,它的结果也可以用一段话总结,那就适合拆。如果发现自己在给 sub-agent 写长长的背景说明,那就说明这个任务不适合隔离。
Write:把信息搬到上下文之外
前面四个方向(Select、Compress、Isolate)都是在上下文窗口内部做文章。Write 换了一个思路:把不需要时刻在场的信息搬到文件系统里,Agent 需要的时候再读回来。
Andrej Karpathy 在 Y Combinator AI School 的演讲里提过一个类比:model weights 是 CPU(固定的计算基底),context window 是 RAM(短期、易失、容量有限的工作记忆)。MemGPT 论文(2023 年 10 月,Charles Packer 等人)把这个类比正式化成了三层架构:
| 层级 | 类比 | 功能 |
|---|---|---|
| Core Memory | RAM | 在 context window 内,agent 直接读写 |
| Recall Memory | Disk cache | 可搜索的对话历史,存在 context 外 |
| Archival Memory | Cold storage | 长期存储,agent 通过 tool call 查询 |
直接借鉴了操作系统虚拟内存分页的概念:当数据超出主存容量时,在主存和磁盘之间进行分页。后来商业化为 Letta 平台。
现在的 Agent 记忆都是怎么做的
在实现上,目前大部分 agent 做得都比较简单,基本以 markdown 文件为基础。不只是 coding agent(Claude Code 的 MEMORY.md、OpenCode 的 AGENTS.md、Codex 的 AGENTS.md),通用 agent 的做法也差不多,例如Hermes和Openclaw都是以markdown文件来记录记忆。往上走一步,有基于 SQLite + 全文搜索的方案(例如agentmemory ,Engram )。再往上是向量数据库(pgvector 现在是趋势,如果已经有 Postgres 就不用再引入独立的向量库),以及专门的记忆平台(例如,Mem0 做三阶段 Extraction→Consolidation→Retrieval,。最近还有把所有东西塞回 Postgres 的趋势。
但对于日常用 coding agent 的场景来说,这些重型方案大多数时候是过度工程化了。coding agent 的记忆需求和通用 agent 不同:不需要管理百万用户的个性化状态,不需要时序推理”三个月前用户说过什么”,需要的是跨 session 保留项目上下文和工作进度。markdown 文件 + 合理的目录结构 + /clear 纪律,基本就够了。
下面具体看 coding agent 场景里怎么做 Write。
内置的 lazy loading 机制
Claude Code 已经内置了不少”存到外面、按需加载”的机制。Auto Memory 的 MEMORY.md 只加载前 200 行到上下文,topic 文件按需读取(官方文档)。AutoDream 在空闲时间后台整合记忆,去重、去过时条目、合并矛盾项。Path-scoped rules(.claude/rules/)只在操作匹配文件时加载。/btw 的提问和回答不进入对话历史。Skill 和 MCP schema 在前面 Select 章节已经讲过的渐进式加载。这些都是 lazy loading,信息在需要时才进入上下文。
社区的 Write 模式
内置机制之外,社区发展出了几种显式的 Write 模式。按用途分。
外置 Spec
其实我认为现在基本都在用的Spec Driven Development也是一种外置的记忆。从 token 成本角度看,讨论了 50 轮累积的 80k token 对话历史,压缩成一个 2k token 的 spec 文件,可以有效的减少coding agent寻找上下文所需要的token。
工作状态持久化和 Session Handoff
如果任务不是一次性的(比如一个持续多个迭代的 feature 开发),需要的就不只是一个 spec,而是能跨 session 持续更新的工作状态。
社区在这个方向上最活跃的几个方案:
Planning-with-files(Manus 模式)。创建三个文件:task_plan.md(阶段划分、进度追踪)、findings.md(调研笔记)、progress.md(session 日志、错误历史)。通过 Hooks 强制执行:PreToolUse 在 Write/Edit/Bash 前自动读取 plan 刷新目标,PostToolUse 提醒更新进度。Manus AI 自己的说法是:“Markdown is my ‘working memory’ on disk.”
Alive。5 个文件 + 12 个 hooks:key.md(项目基础)、now.md(当前状态)、log.md(不可变的决策历史)、tasks.md(工作队列)、insights.md(静态知识)。hooks 会阻止删除 log 条目、强制在 session 边界读写。Alive 和 planning-with-files 的区别在于它侧重项目级别的持久知识,而不只是单个任务的执行追踪。
Agent Kernel。更精简,3 个文件:AGENTS.md(通用指令)、IDENTITY.md(agent 身份)、KNOWLEDGE.md(知识目录索引)。能跨 Claude Code、Codex、Cursor、OpenCode 使用。
这些方案有一个共同点:hooks 是关键。只靠 markdown 文件本身是不够的,随着上下文增长 agent 会跳过或误读文件。社区的经验是用 hooks 在 session 边界(SessionStart、PreCompact、Stop)强制读写,这样 agent 才真的会用这些文件。和 TodoWrite(现在叫 Tasks API)的区别:TodoWrite 是 in-context 的,每一轮都在上下文里,session 结束就没了。planning-with-files 这些是真正的文件,能跨 /clear 和跨 session 存活。
当一个 session 要结束(快到 context limit,或者任务进入下一阶段)的时候,就涉及到 handoff——怎么把当前状态传给下一个 session。
直觉上的做法是把上下文 dump 到文件里让下一个 session 读。但这治标不治本:当前 session 200k token 的上下文,dump 出来给下一个 session 也是 200k,也会溢出。好的接力方式不是”把上下文传给下一个”,而是”先压缩到 5-10% 再传”。
HumanLayer 的 ACE-FCA 是目前写得最具体的方案。他们把工作拆成 Research → Plan → Implement 三个阶段,每个阶段之间做完整的 context reset,只传一个约 200 行(~1,500 token)的结构化文档:
| 阶段产出 | 目标大小 | 内容 |
|---|---|---|
research_doc.md | ~200 行 | 问题摘要、相关文件及用途、信息流、关键发现、推荐方案 |
implementation_plan.md | ~200 行 | 编号步骤、要改的文件、测试方法、依赖关系 |
progress.md | ~100 行 | 目标、已完成步骤(打勾)、当前步骤、剩余工作 |
关键规则:不允许原始文件内容或搜索结果出现在交接文档里。sub-agent 搜索了 3,000 行代码,回来只交 50 行压缩摘要。token 预算目标是工作期间 context 保持在 40-60%。他们报告用这个方法处理过 300k 行的 Rust 代码库。
Matt Pocock(grill-me 的作者)做了一个 /handoff skill,思路类似:把当前 session 压缩成结构化 markdown,引用文件而不是复制内容,存到临时目录,新 session 从这个文件启动。和 /compact 的区别:/compact 是在同一个 session 里继续,/handoff 是切到一个全新的 session。 这个也是我目前在尝试的东西,相比于其他方案更加轻量也好管理。
社区还有一个 context-save/context-restore skill,/context-save 把当前工作状态快照到 .claude/context.md(正在做什么、关键决策、未解决问题 + git 状态),新 session 用 /context-restore 恢复。和 /rewind 的区别:/rewind 恢复代码状态,context-save 恢复”你在做什么、为什么这么做”的理解。
自动化持久记忆
如果觉得手动管理文件太麻烦,社区有自动化方案。claude-mem(~74.8k stars)通过 5 个生命周期 hooks 自动捕获 session 数据,session 结束时 AI 压缩为语义摘要(5,000 tokens → ~500 tokens),下次 session 自动注入。agentmemory(~9.4k stars)基于 Ebbinghaus 遗忘曲线自动压缩和衰减,约 1,900 tokens 存储 240 条观察(对比全塞进 CLAUDE.md 需要 22,000+ tokens,减少 92%)。这些 benchmark 是工具方自己的测试,缺少独立验证。
什么不是 Write
两个常见误区。
过长的 CLAUDE.md。CLAUDE.md 是 always-loaded context,每一轮 API 调用都会带上。把几千行文档塞进去不是”存到上下文之外”,是”强制每次都加载”。一个 5,000 token 的 CLAUDE.md 意味着每条消息都要付 5,000 token 的税。Anthropic 建议控制在 200 行以内(约 1,500-2,000 tokens)。好的 CLAUDE.md 是路标(告诉 Agent 去哪找详情),不是百科全书。
“把所有东西都存文件”。Oracle 开发者博客警告过:“saving everything including full conversation logs, every data point, and all outputs causes memory artifacts that grow unbounded.” 原则是只存 Agent 会用到的信息,不存 Agent 可以从代码或 git 历史推导出来的信息。
写了之后怎么维护:Context Drift
Write 策略有一个绕不开的问题:写了之后代码变了,文档没跟着变,agent 就在错误的前提下工作。在 harness engineering 领域这叫 context drift。Birgitta Böckeler 在 martinfowler.com 上的文章(2026 年 4 月)明确把”how do we keep a harness coherent as it grows?”列为 open question,不是 solved problem。
Context drift 分两种。
- 结构性 drift:文件路径变了,CLAUDE.md 里写”billing 在 src/billing/“但有人把它挪到了 src/services/billing/。
- 语义性 drift:代码逻辑变了但旧路径还在,CLAUDE.md 写”billing 按月计费”但已经改成按用量计费了,src/billing/ 目录还在原地。
结构性 drift 已经有工具了。ctxlint 解析 CLAUDE.md/AGENTS.md 里所有文件路径,检查它们是否还存在,查 git history 里的 rename,交叉检查 build/test 命令是否和 package.json 一致。CI 里加一行 npx @yawlabs/ctxlint --strict 就行。Caliber 的 caliber refresh 分析 git diff 自动更新 CLAUDE.md,可以作为 pre-commit hook 跑。
语义性 drift 基本没有成熟的解决方案。目前最接近的设计思路有两个:一是 Addy Osmani 提的写指针不写描述——“billing 逻辑见 src/billing/README.md”,这样路径断了 ctxlint 能检测到,“billing 按月计费”这种描述性文字 rot 是静默的;二是从代码实时派生(Aider 的 repo map 每条消息从 AST 重新生成,CodeGraph 用文件系统 watcher 增量同步),让上下文永远来自代码现状而不是人写的文档。 公司内部之前也讨论过这个(@Xu kunyu)。其中我个人推荐的方案是用 GitHub Agentic Workflow 定时扫描文档和代码做 alignment,检测到 drift 后自动提 PR 修正。这个想法还没落地,思路是把 context drift 的检测和修复变成 CI/CD 流水线的一部分,而不是靠人记得去更新。
管理输出
到这里讲的都是 input 侧的优化。直觉上 output 单价是 input 的 5 倍(Opus $25 vs $5),应该优化 output 更划算。但看一下实际的 token 分布就会发现不是那么回事。
拿我今天的真实数据举个例子:Input 1.62M tokens,Output 128.1K tokens(Opus 4.6)。
按 token 数量看,input 占 92.7%,output 占 7.3%,这和社区的实测数据一致——Branch8 的 Claude Code session 是 94.5-98.6%,Cursor 一周实测是 97.9%,Stanford/Microsoft 在 2,000+ SWE-bench 轨迹上的论文更极端,达到 153
。原因是每一轮 API 调用都重发整个 context(system prompt + CLAUDE.md + 对话历史 + 读过的文件),input 随轮次线性增长,output 每轮相对稳定。但按花的钱算就不一样了。同样用我今天的数据,全部按 Opus 4.6 标准价格(不考虑 cache):
- Input:1.62M × $5/MTok = $8.10(71.7%)
- Output:0.128M × $25/MTok = $3.20(28.3%)
Output 虽然 token 数只占 7%,但因为单价贵 5 倍,实际花了将近三成的钱。
如果再考虑 cache 命中(假设 70% 的 input 命中 cache,cached input 按 $0.50/MTok),情况更有意思:
- Cached input:1.134M × $0.50 = $0.57
- Non-cached input:0.486M × $5.00 = $2.43
- Output:0.128M × $25 = $3.20
- 总计:$6.20,output 占 51.6%
cache 命中率越高,input 的实际成本被压得越低,output 的占比就越大。做好了前面几步的缓存与上下文优化之后,output 反而变成了成本的大头。所以 output 优化不是没价值,是要在 input 优化做到位之后才凸显出来。但是目前社区里讨论比较多的方案只有一个Caveman。
Caveman
Caveman(61k stars,MIT License)是 Claude Code 里关注度最高的 output 优化 skill。它通过 prompt 指令让模型用极压缩的语言回复,去掉冠词、填充词、客套话,只保留技术实质。
但有争议。有人测试发现简单写 "be brief" 几乎达到同等效果(419 vs 401-449 tokens,质量 0.985 vs 0.970-0.976)。而且 Caveman 只影响 output tokens,在 input 占主导的 session 里,实际成本节省约 7-13%,不是宣传的 65%。
我觉得它更适合个人使用,自己看得懂就行。团队默认开 Caveman 不太合适:模型少写 200 token,人多花 5 分钟理解压缩后的回复,不一定划算。
其他的方向
如果是在开发自己的agent,那么还有一个方案是使用Batch API。Anthropic 和 OpenAI 都提供 Batch API,非实时的批量请求可以拿到 50% 的价格折扣。适合不需要即时响应的场景。比如大量文档的翻译,批量代码审查(把一天积累的 PR 一起跑)、批量测试生成(给一个模块的所有函数批量写测试)、批量文档生成。关键是这些任务可以攒一攒再跑,不用每次都同步等结果。
观测和落地
先看清楚钱花在哪
如果不知道钱花在哪,优化就是盲人摸象。
我个人用的是 CC Switch。它本身是一个跨平台的 API provider 管理工具(切换 API key、endpoint、做 failover),但自带一个 usage dashboard,可以看 token 消耗和成本趋势。支持 Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw。
除了官方提供的工具之外,专门做成本监控的工具,目前社区里比较活跃的有这几个:
| 工具 | Stars(调研时数据) | 支持平台 | 区分 token 类型 | 特点 |
|---|---|---|---|---|
| ccusage | ~14.5k | Claude Code、OpenCode 等 15 个 agent | input/output/cache read/cache write | bunx ccusage 零安装,读本地 JSONL,按日/周/月/session 看 |
| tokscale | ~3.1k | Claude Code、OpenCode、Cursor 等 22+ agent | input/output/cache read/cache write/reasoning | Rust TUI,支持 Cursor(需 API sync),有 contribution graph |
| agentsview | ~1.1k | Claude Code、OpenCode 等 14+ agent | input/output/cache read/cache write | 本地 web dashboard,SQLite 索引,比 ccusage 快 100x |
Claude Code 和 OpenCode 都在本地写 JSONL 日志,ccusage 和 tokscale 直接读这些文件就行。Cursor 不写本地日志,tokscale 通过 API sync(tokscale cursor login)拉取数据。OpenCode 自己的 TUI 里也内置了 session 级别的 token 和成本显示(实时更新),不装额外工具也能看。
如果是自建 Agent 走 API,LiteLLM(~48k stars)可以作为统一 gateway,支持 100+ provider,自带 per-project/per-user 的成本追踪。但需要把所有 agent 流量路由过去,比上面几个工具重得多。
端到端粗算
用我今天的真实数据(Input 1.62M, Output 128.1K, Opus 4.6)把前面讲的几个维度串起来算一笔账。
什么都不做,全部按标准价格:Input 1.62M × $5 + Output 0.128M × $25 = $11.30。
Step 1:缓存纪律。固定 system prompt 和 MCP schema 顺序,关 attribution header,清理不用的 MCP 工具。假设 70% 的 input 命中 cache(cached input $0.50/MTok):Input 成本从 $8.10 降到 $3.00(cached 1.134M × $0.50 + non-cached 0.486M × $5.00)。加 output $3.20,总计 $6.20。省了 45%。这一步只需要改一次配置。
Step 2:减少无效探索。写好 CLAUDE.md 路标(告诉 agent 代码在哪、业务术语叫什么),静态类型项目开 LSP(一步 find-references 替代 5 轮 grep)。假设 session 总 token 砍掉 30%,从 1.62M 降到 1.13M。同样 70% cache 命中:Input 成本约 $2.10,加 output $3.20,总计 $5.30。
Step 3:预防膨胀。装 RTK 压缩 CLI 输出(80% 压缩率),或者用 lean-ctx 覆盖文件读取和 MCP 返回值。假设 session 再瘦 15%,总计约 $4.70。
Step 4:隔离。重的探索任务用 Haiku sub-agent($1/$5 vs Opus 的 $15/$75),不同任务间 /clear。探索部分的成本便宜 15 倍。这一步的节省取决于有多少任务适合拆给便宜模型。
Step 5:输出控制。简单任务降 /effort,自动化流水线走 structured output + Batch API(50% 折扣)。在前面几步把 input 成本压下去之后 output 占比已经超过一半了,这一步的价值变得明显。
从 $11.30 到约 $4-5,大约省了一半多。最大的贡献来自 cache 命中(Step 1)和减少无效 token(Step 2),这两步加起来省了大部分,而且投入最小。
需要强调,这些数字是粗算。实际效果取决于 session 结构、缓存命中率波动、任务类型。但数量级的方向应该是对的。
我个人推荐的落地顺序
下面是我按投入产出比排的执行顺序。需要声明:这只是我个人的推荐,我们团队自己也没有完全走通这一套流程。有些步骤(比如 context drift 的自动化检测)还停留在讨论阶段。这篇文章是调研笔记,不是实战报告。
Step 0: 观测
装 ccusage 或 tokscale,看清楚钱花在哪。
不看数据就优化是盲人摸象。
Step 1: 缓存纪律(改一次配置,长期受益)
固定 system prompt 和 MCP schema 的顺序。
清理不用的 MCP 工具。
本地模型关掉 attribution header。
装 claude-code-cache-fix 修复 resume session 的 cache regression。
Step 2: 减少无效探索(花半天,长期受益)
写好 CLAUDE.md——路标,不是百科全书,控制在 200 行以内。
静态类型项目开 LSP(TypeScript、Rust、Go、Java)。
大型项目装 CodeGraph 或类似的 AST 索引。
Step 3: 预防上下文膨胀(装工具)
装 RTK 压缩 CLI 输出。
如果 agent 大量做文件读取和 MCP 调用,考虑 lean-ctx。
不用的 Skill 定期清理。
Step 4: 隔离(改使用习惯)
重的探索任务拆给 sub-agent,用便宜模型。
不同任务之间 /clear。
并行 agent 用 worktree 避免文件冲突。
不要对顺序推理任务强行拆 multi-agent。
Step 5: Write(改工作流)
复杂任务先生成 SPEC.md,再 /clear 后新 session 执行。
长任务用 planning-with-files 或类似方案持久化工作状态。
调研和实现分开做,调研结论存文件,写代码时按需读取。
定期用 ctxlint 或 Caliber 检查 context drift。
Step 6: 输出控制(锦上添花)
简单任务降 /effort。
自动化流水线走 structured output。
能攒批的任务(翻译、批量审查)用 Batch API 拿 50% 折扣。指标不要只看 token。至少同时看三类:
| 类别 | 关键指标 |
|---|---|
| 成本 | 总成本、cached input 占比、output 成本占比 |
| 效率 | tool call 次数、首次成功率、总耗时 |
| 质量 | 是否漏 context、是否误修、是否需要人工纠偏 |
如果 Agent 开始漏 context、走错路、产出质量下降,说明减过头了。好的优化是成本下降但质量不掉。
References
Context Engineering 框架
- Anthropic: Effective Context Engineering for AI Agents — Context Engineering 概念的系统阐述
- LangChain (Lance Martin): Context Engineering for Agents — Select/Compress/Isolate/Write 四分类框架
- Phil Schmid: The New Skill in AI is Not Prompting, It’s Context Engineering — Google DeepMind 视角
Harness Engineering
- Mitchell Hashimoto: My AI Adoption Journey — “harness engineering” 术语来源,Ghostty AGENTS.md 案例
- OpenAI (Ryan Lopopolo): Harness Engineering — 1M+ LOC 零手写代码的 harness 实践
- Addy Osmani: Agent Harness Engineering — ratchet 原则,指针式 AGENTS.md 设计
- Birgitta Böckeler: Harness Engineering (martinfowler.com) — Guides/Sensors 分类,harness coherence 作为 open question
- MS Tech Community: Context Engineering for Azure SRE Agent — 从 multi-agent 转向 context engineering
- MS Tech Community: The Agent That Investigates Itself — 自修复循环,staleness 问题的诚实承认
定价和计费
- GitHub Copilot Usage-Based Billing
- GitHub Copilot Models and Pricing
- Anthropic Prompt Caching | OpenAI Prompt Caching
LLM 原理
- Jay Alammar: The Illustrated Transformer — Self-Attention 可视化
- Georgia Tech: Transformer Explainer — 交互式 Transformer 演示
- ngrok: Prompt Caching 10x Cheaper — Prompt Cache 原理的渐进式教学
Token 消耗研究
- arXiv 2604.22750: How Do AI Agents Spend Your Money? — Stanford/Microsoft,2000+ SWE-bench 轨迹,input比 153
- Branch8: Claude Code Token Limits Cost Optimization — 真实 session 数据,input 占 94.5-98.6%
- Vantage: The Hidden Cost Driver in Agentic Coding Sessions — 50 轮 session 模型,input 占 ~96%
- Cursor Token Optimization: A Data-Driven Investigation — Cursor 一周实测
Multi-Agent 研究
- arXiv 2512.08296: Towards a Science of Scaling Agent Systems (Google/MIT) — 180 种配置,顺序任务退化 39-70%
- arXiv 2510.26585: Stop Wasting Your Tokens (UIUC) — multi-agent 消耗 4-220x tokens
- arXiv 2604.02460: Single-Agent LLMs Outperform Multi-Agent Systems
- arXiv 2601.12307: Rethinking the Value of Multi-Agent Workflow
- arXiv 2603.24284: The Specification Gap
Agent 记忆
- arXiv 2310.08560: MemGPT — Towards LLMs as Operating Systems — 虚拟内存分页类比
- arXiv 2504.19413: Mem0 — Production-Ready AI Agents with Scalable Long-Term Memory — 三阶段记忆流水线
- arXiv 2501.13956: Zep — A Temporal Knowledge Graph Architecture for Agent Memory — 时序知识图谱
- Mem0: Context Window Is RAM, Not Storage
- Mem0: Reading Kimi K2.6 Memory Traces — K2.6 的记忆是 harness 能力而非模型能力
Select 工具
- CodeGraph | Graphify | GitNexus | Aider Repo Map
- Claude Context (Zilliz) | CocoIndex Code | SeaGOAT
- cclsp | claude-code-lsp-enforcement-kit
Compress 工具
Isolate 工具和文档
- Claude Code Sub-agents Docs | Claude Code Worktrees Docs
- Claude Code Costs Docs | Claude Code Context Window Visualization
- oh-my-claudecode | oh-my-openagent | oh-my-opencode-slim
- Augment Code Model Routing Guide | Augment Prism
- Morphllm: Sonnet vs Haiku — 便宜模型出错导致返工的分析
- GitHub Issue #29966: Subagent Caching Bug
Write 工具和模式
- Claude Code Best Practices — Interview then fresh session 官方推荐
- Claude Code Memory Docs | GitHub Issue #57574: MEMORY.md Truncation
- planning-with-files (Manus 模式)
- Alive: Five Markdown Files for Persistent Memory (HN)
- Agent Kernel — 3 文件跨工具 agent 记忆
- HumanLayer ACE-FCA — Research→Plan→Implement handoff 机制
- Matt Pocock /handoff skill — session 间结构化交接
- claude-mem | agentmemory
Context Drift 和维护
- ctxlint — CLAUDE.md/AGENTS.md 路径验证和自动修复
- Caliber — 从 git diff 自动更新 CLAUDE.md
- claude-code-cache-fix — 修复 resume session 的 cache regression
输出控制和成本监控
- Caveman | LiteLLM | RouteLLM
- CC Switch — API provider 管理 + usage dashboard
- ccusage | tokscale | agentsview
- DEV Community: Token Management 50-70% — 50-70% 数字的原始来源(无实验方法)
- Lessons from Building Claude Code: Prompt Caching Is Everything