☰
AI应用验收烧掉1340万Token?四层验收法把成本降到五分之一
2026/10/10 4:45:15 网站建设 项目流程

先说结论:上个月我验收一个 AI 功能,前前后后发了 92 次请求,烧掉 1340 万 token。这要是在普通对话场景,连想都不敢想;可放到一条 Agent/RAG 链路上,几轮迭代就能把预算吃光。真正让我警觉的是,把同样的输入单独发给模型,二十秒内就能得到合格结果——慢的不是 AI,是我的验收流程。

这篇复盘想把整套经历拆开:那 92 次请求究竟消耗在哪些环节,为什么一个“91 = 25+14+15+12+8+8+5+4”的拆解能说明问题,以及后来我如何把验收改成四层结构,把单轮成本压到五分之一。如果你也在做 AI 应用验收、Agent 调试、批量 prompt 评测,或者只是被 token 账单吓到过,这篇内容应该能帮你少走不少弯路。

1. 这笔账先算清楚:1340万token到底烧在哪

1.1 一个“请求”不等于一次LLM调用

聊 token 成本前,得先纠正一个概念:在 Agent 或 RAG 场景里,用户界面上看到的一次请求,背后往往是很多次 LLM 调用和工具调用的叠加。

我这次验收的对象是一个多层链路:主 Agent 收到问题后,先做意图识别,再触发检索,然后读取文档片段,可能还要调用一个子任务模型去汇总,最后主模型统一回复。表面上我发起了 1 次请求,实际拆开看是这样的:

环节说明大致消耗
主 Agent 调用解析用户意图、规划步骤输入 5000 token
检索工具调用 embedding / 检索 API不计入 LLM token
相关文档片段召回 Top-K 片段填入上下文输入 40000~80000 token
子任务模型对多段内容做一次中间总结输入输出各 5000 token
总结输出主模型生成最终答案输出 3000~5000 token

一次“看起来普通”的请求,实际消耗 5 万到 10 万 token 是很常见的事。92 次请求听起来不多,但如果其中一大半都是这类长上下文调用,1340 万这个数字就一点也不夸张了。

1.2 从“91 = 25+14+15+12+8+8+5+4”拆出优化曲线

标题里那个等式,是我后来翻请求日志时统计出来的:整个验收过程一共 9 轮,前 8 轮都没通过,最后一轮才真正跑通。每一轮的数字,代表这一轮里我为了验证某个改动而重新发起的请求次数,加在一起正好是 91,最后成功那次凑成 92。

看这串数字,最有意思的是递减趋势:25 → 14 → 15 → 12 → 8 → 8 → 5 → 4。第一轮我改了一个检索参数,然后条件反射式地把 20 个测试用例全量跑了一遍,中间还夹杂各种失败重试,直接发出 25 次请求。到了第二轮,我开始做缓存和范围裁剪,数字降到 14。虽然中间有反复,但越往后,每一轮验证所需的请求次数越少,最后一轮只需要 4 次定点验证就确认修复完成。

这不只是一组数字,更是一张流程优化的成绩单:请求次数下降,token 消耗自然跟着降。从第一轮的几十万 token 一次回归,到最后一轮四五个请求,才真正把成本打到可控区间。

1.3 1340万token的构成:大头不是“生成”,是“重复输入”

我后来把账单按输入和输出拆开看,发现一个反直觉的事实:真正烧钱的大头不是模型生成的那部分,而是每次请求都要携带的输入上下文。

算一笔账:假设平均每次请求的输入是 12 万 token、输出是 4000 token。92 次请求里,输入累计约 1100 万 token,输出只有 300 多万 token。也就是说,你花大价钱把同一批文档、同一段系统提示词、同一条对话历史反复送进模型,然后只换来一小段回答。

用一个生活类比:就像你每次去复印店,不是只复印一页新文件,而是把整个书架的书都搬过去复印一遍,最后只取其中一段。你不心疼才怪。更扎心的是,由于我的验收脚本每次都全新拼接上下文、每个用例独立开 session,根本没有复用,相同的 12 万 token 在同一天里被重复计算了几十次。

2. 慢的不是AI,是验收流程断在半路

2.1 没有冒烟测试,全量回归被当成了“调试工具”

