1. 先说结论:92 次请求背后的问题不在模型
先给各位交个底:最近在做一个智能客服场景的概念验证Demo,功能不复杂,就是把历史工单分类、抽摘要,再对接一个知识库问答链路。整个项目从开发到验收,我对着某主流大模型API调了92次请求,后台账单拉出来一看,累计烧掉了1340万token。
看到账单一瞬间我第一反应是模型推理太慢、价格太贵。真正把日志导出来逐条对完之后才发现,模型本身响应挺快的,单次最长也就十几秒,大部分都在3到5秒内返回。那时间去哪了?答案是:我的验收流程。
所谓92次请求,其实括号里写得很清楚:91=25+14+15+12+8+8+5+4。这是我复盘后给所有请求做的分类。每次改动Prompt、每次加一条测试用例、每次回归旧场景、每次对比两个版本的效果,我都在重新调用一次完整链路。而每一次调用,都要把系统提示词、历史会话、知识库上下文、few-shot示例全部重新塞给模型。一次两次不觉得,92次叠加起来,上下文重复加载的消耗就变成了一笔非常可观的数字。
这篇文章不聊模型调优,也不聊算法,就聊一个特别容易被忽视、但真正耗时间耗token的环节——AI应用的验收流程。如果你也在做类似的项目,正在反复跟模型对话、反复试Prompt、反复给领导或客户演示效果,那这篇复盘值得你花十分钟看完。里面每一类请求我怎么拆的、为什么烧token、后来怎么优化,我都会写清楚。
2. 拆解 91 次请求的构成与 token 消耗分布
2.1 总量复盘与平均消耗估算
先对齐一下数据口径。92次是API网关上的总调用日志条数,其中一条是我在联调前发的探活请求,内容就一个“ping”,真正参与验收逻辑的是91次请求。括号里的25+14+15+12+8+8+5+4,就是这91次请求按用途分的类,每类对应的次数加起来正好91。
那1340万token是什么概念?平均一下,每次请求大约消耗14.7万token。如果按输入输出比例粗估,以这类对话模型的常规消耗结构来看,绝大多数是输入侧消耗,因为每次调用我都会把整套对话背景和知识库片段塞进去,输出短则几百、长则两三千token。假设总消耗中大约1000万是输入、340万是输出,按市面上主流模型API的公开价格粗算,输入5美元/百万、输出25美元/百万,折算下来大约是50美元加上85美元,合计135美元左右,人民币不到1000块。
这个金额说多不多,说少也不少。但复盘的价值不在钱,而在时间——我因为这一套低效验收流程,前前后后折腾了近两个星期。每天打开电脑就开始跑测试,跑完看输出、改提示词、再跑,像是在给模型“喂”token,而不是在验证功能。真正让我下决心重构验收流程的,不是账单金额,是我发现自己根本没有一套可复用的回归方法:每次改动都要从零开始重新试,结果试到后面,连自己改过什么都记不清了。
2.2 八类请求逐一拆解
下面这张表是我复盘时做的原始记录,按请求次数、触发场景、消耗占比和“慢在哪”做了整理,先整体看一下:
| 分类 | 次数 | 典型场景 | 消耗占比 | 慢在哪 |
|---|---|---|---|---|
| 基线用例集 | 25 | 用固定10条工单测试分类/摘要 | 约30% | 每次全量重发上下文,结果还要人肉核对 |
| 边界异常用例 | 14 | 空文本、超长文本、纯符号输入 | 约24% | 需要单独构造上下文,且失败后常被误判为模型问题 |
| Prompt探索实验 | 15 | 改措辞、换格式、调整few-shot数量 | 约20% | 无版本管理,同一目标反复试,很多请求是重复劳动 |
| 失败重试 | 12 | 模型输出不符合预期后直接重问 | 约8% | 未做归因就重试,同样的错会犯好几遍 |
| 回归旧用例 | 8 | 改动Prompt后重新验证历史场景 | 约7% | 旧用例集没固化,每次“回归”其实是重新发明轮子 |
| 双版本对比 | 8 | 同一场景用两版Prompt各跑一次 | 约5% | 为了A/B对比但没控制变量,经常白跑 |
| 超时重发与截断调整 | 5 | 请求超时、输出截断后调整参数重发 | 约3% | 上下文没缩短就重发,失败成本直接翻倍 |
| 数据修复验证 | 4 | 修完知识库/标注数据后全量复查 | 约2% | 修复范围没界定,连带把所有场景又跑一遍 |
2.3 每一类请求背后都藏着什么
先说25次基线用例。我这个Demo里设计了10条标准工单,覆盖订单退款、物流查询、售后维修、账号申诉等常见场景。按理说10条工单跑一遍就完,但我犯了两个错:第一,我没有把这10条工单的输入输出固化成“基线文件”,每次想验证某个改动,都是临时从Excel里复制粘贴文本,重新拼成请求体;第二,每拼一次请求体,我都要把完整的系统提示词和角色设定重新写一遍。这就导致“验证一个改动”和“重新构造一次完整请求”变成同一件事,25次请求里至少有10次是在做重复构造工作。
再说14次边界异常用例。当时写用例的人是我自己,所以我很清楚哪些地方容易出问题:空文本输入、超长文本、emoji乱码、纯符号、XML标签嵌套……这类用例我的本意是测试健壮性,但实际执行时发现,边界用例一旦触发模型输出异常,你很难判断是模型能力边界、是输入构造问题、还是Prompt本身有歧义。我当时的处理方式是“多试几遍看看”,这一试就多烧了好几轮请求。尤其是其中一次超长文本测试,单次请求就带着一份3000多字的完整工单上下文,消耗特别大。
Prompt探索实验更不用说了,15次全是我手动调词的记录。同一个任务,先把“提取关键信息”改成“请总结以下工单”,又把输出格式从“JSON”改成“编号列表”,再调整few-shot示例的条数和顺序。改了之后各跑一遍看效果,完全没有做版本对比,纯靠印象。这是最典型、最隐蔽的token浪费:你觉得每次尝试都有价值,实际上很多次改动只是换了个说法,模型输出根本没有质的差别。
失败重试和回归旧用例两类放在一起看很有意思。12次失败重试里,有一部分是在重复问同一个问题,原因是我第一次拿到不符合预期的输出后,没有记录失败模式就直接重试,结果第二次输出的问题跟第一次几乎一模一样。8次回归旧用例更加尴尬:我前面没有固化基线,等到后来想确认“改动没有破坏旧功能”时,发现旧场景的输入输出已经记不清了,只能重新构造然后再跑一遍。所谓的回归,到头来变成了新测试。
双版本对比和超时重发也算典型浪费。双版本对比本来是可以有价值的,但我当时没有做控制变量:对比的时候不仅改了Prompt里的一个词,还把few-shot示例顺序也换了,结果两个版本的输出差异根本没法归因。超时重发那5次更冤,明明是输出长度被截断导致的结果不完整,我重发时还是没有调整最大输出token数,等于用同样的配置期待奇迹发生。数据修复验证的4次倒是相对合理,只是在修复完知识库之后我没确认修复范围,直接把所有场景重跑了一遍,中间有不少无效请求。
3. 为什么会烧掉 1340 万 token:五个根源问题
3.1 问题一:每次请求都带着“完整战场”进场
先说最根本的浪费来源。我的项目里,客服分类和摘要任务的输入包含三部分:一是系统提示词(角色设定加任务说明),二是few-shot示例(每类工单配了一条样例),三是当前工单正文和历史对话记录。这三部分加起来,一个常规请求的输入侧经常超过12万token。
问题就出在,我的验收流程没有做任何“上下文复用”设计。改了一行Prompt,理论上只需要验证Prompt本身的变化,但我每次都会把全部few-shot和历史对话重新发一遍。这就好比你每次改PPT里的一个标题,都要把整份100页PPT重新打印一出来检查。模型没有记忆,这个事实我懂,但我没有在工程上承认它:既然模型无状态,那我应该在应用层做会话缓存,或者在做对比实验时保持其他输入完全不变,而不是让每次请求都变成一个全新的、完整的大请求。
后来我查了一下日志,25次基线用例请求里,平均每次输入是14万token左右,但真正关键的工单正文平均只有3000token。也就是说,超过97%的输入token是在反复搬运相同的背景材料。这是整个1340万里最大的浪费源,没有之一。
3.2 问题二:用“重试”代替“归因”
12次失败重试里,我印象最深的是同一个“售后维修”场景,我连续问了三次都没过。第一次模型输出漏了维修单号,第二次输出格式对但厂家名称错了,第三次又回到了漏单号的问题。为什么?因为我每次重试之前,只是把相同的请求重新发一遍,运气好能拿到稍微完整的结果,运气不好就是同样的问题再来一次。
这就是典型的用重试代替归因。模型输出不合格,原因可能有几十种:输入里单号格式不统一、Prompt里没有强调必填字段、few-shot示例中没有覆盖漏单号的情况、输出JSON的schema定义不够严格……正确的做法是先定位“失败发生在哪一层”,再去改对应的那一个变量。没有归因就重试,失败成本会成倍放大,因为每一次重试都在重复消耗整个上下文。
3.3 问题三:回归集合没有冻结机制
这可能是很多团队都会踩的坑。测试过的人都有经验:手工测试最怕的就是没有版本概念,你测到第三轮的时候,已经记不清第一轮是怎么测的、期望结果是什么。模型测试更严重,因为模型的输出有随机性,同一个输入不同时刻调用,结果可能不完全一样。如果没有把“输入、当时的Prompt版本、模型参数、期望结果”冻结成一份快照,你所谓的回归只能靠人肉记忆,非常不可靠。
我当时没有建立基线集,导致每次回归都像从头开始。25次基线用例和8次回归旧用例,完全可以合并成一个数字——如果一开始就把10条工单跑一遍并冻结结果,后续每次改动只重跑这10条,总数至少能砍掉一半。
3.4 问题四:对比实验一锅端
做Prompt调优时,我特别喜欢做A/B对比:同一场景,两个Prompt版本各跑一遍。这个思路本身没错,但我犯了一个细节上的错误——对比实验没有控制变量。
举例来说,我想验证“把任务描述改成更口语化的措辞是否能提升分类准确率”,理论上应该只改任务描述这一处,其他全部保持一致。但实际操作中,我会顺手把few-shot示例的顺序也调一下,或者把输出格式的说明再细化一点。结果输出确实变了,但你根本说不清是哪个改动起作用。为了搞清楚,我又会再跑几轮组合实验,请求次数就这样从4次变成了8次甚至更多。现在回想,这就是典型的“实验设计不严谨导致样本量膨胀”。
3.5 问题五:超时、截断、并发限流的二次消耗
最后说一类相对细节的问题。我在验收过程中遇到过5次超时或者输出被截断,其中有两次是因为工单文本太长,模型生成摘要到了Max Token上限,输出被硬生生截断。我的第一反应是重发一次,期望这次能生成完整结果,但问题在于请求配置根本没变——最大输出token没调、输入文本没有精简、参数没有调整,所以重发的成功率依然很低,等于浪费双倍token。
还有一次是并发限流导致的报错。我在做知识库更新验证时,为了快点拿到结果,把四条请求同时打出去,结果触发了API的并发限制,其中两条直接返回限流错误。后来我把它们改成串行重发,虽然多花了几分钟,但至少没有再因为排队超时而浪费请求。超时、截断、限流这类“工程性问题”,和模型能力无关,完全可以在请求层加防护,而不是靠盲目重发来碰运气。
4. 我后来是怎么把 token 消耗打下来的:五步优化落地
4.1 第一步:建立基线集与快照基线
我做的第一件事,是立刻停掉所有形式的“随机验证”,先把基线固化下来。具体做法是:从10条标准工单里挑出5条作为核心基线集,每条工单配上固定的输入文件、期望输出要点、使用的Prompt版本号,然后一次性跑完,把结果存成带时间戳的快照。
这份快照的意义在于,从今天开始,任何一次改动都要回答一个问题:它相对于这份快照是变好了还是变差了。我不需要每次重新构造工单文本,不需要回忆旧输出长什么样,只需要把快照里的输入原样重放一遍。这一步做完,25次基线请求变成一件事:每次改动后,重放5条核心用例,先看趋势,再决定要不要扩大范围。
4.2 第二步:Prompt版本化 + 冒烟测试前置
以前改Prompt,我是直接改对话里的文本,改完就发请求验证。现在我把每一版Prompt都存成带版本号的文件,比如 prompt_v1.md、prompt_v2.md,改动前先复制一份再改,保留完整的改动记录。版本化的好处是在对比实验时能精确知道两个版本之间的差异,不再靠记忆。
冒烟测试前置是什么意思?我规定:任何Prompt改动,必须先拿一条覆盖主要流程的“冒烟用例”快速验证方向是否正确,通过了再跑完整基线集。如果这条冒烟用例的输出明显偏离预期,就不值得跑完整回归,先把Prompt修好再说。这个规则看起来简单,但直接帮我挡住了至少10次本该发起的全量回归请求,因为很多改动在冒烟测试阶段就被发现方向不对了。
4.3 第三步:上下文瘦身与缓存复用
上下文瘦身是个技术活。我当时的做法分三步:第一步,压缩系统提示词。把原来一大段角色描述精简成两三条核心指令,去除所有“语气亲切、表达专业”之类的装饰性描述,只保留任务规则、输出格式、注意约束。第二步,削减few-shot数量。原来每类一条示例,一共6条,现在只保留2条覆盖最典型场景的示例,其余删除,减少了大约3万token的输入。第三步,把静态背景文本(比如客服知识库的公共介绍部分)从每次请求里抽出来,改在应用层做一次预加载,然后在Prompt里通过变量引用,而不是把整段文本塞进每个请求。
缓存复用方面我做得更直接:对于相同的“工单正文+Prompt版本+模型参数”组合,我在网关层做了一层缓存,如果完全匹配就直接返回上一次结果,不发起真实调用。模型输出的随机性确实存在,但对于回归验证这种场景,我要验证的核心是Prompt改动是否破坏旧功能,固定输入配上固定Prompt版本出现同样的输出,反而更容易对比。实测做完这步之后,重复请求的调用次数直接归零。
4.4 第四步:失败分级与重试白名单
以前遇到失败就重试,现在我先做一次失败归因分类。我的分类很简单,只有三类:
第一类是输入问题。比如工单里有明显的乱码字符、字段缺失,这类问题重试多少次都没用,因为它不是模型的锅。我的处理是修正输入数据或标注“预期失败”。
第二类是配置问题。比如最大token太小导致截断、并发限流导致报错,这类问题可以通过调整参数解决,但它不是“重新发一次同样的请求能解决的”,必须确认配置已经改变再重发。
第三类是模型输出本身的格式问题。比如JSON解析失败、字段名对不上,这类我会先检查Prompt是否定义了明确的输出schema,再决定是细化Prompt还是在应用层做输出修正,而不是盲目重试。
我还建了一个“重试白名单”:只有满足以下三个条件才允许重试,一、明确是配置或临时故障导致的失败;二、已经针对原因做了参数调整;三、同一场景重试次数不超过一次。
4.5 第五步:对比测试的配对设计与采样回归
做真正需要A/B对比的实验时,我给自己定了三条铁律:只改一个变量、固定随机种子/温度参数、先做单条样例冒烟再做小批量验证。
只改一个变量这条最重要。如果同时改了两处,结果变好变坏根本无法归因,继续实验就是纯粹烧token。所以我每次改动后都会在版本记录里明确标注“v2到v3之间只改了输出格式说明中的日期格式”,其他一字未动。固定随机种子/温度参数也是一样,语言模型的输出有随机性,做对比实验不把随机性控制住,你看到的差异可能只是噪声。当时我使用的模型API支持temperature参数,我就把验收场景统一固定为0,最大程度减少随机性。最后,做对比实验时不再全量跑25条用例,而是先抽5条有代表性的样例看趋势,趋势符合预期再扩大验证范围。
4.6 验收流程的标准化 SOP(七步)
除了上面五步,我把整个验收流程固化成了标准操作步骤,方便团队其他成员也能按同一套逻辑去执行:
- 从需求里提取“可验证的行为清单”,把每一条行为转成一条测试用例,存入用例库。
- 对每条用例固定输入文件、期望输出要点、对应的Prompt版本号,生成基线快照。
- 任何Prompt或代码改动,先检查冒烟用例,方向通过后进入下一步。
- 回归时先跑核心基线集,再按风险等级决定是否需要跑完整集。
- 失败时先归因,再决定是改输入、改Prompt、改参数还是接受预期失败。
- 所有重要请求保存请求日志和响应日志,至少保留Prompt版本、输入长度、输出长度、耗时、失败原因五个字段。
- 每周做一次token消耗回顾,按分类统计哪类请求消耗最多,逐项优化。
这套流程跑下来,同样一次功能迭代,我的请求次数从20多次降到了6次以内,token消耗降了将近一半。更重要的是,每次改动的影响变得可追踪了,我不再需要靠人肉记忆来回答问题。
5. 常见问题与踩坑实录
5.1 “回归不跑全量,上线不放心怎么办?”
这是我在复盘后最常被问到的问题。很多人的第一反应是,你砍了回归范围,万一有场景没覆盖到,线上出了问题谁负责?
我的回答是:全量回归不是目的,覆盖风险才是。你要先问自己,你的回归集中有多少用例其实从来不会失败?有多少场景改了Prompt之后根本不受影响?如果你的回归集是合理的,回归集本身就应该足够小、足够精悍。我当时把25条用例压到5条核心基线,是因为这5条覆盖了所有主要的行为分支,其他用例要么属于异常输入验证,要么属于低频场景,完全可以放在完整集里按周或按迭代去跑,而不是每次改动都全量执行。
5.2 “省 token 有必要吗?成本好像也不高”
有同事跟我算过一笔账,说1340万token按市面价格也就是100多美元,换算成人民币不到一千块,好像不算什么大事。
我的观点是:token消耗是成本,更是效率的镜子。如果你一个月花在模型API上的token是10万,那说明你的业务量小,省不省无所谓。但如果你像我们一样,项目处在频繁迭代、经常做Prompt调优和回归验证的阶段,token消耗高,只能说明你的验收流程是低效的。真正值钱的不是那100多美元,是你为了那一堆低效请求花费的两周时间。两周人力成本,远高于100多美元。所以省token的本质是省时间,是逼着自己优化验收流程,而不是单纯的省钱。
5.3 “超时重发到底要不要做?”
我现在的原则是:超时只重发一次,而且必须在参数调整之后。如果因为输出太长导致截断,我会调大最大输出token数,或者精简输入文本;如果因为并发导致限流,我会把请求改成串行,增加一个随机退避的等待时间;如果上述调整都试过还是超时,基本可以判断是链路里某个环节存在性能问题,应该去查代码和网络,而不是继续发请求。
另外我有个小习惯,不管重发几次,我都会在日志里给每次重发打上“attempt-1”“attempt-2”的标记。这能帮你后来复盘时发现一个真相:大部分所谓超时,其实是你自己上一次请求还没结束就发了下一次,触发了限流规则。场景很常见,但你不打标记永远看不出来。
5.4 “对结果做哈希缓存,会不会管不住变化?”
有次跟朋友聊到我在网关层做的请求缓存,他问:模型输出本来就有随机性,你把相同输入的请求直接复用缓存结果,万一这个输入其实应该有不同的正确答案怎么办?
我的看法是:缓存只用于验收回归,不用于生产。回归验证的目的是稳定地确认“Prompt改动没有破坏已有行为”,那么相同输入、相同Prompt版本、相同参数配置,理应产生一致的输出。如果模型输出真的不稳定,那恰恰说明你要先解决随机性问题,而不是每次跑了不一样的结果还觉得是好事。生产环境当然不能这样做,那是业务逻辑问题,和验收层的缓存设计是两回事。再说,缓存只针对完全相同的请求体,如果工单内容或Prompt有任何变化,缓存键就会不同,不会出现拿旧结果冒充新结果的问题。
5.5 记录日志时最容易漏掉的一个字段
最后分享一个特别小的坑。以前我记录请求日志时,只记了输入输出和耗时,没有记录Prompt版本号。后来想做回归分析,发现日志里根本看不出某次请求用的是哪一版Prompt,等于每次请求的上下文背景缺失了一大块。
现在不管是在网关还是应用层,我要求日志里必须带上prompt_version、model_name、temperature这三个字段,哪怕代码里写死了也要打出来。你永远不知道什么时候会需要回溯一次线上异常,如果那时候才发现缺少关键字段,代价就不是重新写一行日志能弥补的了。这一步让我在后续排查问题时省了很多力气。
最后再分享一点体会
复盘完这92次请求之后,我最大的感受是:很多人说AI应用开发慢,第一反应都归咎于模型能力不行、API延迟高。但从这次经历来看,真正的瓶颈往往在你的验收方法和工程流程上。模型是无状态的,它不会记得你上一次问过什么;你如果也不记得,那每一次请求都会变成一次全新的、昂贵的探索。
我现在每跑一批测试请求之前,都会先问自己三个问题:这次请求要验证什么?它跟上一次请求的差异点是什么?如果结果不好,我能不能定位到原因?如果这三个问题有任何一个答不上来,我就先不动手。
这次复盘也让我把习惯改了:所有Prompt改动都做版本化,所有请求都打日志,所有回归都有基线和快照。一套标准的验收流程,才是省token最有效的方式。希望这篇踩坑记录能帮正在做类似项目的人少走几步弯路。