☰
Coding Agent Token 优化实战:从上下文管理到节省 58.7% 消耗
2026/10/15 7:11:28 网站建设 项目流程

上个月我盯着自己的 AI 编码账单看了很久。我对 Claude Code 这类 Coding Agent 其实相当喜欢——它能自己读仓库、改代码、跑测试、根据报错继续修正,一条龙干到底。可到了月底,把模型 API 的 Token 用量拉出来一看,数字大得让我有点坐不住。一次不算复杂的功能改动,动不动就烧掉几十万 Token,我每个月花在 Agent 对话上的成本比模型订阅费还高。朋友说,你嫌贵就少用呗。我的想法不太一样:工具是好的,问题在于 Agent 太浪费。于是我花了两周时间做了一个自己的 Coding Agent,核心目标只有一个——在完成相同任务的前提下,把 Token 消耗压下来。第一轮实测下来,这个 Agent 比 Claude Code 少用了 58.7% 的 Token。

1. 先算账:一个 Coding Agent 的 Token 到底烧在哪儿

1.1 账单里的三个吞 Token 大户

我把自己用 Claude Code 的一个月会话导出来,按任务类型算了算消耗,发现浪费比我想象的还严重。排在最前面的三个大户,几乎是所有 Coding Agent 的通病。

第一个是整文件读取。Agent 一旦开始干活,常见的做法是把涉及的代码文件从头到尾读一遍。比如修复一个校验逻辑的 bug,主模块一共 800 行,真正和 bug 相关的函数可能就 30 行。但 Agent 不管三七二十一,先把 800 行全塞进上下文,每次调用模型都要重新计费。一次任务来回几十个请求,这 800 行的代价就被放大几十倍。

第二个是对话历史的无限累积。Agent 跑一个任务,往往要执行十几二十次工具调用,每一次工具返回结果、模型自己输出的分析,都会留在上下文里继续传给下一次请求。问题是,等到第 15 步时,第 3 步的消息早就没用了——但模型每次推理还是得把它们重新读一遍。这就好比一个人开着几十个浏览器标签页工作,百分之八十的页面点开过就再也没看过,却一直在内存里占着位置。

第三个是输出回显的废话。很多 Agent 在每次工具调用前都要输出一段说明文字,比如"让我分析一下这个问题""看起来问题可能出在……""我先把相关文件读一遍"。单看一次请求,这些解释也就几十个 Token,不痛不痒。但乘以几十轮,再考虑到 Token 计费机制里输出通常比输入更贵,累积起来非常可观。

1.2 为什么不能靠换模型解决

有人可能会想:嫌 Token 耗费,换一个更小或者更便宜的模型不就行了?我试过,结论是这条路走不通。问题在于,自主改代码这件事对模型能力的下限要求很高。小模型在指令跟随和编辑准确性上明显掉链子,尤其涉及多文件联动、逻辑链条较长的 bug 定位时,它经常给出一个看起来挺合理、实际上根本跑不通的 diff。结果是任务做不完,还得反复重试,重试烧掉的 Token 比省下的还多。

我的结论是:模型能力决定了下限,上下文管理则决定了一个 Agent 到底要多花多少钱。与其换模型,不如优化每次请求里塞给模型的"废话量"。这也是我自己动手做 Agent 的起点——同一个模型,通过改进调度策略来省 Token,而不是牺牲模型能力。

2. 我的 Agent 设计思路:把上下文当成稀缺资源,而不是无限工作台

2.1 定位优先:让模型先找,再读

大部分 Agent 的默认动作是 list files 然后 read file,把文件整个拉进上下文再慢慢看。我反过来了,核心工具链里第一件事是 grep 定位。

我给 Agent 设计的第一个工具是 grep_search,返回的是匹配行的文件路径和行号。比如想确认某个函数是怎么实现的,模型不是直接 read 整个文件,而是先搜:

grep -n "def validate_session" app/auth.py

返回结果是:

app/auth.py:132: def validate_session(user_id):

然后模型再用 read_lines 精确读取第 132 到 150 行,只拿这一段。同一个文件只读相关片段,无关的 700 行代码从头到尾都不会进入上下文。光这一条,输入 Token 就降了一个量级。

为了让这个习惯变成强制规则,我把 read_lines 的默认行数上限设成了 60 行。也就是说,模型如果不显式传一个更大的参数,一次最多只能读到 60 行。我承认这有点"物理逼迫"的意思,但效果出奇好——模型在想要不要传大参数之前,会先想一想自己是不是真的需要这么多内容。

2.2 工具输出裁剪:只把结论喂给模型

