news 2026/9/9 18:21:39

AI Agent 测试死循环烧掉一半配额?日志逆向工程与熔断机制复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI Agent 测试死循环烧掉一半配额?日志逆向工程与熔断机制复盘

上个月我把 Claude Code 接进日常工作流,帮我在一个 Python 服务端项目里改代码、补测试、修 CI 问题。前一周用得很顺,写 CRUD 接口、整理类型注解、修单元测试都比我预期快,结果到周末打开用量面板一看,人直接愣住了:一周时间烧掉了整个月度配额的一半。按正常节奏算,这一周应该只占 10% 到 15% 才对。

我一开始怀疑是并行开的会话太多,或者是哪个后台进程没杀干净。后来把本地日志翻出来做了几天“逆向工程”,才找到真正的元凶:Agent 在一个测试驱动的修复流程里反复执行同一个 pytest 命令,陷入了一场极其低效的自杀式循环。表面看它很努力,一直在改代码、跑测试、看结果,实际上每一次循环都在做无用功,还让上下文窗口越撑越大,单次请求成本水涨船高。

这篇文章我会完整复盘这次配额事故:怎么通过日志和工具调用记录还原 Agent 的真实行为,问题到底出在哪,以及后来我加了哪些“刹车”机制,把同样工作量的配额消耗从 50% 压回了 15% 左右。如果你也在用 Claude Code 或类似的 AI Agent 做开发,这些排查思路和防护手段可以直接抄作业。

1. 事故现场:一周烧掉一半配额,我的第一反应是算错了

1.1 我以为的正常使用节奏

先交代项目背景。这是一个中等体量的 FastAPI 服务,带 pytest 测试,单体仓库,大概 60 多个测试文件。我平时让 Claude Code 干的事情主要分三类:实现新 API endpoint、修复失败的单测、给模块补类型注解和 docstring。

在事故那一周,我并没有开什么大规模重构任务,也没有让它处理全仓库级的自动化改造。每天大概 30 到 50 次以内的对话交互,任务粒度都很小,按道理配额消耗是可控的。我给自己定的心理预期是:一周消耗月度整体配额的 10% 到 15%,这样一个月撑下来还有不少余量。

结果到了周四晚上,我随手打开用量面板瞥了一眼,数字停在差不多 45% 的位置。当时我还安慰自己,可能这周任务确实比平时多,周末应该会缓下来。结果周五还没过完,消耗已经突破了 50%。这个数字让我没法淡定了,因为我粗略一算,如果不做干预,月底之前额度肯定提前见底。

1.2 异常出现的具体症状

第一反应是“会不会记错时间窗口了”。我仔细核对了用量面板里的时间维度,确认这一波消耗就是从周一开始的,跟我的实际使用时间线完全对得上,没有历史包袱。

第二个怀疑对象是并行子任务太多。Claude Code 支持分多个会话同时干活,我偶尔会开两三个会话分别处理不同模块。我把每个会话的活跃时长和工具调用频率翻出来看了一遍,并没有发现某个会话特别夸张,排除。

第三个排查方向是我最不愿意承认的:会不会某个会话从白天一路挂到了深夜,后台还在跑任务。因为我确实点过几次Ctrl+C,但不确定是不是所有子进程都被干净地杀掉。这个怀疑让我花了不少时间在进程管理上,最后也没有实质性发现。

三个怀疑全部落空后,我意识到不能靠猜了,得看底层数据。我决定对本地日志做一次彻底的逆向分析——不是传统意义上的反编译,而是通过 Agent 留下的每一次工具调用轨迹,重构它这一周到底做了什么、按什么顺序做、每件事花了多少成本。这个思路后来救了我,因为日志挖出的真相和我最初的猜测完全不一样。

2. 逆向工程:从日志轨迹还原 Agent 的行为路径

2.1 第一步:翻日志,确认信息源

Claude Code 会把每次会话的完整记录写成本地 JSONL 文件。不同版本的存放路径略有差异,就我用的这个版本来说,会话数据放在~/.claude/projects/<项目名>/目录下,每个会话对应一个.jsonl文件。老版本可能在~/.claude/logs/下面,如果你找不到,把两个路径都扫一遍就行。

每行 JSONL 记录一个事件,包括用户输入、Agent 的回复、工具调用、工具执行结果、API 元信息等。虽然没有直接的“本次请求成本”字段,但根据模型名称、输入输出 token 数量(部分事件会带 usage 数据)和事件类型,可以很精确地还原出每一次 API 调用的规模。