第一轮我能发出 25 次请求,核心原因就是全量回归和调试混在了一起。发现一个 bad case,第一反应不是先用最小复现去定位,而是“我把整个用例集跑一遍看看”。结果就是:全量跑完只能告诉你哪些用例挂了,却完全无法告诉你为什么挂。

这就好比你修水管,每次出现问题不是先判断漏水点在哪片区域,而是直接停掉整栋楼的水,再一家家敲门排查。正确的做法应该是先关小阀门确认影响范围,用冒烟用例验证大方向,再用最小复现去定位细节问题,修复后再决定要不要全量回归。

后来我给自己定了一条死规矩:任何改动,必须先过 3 到 5 条核心用例的冒烟测试。冒烟没过,绝不允许点全量执行的按钮。

2.2 没有缓存,也没有增量计算,同一个语义反复付费

第二部分浪费来自“重复”。同一个测试批次,我跑第一遍和第二遍时,输入内容几乎没变,但因为脚本没有做任何缓存,每跑一遍都是全量新计算。

这个场景其实有两个可以优化的点。第一,如果你的 API 支持上下文缓存,系统提示词和常用文档片段应该放在最前面,并且尽量保持内容稳定;如果每次都在代码里动态拼接、前缀每次都不一样,缓存基本命中不了。第二,在自己的工程层做结果缓存:给定相同输入,如果之前已经得到结果且用例没变,直接读缓存,不重新请求。这样失败用例可以单独重放,没必要把整个批次再跑一遍。

我没做缓存的那几天,一个用例反复跑 5 遍是常有的事。加了缓存以后,每轮验收至少省掉了 30% 到 40% 的重复输入。

2.3 用“重跑整条链路”代替“定点复查”,局部问题被放大成全局问题

第三个断点是我的习惯问题:一改配置,就想“全跑一遍看看”。但仔细想,很多改动的影响面其实很小。比如你只改了总结 prompt 的措辞,影响的是输出风格,没有必要把检索、文档加载、工具调用全部重跑一遍。可我当时没有做影响面分析,每次都用一条完整长链路去验证一个小改动。

后来我学会先写一份影响面清单,列出这次改动可能触发的用例编号和模块,只对有影响的场景做验证。这个习惯让我的单轮请求数从 20 多次直接降到个位数。

2.4 验收和调试混在一起,没有独立基线,结果永远无法归因

最后一个流程问题,是我没有一份“验收基线”。所谓基线,就是改动之前,当前代码的通过率、关键指标和 token 消耗是多少;改动之后,把它和基线对比,才能判断改动到底是变好还是变坏。

但我当时是边调试边改 prompt、边加用例,结果所有数字混在一起,根本说不清哪些消耗来自验证本次改动,哪些来自我随手加的新用例。这也是后来我建立分层验收体系时,第一件事就是把“基线”这个概念加进去的原因:没有基线,所有计算都是糊涂账。

3. 照这套方法搭验收,单轮成本能压到五分之一

3.1 四层验收模型:冒烟、功能、回归、压测分开跑

我最后落地的方案,是把验收拆成四层,每一层的目的不同、规模不同、触发时机也不同。

层级目的规模触发时机建议 token 预算占比
冒烟层确认主链路是否通畅3~5 个核心用例每次改动后必跑5%
功能层覆盖主要功能分支P0 + P1,约 15~20 个用例改动影响面评估后30%
回归层全量验证质量基线P0 + P1 + P2 全量改动稳定后、提交前60%
压测/长链路验证极端场景与稳定性自定义压力集每次发布以前,低频执行5%

以前我的毛病是每次都直接跑到第三层,等于每次改动都在做全量回归。现在冒烟不过,根本不会进入功能层;功能层没有结论,也不会启动回归层。

3.2 用例分级:P0是核心链路,P1是主要分支,P2是边界异常

分层能不能生效,取决于用例怎么分级。我现在的打标逻辑很简单:

  • P0:主流程最关键的核心链路,比如“用户提问后能否得到正确回答且不报错”,通常只放 3 到 5 个用例。
  • P1:主要的功能分支,比如“当检索结果为空时,模型能否正确提示”,属于高频路径但非核心。
  • P2:异常、边界、极端输入,比如超长文本、空输入、格式错误。

