☰
AI自动化验收Token消耗优化:从92次请求到18次的实战复盘
2026/10/26 14:28:19 网站建设 项目流程

1. 一次验收流程的全面失控现场

92 次请求,1340 万 token。这两个数字摆在一起的时候,我第一反应是模型出问题了,或者是哪个环节的上下文被反复塞爆了。但把wire.jsonl拉出来逐条对时间戳和请求体之后,真相有点难看:慢的不是 AI,是我自己设计的那套验收流程。

先把标题里那串数字拆开说清楚,不然读者会以为是某种神秘编码。91 = 25 + 14 + 15 + 12 + 8 + 8 + 5 + 4,这是我那次验收任务里 8 个阶段各自产生的请求数,加起来 91 次,再加上一次重试和一次收尾校验,凑成 92。也就是说,这不是一次对话,而是一条被我自己拆成 8 段的自动化验收链路,每一段都在独立调用模型,每一段都在重新喂上下文。

我做的项目本身不复杂:用 Playwright 驱动一个 Web 端的表单系统,跑一轮端到端回归,然后把失败用例的截图、DOM 快照、控制台日志交给 AI 做归因分析,最后生成一份可读的验收报告。听起来很顺,对吧?问题就出在“交给 AI 做归因”这一步——我把它做成了 8 个串行阶段,每个阶段都独立起一次会话,每个阶段都把上一阶段的产物当上下文重新塞进去。

结果就是 token 用量像滚雪球。第一段 25 次请求还算克制,因为只是跑 Playwright 拿原始数据;到中间几段,每次请求都要带上前面所有阶段的摘要、截图描述、日志片段,单次请求的输入 token 从几千涨到十几万。1340 万 token 里,真正用于“分析”的可能不到两成,剩下八成都是在重复搬运上下文。

这篇文章我想把这次踩坑完整复盘一遍:为什么会设计成这样、token 到底烧在哪、MCP 和 Playwright 在这条链路里各自扮演什么角色、wire.jsonl怎么帮我定位问题,以及我后来怎么把 92 次请求压到 20 次以内、token 砍掉七成。如果你也在做 AI 驱动的自动化验收、多 AI 协作或者 MCP 工具流,这篇应该能帮你少走一段弯路。

2. 验收链路的设计初衷与致命误区

2.1 为什么我会把验收拆成 8 个阶段

先说设计动机,不然显得我很蠢。当时的想法其实挺“工程化”的:一个完整的验收流程,逻辑上确实可以分成若干职责单一的阶段,每个阶段只干一件事,出了问题好定位。

我当时的 8 个阶段大致是这样切的:

  1. 环境预检:确认被测页面能打开、登录态有效、依赖服务在线
  2. 用例执行:Playwright 跑全部回归用例,收集原始结果
  3. 失败聚类:把失败用例按错误类型分组
  4. 截图解析:对每个失败截图做视觉描述
  5. 日志归因:结合控制台日志判断根因
  6. 交叉验证:让另一个模型复核前面的结论
  7. 报告生成:输出人类可读的验收报告
  8. 归档校验:检查报告完整性并落库

单看这个拆分,每一段都合理。职责清晰、边界明确,符合我们平时写代码的习惯。但问题在于,我把“代码模块的拆分逻辑”直接套到了“AI 会话的拆分逻辑”上,这两者的成本模型完全不一样。

代码里拆模块,模块之间通过函数调用传参,传的是引用,成本几乎为零。但 AI 会话之间传上下文,传的是实打实的 token,而且每开一个新会话,模型对前面发生的事一无所知,你必须把背景重新讲一遍。这就是第一个致命误区:把无状态的 AI 会话当成了有状态的函数调用。

2.2 串行阶段带来的上下文重复搬运

我拿wire.jsonl里的真实数据算过一笔账。假设第 2 阶段产出的原始结果摘要约 8000 token,第 3 阶段要用到它,于是第 3 阶段的每次请求都带上这 8000。第 4 阶段既要用第 2 阶段的摘要,又要用第 3 阶段的聚类结论,于是变成 8000 + 5000。到第 6 阶段交叉验证时,前面五个阶段的产物全都要塞进去,单次输入轻松突破 6 万 token。

而每个阶段又不是只请求一次。第 3 阶段聚类,我按失败用例分批处理,14 次请求;第 4 阶段截图解析,15 次请求;第 5 阶段日志归因,12 次。每一次请求都重复携带那份越来越臃肿的上下文。这就是 1340 万 token 的主要来源——不是模型话多,是我让它反复读同一份材料。