我写了个 Python 脚本,把事件全部读进来先看概览:

import json from pathlib import Path from collections import Counter def load_events(root): events = [] for f in Path(root).rglob("*.jsonl"): with f.open(encoding="utf-8") as fp: for line in fp: line = line.strip() if not line: continue try: events.append(json.loads(line)) except json.JSONDecodeError: continue return events if __name__ == "__main__": events = load_events(Path.home() / ".claude" / "projects") print("total events:", len(events)) types = Counter(e.get("type", "unknown") for e in events) for t, c in types.most_common(15): print(t, c)

跑完之后第一眼就很刺眼:Bash 工具调用非常密集,而且集中在某两天的晚上时段。光看数量就比 Read 和 Edit 多了好几倍,这在我们这种常规开发任务里是不正常的——改代码的频率不应该比跑命令低那么多。

2.2 第二步:还原请求序列,拼出执行节奏

事件统计只能说明“命令多”,还不能说明“命令在同一个坑里反复跑”。我接着把 Bash 工具相关的事件拆出来,按时间排序,看每一天的工具调用是怎么串起来的。

bash_cmds = [] for e in events: if e.get("type") == "assistant": content = e.get("message", {}).get("content", []) for block in content: if isinstance(block, dict) and block.get("type") == "tool_use": name = block.get("name", "") if name == "Bash": inp = block.get("input", {}) cmd = inp.get("command", "") bash_cmds.append((e.get("timestamp", ""), cmd)) # 按时间排序,打印前50条 for ts, cmd in sorted(bash_cmds)[:50]: print(ts, cmd[:120])

看到输出之后,我整个人是愣住的。在周二晚上 20:14 到 21:02 这 48 分钟里,Bash 命令出现了 22 次,其中有 18 次是同一行命令:

python -m pytest tests/test_order_flow.py::test_create_order_with_coupon -x -s

18 次重复跑同一个测试用例,中间穿插的 Read 和 Edit 操作全部集中在app/services/order.pyapp/api/order_api.py这两个文件上。也就是说,Agent 在那 48 分钟里只做了一件事:改一段代码 → 跑同一个测试 → 失败 → 再改 → 再跑,循环了接近二十轮。

这是我第一次真正意识到,Agent 的行为轨迹可以像视频录像一样被完整还原。只要日志里保留了工具调用和时间戳,完全可以把一段会话拆成逐帧画面来看。而“逐帧”看下来,你就能看穿它每个决策到底是在朝正确方向走,还是在原地空转。

2.3 第三步:统计模式,确认成本重心

为了把“成本重心”坐实,我又统计了两组数据。

第一组是工具调用类型的消耗占比。我按事件里能拿到的 token 估计值粗略算了一下——有 usage 字段的直接用,没有的就按输入输出文本长度估算。结果 Bash 工具这一项占所有工具调用消耗的 60% 左右;其中 pytest 相关命令又占了 Bash 消耗的 70% 以上。

第二组是重复命令的出现次数。同样的测试命令在同一天出现超过 5 次的比例非常高,覆盖了周二和周四两个晚上。这说明不是偶发事件,而是 Agent 多次陷入同一种循环。

到这里,谜底已经很清晰:我的配额不是被“正常使用”烧掉的,而是被一个隐藏在测试驱动流程里的死循环吃掉的。Agent 确实在做它被委派的工作——修复测试、保证通过——但它完全没有意识到,每次重试的投入和产出已经严重失衡。它每次都以为“这次改动应该能让测试过了”,结果每次都当面撞墙。

3. 真凶落网:Agent 在测试循环里的自杀式反复

3.1 一个可以复现的死循环案例

我把周二那段会话单独还原出来,理清了整个循环的触发链条。

当天我给 Agent 的任务是:“修复test_order_flow.py里失败的测试,确保 order 创建流程相关用例全部通过。”Agent 的第一反应是读测试文件、读相关业务代码,然后跑一遍测试收集失败信息。

命令是:

python -m pytest tests/test_order_flow.py::test_create_order_with_coupon -x -s

第一次运行直接抛OperationalError: no such column: coupon.code。这是一个典型的数据库迁移问题——测试库还是旧 schema,新加的列没有迁移进去。正常经历过这种坑的开发者,看到这个报错的第一反应是检查迁移脚本和测试夹具,而不是去改业务代码。