每次改动前,我会用 5 分钟做“影响面评估”:这次改的是检索参数,那就圈出所有依赖检索结果的 P0/P1 用例;改的是总结 prompt,就只圈出长文档汇总类用例。范围裁剪以后,大多数改动根本不需要跑全量回归。

3.3 启动验收前强制“三项检查”

我现在把验收启动前要做的事固定成了三件:

第一个检查:有没有可复用的结果。同一个用例之前跑过且输入没变,直接读缓存;失败用例单独重放,而不是全量重跑。第二个检查:本次影响范围。明确列出这次改动会影响的用例编号,避免“管他呢全跑一遍”的惰性操作。第三个检查:冒烟结果是否已通过。冒烟未通过的代码,不进入功能层或回归层。

这三项检查在工程上实现起来并不复杂,本质上是给验收流程加了一层“前置门槛”。别小看这 5 分钟,它直接决定你这一轮要花多少 token、跑多少请求。

3.4 一键验收脚本:从全量回归到分层调度

为了让这套流程不依赖个人意志,我把它写成了脚本结构。核心逻辑不复杂,简单描述一下:

start_acceptance(config): smoke_result = run_smoke(config.p0_cases) if not smoke_result.passed: stop("冒烟未通过,不允许继续") for layer in ["smoke", "functional", "regression"]: layer_cases = filter_cases(config, layer) run_cases(layer_cases) collect_token_usage() if token_used > layer.budget: alert_and_stop()

脚本不用很完美,但一定要把“冒烟未过就停”、“每层有独立预算”、“超预算即熔断”这三条写死。这样验收就不会再靠人的冲动来驱动,而是被流程约束。

4. 五类token刺客:症状、定位和手术方案

4.1 历史包袱:对话上下文越滚越大的雪球

最容易忽略的 token 消耗来源是对话历史。有些用例设计成了多轮交互,模型每回答一轮,下一轮就把之前所有轮次全部带上。我的一个测试场景跑了 8 轮对话,到最后一轮时,仅历史消息就已经逼近 3 万 token,比新问题本身多了几十倍。

定位方法很简单,每次请求日志里看 prompt_tokens 是不是成倍增长。手术方案也不复杂:长对话定期做一轮“摘要化”,把早期轮次压成一个概要;或者只保留最近 N 轮完整消息加上关键状态,其他内容截断。

4.2 数据倾销:检索结果被整段塞进上下文

第二个刺客是检索召回。我一开始为了让模型获得足够信息,把 Top-5 的文档片段整段塞进系统上下文,每段可能上千字,一轮请求直接多出 4 万 token。更离谱的是,这些片段里大部分内容是重复或无关的,模型真正用到的可能只有其中两三句话。

我的手术方案是“裁剪 + 摘要”双管齐下:召回后先按相关性排序,只取每段的前面几百字符;如果有必要,再用一个小模型对文档片段做摘要,摘要完成后再传给主模型。同样的场景,prompt_tokens 从 5 万降到了不到 1 万,质量反而没有下降。

4.3 系统提示词过度膨胀:每次请求都要背一遍“法典”

有些团队喜欢把产品规则、安全约束、格式要求、历史背景全部堆进系统提示词,动辄三五千字。这个做法不是不行,但你要意识到:系统提示词会随每一次请求重复计费。一个 3000 token 的提示词,在 92 次请求里累计就是 27 万 token,而这还只是最基础的消耗。

我的建议是对系统提示词定期做“瘦身审计”:检查哪些词始终没被模型行为体现,哪些规则是重复的,哪些描述可以压缩。保留必要指令,删掉自我感动型的长篇大论。一个精简但有效的系统提示词,通常能控制在 1000 token 以内。

4.4 失败重试引发的“重试风暴”:同样的请求连环炸

比正常请求更烧钱的,是请求失败后的重试。我在前两轮验证时遇到了几次超时和限流,脚本默认对失败请求立即重试。一次超时后,整个链路上下文原封不动又发了一遍;如果还失败,又发第三遍。三次循环下来,同一个任务多烧了 6 万 token,问题还没解决。

后来我把重试策略改成了“指数退避 + 上限控制”:第一次失败等 1 秒重试,第二次等 2 秒,最多重试 3 次;如果仍然失败,立刻把失败用例上下文写入日志并停止,等人工来处理。另外,所有失败重试都不能盲目重放整条链路,而要在结果缓存中标记该用例为失败,只对这个用例单独做排查。