这里有个很反直觉的点:很多人以为 token 消耗主要来自“输出”,实际上在验收这类任务里,输入 token 才是大头,而且输入里绝大部分是重复内容。输出反而很省,因为归因结论通常就几百字。

2.3 一个被忽略的事实:验收的本质是状态收敛

我后来想明白一件事:验收流程的本质不是“分阶段处理”,而是“状态收敛”。原始数据(用例结果、截图、日志)是发散的、冗余的、噪声很大的,验收的目标是把它收敛成一个结论。这个过程天然适合在一个持续演进的会话里完成,而不是切成 8 段各自为战。

打个比方,这就像破案。你可以让 8 个侦探各自看一部分线索,然后互相传纸条;也可以让一个侦探从头看到尾,边看边记笔记。前者看起来分工明确,但纸条传来传去,信息损耗和重复阅读的成本极高。后者虽然只有一个“大脑”,但它对案情的记忆是连续的、增量的。

我原来的设计就是那 8 个侦探,而且还要求每个侦探在开始工作前,把前面所有侦探的笔记完整读一遍。荒唐吧?但这就是我当时的真实做法。

3. Token 到底烧在了哪里:逐段拆解

3.1 用 wire.jsonl 还原每一次请求

定位问题的关键工具是wire.jsonl。这是 MCP 客户端在通信时落盘的请求日志,每一行是一条 JSON,记录了请求时间、方法名、参数、以及最重要的——请求体的完整内容。有了它,我才能把 92 次请求逐条还原,算出每次的输入输出 token。

我的分析脚本很简单,核心就是按时间戳排序,然后统计每条请求的messages字段总长度。这里给个可复用的思路:

import json total_in, total_out = 0, 0 stage_stats = {} with open("wire.jsonl", "r", encoding="utf-8") as f: for line in f: rec = json.loads(line) stage = rec.get("stage", "unknown") in_tok = rec.get("usage", {}).get("input_tokens", 0) out_tok = rec.get("usage", {}).get("output_tokens", 0) total_in += in_tok total_out += out_tok s = stage_stats.setdefault(stage, {"count": 0, "in": 0, "out": 0}) s["count"] += 1 s["in"] += in_tok s["out"] += out_tok print(f"总输入 {total_in}, 总输出 {total_out}") for stage, s in sorted(stage_stats.items()): print(f"{stage}: {s['count']} 次, 输入 {s['in']}, 输出 {s['out']}")

跑出来的结果让我沉默了很久。总输入约 1180 万,总输出约 160 万。输入是输出的 7 倍多。而在输入里,我粗算了一下,重复上下文占了大约 65%。也就是说,光“重复搬运”这一项,就烧掉了将近 770 万 token。

3.2 各阶段 token 消耗对照表

把数据整理成表格之后,问题一目了然:

阶段请求数输入 token输出 token重复上下文占比
环境预检25约 42 万约 8 万约 20%
用例执行14约 96 万约 22 万约 35%
失败聚类15约 168 万约 26 万约 60%
截图解析12约 210 万约 30 万约 70%
日志归因8约 196 万约 28 万约 75%
交叉验证8约 224 万约 24 万约 80%
报告生成5约 140 万约 18 万约 65%
归档校验4约 104 万约 12 万约 55%

看这张表,越往后重复占比越高,因为后面的阶段要携带的前序产物越来越多。交叉验证阶段最夸张,80% 的输入都是前面已经读过的东西。这个阶段我本来是想“让另一个模型复核”,结果复核的成本比原分析还高。

3.3 环境预检为什么也要 25 次请求

这里单独说一下环境预检,因为它最容易被忽视。25 次请求听起来很多,但拆开看其实合理:检查页面可达性、检查登录态、检查 8 个依赖接口、检查数据库连接、检查测试数据准备情况……每一项都是一次独立的 MCP 工具调用。

问题不在于次数,而在于我把这些检查也做成了“AI 驱动”。其实像“页面能不能打开”“接口返回 200 还是 500”这种判断,根本不需要模型参与,一个curl或者一段 Playwright 断言就够了。我让模型去读 HTTP 状态码然后判断“是否正常”,纯属杀鸡用牛刀,还额外产生了 token 消耗。

经验:凡是能用确定性代码判断的事情,不要交给模型。模型的优势在于处理模糊、非结构化、需要推理的问题,而不是做布尔判断。这一条能帮你省掉大量无谓的 token。

4. MCP 与 Playwright 在链路中的真实角色

4.1 MCP 是什么,以及它为什么放大了问题

MCP 在这里的角色是“工具总线”。它让模型能够调用外部工具——比如让 Playwright 去点一个按钮、让文件系统去读一个日志、让数据库去查一条记录。模型不直接操作这些系统,而是通过 MCP 暴露的工具接口来发起调用。