Agent 执行命令之后,工具返回的内容同样会进入上下文,这是另一个容易被忽视的浪费点。跑一次 pytest,输出几百行;跑一次 git diff,输出上千行。而模型在一轮推理里真正需要的信息,可能只是最后那一段断言失败,或者报错信息里的一行异常堆栈。

我在工具层做了一层裁减。统一规则是:工具输出超过 1500 字时,只保留头和尾,中间省略并注明省略量,同时把包含 error、exception、failed 的片段单独拼在尾部。实现很简单:

def clip_output(text: str, head_chars: int = 1500, tail_chars: int = 1500) -> str: if len(text) <= head_chars + tail_chars: return text head = text[:head_chars] tail = text[-tail_chars:] omitted = len(text) - head_chars - tail_chars return f"{head}\n...[中间省略约 {omitted} 字符]...\n{tail}"

可能有人担心裁剪会把关键信息剪掉。实际上测试输出里真正重要的错误信息大概率在尾部,把 tail 完整保留已经覆盖了绝大多数情况。如果模型看完头和尾发现不够,它可以再执行一条更有针对性的命令去拿细节。让工具输出保持精简,相当于每次请求省下几百到几千个输入 Token,在长任务里算总账非常可观。

2.3 任务笔记机制:用摘要替代完整历史

这是整个设计里我认为最关键的一环。很多 Agent 把对话历史直接当成上下文,认为"历史越长=模型越了解情况"。但模型真正需要的并不是每一轮原始对话,而是一个高度浓缩的状态摘要:之前改了什么、改到哪了、下一步要做什么。

我把任务拆成子任务,每个子任务结束时,强制模型输出一行结构化笔记。笔记内容包含文件路径、行号、改动内容和下一步计划。举个例子:

note: - loc: app/routes/profile.py:45-72 - change: 新增 GET /api/profile 接口,参数校验委托 validate_query() - next: 补充非法参数的单元测试

笔记写入一个全局列表。到了下一个子任务,Agent 的 system prompt 里只拼接这些笔记,不再携带上一个子任务的完整消息列表。也就是说,不管任务执行到第 10 步还是第 50 步,模型看到的开头始终是几十行笔记,而不是已经被灌得又长又臭的原始对话。

这个机制直接解决了 1.1 里说的"历史无限累积"问题。从第一个子任务到第十个子任务,上下文前缀只增长几十行,而不是每一次工具调用都在累加。

3. 关键机制实现:定位、裁剪、旋转、手术刀式编辑

3.1 工具调用循环:六个工具够用了

整个 Agent 的核心调度是一个标准的工具调用循环,但工具集被我砍得很小,一共只有 6 个:grep_search、read_lines、run_command、apply_patch、write_note、finish。工具越少,模型越难挥霍。

循环结构大概是这个样子:

def agent_loop(task): messages = [build_system_prompt(task, notes)] while True: response = call_model(messages, tools=TOOLS) messages.append(response) for call in response.tool_calls: result = dispatch_tool(call) messages.append(format_tool_result(call, result)) if call.name == "finish": return response

这里没有给模型提供"读取整个文件"或"列出全部目录"这种奢侈操作。它想了解某个符号,就得用 grep 找;想读代码,就得指定行号范围。每一条路径都逼着模型先思考再行动,而不是一上来就把半个仓库拉进上下文。

3.2 apply_patch:手术刀式编辑

编辑动作是 Token 消耗的重头戏,因为输出 Token 往往比输入 Token 贵。很多 Agent 改文件时是整个文件重写,哪怕只是改一行,也要输出几百行新内容,模型还要重新理解整个文件结构。我做的替代方案是 apply_patch 工具——局部替换。

模型只需要给出定位信息和替换内容:

old_str = "def handle(data):\n if not data:\n return []" new_str = "def handle(data):\n if not data:\n return []\n return sanitize(data)"

程序负责在文件里找到唯一的 old_str 位置,执行替换,然后把一个简短的新 diff 摘要返回给模型。模型看到的反馈是"已替换 3 行",而不是重新输出整个文件。这样输出 Token 大幅减少,而且由于不用重写整段代码,编辑准确性反而更高。

实现这个工具要注意匹配唯一性问题。如果 old_str 在文件里出现多次,程序不能直接替换,应该返回冲突提示,让模型补充更多上下文来精确定位。这是常见的小坑,后面会专门讲。

3.3 上下文 rotate:兜底策略

就算有定位、裁剪和笔记机制,某些超长任务还是可能把上下文堆得很高。我在消息长度超过阈值时做了一个 rotate 操作。

rotate 的思路不算复杂:当消息总字符数达到某个阈值(比如 70 万字符),就先让模型把当前状态重新压缩成一条笔记,然后清空除笔记以外的所有消息,重新开始一轮对话。这个额外的压缩调用会花掉一些 Token,但远比让整个上下文继续膨胀划算。