但 Agent 没有这个“条件反射”。它的推理链条把“测试失败”直接映射成了“业务代码有问题”。于是它开始读order.py里创建订单的逻辑,试图找出为什么coupon.code不存在。第一次 Edit 之后重新跑测试,报错没变;第二次它换方向去读 SQLAlchemy model 定义,怀疑是模型里没有声明这个字段,于是改 model;但数据库迁移层面的问题,改 model 声明也解决不了;第三次它甚至翻了 fixture 文件,想往测试夹具里补数据,但测试数据库本身没有跑过新迁移,补 fixture 同样无效。

整个过程里,它始终没有做两个我最期待的动作:第一,查一下迁移状态;第二,把报错信息当成“环境问题”来处理。它更不会想到,这个测试在本地本来就跑不过,因为它依赖前置的 schema 迁移步骤,而迁移只在 CI 里自动执行。

3.2 致命盲区到底在哪里

这个案例背后暴露了三个叠加的盲区,这才是“致命盲区”的真正含义。

盲区一:Agent 无法区分测试失败的类型。测试失败至少有三类:业务代码 bug 导致的失败、测试环境/基建问题导致的失败(比如数据库没迁移、端口被占、mock 没配好)、测试本身写得不稳定或断言不合理的 flaky。有经验的开发者靠直觉能在前几秒内做初步分类,但 Agent 的默认策略基本是“失败 = 我要改代码”,分类能力非常弱。

盲区二:Agent 没有熔断机制。一个正常开发者在连续三次看到同样的报错后,一定会停下来重新审视前提条件——是不是环境问题?是不是这个命令在本地本来就跑不过?但 Agent 的默认策略是只要任务没完成就继续尝试,直到上下文窗口接近上限或配额耗尽。它不会“痛苦”,所以也不会“停下来想想”。

盲区三:Agent 对“验证成本”没有感知。每次跑测试,工具输出会整个塞进上下文。pytest 失败带堆栈可能要几百到几千 token,Agent 读完这些堆栈再思考再改代码,又是一轮不小的消耗。更关键的是,这个消耗有强烈的正反馈效应:上下文越长,后面每一轮的输入 token 就越多,单次请求成本就越高,越到后面越贵。

3.3 成本的正反馈:为什么看似小循环会吃掉大配额

我专门按 token 量级做了个粗略估算表:

动作单次估算 token 消耗说明
运行一次 pytest(含堆栈输出)800 ~ 2500取决于失败堆栈长度和 -s 输出量
读取一个待修改文件300 ~ 800取决于文件长度,按实际读取内容计算
一次代码编辑与后续回复1500 ~ 3000包含 diff、推理过程、总结
上下文累积带来的额外成本逐轮递增前面所有对话历史都会进入后续请求

按 18 次循环来算,光 pytest 命令和代码修改的直接消耗就在 5 万 token 以上。真正可怕的不是单次消耗,而是上下文累积:到第 18 轮时,模型要“读完”前 17 轮的所有历史,输入 token 已经爆炸式增长。换算到实际配额,这一晚上就相当于吃掉了整个月度配额的几个百分点。

这还没算模型思考过程产生的推理 token。Claude Code 在后台的 reasoning 也会计入消耗,在它停下来之前,每一刻都在烧钱。

4. 对症下药:给 Agent 装上刹车片

4.1 任务设计层面的约束

搞清楚事故原因之后,我先从最便宜的地方入手:改任务设计。之前我下命令的习惯太“老板”了,上来就给开放式目标,比如“把测试全部跑通”“把这个模块的错误处理补全”。这种任务对 Agent 来说自由度太高,它很容易在验证环节原地打转。

现在我改成更收紧的指令模板。核心原则是:明确范围、明确验证方式、明确终止条件。

请先运行 python -m pytest tests/test_order_flow.py --collect-only 确认能收集到用例。 然后只运行该文件,不要运行整个测试套件。 如果报错中包含 OperationalError / ConnectionRefused / EnvironmentError 等环境类关键字,请停止修改业务代码,在回复里列出环境问题排查清单。 同一个测试命令如果连续执行 3 次仍然失败,请停止,报告当前状态和后续建议。 只允许修改 app/services/ 和 app/api/ 下的文件,不要动 tests/ 目录。