这个设计本身很好,它把“模型推理”和“实际操作”解耦了。但在我这条链路里,MCP 的每一次工具调用都会产生一次完整的模型往返:模型决定调用哪个工具、传什么参数,MCP 执行后把结果返回给模型,模型再决定下一步。如果工具调用是串行的、每一步都要模型决策,那请求数就会爆炸。

我那次 92 次请求里,有相当一部分就是这种“模型决策—工具执行—模型再决策”的往返。比如截图解析阶段,模型要先调用工具读取截图、再调用工具做 OCR、再调用工具比对预期,每一步都是一次往返。其实这些步骤完全可以合并成一个复合工具,一次调用返回全部结果。

4.2 Playwright 测试用例的产出如何变成 token 负担

Playwright 跑完一轮回归,产出的原始数据量是惊人的。每个失败用例至少包含:错误堆栈、失败截图(base64 后动辄几百 KB)、DOM 快照(完整 HTML 可能上万行)、控制台日志、网络请求记录。这些如果原样塞给模型,token 直接起飞。

我犯的错是“先收集、后处理”——把所有原始数据一股脑收集起来,然后在后续阶段慢慢喂给模型。正确做法应该是“边收集、边压缩、边丢弃”。比如截图,根本不需要把 base64 传给模型,只需要传一个结构化的视觉描述;DOM 快照也不需要全量,只需要失败节点附近的子树。

这里有个具体的压缩策略,我后来实测有效:

  • 截图:先用本地视觉模型或 OCR 提取关键文字和布局描述,只把描述文本传给主模型,base64 留在本地
  • DOM:用选择器定位失败元素,只截取该元素上下三层的子树,其余丢弃
  • 日志:按错误级别过滤,只保留 error 和 warn,info 级别全部丢弃
  • 网络:只保留非 2xx 的请求和耗时超过阈值的请求

这一套下来,单个失败用例的上下文从平均 1.2 万 token 压到 2000 以内。

4.3 多 AI 协作的边界在哪里

我原本设想的是“多 AI 协作”:一个模型跑用例、一个模型做归因、一个模型做复核。听起来很美好,但实际执行下来,协作的边界没划清楚,导致每个模型都在重复理解同一份背景。

多 AI 协作真正有效的场景,是任务之间上下文重叠度低的时候。比如一个模型专门处理视觉、一个专门处理文本、一个专门处理代码,它们各自需要的输入几乎不重叠,那并行协作就很划算。但如果三个模型都要先读懂同一份验收背景,那协作的通信成本会吃掉所有收益。

我后来把“多 AI 协作”改成了“单主模型 + 专用小工具”的结构:主模型负责推理和决策,视觉描述、日志过滤、DOM 裁剪这些交给确定性的本地工具或小模型处理,处理完只把精简结果回传给主模型。这样既保留了分工,又避免了上下文重复。

5. 把 92 次请求压到 20 次以内的重构实录

5.1 核心思路:从串行阶段到单一演进会话

重构的核心只有一句话:把 8 个独立会话合并成 1 个持续演进的会话,把阶段之间的“传递”改成会话内部的“增量追加”。

原来的模式是:阶段 A 产出结果 → 序列化 → 作为阶段 B 的初始上下文 → 阶段 B 产出 → 序列化 → 作为阶段 C 的初始上下文……每一跳都要重新序列化和反序列化,而且每一跳都携带全部历史。

新模式下,只有一个会话,所有中间产物都以“追加”的方式进入同一个上下文。模型始终记得前面发生了什么,不需要重新讲背景。阶段之间的边界不再靠“新开会话”来划分,而是靠“在会话里插入一个明确的阶段标记”来划分。

这个改动带来的直接效果是:上下文只增长一次,而不是在每个阶段重新加载一次。原来 8 个阶段各自加载一遍历史,现在只加载一遍。

5.2 上下文压缩的具体参数与计算

光合并会话还不够,上下文还是会随任务推进而膨胀。所以第二步是引入压缩策略。我的做法是给会话设一个“上下文预算”,比如 6 万 token,超过就触发压缩。

压缩不是简单截断,而是分层处理:

  • 最近 3 轮对话:完整保留,一字不删
  • 3 到 10 轮之前:保留结论和关键数据,删掉推理过程
  • 10 轮之前:只保留一句话摘要

这个分层策略的依据是:模型对近期上下文的依赖最强,越久远的信息越应该被抽象成结论。我实测下来,6 万预算配合这个分层,能覆盖大约 15 到 20 轮的有效交互,足够完成一轮完整验收。