需要注意,rotate 之后模型会丢掉具体细节,所以笔记里必须包含足够的信息:涉及的文件、行号、函数名、已完成的改动、待办的下一件事。如果笔记写得模棱两可,后面模型就要重新探索,反而更费。我见过不少复刻这个方案的人把 note 写成"完成了登录模块优化"这种废话,接着模型就得从零开始找代码,整体消耗不降反升。

4. 同一模型下的 A/B 测试:58.7% 是怎么量出来的

4.1 基准任务集是怎么设计的

要得出一个可信的"少用 58.7% Token"结论,最关键的是对比条件公平。我特意做了这么几件事。

首先,两个 Agent 跑的是同一个仓库——一个大约 40 个文件的 Python 后端服务(模拟项目X),跑完全相同的 4 个任务:新增一个 API 接口、修复一个重复提交导致的 bug、给某个 util 模块补 8 个单元测试、把一个 200 行的大函数拆成 3 个小函数并保持行为不变。

其次,两个 Agent 调用的是同一个 Claude 模型 API,temperature 设为 0,没有人为微调任何参数,确保差异来自 Agent 调度而不是模型本身。我把 Claude Code 的日志导出,解析每次请求的 input_tokens 和 output_tokens;自己的 Agent 就在调度循环里记录每一次调用的 usage 字段,最后统一汇总。

另外,每个任务各跑 3 轮,取中位数,避免随机性干扰。任务是否完成的判定标准是最终 git diff 是否满足要求、测试是否通过。如果某个任务没跑通,那次数据直接不算,重新跑。所有跑通的轮次里,我看 Token 用量的差距。

4.2 数据汇总与分组拆解

汇总数据如下:

任务Claude Code 总 Token我的 Agent 总 Token节省幅度
T1 新增接口132,40048,20063.6%
T2 修复 bug96,80041,30057.3%
T3 补单元测试84,50039,70053.0%
T4 拆分重构157,90065,60058.5%
合计471,600194,80058.7%

再拆细一点,看输入和输出的差别:

指标Claude Code我的 Agent节省幅度
输入 Token398,200142,60064.2%
输出 Token73,40052,20028.9%
总 Token471,600194,80058.7%

总节省 58.7% 就是这么来的:(471,600 - 194,800) / 471,600 ≈ 58.7%。

4.3 为什么输入省了 64%,输出只省了 29%

这两个数字之间的差异,正好印证了前面设计思路里的判断。

输入 Token 省得最多,靠的是三招组合:定位优先让模型只读相关代码,工具输出裁剪让命令结果不带噪音,任务笔记替代历史消息避免了无限累积。输入侧的冗余本来就很大,整文件读取、全量日志、无关对话,这些在 Claude Code 默认流程里几乎属于"必经之路",但对任务完成并没有实质帮助。只要把这些剪掉,输入立刻瘦了一大截。

输出侧能省的空间相对有限。原因在于模型无论怎么优化,最后总得输出真正的代码改动、工具调用参数这些"必需品"。我在 system prompt 里明确要求模型不要解释、不要复述、直接调用工具,以及用 apply_patch 做局部替换而不是重写整个文件,这些动作把解释性输出压掉了不少,但不可能再压掉一个数量级。

这个对比也提醒了我一件事:Token 优化的主战场永远在输入侧。输出 Token 再省也有下限,输入 Token 的冗余才是真正可以系统性消灭的部分。

5. 省下来的 Token 不是白拿的:代价、边界与适用场景

5.1 我多付出的工程成本

自己做一个 Coding Agent,省的是 Token,花的是时间。我前后投入了大约两周业余时间,内容包括核心调用循环、六个工具的实现、上下文裁剪、笔记机制,以及后面用于对比的测试脚本。

调试阶段最大的成本不在写代码本身,而在于找平衡。上下文切得太狠,模型会漏掉信息,改出来的代码不对;切得太松,Token 又压不下来。那段时间我几乎每天都在调参数:read_lines 默认行数设多少、输出裁剪保留多少字符、笔记里必须包含哪些字段。这些参数没有标准答案,只能在自己常用的任务类型上反复试。

如果你只是偶尔用一次 Coding Agent,这个成本其实不划算。但如果你像我一样,每天有大量修改代码、修 bug、补测试的需求,靠省 Token 省下来的费用很可能几周内就覆盖了开发投入。

5.2 这套方案搞不定的任务

必须坦白,省 Token 的思路不是万能的。有几个场景我的 Agent 明显不如 Claude Code。