4.5 多Agent协作:无人记账的隐性调用

最后一个刺客最隐蔽。如果你在做多 Agent 协作,一个“任务”可能被拆分成十几个子调用,而每个子调用都在消耗 token。用户侧看到一次请求,实际可能已经发生了 30 次 LLM 调用。由于没有按子任务记账,你很可能根本发现不了成本是从哪里涨上来的。

我的解法是给每一次子调用都加一个“task_id”字段,在日志里按任务维度聚合 token 消耗。这样一眼就能看出:哪个协作任务消耗最大、哪次子调用出现了异常、哪一段可以切成小模型处理。

5. 顺手能抄的降本工具和账本机制

5.1 每次请求都读usage字段,记账先于省钱

我优化流程的第一步,不是省钱,而是记账。每次调用模型时,把返回结果里的 usage 字段拆开记录,至少包含模型名称、prompt_tokens、completion_tokens、总延迟和状态码。

伪代码大致是这样:

response = client.chat.completions.create(...) usage = response.usage log_usage({ "model": model, "prompt_tokens": usage.prompt_tokens, "completion_tokens": usage.completion_tokens, "latency_ms": response.latency_ms, "status": "ok", })

记录不需要很复杂,但一定要每一条都落盘。等积累到一定量,按用例维度或者按请求路径维度聚合,异常消耗会自己浮出水面:哪个用例的输入是平均值的十倍、哪个任务的重试次数异常、哪个模块的上下文在持续膨胀,全部一目了然。

5.2 四种复用层:从缓存到会话复用

记账之后,降本主要靠复用。我实际用到的有四层:

第一层是 API 侧的上下文缓存,把系统提示词和常用文档放在前缀位置并保持稳定,让相同前缀可以命中缓存。第二层是结果缓存,输入不变时直接复用历史结果。第三层是 mock 桩:在验证 prompt 逻辑时,用固定的 mock 结果替代真实工具返回,先把确定性找回来,只有专门验证工具链路时才真实调用。第四层是会话复用,同一个测试用例的多轮对话尽量放同一个 session 里,不要每轮都新建带全量历史的独立请求。

这套组合下来,我同类型验收任务的 token 消耗下降了大约七成。

5.3 预算告警与自动熔断:给验收装上“刹车”

最后一步是给验收流程加预算控制。我给每个验收批次设定一个 token 上限,每执行一批用例就累加一次用量,接近阈值时发出警告;超过预算时直接停止执行,只输出已完成的明细和失败清单。

此外,我还在请求层做了阈值熔断:单次请求的 prompt_tokens 若超过设定的最大值,直接拒绝执行并且打印异常上下文。这样即使某次召回异常导致文档被整段塞入,也会在发生之前被拦下来。

5.4 小模型冒烟、大模型精测:两段式执行策略

还有一个容易落地的调优思路:冒烟阶段不要总用重型模型。很多 P0 冒烟用例只是确认链路通不通、格式对不对,用小模型或者快速模型跑,既省 token 又能快速反馈。只有进入了功能层和回归层,需要对输出质量做精细判断时,才切换回能力更强的大模型。

我实际跑下来,冒烟层的 token 成本大约只有原来的三成,而且速度更快。注意不要所有用例都切成小模型——那些真的考验语义理解、推理能力、指令跟随的场景,还是需要大模型来兜底。

6. 写在最后:我现在的验收SOP

现在再遇到 AI 功能验收,我的流程变得非常固定:动手改配置前,先花 5 分钟写影响面清单和 token 预算;任何变更先跑冒烟,冒烟不过绝对不点全量回归;所有请求全程记账,超预算自动熔断。

那个 92 次请求的故事后来也有了个更好的结局:同样的功能,下一轮迭代我只用了 23 次请求、不到 90 万 token 就完成了验收。这不是因为模型变强了,而是因为我终于把“验证一个改动”和“跑一遍全量”这两件事分开了。

对 token 保持敏感,不是抠门,而是让你把注意力放回到真正需要验证的问题上。每一次请求,都应该有一个明确要回答的疑问;每一次重试和回归,都应该有清晰的理由。能做到这一点,AI 本身就够快。

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

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

立即咨询