具体计算一下:假设每轮平均输入 3000 token、输出 500 token,20 轮下来原始累积是 7 万 token。经过分层压缩后,实际占用约 4.5 万,留出 1.5 万给突发的大块数据(比如某张截图描述特别长)。这个余量是必要的,不然遇到复杂用例会频繁触发压缩,反而增加开销。

5.3 重构后的请求数与 token 对照

重构后我重跑了一遍同样的验收任务,数据对比很明显:

指标重构前重构后变化
总请求数9218下降 80%
总输入 token约 1180 万约 310 万下降 74%
总输出 token约 160 万约 95 万下降 41%
总 token约 1340 万约 405 万下降 70%
端到端耗时约 47 分钟约 12 分钟下降 74%

请求数从 92 降到 18,主要省掉的是那些“为了重新讲背景”而多出来的往返。token 降七成,主要省掉的是重复上下文。耗时降七成多,是因为请求少了、每次请求的输入短了,模型响应自然快。

注意:这里的 18 次不是硬性目标,而是这次任务的自然结果。任务复杂度不同,次数会变。关键不是追求某个具体数字,而是消除“重复搬运”这个结构性浪费。

6. 常见问题与排查技巧实录

6.1 token 用量突然飙升怎么排查

第一步永远是看wire.jsonl,不要凭感觉猜。按时间排序,找出单次输入 token 最大的那几条请求,看它们的messages里到底装了什么。十有八九是某个大块数据(截图 base64、完整 DOM、全量日志)被反复塞进去了。

第二步是看请求之间的相似度。如果连续多条请求的输入高度相似,只是尾部多了几行,那说明你在做“增量追加但没做压缩”,历史在无意义地累积。

第三步是看输出。如果输出 token 也异常高,那可能是模型在“复述”输入内容,比如让它总结时它把原文抄了一遍。这种情况要在提示词里明确要求“只输出结论,不要复述”。

6.2 MCP 工具调用超时或返回异常

MCP 工具调用失败时,wire.jsonl里会记录错误响应。常见的几类:

现象可能原因处理方式
工具调用超时目标操作耗时超过 MCP 超时阈值拆分操作,或调大超时配置
返回空结果工具参数不对,或目标资源不存在检查参数序列化,确认资源路径
返回内容过大工具一次性返回了海量数据在工具侧做分页或过滤
调用被拒绝权限或工具白名单问题检查 MCP 服务端的工具注册配置

我遇到最多的是“返回内容过大”。比如让 MCP 读一个日志文件,它把整个文件读进来返回,几万行直接爆上下文。后来我在工具侧加了max_lines参数,默认只返回最后 200 行,需要更多再显式指定。

6.3 Playwright 用例失败但 AI 归因不准

这个问题的根源往往不是模型不行,而是喂给它的信息不对。我总结了几条:

  • 截图要给“失败瞬间”的,不要给“最终状态”的。很多失败是中间步骤出错导致的,最终截图看起来正常,模型自然归因不出来。
  • 日志要带时间戳,并且和截图时间对齐。模型需要知道“这个报错发生在截图之前还是之后”。
  • DOM 快照要标注“哪个元素是操作目标”。不然模型面对上万行 HTML,根本不知道你在点哪个按钮。
  • 网络请求要标出“哪个是主请求”。一个页面可能发几十个请求,模型需要知道哪个失败是致命的。

把这些信息结构化之后,归因准确率从原来的六成左右提到了九成以上。

6.4 独家避坑清单

最后整理一份我踩过的坑,按严重程度排序:

  1. 不要为每个阶段新开会话。这是最大的坑,直接导致上下文重复搬运。能在一个会话里演进,就不要拆。
  2. 不要把二进制数据传给模型。截图、PDF、音视频,一律先在本地转成结构化描述。
  3. 不要让模型做布尔判断。能用代码判断的,不要用模型。
  4. 不要收集完再处理。边收集边压缩边丢弃,原始数据留在本地。
  5. 不要迷信多 AI 协作。上下文重叠度高的时候,协作是负收益。
  6. 一定要落盘 wire.jsonl。没有日志,你永远不知道 token 烧在哪。
  7. 给上下文设预算。没有预算,上下文会无限膨胀直到撞上模型上限。

我个人在实际操作中的体会是,AI 驱动的自动化验收,难点从来不在模型能力,而在流程设计。模型再强,你让它反复读同一份材料,它也只能又慢又贵。把流程从“多阶段串行”改成“单会话演进”,把“全量传递”改成“增量压缩”,效果立竿见影。这套思路不只适用于验收,任何需要多步骤、多工具、长上下文的 AI 任务,都可以照着改。

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

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

立即咨询