首先是跨 15 个以上文件的大规模重构。上下文切得太碎之后,模型容易失去全局视野,经常改完 A 模块忘了 B 模块有依赖,最后跑测试才发现一堆联动问题。Claude Code 保留的长上下文在这种场景下反而是刚需。

其次是不熟悉的代码库探索。面对一个没见过的巨型项目,Agent 需要大量浏览代码结构才能建立心智模型,这时候频繁 grep 定位反而低效,不如老老实实把整个目录结构看一遍。

第三类是开放式需求。比如"帮我把这个模块的重构方案设计一下,风格你来定",这种需要跨多个文件反复权衡的任务,笔记机制存下的信息根本不够用,模型必须保持完整的思考链条。这类场景里,强行省 Token 是在牺牲质量。

5.3 该用的时候还是用完整上下文

认识到边界之后,我的选择是不做二选一,而是把两种模式放进同一套 CLI 里,根据任务类型自动路由。

小改动、修 bug、补测试、单文件或少量文件的小迭代,走省 Token 模式,用我的 Agent 调度逻辑。大重构、探索不熟悉的仓库、开放式设计任务,直接走完整上下文模式,让对话历史自然累积。这样既不牺牲复杂任务的质量,又能在日常高频任务里把 Token 账单压下来。

说白了,省 Token 的本质是避免浪费,而不是牺牲必要信息。真正需要全局上下文的时候,Token 不是优先级;不需要的时候,一个字都不该多花。

6. 想复刻这个 Agent?从三个模块起步,还有几个坑替你踩过了

6.1 最小可用版本:三个模块,一个晚上

你要是想复刻,我建议不要一上来就追求我的完整设计,先搭一个最小版本,跑通流程之后再加功能。

最小版本只需要三块。第一块是 grep_search 和 read_lines 两个工具,让模型先定位再读取。第二块是 run_command 的输出裁剪函数,就是上面说的 head + tail 策略。第三块是 apply_patch 局部替换工具。这三个模块加起来,一个熟练的开发者一个晚上就能搭出雏形。

搭出来之后先别急着加笔记机制和 rotate。你自己跑几个任务,对比一下和直接裸调的差异,再决定哪些优化值得加。我见过太多人一上来就把上下文压缩、笔记归档、任务调度全做齐,结果出了 bug 根本不知道是哪一层的问题。

6.2 复刻路上我踩过的四个坑

第一个坑是 apply_patch 匹配失败。模型生成的 old_str 和文件里的实际内容经常差几个空格或者换行符,导致替换不了。后来我在程序里加了容错:先把 old_str 每行的首尾空白 trim 掉,再做匹配,还是匹配不上时再尝试按行数定位。

第二个坑是裁剪日志时把真正的报错信息也裁掉了。起初我以为保留头部和尾部就够,但某些测试框架的报错堆栈偏偏出现在中间段。后来我把包含 error、exception、failed 关键词的连续行单独提取出来,强行拼在返回内容的最后,才算稳定下来。

第三个坑是笔记写得太抽象。模型两次输出"完成 auth 逻辑",第三个子任务开始之后,它根本不知道 auth 逻辑改在哪个文件哪一行。我被迫在系统提示里加了一条硬性要求:笔记必须包含文件路径、行号和符号名,缺一个字段就要补全才能继续。

第四个坑和 Token 无关,但很影响体验——调用模型 API 时频繁被限流。解决办法是加动态退避重试,遇到限流先等几秒钟再重试,同一个任务里多个请求之间也要控制并发。

6.3 如果重做,我会调整什么

复盘整个项目,有三件事如果再做一次,我会从第一天就改过来。

第一件是把 Token 统计埋点在开发初期就做进去。我是后面补的日志解析,导致前期调参时全靠感觉,浪费了不少时间。如果一开始就让每一次请求的 input/output token 都落盘,参数调优会快很多。

第二件是尽早跑完整的基准测试。我在核心逻辑调通之后才想起做 A/B 对比,导致前期很多优化方向其实是在盲调。先建立"Claude Code 跑同样任务的 Token 基线",后面每一个改动都有参照物,效率会高很多。

第三件是更早地关注输出 Token 的压缩空间。虽然我最终输出侧也省了 29%,但这是后来才优化的。如果重新做,我会在一开始就明确让模型输出最简风格的回复、用 apply_patch 而不是整文件重写,输出侧的优化会和输入侧同步推进,整体节省比例还能再高一些。

说回这几个坑,其实每个都对应一个很朴素的教训:Token 优化不是把模型能力削弱,而是让模型每次都只看到当前需要的少量高质量信息。裁掉的是冗余,不是判断力。这个度把握好了,省 58.7% 一点都不夸张;把握不好,你会得到一个又省 Token 又总写错代码的 Agent,那就本末倒置了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询