听起来啰嗦,但实测下来效果立竿见影。Agent 不再把每一次失败都当成“继续改代码”的信号,而是会在第 3 次失败后主动停下来,把问题交还给我。代价是它自动修复那些简单 bug 的成功率确实降了一点——因为有些问题确实需要多试几次才能定位——但整体配额消耗下降了一个数量级,这笔账非常划算。

4.2 Claude Code 配置层的防线

任务指令只是个软约束,更可靠的是把规则写进工程配置,让 Agent 每次都自动加载。

Claude Code 支持通过项目根目录的CLAUDE.md给 Agent 附加“人设”和规则。我在这里写了专门的测试行为规范:

## 测试执行规范 - 运行测试前,先执行 collect-only 确认测试可收集;如果 collect 阶段报错,说明是测试基建问题,禁止修改业务代码。 - 连续运行同一测试命令最多 3 次;第 3 次失败后必须停止,报告失败模式和可能的环境原因。 - 失败信息中出现 OperationalError、ConnectionError、TimeoutError、PermissionError 等关键词时,优先怀疑环境与配置,而不是业务逻辑。 - 所有测试运行命令建议自动追加 `--timeout=30`,防止用例挂起导致长时间无输出。 - 本地开发调试只跑被修改模块的关联测试,禁止默认全量执行 pytest。

这份文件在会话开始时会被自动加载,相当于给 Agent 装了一本“操作守则”。实测下来,这些规则能有效干预默认行为模式。尤其是连续失败 3 次的熔断规则,直接把之前那个 18 次循环的案例压到了 3 次以内。

如果你用的是较新的 Claude Code 版本,还可以尝试配置 pre-tool-use hooks,在工具调用前检查即将执行的命令是否触发了“重复命令熔断”条件,命中就直接拒绝。具体语法每个版本略有差异,这里不展开,但思路是可行的:在工具调用这一层做硬拦截,比靠模型自觉可靠得多。这是硬刹车片,规则是软刹车片,两个配合起来才安全。

4.3 测试基建的可诊断性改造

除了管住 Agent,我还把测试工程本身做得更“抗瞎搞”。

第一件事:统一测试夹具的自包含性。以前不少 fixture 依赖真实数据库连接,经常出现“本地先跑迁移才能跑测试”的隐性前提。我把这些依赖尽量重塑为自包含的:用tmp_pathmonkeypatch隔离文件系统与外部服务,数据库依赖换成内存 SQLite 或测试容器。改完之后,本地测试对运行环境的假设大幅减少,报错信息也更贴近真实代码问题。

第二件事:给所有用例加超时。我们用pytest-timeout给整个测试套件设置了默认 30 秒超时。这个改动对 Agent 特别友好:即使某个用例真的挂起,也不会出现完全无输出的“死等”状态,失败信息里至少会带Timeout字样,而Timeout是规则里明确要求它优先怀疑环境问题的关键词。

第三件事:给用例分层次打标签。我把依赖网络、数据库等外部资源的用例统一标记为@pytest.mark.integration,并配置成默认不执行。Agent 日常开发只跑单元测试,集成测试留给 CI:

# pyproject.toml 的关键配置 [tool.pytest.ini_options] markers = [ "integration: depends on external resources", ] addopts = "-m 'not integration' --timeout=30"

打完标签之后,Agent 在本地跑测试几乎不会碰到需要真实外部服务的用例,环境类失败率大幅下降。之前那种“疯狂重试同一条命令”的场景,触发条件少了一大半。

5. 验证与复盘:配额从 50% 降回 15%

5.1 同任务下的对照组

为了确认这些改动不是心理安慰,我专门做了个不严谨但足够说明问题的对照。事故后第二周,我用同样类型的任务(修接口、补测试、处理代码审查意见)干了几乎等量的活,对比结果如下:

维度事故周修复后一周
周配额消耗约 50%约 15%
其中测试相关消耗占比约 30%约 5%
单个会话最长工具调用次数40+12
同命令重复执行超 3 次的事件5 次0 次

最夸张的改善来自重复循环那一项。不是说 Agent 突然变聪明了,而是我给了它“什么时候该停下来”的边界条件。以前它靠本能试错,现在它靠规则工作,行为轨迹完全不同。

5.2 从这次事故中总结的三条规律

第一,Agent 的效能瓶颈不在写代码,而在验证环节的收敛速度。它改代码的能力很强,但“确认这次改动是否有效”的过程如果不可控,成本就会失控。

