第 13 章 成本账:token、缓存与路由
难度 ★3 | 约 4 学时 | 6 节
这一章要把成本从「月底看账单」变成一张能预算、能监控、能优化的表。它的判据不是「花了多少」,而是「能不能说清花在哪」。
学习目标:把成本变成可以预算、可以监控、可以优化的一项工程指标。
本章交付物:一份成本归因表(四份拆解)+ 一条告警规则(阈值与动作)
13.1 成本是 token × 单价——而「单价」是四个乘数
大纲的诊断必须保留,因为它是这一章的起点:同样次数的请求可以差出数量级,所以按请求数做预算必然失控。
但这条公式里最容易被当成常数的那一项——单价——其实有四个乘数在动,而大纲一个都没写。「token × 单价」这句话会让人以为查一张价目表就够了,而价目表只是第一个乘数。
四个乘数,按影响从大到小:
| 乘数 | 它怎么动 | 记账时最容易漏的 |
|---|---|---|
| ① 缓存读写 | 缓存读约按标准输入价的 0.1 倍计费;缓存写约 1.25 倍(短存活期)或 2 倍(长存活期) | 缓存写是多付,不是省——正负号会记反 |
| ② 异步批量 | 对输入与输出同时打五折,且走独立的速率限制、不占实时配额 | 它和缓存折扣可以叠加 |
| ③ 模型档位 | 档位之间可以有几十倍差距 | 混在一张表里平均,等于什么都没算 |
| ④ 分词器 | 换代会让同一段固定文本的 token 数变化(新一代可能多约三成) | 它不在任何一张价格表的标题里 |
这张表最重要的两行,值得单独说。
先说第 ① 行的正负号:缓存写是多付,不是省。
因为缓存读 0.1 倍、缓存写 1.25 倍,所以命中两次才开始赚,只命中一次是亏的。 官方口径把这条写成了回本点:短存活期的缓存写,读回一次就回本;长存活期要读回两次。
这条一写清,一个非常常见的误用就消失了:一次性的调用开缓存是纯亏——付了 1.25 倍的写价,却永远不会有人来读。「开了缓存」不是优化,是一个只有当同一前缀被重复使用时才成立的交易。
再说第 ④ 行:费率下降 ≠ 账单下降。
这是本轮最容易被漏的一条成本事实,因为它不在任何一张价格表的标题里:新一代模型的分词器会让同一段文本的 token 数变化,于是**「每百万 token 的单价降了三成、同一个提示的账单只降了一成多」**是完全可能的。
请体会它的形状:你按「单价降了」去做预算,然后每个月都比预算多花一点。 这种偏差不会报警,它只是让你的预算表慢慢失去信用。
还有第五项,它不属于乘数但同样算钱:重试也计费。 一次失败的上游调用同样产生 token,而重试会把同一段输入再付一遍。所以成本表里必须有一栏叫「重试」,否则你会以为那部分开销不存在(13.6 会把它变成四份里的一份)。
收口:成本是一个四项乘积,不是两个数的乘法。 四行里只要能说出三行,「按请求数做预算」这个错就不会再犯。
13.2 缓存的收益取决于前缀命中率:一次读回本就够
这一节要先把一句「风格建议」升级成一句「机制句」。
大纲写的是「静态内容前置、动态内容后置」——听起来像排版习惯。而真正的机制是:缓存按前缀匹配,所以你在靠前的位置改一个词,它后面的整个缓存全部失效。
这句话一写,那条「顺序」就不再是建议,而是可计算的:静态的、不变的东西放在最前面;变量放在最后面。 反过来做的代价不是「命中率低一点」,而是从你改动的那一个字开始,后面全部重算。
三条硬参数,直接可用:
| 参数 | 值 | 用法 |
|---|---|---|
| 缓存读 | 约标准输入价的 0.1 倍 | 省下来的那一份 |
| 缓存写(短存活期) | 约 1.25 倍 | 读回一次就回本 |
| 缓存写(长存活期) | 约 2 倍 | 读回两次才回本 |
于是「要不要开缓存」变成了一个除法题:这个前缀会被复用几次? 一次就别开,两次以上才谈收益。
接下来是三条反直觉边界,每一条都对应一种「我开了缓存但没省到钱」的真实情况:
① 一次性的调用开缓存是纯亏。(13.1 那条的正反面。)
② 低于最短长度的提示会被静默地不缓存。 各家的最短长度阈值不同(有 512、1024、4096 几档),而**「没报错、也没省到钱」是这种情形唯一的症状。**
这条的解法是本篇最实用的一个可观测动作:把「缓存写 token」与「缓存读 token」两个计数都记下来。两个都是 0,就说明压根没缓存上。
请注意这个动作的性质:它把「我开了缓存」从一句声明变成了一件可以验证的事。这与全书反复用的判据是同一个形状——没读到数,就等于没发生。
③ 并发的首批请求会各付一次写价。 因为缓存条目要等第一次响应开始才可用——同一瞬间打进来的请求谁都读不到,全都按写价付费。
这一条在「突发流量 + 缓存」的组合里特别贵:你以为缓存会摊平成本,结果高峰期第一批请求全在付溢价。
收口:缓存不是开关,是一个需要被观测命中率的机制。 判据只有两个数:缓存写 token 与缓存读 token——前者非零说明你没白花钱,后者非零说明你真的省到了。
13.3 不是所有请求都值得用最强模型——而验证要看降级那一档
方向是对的,而且可以把理由说得更硬:把全部流量打给最贵的模型,是把「不确定性」当成了「统一质量」。
但真正可用的分档口径不是「任务重不重要」,而是:按「错了会不会被发现」分。
| 类型 | 例子 | 能不能降级 |
|---|---|---|
| 有明确对错或可快速抽检 | 分类、摘要、改写、抽取 | 可以降 |
| 开放式推理与多步工具编排 | 方案设计、多工具串联、长链判断 | 不能随便降——它降级之后的失败往往表现为「看起来对」 |
第二行是这一节的价值所在:「看起来对」比「明显错」贵得多,因为它会被用户当成真的用下去,而你在任何指标上都看不到异常。
三条定界:
① 路由本身不免费。 先判断「这题难不难」再决定用哪个模型——这个判断要么多一次模型调用,要么用规则近似。「加一层路由」不是免费的:它要么花 token,要么损失准确率。 所以路由的收益必须大于它自己的成本,而这条账很少有人算。
② 降级要写成「带兜底的分级」,不是「按概率分流」。 一旦被降级的那一档失败,应当能升回强模型重试一次;否则你只是把失败从「答得差」变成了「答不出」——后者的用户感受更差,因为它连一个可用的答案都没有。
③ 验证质量必须单独看降级那一档。
这是本节最重要的一条:整体通过率会被强模型那部分稀释。 举个具体形状——九成流量走强模型、一成走降级档;降级档全错,整体通过率只掉一成,而那一成的用户拿到的是全错的答案。
更糟的是它的反向:降级比例越高,整体指标越好看(因为便宜档通常处理更容易的任务),而问题恰好全在被降级的那一档里。
所以「降级之后要能验证质量没垮」这句必须改成一句可执行的:降级必须挂一条评测,而且这条评测的结论要按档位分开报(第 11 章的归因粒度在这里正好用上)。
收口:降级不是省钱开关,是一条需要单独监控的分支。 一个只在总分上看降级的团队,会得到一个「省钱且质量稳定」的假象。
13.4 长上下文的隐性成本:三个来源与三条质量代价
「每一次顺手再塞一点都在加价」——这句对,但太笼统。它有三个具体来源,而其中两个不由写业务代码的人决定。
① 工具定义与系统提示本身就是输入 token。
官方定价口径明确把工具定义、系统消息与历史轮次都算作输入。所以「多加一个工具」不是零成本的:它会在每一次调用上重复收费。
这句话有一个很实际的推论:工具集越大,单次调用的固定成本越高——哪怕这次根本没用到那个工具。这和第 6 章那条「限制工具集」是同一件事的两个视角:安全与成本在这里重合。
② 多轮对话的历史会被每一轮重发。
第 2 章算过这笔账,这里要补的是它与缓存的关系:历史恰好是最有效的可缓存前缀。 于是「贵」与「没开缓存」经常是同一件事——你以为你在为长对话付钱,其实你在为「没有命中前缀」付钱。
③ 检索塞入是最容易失控的一项。
检索条数、切片大小、去重策略——三个参数各自都能让输入翻倍,而它们通常不由写业务代码的人决定(在检索服务或配置中心那边)。这就意味着:成本问题的责任人和成本的产生地,经常不是同一个人。
两条定界:
① 「长上下文」不等于「贵」。
同样一万 token,命中缓存的读价与新鲜输入价差一个数量级。所以判断贵不贵,要先问「这段内容在不在缓存前缀里」,而不是先问「它有多少 token」。 问错了顺序,你会去砍一段本来不要钱的内容。
② 省 token 省出问题的方式很具体。
| 你砍的 | 掉的是什么 |
|---|---|
| 系统提示 | 指令遵循(它开始自作主张) |
| 工具定义 | 调用准确率(选错工具、填错参数) |
| 检索条数 | 事实性(它开始编) |
三行合起来是一条纪律:每一处「省」都要配一个「省了之后看哪个指标」。 不配的话,你会在成本下降的同一周里得到三封用户投诉,而没人把这两件事联系起来。
于是这一节的产出是一张三列表,每段上下文都要能填满:
| 哪一段 | 在不在缓存前缀里 | 砍了掉什么 |
|---|---|---|
| 系统提示 | 在(最前) | 指令遵循 |
| 工具定义 | 在(若稳定) | 调用准确率 |
| 历史轮次 | 在(累积前缀) | 上下文连续性 |
| 检索材料 | 不在(每轮变) | 事实性 |
这张表最有用的一格是第三列:它逼你在砍之前先说出代价。
收口:长上下文的账要按「哪一段、在不在缓存里、砍了掉什么」三列记。 缺任何一列,你都只能得出「贵」或「省了」这种无法行动的词。
13.5 预算要设在人能看到的地方,而且必须带动作
「看不到的预算等于没有预算」这句对,但这一节真正的分界线在它属于哪一类控制。
行业风险清单把「无限消耗」单列成一类安全风险,并给了它一个很形象的名字:「钱包拒绝服务」。
这个定位一换,优先级就变了:额度、速率、循环上限与熔断,都属于安全控制,而不属于财务控制。 财务控制是月底对账,安全控制是事发时自动生效。把这件事放在成本章而不是安全章,是大纲的编排;但把它当成财务事项,是读者的误读。
三条定界:
① 预算必须带动作,否则它只是一个数字。
三档动作要写清:
| 档 | 动作 | 它防的是什么 |
|---|---|---|
| 告警 | 通知到人(邮件 / 群 / 工单) | 防「没人知道」 |
| 降级 | 自动切便宜档、缩短上下文 | 防「继续烧」 |
| 熔断 | 拒新的高成本请求,保留低成本的 | 防「一起挂」 |
只设阈值不设动作,等于给自己发了一封没人读的邮件。 这句话可以当验收标准用:你的预算规则里有没有一个「自动执行」的分支?没有就等于没设。
② 三个维度必须同时设。
按功能(哪个功能在烧钱)、按用户(是不是有单个账号在刷)、按天 / 小时(是不是有异常时段)。只设总额度时,你只能看到「超了」,看不到「谁超的」——而「超了」这个信息无法行动。
③ 熔断要考虑「谁先被拒」。
按总量熔断会让所有用户一起挂;而按配额熔断则要保证它不会误伤付费用户——这条在 B 端产品里是合同问题,不只是技术问题。
所以正确的顺序是 15.4 会再讲一遍的那个:先降级,再按配额熔断。 先降级能把「全体不可用」变成「一部分人体验差一点」,而这一步的差别就是可用性。
收口:预算的价值不在那个数字,在它触发的那个动作。 一个只写「超过 X 美元时通知我」的规则,防不住钱包拒绝服务——因为攻击发生的速度比人读邮件快。
13.6 一个可观测的验证:把成本拆成四份
这一节是本书构造的诊断口径,先说清身份:官方能给的是「按模型、按 token 类型、按缓存命中分别计量」,而「拆成四份」在官方材料里没有对应物。
四份是:输入(新鲜)/输出/重试/缓存未命中导致的溢价。
为什么是这四份?因为它们分别对应四种可以行动的干预:输入多了→压上下文(13.4);输出多了→限长度;重试多了→修错误处理(第 9 章);未命中多了→调前缀顺序(13.2)。四份里每一份都有一个明确的「该改哪里」,这就是这个拆法的全部价值。
而这条工具的判据只有一句:任何一份「拆不出来」,就说明那一部分没有被正确计量,而没有被正确计量的部分,就是预算管不住的部分。
三个前提,缺一个这个拆法就不成立:
① 依赖按调用记账,而不是按账单汇总。 账单是月度的、聚合的,它拆不出「重试」这一项。要做到这一点,必须在调用层记录每一次请求的 token 与命中情况。
② 「重试」这一份要能单独识别。 如果重试在代码里被包在同一个函数里、共用一个请求标识,它就永远拆不出来——所以要在发起之前就打上「这是第几次尝试」的标记。
这条有个反直觉的地方:它不是「记账问题」,是「埋点位置问题」。 等你看到账单时,重试已经和正常调用混成一份了;能拆的前提是你当初就分开打了标记。
③ 四份相加不必然等于账单。 批次折扣、订阅额度、免费额度都会让账单偏离调用层的累加值。
这一条听起来是缺陷,其实是一个额外的诊断信号:四份之和与账单的差额本身,就说明「你有一个不知道的折扣或额度在生效」。 差额稳定说明规则稳定,差额突然变化说明计价规则变了——这比任何一条告警都更早。
收口:能拆开是前提,能对账是验证。 只有前者时你知道钱花在哪,两者都有时你才知道钱花得对不对。