大模型Token计费与成本优化:缓存命中、上下文膨胀和输出控制 1. Token计费的第一性原理你到底在为什么付费很多人第一次看到 token 账单时的反应是我明明只问了一句话怎么就扣了这么多。这个问题我在带团队做接入的头两个月里被问过不下二十次答案几乎从来不在你问的那句话上而在你没有意识到自己付费的那些部分。token 计费这件事本质上不是按次数收费也不是按字符数收费而是按模型实际读进去和写出来的最小语言单元收费。理解这一点你后面所有的优化动作才有支点否则就只是盲目调参数。我先把结论摆出来一次请求的成本由四段构成——系统提示与工具定义、历史上下文、当前用户输入、模型输出。前两段是绝大多数人账单失控的根源第三段通常小到可以忽略第四段因为单价最高是最值得动手的地方。这四个部分各自独立计价其中输入部分还分缓存命中和缓存未命中两个价位价差经常在一个数量级上下。把这四段拆开看你会发现所谓省 token根本不是玄学而是一笔有明确科目、可以逐项对账的成本账。这篇内容适合三类人看一是刚接入、账单还在几百块量级、想搞清楚钱花在哪的开发者二是在做产品化、日调用量上万次、需要把单次成本压到可预测区间的团队三是对模型调用成本没有概念、想建立一套估算方法的产品和运营同学。不需要你已经懂分词器我会从最基础的地方讲起但每一节都会落到能直接改代码、改提示词的动作上。文中出现的具体单价数字仅用于演示计算口径真实价格记得以你下单时官方定价页展示的为准不同模型系列、不同时间点价差很大。1.1 分词器决定了你花的每一分钱token 不是字也不是词它是分词器切出来的片段。同一段话在不同的分词方案下会得到不同的 token 数量而计费只认 token 数量。这件事的直接后果是你写提示词时的手感和账单上的数字之间隔着一层翻译。中文语境下这个翻译尤其不直观。我的经验值是现代主流模型对中文的分词效率已经比早期好很多一个常用汉字大概率落在 0.6 到 1 个 token 之间成语、专有名词、罕见字会被切得更碎英文大致是四个字符左右对应一个 token代码因为有大量符号、缩进和标点通常会比同长度的自然语言更贵一些尤其是 JSON、YAML 这类嵌套结构密集的文本。但这些都只是估算手感不能当账本用。提示不要把字数当成token 数尤其不要在中文文本上按 1:1 估算。用错了比例你的预算模型会整体偏移 30% 以上而且偏的方向往往是低估。真正可靠的做法只有两个一是直接用语言模型处理一遍读取返回结果里的 usage 字段二是用官方提供的分词工具或对应分词器在本地做离线估算。前者准确但要多花一次调用的钱后者免费但需要和线上模型版本对齐。我在实际项目里是两者结合本地估算用于开发阶段的快速反馈线上真实 usage 用于账单归因和月度复盘。这两套数字长期并行你才会对自己写的这段提示词大概多少钱形成肌肉记忆。这里还有一个容易被忽略的细节多模态输入和工具定义也占 token。你给模型挂了一堆函数工具那些 JSON Schema 描述是实打实算在输入里的而且往往每轮对话都要重新带上。见过一个项目挂了十二个工具光工具定义就两千多 token用户问一句今天天气怎么样实际输入是两千多 token 起步。这不叫浪费这叫设计成本但它必须被看见。1.2 输入、输出与缓存的三档价差理解了 token 的计量单位接下来要理解价格结构。当前主流 API 的定价基本遵循一个三段式结构输入未命中缓存、输入命中缓存、输出。这三档之间的价差不是随便定的背后有非常朴素的工程逻辑。输入未命中缓存意味着模型要把你这批 token 从头做一次前向计算算力消耗是实打实的。输入命中缓存意味着这段前缀之前已经被算过、结果被存下来了这次直接复用只需要做增量计算所以价格能压到未命中的四分之一甚至更低。输出之所以最贵是因为输出是逐 token 串行生成的每生成一个 token 都要跑一次完整的前向而且不能并行。输入的几千个 token 是一次矩阵运算批量算完的输出的几百个 token 是几百次独立的串行计算。这就是为什么输出单价通常是输入未命中的三到四倍。我把这套结构整理成一张表方便你对照自己的账单科目计费项计算特征相对成本量级主要优化手段输入缓存未命中批量并行计算算力实打实基准 1x精简提示词、减少冗余上下文输入缓存命中复用已有前缀计算结果约为基准的 1/4 甚至更低前缀稳定化、命中率监控输出逐 token 串行生成无法并行约为基准的 3 到 4 倍控制长度、结构化输出、提前截断这张表最重要的信息不是具体倍数而是优先级顺序。很多人优化时的第一反应是我少写点提示词但如果你的输出占了总成本的六成你把提示词砍掉一半也只省了两成不到。反过来如果你的场景是长文档问答输入动辄几万 token那么缓存命中率就是唯一的杠杆输出优化在这里几乎没意义。先看清楚自己的成本结构再决定动哪里这个顺序不能反。1.3 一笔请求的成本拆解示例光讲结构太抽象我们拿一笔具体请求算一遍。假设一个典型的多轮对话场景系统提示加工具定义 800 token历史对话累积 6000 token用户本轮提问 200 token模型输出 600 token。为了把口径讲清楚这里用一组示例单价输入缓存命中 0.5 元每百万 token输入未命中 2 元每百万 token输出 8 元每百万 token。无缓存的情况下输入 7000 token 全部按未命中计7000 ÷ 1,000,000 × 2 0.014 元输出 600 token600 ÷ 1,000,000 × 8 0.0048 元。单次合计 0.0188 元。如果那 6000 token 的历史上下文命中缓存情况立刻变了命中部分 6000 ÷ 1,000,000 × 0.5 0.003 元未命中部分是系统提示加当轮提问共 1000 token1000 ÷ 1,000,000 × 2 0.002 元输出不变0.0048 元。单次合计 0.0098 元。成本直接砍掉 47.9%而你什么都没改只是让前缀命中了缓存。这个数字说明了一件很关键的事在多轮对话这类场景里缓存命中率是你成本结构里最大的单一变量比任何提示词雕花都有效。再往下算如果对话轮次继续增长到 20 轮历史上下文膨胀到 20000 token缓存命中与不命中的差距会拉到接近四倍。这时候一次请求的绝对金额不大但乘以日调用量就是另一个数量级了。我在实际做容量规划时习惯用单次请求成本 × 日调用量 × 30来倒推月账单然后拿这个预估和真实账单做偏差分析。偏差超过 20% 就一定有问题要么是缓存没按预期命中要么是某个分支的输出长度失控要么是重试逻辑在偷偷重复计费。这三种情况我都踩过后面章节会逐个说怎么查。2. 成本结构解剖钱都漏在哪些环节前面讲的是账面结构这一节讲的是真实的漏损点。账单超预期的原因九成集中在三个地方上下文膨胀、缓存失效、输出失控。这三件事有个共同特点——它们都不是你主动做的决定而是在系统演进过程中慢慢长出来的。等你在账单上看见它们的时候往往已经持续了一两个月。我在一个项目上做过一次完整归因把某个月的账单按调用来源切片结果很有意思真正的用户提问只占输入 token 的 11%剩下的 89% 里历史上下文占 47%系统提示与工具定义占 28%另外 14% 是各种我以为可以省略但实际每次都带上了的附加信息。这个分布和大多数人的直觉完全相反。2.1 上下文膨胀是最隐蔽的账单放大器多轮对话的成本增长不是线性的是近似平方级的。第一轮你带 1000 token 历史第二轮带 2000第三轮带 3000到第二十轮就是 20000。把二十轮加总总输入量是一开始单轮的 210 倍。这就是为什么一个聊着聊着就变贵的对话产品账单曲线看起来完全不符合直觉。解决这个问题有三条路各有取舍。第一条是滑动窗口只保留最近 N 轮。简单粗暴效果立竿见影代价是模型会忘事用户提到十轮前说过的内容时会出现明显的失忆感。第二条是摘要压缩把早期对话用一次额外的模型调用压缩成一段摘要替换掉原始历史。成本上很划算但引入了额外的一次调用开销和信息损失而且摘要本身也会随轮次增长。第三条是检索式记忆把历史对话切片存起来每轮根据当前问题只召回相关的几段。我个人的选择是混合方案滑动窗口打底保留最近 6 到 8 轮原文超过部分走摘要摘要定期合并。这个组合在成本和体验之间找到了一个比较舒服的平衡点。但关键是这件事必须在设计阶段就定下来而不是等账单爆了再回头改因为改动会牵涉整个提示词拼装逻辑。有一个反直觉的实测结论值得分享把历史上下文从 20 轮砍到 8 轮用户主观体验的下降远小于我的预期大概只有不到 15% 的会话会明显感觉到它不记得了。原因很简单真实对话里大量信息是冗余的用户重复提问、寒暄、修正说法这些内容占用了大量 token 却不携带有效信息。压缩这段性价比极高。2.2 缓存命中率单价差出一个数量级的指标缓存这件事机制本身不复杂模型服务端会把你这次请求的前缀和之前处理过的前缀做匹配如果前缀完全一致直接复用之前的计算结果。关键词是完全一致和前缀。这两个词决定了几乎所有缓存失效的原因。前缀不一致命中失败。哪怕你在系统提示最开头加了一个当前时间戳整段前缀就变了缓存全废。我见过最典型的一个案例是动态注入用户名你是一个助手现在正在和 {user_name} 对话。用户名一变前缀就断在最前面几个字符上后面的几万 token 全部重新计算。改法很简单把动态内容挪到前缀的末尾——先放完全静态的系统规则和工具定义再放所有静态示例最后才是动态拼接的部分。这样前 90% 的内容是稳定的依然能命中。匹配粒度也是一个坑。缓存通常按一定的 token 块为单位存储如果你的稳定前缀太短短于最小存储粒度那就根本不会被缓存下来。这意味着我只放了两百字固定人设很可能一分钱都省不到。我的经验是稳定前缀至少要达到几百 token 的规模缓存收益才值得期待。注意缓存的有效期通常不是永久的而且不同模型、不同服务端节点可能有差异。高温场景下的高频短请求很容易全部落空因为前缀还没被复用就过期了。做成本预估时把命中率按 0% 和按 70% 各算一遍取区间而不是取单点会更稳妥。监控命中率的方法不复杂每次调用都记录 usage 里的缓存命中字段和未命中字段累计之后算命中率。这个指标应该和 QPS、平均延迟一起放在监控面板上日维度看趋势。命中率突然从 70% 掉到 20%通常意味着有人改动了提示词模板或者上线了带时间戳、带随机 ID 的新版本。这类问题发现得越早损失越小。2.3 输出token为什么定价最高输出的单价高前面解释过算力原因但对做优化的人来说更值得关注的是输出的不可预测性。输入的 token 数你在发送前完全可控输出的 token 数你只能通过参数约束无法精确定死。这个不对称性是很多成本事故的来源。最常见的失控是模型话痨。你问一个可以用是或否回答的问题它给你三段分析加一段总结。在不加约束的情况下输出的平均长度会显著高于你的实际需求。这不是模型的问题是提示词没有表达清楚期望。解决办法是明确格式要求输出为 JSON、要求只输出一个字段、要求回答控制在多少字以内。这些约束的有效性远超多数人的预期实测下来输出长度通常能压掉一半以上。第二种失控是推理型任务的长输出。如果模型需要先思考再作答思考过程往往也计入输出 token 并且单价不低。这类任务不能简单粗暴地截断因为思考链条一断答案质量就崩。我的做法是给它一个明确的上限预算在提示词里说明如果信息不足以得出结论直接说明缺什么不要展开长篇推测把它的发散倾向收住。第三种最容易被忽视重试与超时。请求超时后你重发一次前一次如果已经产生了部分输出那部分可能照样计费。批量任务里如果重试逻辑写得糙一次失败能导致三次计费。这一点在后面排查章节会展开这里先记住结论重试必须有次数上限和退避策略且要区分可重试错误和不可重试错误。3. 从估算到落地一套能跑起来的预算与优化流程讲完原理进入实操。这一节给的是我自己在项目里跑了半年多的一套流程从估算到上线到复盘全部串起来。它不复杂但需要你把它变成习惯否则做一次就荒废了。整个流程分四步建立 token 账本、改造提示词结构、给输出设闸门、管住并发与重试。四步的顺序不能乱因为后一步的效果依赖前一步的数据。3.1 第一步建立自己的token账本没有账本一切优化都是凭感觉。账本要做的事很简单每次调用都记下模型名、输入未命中、输入命中、输出、耗时、业务标签落库之后按天、按业务线、按模型聚合。有了这张表你才能回答哪个功能最贵这种问题。下面是我常用的封装骨架核心就是别把 usage 丢掉import time import logging from dataclasses import dataclass, asdict dataclass class CallRecord: biz: str # 业务标签比如 chat、summary、extract model: str prompt_tokens: int cached_tokens: int completion_tokens: int latency_ms: int ok: bool def call_model(client, biz, model, messages, **kwargs): start time.time() try: resp client.chat.completions.create( modelmodel, messagesmessages, **kwargs ) usage resp.usage # 不同实现的字段名可能不同按实际返回取 cached getattr(usage, prompt_cache_hit_tokens, 0) or 0 rec CallRecord( bizbiz, modelmodel, prompt_tokensusage.prompt_tokens, cached_tokenscached, completion_tokensusage.completion_tokens, latency_msint((time.time() - start) * 1000), okTrue, ) logging.info(asdict(rec)) return resp except Exception as e: logging.warning({biz: biz, model: model, error: str(e)}) raise这段代码本身没什么技术含量价值全在每次都记这个纪律上。我要求团队里所有调用入口必须走这个封装禁止绕过它直接调 SDK。理由很实际一旦有旁路调用你的账单归因就会永远差一截而且这差额你还查不出来。有了原始记录接下来算三个派生指标。单次平均成本用总成本除以调用次数看趋势。缓存命中率命中 token 除以总输入 token低于 40% 就说明提示词结构有问题。输出输入比输出 token 除以输入 token这个比值超过 0.5 通常意味着输出侧失控尤其是那些本该短答的场景。这三个指标我每周看一次出现异常就往下钻。有一次发现某个业务的输出输入比突然从 0.2 涨到 1.1追下去是有人把输出的格式约束从返回 JSON改成了请详细说明你的判断依据改了两个字成本翻了三倍。3.2 第二步把提示词改造成缓存友好结构提示词的组织方式直接决定命中率。核心原则只有一条静态内容在前动态内容在后且静态部分要足够长。这条原则听起来简单但实际改造时有一堆细节。先看一个典型的反面写法system f当前时间{now} 当前用户{user_name} 你是客服助手需要遵守以下规则 1. ...此处省略三百字规则 2. ... 以下是产品知识库 {kb_text} 这段代码里now和user_name在最前面任何一个变化都会让后面所有内容的前缀失配。改造成缓存友好的版本system f你是客服助手需要遵守以下规则 1. ...此处省略三百字规则 2. ... 以下是产品知识库内容相对稳定建议单独维护版本号 {kb_text} --- 以下是本次会话信息 --- 当前时间{now} 当前用户{user_name} user_msg question # 用户问题永远放在 messages 的最后改造后前面那一大坨规则和知识库是稳定前缀时间戳和用户名挪到最后只有后面一小段是变量。实测命中率能从个位数拉到 70% 以上。再进一步把知识库按版本切片管理。知识库更新时把变更部分追加到末尾而不是原地修改这样旧前缀依然有效。这个技巧在知识库频繁更新的场景特别有用相当于把改内容和改前缀解耦了。几个实操细节值得注意不要在系统提示里放random、uuid、session_id这类每次都变的东西除非你真的需要模型知道它。不要在提示词开头放工具定义的动态排序工具列表顺序固定下来。多轮对话里把系统提示和工具定义固化在最前面历史消息追加在后不要每轮重新拼装顺序。如果用了提示词模板引擎注意模板渲染后不要产生随机空白或不一致的换行一个多余的空格也可能让前缀失配。提示改完提示词结构后别只看单次请求的返回是否正常一定要拉一次命中率数据对比。有些改造看起来合理实际因为触发了不同的路由节点命中率反而下降。以数据为准不要以直觉为准。3.3 第三步输出侧的三道闸门输出单价最高所以这里值得设三道闸门。第一道是max_tokens这是硬上限必须设而且要设得比你预期合理值略高一点但不能敞开。敞开的后果是在模型跑偏时无限生成一次请求能把一天的预算吃掉。第二道是格式约束。要求输出 JSON、要求只返回指定字段、要求用列表而不是段落。这些约束不只是为了解析方便它同时也是长度约束。我做过对比同一个信息抽取任务自由文本输出的平均长度是结构化输出的 2.8 倍而信息量完全一样。第三道是提示词层面的收敛指令。比如如果问题信息不足只输出缺少哪些字段不要推测、回答控制在两句话内、不要复述问题。这类指令对压长度的效果非常直接尤其对付先总结再回答最后再来一段温馨提示这种典型话痨模式。resp client.chat.completions.create( modelmodel, messagesmessages, max_tokens512, # 硬上限防止跑飞 temperature0.2, # 抽取类任务降低发散 response_format{type: json_object}, # 结构化天然压长度 )顺便说一句temperature和成本的关系。高温会让输出更发散、更长、更容易出现重复表述。在信息抽取、分类、摘要这类任务上把温度降到 0.1 到 0.3既能稳定输出格式也能顺带把长度压下来。创意类任务当然要放开但那是另一回事。3.4 第四步批处理、并发与重试的成本控制到了这一步单个请求的成本已经压得差不多了剩下的问题是总量控制和异常放大。批量任务的第一原则是能一次问完的不要分十次问。比如你有一百条短文本要分类逐条调用一百次每次都带一遍相同的系统提示那一百份系统提示就是纯浪费。改成一次请求里塞十条让模型返回数组总 token 能省下相当可观的比例。当然这里有个度塞太多会导致准确率下降我的经验是一次 10 到 20 条比较稳超过 30 条后模型对靠后条目的注意力会衰减。第二原则是并发要设上限。这看似是性能问题其实是成本问题。无上限并发在遇到服务端限流时会触发大量重试而重试是重复计费的重灾区。import random import time def call_with_retry(fn, max_retries3, base1.0): for attempt in range(max_retries): try: return fn() except RateLimitError: # 限流类错误退避后重试 sleep base * (2 ** attempt) random.uniform(0, 0.5) time.sleep(sleep) except (BadRequestError, AuthError): # 参数类错误重试没有意义只会白花钱 raise raise RuntimeError(retries exhausted)这段代码的重点不在退避算法而在那个except分支的划分。参数错误绝对不能重试。我踩过一次坑一个提示词模板里有个占位符没替换请求体超长被拒绝而重试逻辑没区分错误类型闷头重试了五次。那批任务跑了两万条等于白花了十万次调用的钱而且因为都是失败请求日志里连输出都没有只能从账单侧发现异常。还有一类隐性成本超时后已经生成的输出。请求超时了但服务端可能已经生成了一部分内容这部分是否计费取决于具体实现。稳妥的做法是把超时时间设得比你预期的最大生成时间稍长减少生成到一半被掐断的概率。流式场景下这个风险更高因为流式本来就慢。4. 分场景策略不同业务线的省法完全不同前面讲的是通用方法但真实项目里不同场景的成本结构差异极大一套参数打天下必然有一半是浪费。这一节我按三种典型场景给具体组合你可以对照自己的业务挑。4.1 长文档与代码库分析这类场景的特征是输入极长、输出相对短、单次调用成本高但调用频次低。成本结构里输入占八成以上所以所有优化都应该围绕输入。第一件事是把缓存吃满。长文档分析天然适合缓存因为文档本身是不变的变的只是用户的问题。正确的做法是把文档放在前缀位置问题放在最后这样同一份文档的多次提问只有第一次是全价。我做过一个项目用户平均对同一份文档提问 6 到 8 次命中率起来之后整体成本降了七成以上。第二件事是避免重复塞文档。常见错误是把文档塞进系统提示同时又塞进用户消息理由是怕模型忽略。这纯属浪费。更隐蔽的一种是检索增强场景里召回的片段和已经放进上下文的全文中重复。做切片检索时记得对召回结果去重并且在提示词里明确说明以下内容可能与上下文重复重复部分以全文为准。第三件事是控制输出粒度。长文档分析最怕模型做全文摘要。如果你只关心某个具体问题就只问那个问题不要让它先总结全文再回答。这两种写法的输出长度能差五到十倍。代码库分析还有个额外注意点代码的 token 密度高同样字符数下比中文更贵。而且代码里的注释、空行、格式化空格都算 token。如果只是做结构分析可以考虑预处理时压缩掉多余空白和注释实测能省 15% 到 25% 的输入量。当然涉及代码风格或注释质量的任务不能这么干。4.2 高频短问答客服、意图识别、简单问答这类场景单次成本极低但调用量巨大任何一个细微的浪费都会被调用量放大。这里的核心矛盾是系统提示不能太长但业务规则又确实很多。我的做法是把系统提示分层。最核心的几十条规则常驻占用几百 token长尾规则走检索根据用户问题召回相关条款动态拼接。这样平均输入能压到常驻版本的三分之一而覆盖率损失很小。另一个关键动作是给小模型分流。不是所有请求都需要最强模型。意图识别、简单FAQ、格式校验这类任务用小模型甚至规则引擎处理成本能降一到两个数量级。分流器本身也可以很轻关键词匹配加一个轻量分类模型就够了。我见过一个项目把 60% 的流量分流到小模型整体成本降了 55%而用户满意度几乎没变。注意分流策略一定要有兜底。当小模型置信度低、或者用户连续追问、或者检测到情绪激烈时要能自动升级到大模型。只省钱不管体验最后会以另一种方式还回去。高频场景还有一件事必须做结果缓存。相同或高度相似的问题直接返回缓存答案不调模型。这在客服场景里命中率往往能到 20% 以上。注意缓存键要做得聪明一点做归一化处理去标点、统一同义词、去掉语气词否则命中率会低得可怜。4.3 离线批量任务与数据清洗批量任务的特点是可以用时间换成本也最容易做精细的成本控制。因为它不要求实时性你有充分的空间做预处理、分批、失败重跑。第一件事是先小样本试跑。拿 100 条数据跑一遍量出真实 token 消耗和成功率再乘以总量做预算。跳过这一步直接全量跑是新手最常见的烧钱方式。我自己就干过一次一个清洗任务以为单条只要几百 token实际因为文本超长触发截断逻辑失败重试单条平均到了四千最后账单是预估的八倍。第二件事是做好断点续跑。批量任务跑一半挂了如果没有进度记录只能从头再来前面那些调用全部白花。用任务 ID 做幂等把已完成的条目落库标记重启时跳过。这个机制虽然不直接省钱但它防止的是最大的一笔浪费。第三件事是把不需要模型的部分剔出去。批量清洗里有大量规则可解的部分格式规范化、去重、空值处理、正则匹配。这些交给代码只把真正需要语义理解的判断交给模型。我通常会让数据先过一遍规则层能砍掉 40% 到 60% 的调用量。5. 踩坑实录与排查手册前面讲的都是应该怎么做这一节讲做错了长什么样。这些坑我基本都踩过列出来是为了让你少走一遍。5.1 账面消耗和预估差三倍从哪查账单和预估对不上时不要凭感觉猜按固定顺序排查通常五分钟内能定位。我的顺序是这样的先看调用次数是否和预期一致。某些框架在内部会自动重试或者你的客户端在超时后重发导致实际调用数远高于业务请求数。这一条能解释最多的偏差。再看输入 token 均值。如果均值明显高于你的估算八成是历史上下文或者工具定义膨胀。把某一条真实请求的完整 messages 打出来看一眼问题一目了然。我遇到过一次某框架把上一轮的完整响应也塞回了下一轮的 messages等于每轮都把输出又当输入付了一次钱。然后看输出 token 均值。输出均值突然翻倍通常是模型版本变更或者提示词被改动。查一下最近的发布记录对照一下模板 diff。最后看缓存命中率。命中率掉了成本自然上去。这一项在账单上不会单独列但对总成本影响最大。现象最可能原因快速验证方法处理动作调用次数异常偏高框架自动重试、客户端超时重发对比业务日志与 API 日志条数加幂等键、限重试次数输入均值高历史上下文未压缩、工具定义过多打印单条请求的 messages 全文加滑动窗口、精简工具描述输出均值高提示词改动、格式约束丢失抽样看原始输出内容恢复格式约束、设 max_tokens命中率骤降前缀混入动态内容、模板渲染差异连续两次相同请求比对 usage静态前置、动态后置单条成本忽高忽低长尾超长文本未做切片统计输入长度的分布而非均值超长文本切片处理提示统计输入长度时一定看分布而不是平均值。平均 2000 token 的任务里可能藏着 5% 的条目单条就要 50000 token这 5% 往往贡献了过半成本。把 P95 和 P99 拉出来看问题通常就在那。5.2 缓存看起来生效了但其实没命中这是最让人抓狂的一类问题因为看起来一样和实际一样是两回事。前缀匹配是逐 token 精确比对的一个字符不同就前功尽弃。我整理过一份实际排查清单按出现频率排序模板渲染产生了不可见差异。比如不同环境下的换行符、多余的尾部空格、模板变量为空时的残留标点。键的顺序不稳定。JSON 序列化如果没指定排序不同次序列化出来的键顺序可能不同前缀就不一致了。时间戳或随机 ID 混入了前缀。这一条我在不同项目里反复见到包括藏在当前时间、藏在 session 标记里、藏在日志追踪 ID 里。工具定义的顺序或描述变动。有些框架会按字典遍历顺序输出工具定义字典变了顺序就变了。前缀长度不够。稳定部分太短短于缓存的最小粒度等于没缓存。换了模型或节点。缓存通常是按模型、按节点隔离的切换之后自然命中率归零。验证方法很直接连续发两次完全相同的请求比对 usage 里的命中字段。第一次应该是零第二次应该有值。如果第二次还是零就构造一个最小复现用例逐步删减提示词内容直到命中为止那个被删掉的部分就是元凶。这个方法笨但极其有效我用它定位过好几次藏得很深的动态字段。5.3 问题速查表与几条长期习惯最后整理一份可以贴在工位上的速查表都是高频问题的直接答案问题直接处理对话越长越贵滑动窗口保留 6 到 8 轮超出部分摘要压缩同样问题重复收费加结果缓存层缓存键做文本归一化提示词改一点成本翻倍检查是否破坏了静态前缀比对命中率输出话痨max_tokens JSON 格式约束 明确长度指令批量任务失败重跑任务幂等 进度落库跳过已完成单条任务费用异常看输入长度分布抓 P99 长尾切片处理换了模型成本乱跳重新标定单价与分词效率不要沿用旧估算除了这些具体动作还有几个我认为值得长期坚持的习惯。一是每次发布前跑一遍成本回归。拿一组固定的代表性请求记录改动前后的 token 消耗和命中率变动超过 20% 就必须说明原因。这一条拦下过好几次顺手改了个提示词导致的成本事故。二是给每个业务线设月度预算上限和告警线。超过 80% 就告警超过 100% 就限流降级。这不是不信任团队而是给意外加一道保险。真出事的时候早一天发现和晚一天发现差别可能是几万块。三是定期做一次全量归因。每个月花半个小时把账单按业务、按模型、按调用类型切一遍找出那个看起来不起眼但占比惊人的项。我的经验是每次都能找到至少一个可以立刻优化的点而且通常是那种原来这个功能这么贵的意外发现。四是不要迷信单一模型。模型在迭代价格也在变半年前的最优解今天可能完全不是。我现在的做法是每季度重新跑一次基准测试用同一组任务对比几个候选模型的成本和质量按任务类型重新分配路由。这件事花不了多少时间但每次都能挤出一点空间。关于 token 计费这件事我用一句话总结自己的体感它不是一门需要天才的学问而是一门需要纪律的学问。原理就那么几条难的是把账本记起来、把命中率盯住、把回归跑起来坚持三个月你对成本的掌控力会完全不一样。最怕的是那种反正也没多少钱的心态等调用量涨十倍的时候你会发现自己连钱花在哪都说不清楚。