第二,“测试失败”对 Agent 来说是一个非常模糊的信号,它默认的归因方式就是“代码有问题”,这是最需要人工干预的地方。任何能帮它区分“代码问题”和“环境问题”的手段,都在同时帮你省时间和省配额。

第三,逆向追踪日志应该成为使用 Agent 的日常习惯。不用每次都做精细分析,但至少每周扫一眼工具调用统计,重点关注重复命令和 Bash 调用占比。成本异常一定会在日志里留下清晰的模式,关键是你要去看。

5.3 长期维护建议

最后分享几个我目前仍在沿用的习惯。

每周五花十分钟跑一遍日志统计脚本,看看有没有新的重复命令热点。给 CLAUDE.md 里加的任何新规则,都先跑一个真实任务验证效果。凡是涉及外部依赖的用例,一律打 integration 标签,绝不混入默认测试集。

另外,我也养成了一个“手动熔断”的习惯:如果远程观察到一个会话在同一命令上反复执行,我会直接结束会话,换一个新会话接着干。新会话上下文干净,解决同样问题的成本往往比在旧会话里硬扛低很多。这个技巧听着朴素,但省钱效果极好。

那天在用量面板看到 50% 数字的时候,我第一反应是工具坏了或者我操作失误。现在回头看,其实问题一直摆在日志里,差的就是一个“慢下来看录像”的动作。以后谁再跟我抱怨 Agent 吃配额,我都会先问一句:你翻过它的工具调用记录了吗?

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 18:20:04

forEach的隐藏陷阱:异步、中断与this指向全解析

1. 先搞清楚:forEach到底哪里会"坑"用了一年多JavaScript,我一直觉得forEach是数组方法里最老实巴交的那个:没有奇技淫巧,参数固定,行为清晰,几乎不会写出让人眼前一黑的代码。直到有一次我在一个数据清洗项目里,用forEach处理一组需要异步获取详情的用户列表,页面渲…

作者头像 李华
网站建设 2026/9/9 18:20:01

Playwright定位器完全攻略:从基础到动态元素

1. 定位思路&#xff1a;先搞懂Playwright的定位器哲学1.1 为什么传统CSS/XPath定位在Playwright里不够用如果你之前是Selenium的老用户&#xff0c;刚转到Playwright时最不习惯的一件事就是&#xff1a;怎么感觉它的定位方式跟以前不太一样&#xff1f;在Selenium里我们习惯直…

作者头像 李华
网站建设 2026/9/9 18:19:13

半桥LLC并联均流实战:硬件均流+PI控制+PFM调制的完整方案

半桥LLC做并联并机&#xff0c;最怕的不是环路调不好&#xff0c;而是两个模块的参数天生就不一样。同一型号、同一批次的谐振电感和谐振电容&#xff0c;容差叠加起来谐振频率能差好几个千赫兹。这个时候你把两个模块直接并联&#xff0c;不加任何均流措施&#xff0c;电流就往…

作者头像 李华
网站建设 2026/9/9 18:16:56

box-shadow不生效的完整排查指南:从overflow裁切到层叠上下文

让人头大的box-shadow失效&#xff1a;真正的问题根本不只在阴影本身如果你在前端写过几天样式&#xff0c;大概率碰到过这种情况&#xff1a;box-shadow属性明明写进了 CSS 文件&#xff0c;类名对得上&#xff0c;DevTools 里也显示这条声明生效了&#xff0c;但页面上就是一…

作者头像 李华
网站建设 2026/9/9 18:14:43

虹软ArcFace动态人脸识别画框实战:Camera2坐标映射全解析

简介&#xff1a;基于虹软ArcFace的人脸识别工程解决方案&#xff0c;面向需要在Android等移动端实现摄像头动态人脸检测、追踪与画框识别的开发者&#xff0c;覆盖从视频流采集、人脸定位到特征匹配的完整链路。压缩包共137个文件、约65.77MB&#xff0c;核心文件包括12个so动…

作者头像 李华
网站建设 2026/9/9 18:14:26

Flutter插件移植OpenHarmony实战:以feedback为例打通MethodChannel与真机调试

做跨端开发的人这两年应该都意识到一件事&#xff1a;Flutter 在 OpenHarmony 上已经不是能不能跑的问题&#xff0c;而是业务插件能不能迁的问题。大家都会跑 Hello World&#xff0c;但一接 feedback、地图、推送这类强原生依赖的插件&#xff0c;直接卡住。feedback 这种插件…

作者头像 李华