如何减少 Claude Code、Codex 和 Cursor 里的 token 用量
本文内容
想减少 Claude Code、Codex 和 Cursor 的 token 用量,关键不是管住 agent 写了什么,而是管住它读了什么。我把自己在 RocketSim、RocketTrace 以及其他 Swift 项目里跑过的 1,674 次 agent 会话翻了一遍:只有 0.6% 的 token 是 agent 的回复,剩下全花在反复重读同一份上下文上。
这篇文章里,我会分享几点发现:我全局加进每个 agent 配置的那 10 行代码,以及一个 hook 脚本——它能让每个 Xcode 项目都变得省 token,而且不用改动任何一个 AGENTS.md 文件。下面正式开始。
我是怎么分析 token 用量的
我现在主要用 Cursor,它会为每次 agent 会话保存一份记录,包括 agent 调用的每一个工具。我写了个小脚本把这些记录全部扫了一遍,统计那些浪费 token 的行为:整文件读取、重复读取、不带过滤的构建、轮询、技能加载,以及子 agent 的模型选择。另外,我从 Cursor 仪表盘里导出了一周的使用数据,能看到每次请求的 token 构成。
有几个数字很扎眼:
- 94.2% 的 token 都是缓存读取——也就是 agent 把已有的上下文又重发了一遍。输出 token 只占 0.6%。
- 13,323 次
xcodebuild和swift test运行里,48% 没有加输出过滤。即使在 RocketSim 里,AGENTS.md里已经明确要求加过滤,2,141 次构建中还是有 751 次是裸跑的。 - 53% 的文件读取是把整个文件读进来;而且有一半会话里,agent 会重读一个它已经读过、期间又没改过的文件。这种情况出现了 18,683 次。
- 961 次重复技能加载:同一个
SKILL.md在一次会话里被加载了两遍。 - 85% 的子 agent 直接继承了我的主模型,而它们其实只是在做搜索或探索。
当然,这些都是基于我的工作方式统计出来的。如果你用的是 Codex 或 Claude 之类,也能做类似的分析。我导出的使用数据里,中位数请求大约是 755,000 个 token。这个数字只有在你理解了 agent 是怎么消耗上下文之后,才有意义。
为什么上下文决定 token 用量
智能体(agent)在每一步之间什么都不记得。每当它调用一个工具、继续往下走,它都会把完整的对话重新发给模型:你的指令、规则、它读过的每个文件、收到的每一份构建日志。有了缓存,这个过程会更快,但 token 照样要算,这也是为什么我的用量里「读缓存」占了大头。
可以把它想象成一位同事:每回答一个问题之前,都要把整个项目 wiki 重读一遍。wiki 越大,每次回答就越慢、越贵。会话早期收到的一份 40 万 token 的构建日志,会在之后的每一步里被一起发出去。
这还会引出第二个问题。上下文窗口一旦填满,智能体就会对对话做摘要,或者你只能从头开始。两种情况都意味着它得重新读文件和规则,重建理解。上下文填满得越早,你就越频繁地为这次「重置」付出代价。
这也是为什么,每当我切换到不相关的任务时就会新开一个对话。否则,上一个任务里的每个文件和构建日志都会被带进根本用不到它们的新工作中。
换句话说:管好上下文是减少 token 用量最有效的办法。回复短一点也有帮助,但那是在优化那 0.6% 的部分,暂时不是我的重点。
让 AGENTS.md 文件保持精简
你的 AGENTS.md 文件也是上下文的一部分,每一步、每个会话都会被发送。一个疏于维护的文件会随着每一条经验教训不断长大,等你反应过来,改一个简单的 SwiftUI 小样式,都会连带着把发布流程和 CI 细节一起发出去。
这种事情发生得比你想象的快。Video Compression 是我最新的一款应用。两周前,它的 AGENTS.md 大约 2000 token。今天,它已经超过 15000 token、228 行,塞满了这一路上积累的经验。而这些东西每一步都会被发出去,哪怕智能体只是改了一个标签。
我在 9 月清理了其他几个项目。RocketTrace 根目录的 AGENTS.md 从 8677 字节降到 1454 字节,MCPBeast 的从 8166 降到 1384 字节。内容一点没删,我只是把带有范围限定的知识挪进了各自的子文件夹(里面有自己的 AGENTS.md),或者挪进文档里,等智能体真的在那个区域干活时再去读:
# RocketTrace
- App code: see [app/AGENTS.md](app/AGENTS.md)
- Website: see [website/AGENTS.md](website/AGENTS.md)
- Release process: see [docs/releasing.md](docs/releasing.md). Only read when releasing.
Codex 甚至为此提醒过我。它的日志里显示,某个项目的 AGENTS.md 文件超出了 32 KB 的配额,接着它就把超出的部分直接截掉了。你以为自己的指令会被完整读取,但文件一大,就会被悄悄砍掉。
别再瞎猜怎么在代码里用 AI 智能体
学一套清晰、不绑定具体工具的方法来和 AI 智能体协作——涵盖上下文、指令和校验闭环——这样无论你用哪种工具或模型,都能更快交付,又不会越积越多技术债。
重复的 Agent Skill 白白占用上下文
Agent Skill(智能体技能)和「按需加载的文档」解决的是同一个问题:平时智能体只看到一句简短描述,等任务真需要这项技能时,才把完整内容读进来。我在《Agent Skills:用可复用的 AI 知识替代 AGENTS.md》里写过这一点。但只有把技能库收拾干净,技能才真能省下 token。
我自己就装了两个本地 SwiftUI 技能,和我开源的那个 SwiftUI Agent Skill 放在一起。它们覆盖的是同一个框架,所以智能体经常把两个都加载进来。我做的会话审计显示,其中那个重复的技能在被删掉之前,一共出现在 41 次会话里,可它提供的内容另一个技能早就有了。删掉之后,SwiftUI 相关的活都只走一个技能。而且每多装一个技能,它的描述就会被塞进智能体每次会话都会收到的技能目录里。
我的建议是:一个框架只装一个技能,几个月没用过的直接删。如果拿不准该留哪个,可以参考我那套「挑对 Agent Skill 的 9 步框架」。
Xcode 构建输出最烧 token
这条是 Swift 开发者独有的问题。Xcode 的构建输出量大得离谱。我拿自己的 Mac 应用 MCPBeast 跑了一次完整测试(clean test run),共 117 个测试,实测结果如下:
通过优化 Xcode 构建输出,降低 Claude Code、Cursor 和 Codex 的 token 消耗。
增量构建的输出会小一些,但比例一样:原始输出估算约 18,200 token,加一层过滤后只剩 32。过滤后的输出仍然列出了两条编译器警告(带文件名和行号),以及通过的测试数量——智能体需要的信息全都在了。
xcsift 是一个小型命令行工具,能把 xcodebuild 和 Swift Package Manager 的输出解析成简洁的摘要。用 Homebrew 安装,然后把构建输出通过管道传给它:
brew install ldomaradzki/xcsift/xcsift
xcodebuild test -scheme MyApp -destination 'platform=macOS' 2>&1 | xcsift -f toon -w
RocketSim 的 AGENTS.md 就是让 agent 这么做的。但从前面看到的数据来看,在那个仓库里 agent 仍然跑了 751 次原始构建。对 agent 来说,指令只是建议而已。而我后来新建的项目里压根没有这条指令,于是每次构建都在悄悄燃烧 token。
所以我干脆不再指望 AGENTS.md,而是把这件事挪到了一个全局 hook 里。
一个全局 hook,通吃 Claude Code、Codex 和 Cursor
Cursor、Claude Code 和 Codex 都支持 hook:就是一些在 agent 执行工具之前运行的小脚本。hook 可以检查一条 shell 命令,并在它真正运行前改写它。这意味着你只需在自己的机器上配置一次,之后打开的任何项目就都修好了。
我的 hook 脚本会检查每一条 shell 命令。如果它运行 xcodebuild、swift build 或 swift test 却没加过滤器,就在后面追加 2>&1 | xcsift -f toon -w。其他命令一概不动,包括已经用了过滤器的命令、xcodebuild -list,以及带重定向或管道的命令。如果机器上没装 xcsift,就什么都不做。
把它保存为 ~/.agents/hooks/xcode-output.py:
#!/usr/bin/env python3
"""Route raw xcodebuild and SwiftPM build/test output through xcsift."""
import json
import re
import shutil
import sys
FILTER = "xcsift -f toon -w"
BUILD = re.compile(
r"(^|[\s;&(])(xcodebuild\b(?!.*\s-(version|showsdks|list|showBuildSettings|showdestinations|help)\b)"
r"|swift\s+(build|test)\b)"
)
ALREADY_FILTERED = re.compile(r"xcsift|xcbeautify|xcpretty|\s-quiet\b")
UNSAFE = re.compile(r"[|<>`\n]|\$\(|(?<!&)&(?!&)|;|\|\|")
def command_of(tool_input):
command = tool_input.get("command") if isinstance(tool_input, dict) else None
if isinstance(command, list):
command = command[-1] if command else None
return command if isinstance(command, str) else None
def filtered(command):
command = re.sub(r"\s+2>&1\s*$", "", command.strip())
if not BUILD.search(command) or ALREADY_FILTERED.search(command):
return None
if UNSAFE.search(command) or shutil.which("xcsift") is None:
return None
*prefix, last_segment = command.split("&&")
if not BUILD.search(last_segment):
return None
# Claude auto-approves rewritten commands, so only `cd <path> &&` may precede the build.
if any(not re.fullmatch(r"\s*cd\s+[^\s].*?\s*", part) for part in prefix):
return None
return f"set -o pipefail; {command} 2>&1 | {FILTER}"
def main():
mode = sys.argv[1] if len(sys.argv) > 1 else "--cursor"
try:
tool_input = json.load(sys.stdin).get("tool_input") or {}
command = command_of(tool_input)
rewritten = filtered(command) if command else None
except Exception:
rewritten = None
if rewritten is None:
print("{}")
elif mode == "--cursor":
print(json.dumps({"updated_input": {**tool_input, "command": rewritten}}))
elif mode == "--claude":
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {**tool_input, "command": rewritten},
}}))
else:
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Raw build output wastes context. Re-run exactly as: {rewritten}",
}}))
if __name__ == "__main__":
main()
set -o pipefail 保证了:即使构建失败,整条管道最后返回的仍是非零的失败退出码——哪怕 xcsift 是管道里的最后一个命令。顺带一提,这段代码就是 agent 帮我写的,你完全可以让自己的 agent 也写一版,再继续优化。
在 Cursor 中注册 hook
Cursor 从 ~/.cursor/hooks.json 读取用户级 hook。新建这个文件,写入以下内容:
{
"version": 1,
"hooks": {
"preToolUse": [
{
"command": "python3 ~/.agents/hooks/xcode-output.py --cursor",
"matcher": "Shell",
"timeout": 5
}
]
}
保存后 Cursor 会自动重新加载该文件。你可以在 Cursor 设置的 Hooks 标签页里确认是否生效。
在 Claude Code 中注册 hook
Claude Code 从 ~/.claude/settings.json 读取 hook:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.agents/hooks/xcode-output.py --claude",
"timeout": 5
}
]
}
]
}
Claude Code 只有在 hook 同意时才会执行改写后的命令。所以脚本只改写构建命令,以及可选地出现在它前面的 cd。像 rm -rf build && xcodebuild 这样的命令链不会被动到,照常走你平时的审批流程。
在 Codex 中注册 hook
先在 ~/.codex/config.toml 里启用 hooks,Codex 就支持在 ~/.codex/hooks.json 中使用同样的 hook 格式:
[features]
hooks = true
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.agents/hooks/xcode-output.py --codex",
"timeout": 5
}
]
}
]
}
Codex 的做法略有不同。它会拿命令和你之前批准过的命令做匹配,所以改写后的命令会被当成新命令,重新要求审批。于是这里的 hook 不再改写,而是直接拦下原始的构建命令,并告诉 agent 应该运行哪条确切的命令。这会多出一个很短的步骤,但跟整份构建日志比起来根本不算什么。
不靠截图操作模拟器
同样的思路也适用于模拟器(Simulator)。截图是一张 agent 必须处理的图片,而且它只能告诉 agent 屏幕上现在有什么。无障碍树(accessibility tree)则是文本:每个按钮、标签、标识符都在里面,可以直接拿来操作。
在统计时段内,我的 agent 调用了 RocketSim CLI 共 8,145 次,而原始的 simctl 截图只有 378 张。这个比例是刻意保持的。具体细节我在《面向 token 效率的模拟器自动化》一文里讲过,这里就不再重复了。简单说:让 agent 通过 RocketSim 的无障碍输出检查模拟器,只在需要确认视觉效果时才截图。
换更便宜的模型能减少 token 用量吗?
不一定。便宜模型如果需要多试几次,总 token 消耗反而更高;上下文窗口小的模型也会更频繁地重置。选模型会从两个不太容易想到的角度影响 token 用量。
第一,各模型的上下文窗口大小不同。窗口越小,装得越满,意味着更多的摘要和重置,每次重置后还要重读一遍内容。选模型时留意上下文大小,在大项目里跑长会话时尤其重要。
第二,便宜并不自动等于 token 更少。每一步都要重读完整上下文,所以一个试了三次才修好失败测试的模型,读上下文的次数是一次就搞定的模型的三倍。遇到复杂的改动,我更愿意用更强、能快速收尾的模型。
子 agent 的情况正好相反。我的子 agent 里有 85% 继承了主模型,但它们大多只是在搜索代码库。搜索用不着最强的模型。让负责探索的子 agent 用又小又快的模型,主模型留给做决策和改代码。
Caveman 和 Ponytail 呢?
这两个项目你大概在 GitHub 上见过,都挺火。它们都是有价值的思路,我下面的规则里也有几条受它们启发。
Caveman 让 agent 用原始人式的短句回复。JetBrains 在 86 个真实编码任务上测试了这个 skill,输出 token 减少了 8.5%,质量没有下降。但别忘了我的数据:输出只占我 token 总量的 0.6%。这个项目在自己的文档里也坦承了这一点,也正是出于这个原因,他们又做了一个单独的代理,用来压缩 agent 读取的内容。我只保留了其中一个思路:先给结果,别再去旁白式地描述每一次工具调用。
Ponytail 则让 agent 像一位「偷懒」的资深开发者那样思考:这东西真的有必要存在吗?代码库里是不是已经有了?平台本身是不是就覆盖了?对我们来说,就是在自己写代码或引入依赖之前,先想想能不能直接用 DatePicker、ShareLink 或 URLSession。代码越少,要写的 token 和之后
- 先搜索,再只读需要的行。已经读过的文件不要重复读,除非它变了。
- 先跑范围最小的测试(
-only-testing:或--filter),最后再跑一次完整测试套件。 - 把等待放在一条命令里完成(
gh run watch、gh pr checks --watch,或者until循环),而不是反复「睡一会儿—看一眼」地占用多轮对话。 - 通过无障碍树(accessibility tree)检查模拟器,比如用 RocketSim CLI。截图只用来确认视觉效果。
- 只在任务需要时才加载技能(skill)。覆盖同一框架的两个技能,绝不要同时加载。
- 把搜索和探索类子代理(subagent)配上小而快的模型,主力模型留给决策和改代码。
- 加代码之前,先看这个代码库、Swift 标准库或 Apple 框架是否已经解决了问题。然后写能用的最小改动,同时别丢掉校验、错误处理和无障碍支持。
- 先说结果。别复述计划、别旁白式播报工具调用,但句子要保持完整。
- 同一个修复尝试两次失败后,停下来汇报你了解到的情况,不要继续死循环。
- 把自成体系的搜索和排查交给子代理,让它们读的内容不进入当前对话。当用户切换到不相关的任务时,建议开一个新会话,并附上简短的交接说明。
关于「等待」那一条,值得多说几句。我的代理一共用了 4,209 次 sleep,但其中大多数是命令内部的正常暂停,比如重启一个 app。真正的浪费在 740 轮「睡一下、查一下 CI、再睡一下」的循环里,因为每一轮都会重新读取完整上下文。而 gh run watch 一步就能完成等待。
这些规则不必写进每个项目。每个代理都支持全局指令文件:
- Cursor:Settings → Rules → User Rules
- Claude Code:
~/.claude/CLAUDE.md - Codex:
~/.codex/AGENTS.md
我把这些规则放在一个文件里,在 ~/.claude/CLAUDE.md 中用 @~/.agents/token-efficient-agents.md 导入,同时把 ~/.codex/AGENTS.md 做成指向同一文件的符号链接。这样,各项目的 AGENTS.md 就能专注写项目独有的内容。
常见问题
上下文重复读取。 在我的使用数据里,94.2% 的 token 都是缓存读取:代理每一步都会把它已有的指令、文件和工具输出重新发一遍。真正的回复只占 0.6%。庞大的构建日志、整文件读取和冗长对话,会让之后的每一步都更贵。
Caveman 压缩代理的回复。 JetBrains 在 86 个编码任务上测得输出 token 少了 8.5%,质量没有下降。但由于回复只占我总 token 的 0.6%,对总量的影响很小。真正影响大的是减少代理读取的内容,比如构建输出和重复的文件读取。
Claude Code 读 ~/.claude/CLAUDE.md,Codex 读 ~/.codex/AGENTS.md,Cursor 用设置里的 User Rules。 这些位置的指令对每个项目都生效,适合放通用习惯,比如只读需要的行范围。项目专属的知识则放在各自仓库的 AGENTS.md 里。
把构建输出通过 xcsift 处理。 例如:xcodebuild test ... 2>&1 | xcsift -f toon -w。它会保留错误、警告和测试结果,其余全部丢掉。我实测一次干净的 MCPBeast 测试运行,从约 406,500 个估算 token 降到约 168 个。用一个全局 hook,就能自动应用到所有项目。
结语
减少 token 用量,核心在于守住上下文。对我来说,输出 token 几乎不重要;真正拖累每一步的是构建日志、整文件读取和重复加载的技能,它们会被反复随行发送。以我的经验,对 Swift 开发者来说,原始的 xcodebuild 输出是最大的一块,而一个全局 hook 能一次性解决所有项目的问题,不用指望每个 AGENTS.md 指令都被老老实实执行。
如果你想进一步优化 AI 开发工作流,可以看看 AI Development 分类页面。有其他技巧或反馈,欢迎联系我,或者在 Twitter 上找我。