Claude Code额度回落:Agent正在制造新的「祖传代码屎山」?
代码确实是 Agent 写的,只是后来的 Agent 已经不知道前面的 Agent 为什么这么写了。
作者丨 郑佳美
编辑丨岑 峰
Claude Code 原定于 8 月 19 日结束的 +50% 周额度加成,又被 Anthropic 延长到了 8 月 31 日。也就在原定截止日前后,Hacker News 上出现了一轮关于 Claude Code 使用成本的讨论: 不少人发现,一个并不复杂的任务,Agent 跑上几轮,额度就会掉得很快。
问题在于,Claude Code 消耗的并不只是最后生成的那几行代码。读文件、搜调用链、跑测试、处理日志,每一步都会继续进入后面的上下文。任务越长,Agent 背着的历史越重,系统也越依赖清理和压缩。
代码可以完整留在仓库里,早期的设计理由却可能在压缩中逐渐变薄。 于是 Token 消耗和代码屎山开始在同一个地方汇合。
01
修个小 Bug,为何需要几十次推理?
普通 Chat coding 的计算边界很清楚。输入一段代码,模型读完以后给出解释或者修改方案,这一轮基本结束。雷峰网 (公众号:雷峰网)
而 Claude Code 的基本单元换成了 agent loop。模型先观察当前状态,决定下一步要读哪个文件或者执行什么命令;工具返回结果以后,模型再进行下一轮判断。
读源码、搜索引用、运行测试、查看 Git diff、修改文件,看上去像一个连续动作,在模型侧其实是一串独立的推理请求。Claude Code 官方文档也把这种“模型判断—调用工具—根据结果继续判断”的循环作为 Agent 工作方式的核心。
比如一个登录状态偶发失效的问题。Agent 先找到入口,发现状态来自 service,于是继续读取 service;看到缓存后搜索谁在写它;接着跑测试,测试出现另一个异常,于是去看 fixture;修完以后再次验证,旧测试又暴露出兼容问题。
可能直到这时,它才真正开始写那几行代码。因此, diff 大小和计算量之间几乎不存在稳定比例。5 行补丁背后可能只有 3 次推理,也可能已经经过 30 次工具交互。
如果把一次 Agent 任务拆开,可以先得到两个变量:一个是 step count ,Agent 为了完成任务走了多少步;另一个是 working set ,走到当前这一步时,模型还需要掌握多少项目状态。
只增加 step count 已经会提高消耗。如果 working set 还在同步变大,情况就完全不同了。第 3 步也许只需要处理几千 Token,第 30 步却可能已经背着项目规则、相关源码、测试结果、修改历史和工具返回继续推理。雷峰网
这也是 Coding Agent 成本结构发生变化的起点: 计算量开始取决于“走多少步 × 每一步背多重”,而不再取决于写了多少行代码。
02
Token 到底烧在哪里?
把 Agent 的一次模型请求拆开,可以粗略看成三块。相对稳定的部分包括 system prompt、CLAUDE.md、工具定义和项目规则;不断变化的部分包括代码文件、搜索结果、测试日志、Git diff 和此前的任务轨迹;最后还有这一轮模型生成的 reasoning、文字与代码。
这里容易产生一个误区:只要前面的内容已经读过,就不应该重复产生太多成本。问题在于,LLM 两次请求之间不存在一个传统程序那样可以随时访问的内部内存。上一轮知道的信息,如果下一轮判断仍然依赖它,相关状态还得继续出现在可用 context 中。
Prompt cache 能缓解这个问题。Claude Code 官方文档明确说明,如果没有 prompt caching,每轮请求需要重新处理整段历史;缓存命中后,已经处理过的稳定前缀可以复用,从而降低重复计算和成本。
但 cache 解决的是“同样的历史能不能便宜一点重新使用”,没有解决“这段历史还要不要继续存在”。100K Token 的旧状态命中缓存后便宜了,它依然占据 context,也依然是当前推理建立在上面的状态。
于是可以把一个长任务粗略写成:第 t 步的输入规模,大约等于稳定前缀 S ,加上当前有效工作集 W_t ,再加这一轮刚产生的新信息 Δ_t 。
真正麻烦的是 W_t 。如果每走一步,Agent 又多读一点源码、多得到一点日志、多留下一个决策,而旧信息没有及时退出,那么 W_t 会随着任务推进不断增加。
在一个极端简化、完全没有缓存和清理的模型里,如果每轮新增的有效状态大致相同,总处理量会出现接近 1 + 2 + 3 + … + n 的累积结构。也就是说,step count 只增加了一倍,整个任务处理过的历史状态可能增加得更快。
实际系统有 cache、context editing 和 compaction,不会机械遵循这个增长曲线,但问题的形状没有改变: Agent 运行时间越长,每一个新动作越可能建立在一块更重的历史之上。
所以长任务里那个很短的用户 prompt 很快就会失去存在感。真正开始主导成本的,是模型为了保持任务连续性而不断携带的工作集。
03
删得太狠会出现语义缺页
working set 为什么膨胀得这么快,工具输出是一个很大的来源。源码至少有结构,日志经常没有。
一次 grep 可以返回几百处引用,一次构建可能吐出大片 warning,一次测试失败可能带着完整 stack trace,Docker、编译器、包管理器也会制造大量对任务没有长期价值的文本。
假设第 10 步测试产生了 8K Token 日志。它第一次进入 context 时,只是 8K Token。可 Agent 还要继续检查源码、修改、重新测试,只要这段日志仍然处于有效历史中,它就会提高后续很多轮请求的基础重量。
这很像存储系统里的 write amplification: 一次逻辑写入,造成了后续更多底层处理。 Agent 里的情况是,一次 tool output 被写进执行历史,随后跟着后面的推理一起移动。
于是同样是 8K Token,放在任务结束前一轮和放在任务刚开始时,带来的整体影响完全不同。Claude Code 现在也在主动减少这种污染。官方建议用 sub-agent 隔离高输出任务,并明确提到搜索结果、日志和大量文件内容会消耗主会话 context;工具定义本身也会占用空间,因此工具集过大同样会增加状态负担。
可这里又出现一个反方向的问题:不能因为日志很贵,就把它们全部裁掉。一段 3000 行日志里,可能只有 20 行与根因有关。系统事先并不知道是哪 20 行。如果清理过早,Agent 到后面突然需要其中一个细节,只能重新跑测试或者重新打开文件。
这可以叫做 semantic page fault,语义缺页 。 传统虚拟内存里,程序访问一个已经不在内存中的页面,系统会从磁盘重新加载;Coding Agent 丢掉某段早期证据以后,也会发生类似现象,只不过表现成重新搜索仓库、重复读取文件、再次运行命令,甚至重新推导一个早就分析过的问题。
于是长任务陷入一个两难:留下过多历史,后续每一步越来越重;清理得过于激进,Agent 又会不断重新获取以前见过的信息。
这也解释了为什么 context 管理不能简化成“少塞一点 Token”。真正需要解决的是 working set selection : 此刻有哪些信息必须留在工作区,哪些只是已经完成使命的中间产物。
到了这里,compaction、memory 和 sub-agent 才真正有了存在的理由。
04
什么信息可以被忘掉?
Claude Code 接近 context 边界时会自动压缩会话,同时还会清理部分较旧的工具结果。官方也提醒,长 session 里的无关对话、文件内容和命令结果可能占满窗口并干扰模型表现。
从系统角度看,compaction 很像一次语义垃圾回收。麻烦在于,普通垃圾回收判断的是“这个对象还有没有引用”,而 Agent 必须判断“这段信息以后还有没有意义”。
后者难得多。例如早期有这样一段设计结论:某个模块不能自己缓存用户状态,因为系统要求状态只有一个 owner,所有修改必须经过 service。
几十步之后,如果这段信息被压成:“之前通过 service 调整解决了状态问题。”事实没有错,但信息已经发生了变化。原始内容包含的是 constraint ,后面的摘要保存的只是 even t 。
下一次 Agent 碰到性能问题,看到 service 调用很慢,很可能又在模块里增加缓存。它并没有违反自己当前掌握的信息;当初禁止缓存的因果关系已经不在有效状态中了。
Claude Code 的 context 文档甚至明确指出,部分 path-scoped rules 和嵌套的 CLAUDE.md 会随着会话一起被 compaction 摘要掉,需要再次读取匹配文件才会重新加载。
Memory 试图解决长期知识保存的问题。项目根目录的 CLAUDE.md 和 auto memory 可以把构建命令、项目规范、调试经验等内容从短期对话中抽出来,并在会话开始时重新加载。但 Anthropic 对它的定位也写得很清楚:这些 memory 仍然是 context,不属于强制配置。
这个区别非常关键。“这里不能直接访问数据库”如果只写在 memory 里,它仍然是一句模型需要理解并遵循的自然语言。如果同一条规则被写成 dependency lint、类型约束或者 CI 检查,它才变成一个无法轻易绕开的软件 invariant。
Sub-agent 解决的是另一块:隔离工作集。让一个独立 Agent 去扫描仓库或者分析长日志,再把压缩后的结果交还主 Agent,可以避免原始噪声进入主线程。Claude Code 官方给 sub-agent 的用途之一就是 context isolation。
它的代价也很有意思:主 Agent 得到了更干净的状态,却失去了部分原始证据;多个 Agent 同时运行,还会建立各自的 context。因此 compaction、memory、sub-agent 放在一起看,其实已经很像一套 Agent 时代的内存层级:
当前 context 是昂贵工作内存,compaction 负责压缩,memory 保存跨 session 状态,sub-agent 用独立地址空间隔离噪声。问题也从“context 够不够大”变成了另一个层级:
哪些状态需要高保真保存,哪些状态只需要留下摘要。 这个问题会直接影响后面的代码质量。
05
无法提前预知要跑多久的程序
理解前面这套执行结构后,再看 Claude Code 的周额度,会发现平台很难继续按“消息条数”计量 Agent。因为一条消息已经失去稳定意义。
把变量改个名字是一条消息,重构整个认证模块也是一条消息。前者可能几步结束,后者可能运行几十轮,读取几十个文件,再启动多个 Agent。同样的一个 request,背后的资源需求可以完全不是一个量级。
Claude Code 用滚动限制和周额度包装这件事;Codex 现在已经明确按照 input token、cached input token 和 output token 折算 credits;Cursor 的套餐则给 Agent 提供不同 usage pool,第三方模型的消耗会受到模型 API 价格影响。
三个产品的界面语言不同,底层需要解决的问题却很接近: 怎样给一个执行路径事先无法确定的智能程序分配推理资源。 一个 Coding Agent 到底会跑多久,在任务开始时很难确定。
模型可能很快找到根因,也可能连续提出几个错误假设;可能一次测试就通过,也可能进入长时间 debug loop;可能只需要一个 Agent,也可能拆出多个 sub-agent。
传统 API 很喜欢按 request 计费,是因为一次 request 的资源波动还能控制在一定范围。Agent 把这种稳定性打散了。所以 Token 在这里开始有一点 CPU time 的味道。
这个类比不能画等号。不同模型处理同样数量 Token 的算力成本不同,input、cached input 和 output 也有不同成本。但站在开发者这一侧,它们承担的功能越来越相似:都是在描述一个任务为了继续运行,到底占用了多少计算资源。
Anthropic 在今年提高 Claude Code 使用上限时,也直接把额度提升和新增 compute capacity 联系在一起。这会带来一个很有意思的指标变化。以前看 Coding Agent,容易比较“同一道题谁一次写得好”。往后可能更有意义的是: 完成同样的工程状态变化,谁消耗的有效计算更少。
如果一个 Agent 花掉大量 Token,只是在重复打开文件、重新跑测试、重新恢复已经丢掉的上下文,那些 Token 并没有换来对应程度的工程推进。
而这类低效状态恢复,恰好会和技术债在下一层碰到一起。
06
AI 祖传代码怎么形成
这里可以把一个 Coding Agent 维护的软件抽象成两套同时演化的状态。一套是代码状态 R_t 。文件、类型、接口、测试、Git commit 全部属于这一层。Agent 第 20 步加进去的一行 retry ,只要没有被删,第 100 步打开文件时仍然完整存在。代码对过去修改的保存精度非常高。
另一套是设计状态 M_t 。为什么这里需要 retry ,为什么那个缓存只能放在 service,为什么这个状态不能有两个 owner,为什么一个看起来多余的判断暂时不能删,这些信息属于设计因果。
M_t 没有像 Git 一样天然的无损存储。它分散在对话、推理、工具返回、memory、规则文件和 compaction summary 里。任务不断推进以后,部分内容被清理,部分内容被摘要,部分内容需要重新检索。
于是会产生一个很关键的不对称: 实现结果能够高保真累积,生成这些结果的因果关系却会不断降采样。 这比单纯说“Agent 会忘东西”严重得多。
假设一次并发问题中,Agent 分析后加入了一个 queue。当时它掌握的完整结论是:只有写路径 A 存在竞争,因此 queue 只能包住 A;写路径 B 需要低延迟,不能进入这个队列。
代码把 queue 完整保存下来了。经过长时间执行以后,设计状态可能只剩下“这里用 queue 解决 race condition”。
后来 B 也出现一个偶发错误。Agent 再次读到代码时,很自然地把 B 也接进现有 queue。
随后延迟上升,于是再加 bypass。bypass 又引出偶发状态不一致,于是外围补 retry。到了这里,没有任何一次修改必然是荒谬的。每个补丁在当时看到的局部状态下甚至可能相当合理。代码却已经从“一个明确的并发模型”,变成了 queue、bypass 和 retry 互相补偿。
AI 代码屎山很可能就是这样长出来的。它不一定表现成模型突然写出一团垃圾,更可能表现成 局部正确不断累积,整体模型逐渐消失 。
传统软件里这类问题通常经过人员交接慢慢形成。原作者离开,新开发者看到旧代码,却不知道它为什么存在,于是在外面再包一层兼容逻辑。
Coding Agent 把“人员交接”变成了“上下文交接”。第 20 步和第 100 步看起来还是同一个 Claude Code session,但它们实际拿到的设计状态已经不完全相同。从信息角度看,更像两名工程师通过一份不断缩水的交接文档维护同一个仓库。
测试也只能解决其中一部分。测试擅长保护行为:接口应该返回什么,某种输入不能崩溃,过去的 bug 不能重新出现。很多架构约束却不天然表现成输入输出。
状态只能有一个 owner、领域层不能反向依赖 UI、某个 package 不允许直连数据库、写操作必须经过统一事务边界,这些约束如果只存在于文档或者 Agent 记忆里,就很容易在局部修复中被穿过去。
结果会出现一种很麻烦的工程状态: 测试还是绿的,代码已经越来越难解释。 更危险的是,这里面存在反馈回路。
架构开始变乱,Agent 下一次理解功能就需要读取更多文件;依赖关系越绕,working set 越大;工作集越重,系统越需要清理和压缩;设计因果保存得越薄,后面的修改又越容易依赖眼前代码和局部测试。
于是代码复杂度开始提高 Token 成本,Token 压力又反过来鼓励更短的状态保留和更局部的修补。这才是 Agent coding 里“越迭代问题越多”背后比较值得警惕的机制。
它不是一个单独的模型能力问题,而是一种 代码状态和设计状态保存精度不一致 的系统问题。
07
Agent 需要「状态保真率」
Coding Agent 已经越来越能长时间行动,但“能跑几个小时”本身未必是一个很好的能力指标。
如果一个 Agent 工作 3 小时以后,需要重新阅读自己 2 小时前改过的文件,重新推理某个抽象为什么存在,再重新跑一次此前已经跑过的测试,那么这 3 小时里有相当一部分计算其实花在了状态恢复上。
接下来的问题会变成:一个 Agent 在经历 50 步、100 步之后,还能保留多少对后续决策有价值的因果信息。
可以把它叫作 状态保真率 。
因为它衡量的不是 context 能塞多少 Token,而是经过工具调用、压缩、跨 session 和记忆检索之后,多少关键设计信息仍然以可用的形式存在。这也意味着,Agent 的长期记忆不能只靠更长的 context。
有些知识适合存在 memory 里,比如项目构建方式和开发习惯;有些决策应该进入结构化的 ADR 或代码索引;而那些一旦违反就会破坏系统的架构边界,更适合直接写进类型、测试、 lint、依赖规则和 CI。
一条规则如果已经变成软件可以执行的约束,Agent 就不需要“记住”它。下一轮 Agent 可以忘掉一段对话,却无法轻易越过编译器和测试。
如果保真率低,Agent 跑得越久,其实是在给系统埋越多的地雷。
这可能也是 Coding Agent 从“会写代码”走向“能长期维护软件”需要跨过去的一道线:把设计知识从概率性的语言记忆,逐渐迁移到可检索、可验证、可执行的软件状态里。
否则自主运行时间越长,会出现一个很荒诞的场景。Agent 写代码的速度越来越快,项目也在飞快变化,可每隔一段时间,它又要重新理解上一段时间留下来的世界。
传统祖传代码常见的一句话是:“这段别动,不知道为什么会炸。”
AI 祖传代码可能更离谱: 代码确实是 Agent 写的,只是后来的 Agent 已经不知道前面的 Agent 为什么这么写了。
参考链接: https://news.ycombinator.com/item?id=49348751
进群传送门:添加微信Qvv0909777,备注:单位/学校+姓名+方向。