
1. 事故现场还原Token激增与成本超标是从哪儿开始的1.1 业务没涨Token账单却翻了四倍凌晨两点告警群被一条消息炸醒。Openlaw网关的Token日消耗量在三个小时内翻了四倍成本监控面板上的曲线像坐过山车一样往上冲。按当时的速率估算月底账单会比预算高出六位数。这个事故最让人困惑的地方在于业务量没涨、用户量没涨、模型没换、价格没调所有常规指标都平稳得不能再平稳唯独Token成本在疯狂飙升。Openlaw是法律问答场景的AI服务用户会把合同条款、判决书片段贴进来进行多轮法律咨询。这类请求天然是输入重、输出轻的结构贴一份租赁合同就是四五千字的输入系统提示词又要求模型扮演法律专家、输出严谨的分析意见。单个请求的Token消耗本来就比普通闲聊高一个量级。在这个背景下网关层任何一次多余的转发、一次不必要的重试、一段被反复携带的历史对话都会被放大成数倍的账单。我接手时的第一反应是有人在刷接口。但翻完网关访问日志这个可能被排除了——请求量、并发数、来源IP分布全部正常。问题一定出在单个请求的Token消耗结构上而不是请求数量上。这个判断方向后来被事实证明完全正确。带着这个思路去比对网关日志和模型计费日志很快就把异常请求的共性特征找了出来。1.2 为什么网关是排查Token成本的第一现场Openlaw的架构里API网关是所有流量进入模型服务之前的最后一站。认证鉴权、限流熔断、请求日志都在这一层完成。按道理说Token的计量和成本管控也应该落在这里。但现实情况是大多数网关只做了QPS级别的流量控制完全没有Token维度的计量能力。你问网关运维今天烧了多少Token他基本答不上来。你能看到每秒请求数、错误率、P99延迟却看不到每个请求带了多少输入Token、模型生成了多少输出Token、这些Token分别被哪个业务方、哪个接口、哪个用户消耗。没有这层数据成本超标就只能等月底收到账单之后被动认账。这个问题在很多团队都存在直到账单一来才着急但那时已经晚了。所以我处理Openlaw这个问题的第一步不是改代码而是先把网关访问日志和大模型服务的调用日志对齐逐条重建每个请求的Token消耗明细。这一步做完之后导致成本超标的四个泄漏点全部浮出水面。下面我把这四块逐一拆开讲。这些问题在绝大多数AI应用的网关层都普遍存在排查思路也是通用的换个产品名就能照搬。2. 根因定位Openlaw网关链路里的四个Token泄漏点2.1 多轮会话历史被全量重复携带法律问答几乎都是多轮交互场景。用户问我在县城买了套商品房开发商逾期交房怎么办网关把问题转发给模型用户接着追问那我能不能退房网关转发时会把前面所有轮次的对话历史完整带上。看起来合理但细算一下触目惊心。我拉了一份典型请求日志一个会话聊到第8轮时单次请求的输入Token已经接近1.2万而本轮用户真正新增的内容只有两百多字。前面7轮的问答、用户最早粘贴的购房合同条款、系统提示词全部原样再送一遍。会话越往后每轮请求携带的历史就越臃肿Token消耗几乎是线性往上推的。我做过一个简单的成本测算假设每轮平均携带6000Token历史、用户本轮新增200Token一个会话聊到第10轮时单次请求的输入Token约6.2万。而前面9轮累计消耗的Token已经超过10万。一个用户在一个会话里烧掉10万Token100个这样的会话就是1000万。多轮历史这个泄漏点通常能解释30%到50%的异常成本。这还是保守估算没有算上输出Token。2.2 网关重试风暴让同一请求被多次计费第二个泄漏点藏在网关的重试逻辑里。为了保证可用性网关一般都会给上游配置超时重试和5xx错误重试。但大模型服务有个本质区别请求一旦到达模型端并开始生成算力和计费就同时发生了。哪怕模型已经生成了一半网关因为超时判定失败并触发重试前面那一次已经产生的费用也不会退回。Openlaw的网关当时配置了最多3次重试超时时间15秒。法律场景的回复本来就长一个严谨的法律意见经常要生成七八百甚至上千Token慢的时候十几秒出不来很正常。结果就是一个正常请求因为响应稍慢被网关误判为超时自动重试两次模型端实际执行了三次完整生成账单直接乘以3。更隐蔽的是部分网关的重试逻辑会把原始请求体再发送一遍而请求体里已经带上了整段对话历史。重试一次相当于把同一份历史又完整送了一次。这种损耗叠加起来非常可观我在日志里见过一个极端案例一次业务请求最终触发了8次模型调用成本是最初预估的8倍。2.3 系统提示词与工具描述持续膨胀第三个泄漏点是系统提示词的无序膨胀。Openlaw最初给模型设定的提示词很简单你是资深法律顾问回答要严谨大约400Token。后来产品功能越来越多提示词里陆续加入了回答需引用具体法条输出格式需包含风险提示需区分事实与法律意见等要求。同时网关还集成了法条检索、案例检索、文书模板生成三个工具每个工具的描述和参数Schema都有几百Token。一年跑下来每次请求的固定Token消耗从400涨到了2500左右。这部分Token是典型的固定税每来一个请求就得付跟用户问什么无关也不产生增量价值。对于日请求量过万的服务每天就是几百万Token的固定支出一个月下来是千万级。工具描述尤其容易被忽略。如果网关每次请求都把全部工具的定义发给模型哪怕模型这次根本没有调用任何工具这些描述也照常计费。我当时把三个工具的描述精简了一遍又改成按需携带——只有请求路径涉及对应业务时才附加工具定义。这一步就减少了约15%的输入Token算是在提示词层面捡回了一笔钱。2.4 缺少语义缓存重复请求反复烧钱第四个泄漏点是缓存缺失。法律问答有很强的重复性同一个问题会被大量用户反复提问。劳动仲裁时效是几年离婚冷静期怎么算这些问题的标准答案高度稳定但当时Openlaw没有任何缓存机制。每个用户问一次网关就转发一次模型就完整生成一次Token就烧一次。还有一种更常见但也更难处理的场景相似但不完全相同的请求。用户A贴了一份房屋租赁合同用户B贴了一份几乎相同的模板合同模型对两份合同的回答至少有80%重合。但系统把每次请求都当作全新任务处理两份合同文本各自完整送入模型Token就烧了两份。合同文本动辄四五千字折算成Token就是四五千的输入成本这部分几乎全部可以通过语义缓存省下来。等我在第3章讲具体做法时你会看到这个方向的收益最大也是四个泄漏点里投入产出比最高的一个。3. 网关侧优化从拦截到压缩再到缓存的完整方案3.1 先立规矩Token预算控制的双层设计定位了四个泄漏点之后我开始在网关层动手改造。第一件事是建立Token预算控制分两层单请求预算和单会话预算。单请求预算限制每个请求最多携带多少输入Token超出就触发裁剪单会话预算限制一个会话在其生命周期内累计消耗多少Token超出就提示用户开启新会话。预算数值怎么定我没有拍脑袋而是统计了Openlaw两周的生产流量80%的正常请求输入Token在4000以内95%在6000以内。于是把单请求预算定为6000单会话预算定为30000。这样既不影响绝大多数正常请求又能把异常的大请求拦住。这个数字需要根据自己业务的请求分布来定直接抄作业不一定合适。实现上我写了一个网关头过滤器在请求转发之前先估算输入Token。估算不追求精确——用分词器逐个数Token在网关上太慢了会拖垮性能。我用字符和Token的经验比例中文大约1个汉字对应0.7到1个Token英文约4个字符对应1个Token。做预算拦截和裁剪判断完全够用。// 网关Token预算过滤器核心逻辑伪代码 public class TokenBudgetFilter implements GatewayFilter { private static final int MAX_INPUT_TOKENS 6000; private static final int MAX_SESSION_TOKENS 30000; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String sessionId exchange.getRequest().getHeaders() .getFirst(X-Session-Id); String body exchange.getAttribute(cachedRequestBody); int estimatedTokens estimateTokens(body); int sessionUsed tokenStore.getSessionUsage(sessionId); if (sessionUsed estimatedTokens MAX_SESSION_TOKENS) { return respondWithMessage(exchange, 上下文过长请开启新会话后继续提问); } if (estimatedTokens MAX_INPUT_TOKENS) { String trimmed trimContext(body, MAX_INPUT_TOKENS); exchange.getAttributes().put(cachedRequestBody, trimmed); } return chain.filter(exchange); } }这里有个一定要提醒的坑预算过滤器必须在请求体缓存之后、转发之前执行。Spring Cloud Gateway默认不会缓存请求体需要先用一个专门的过滤器把Body读出来存到exchange属性里后面的过滤器才读得到内容。我第一次实现时就是过滤器执行顺序写错了拿到的是空BodyToken估算全部失效排查了大半天。3.2 上下文智能裁剪把历史瘦身再转发预算控制解决的是超了怎么办的问题要真正把成本降下来还得让请求内容本身更精炼。我在网关里做了一套上下文的智能裁剪策略分三层最近的3轮对话完整保留保证模型对当前问题的理解连贯更早的对话用摘要压缩把每一轮的核心信息浓缩成一两句话已经被摘要覆盖的原始历史直接丢弃不再携带。摘要生成本身要调用一次模型所以触发频率必须控制好。我的策略是会话到达第6轮时做第一次压缩之后每增加5轮再压缩一次。这样既不会因为频繁压缩而增加额外的模型调用成本又能把历史记录的长尾膨胀控制住。实测下来一个20轮的会话平均每次请求的输入Token从1.2万降到了5000左右。另外针对用户频繁粘贴长文本的场景我在网关层加了一个轻量的文本清洗步骤去掉连续的空行、多余空格、无意义的备注和格式标记。这层清洗不调用模型纯文本处理成本几乎为零却能让长文本的Token消耗减少10%到15%。不要小看这个比例对于每天都有人贴合同、贴判决书的法律产品这是实打实的省钱。3.3 语义缓存落地高频法律问题只付一次费语义缓存是这次优化里收益最大的一步。具体做法是用户的输入到达网关后先编码成向量到向量数据库里做相似度检索。如果找到相似度超过0.92的历史问题和标准答案直接返回缓存结果不再调用模型没有命中才走正常链路同时把问题和标准答案写入缓存。法律场景有个特殊性很多问题表面上相似但背景差异极大。离婚财产怎么分割和离婚财产怎么分割房子是婚前父母出资买的这两句话的向量相似度可能很高答案却完全不同。如果只看句子相似度就命中缓存会出大问题。所以我把阈值设得比较高0.92并且把用户本轮粘贴的材料也计算一个语义指纹放进缓存键。用户带了新材料即使是同样的提问也直接绕过缓存走模型。这样在省成本和回答质量之间取了一个比较稳的平衡。缓存上线后的效果非常直接高峰时段的模型调用量降了34%Token成本降幅接近三成。而且网关从缓存取结果的响应时间是毫秒级用户完全感知不到差别体感反而更快。向量数据库的选型建议用轻量方案比如开源的Milvus或者单机的ChromaOpenlaw这种百万级问题量的规模完全不需要上重型的分布式向量库。3.4 模型路由与降级成本告急时的自动开关最后一项优化是模型路由和降级。Openlaw的业务中法条检索案情要素抽取这类结构化任务用便宜的小模型就能做得很好而争议焦点分析合同风险审查这类复杂推理任务必须用能力最强的模型。在优化之前所有请求都走同一个模型相当于用跑车的油耗去跑买菜的路成本必然偏高。我在网关里增加了一个路由策略根据请求路径或请求体里的业务字段动态选择模型供应商和模型规格。同时配置了降级规则当高规格模型的调用成本达到当日预算阈值的80%时自动把非核心流量降级到低成本模型保证核心业务先活着。流量回落、成本压力解除后再自动恢复原路由。这个策略的日常收益不明显但在极端情况下的价值极大。比如新功能上线导致请求量短时间翻倍如果没有降级规则月底账单直接爆掉有了降级规则系统会牺牲部分非核心请求的回复深度把成本牢牢压在预算线内。这个自动开关在Openlaw后续的活动流量高峰期顶住了好几次压力是我认为最值得做的一项保险。4. Token可观测性建设把每一分钱都记在账上4.1 四张关键指标表盯住Token消耗趋势优化做完之后我心里很清楚一件事如果不把Token计量做扎实过不了多久新的泄漏点还会冒出来而且不会有人提前发现。所以可观测性建设必须跟上。我沉淀了四个核心指标每次成本复盘只看这四个数指标定义用途输入Token数每次请求送入模型的Token总量观察上下文膨胀趋势输出Token数模型生成的Token总量观察生成长度与业务目标是否匹配缓存命中率语义缓存命中的请求占比评估缓存收益发现失效原因Token转化率输出Token / (输入Token 输出Token)衡量成本中真正产生价值的比例Token转化率是我最看重的一个指标。它回答的问题是花掉的每一块钱里有多少真正变成了给用户看的回复。Openlaw正常场景下转化率应该在25%左右改造之前这个数字一度低到6%。换句话说94%的Token都消耗在了输入侧真正的回答只占6%成本结构明显失衡。把这四个指标纳入看板之后成本变化的原因基本一眼就能看清楚。4.2 告警阈值这样设既不漏报也不误报告警设置也有讲究。只按Token总量的绝对值设阈值很容易误报业务高峰期Token用量天然高凌晨流量又特别低每天都有强烈的周期性波动一个固定阈值根本没法覆盖。我的做法是双层告警。第一层是环比告警对Token消耗按小时做同比和环比如果上午10点到11点这个时间段比昨天同一小时高出50%以上就触发告警。这种告警能敏锐捕捉到突发的Token激增。第二层是预算消耗速率告警用当月剩余预算除以剩余天数得到每日可消耗额度再除以24得到每小时的理论消耗上限。如果实际消耗速率连续30分钟超过理论上限的80%就触发预警。预算倒推速率的告警设计是我比较得意的地方。它的意义在于不用等到月底看账单当天就能预判这个月的预算能不能兜住而且它天然和企业的预算管理绑定。老板问起来的时候你能直接报出按当前速率这个月预计超支X%而不是含糊地说感觉有点高。另外务必给每个业务线、每个接口打上独立的标签。否则一份账单摊在所有业务上出了成本波动根本说不清是哪个功能在烧钱。Openlaw的网关在转发请求时会把业务来源写入日志的MDC日志采集后按标签聚合每日成本报表就能精确到哪个页面、哪个功能、消耗了多少Token归因效率高了一个数量级。4.3 全链路日志与成本归因Token成本排查的效率和调用链日志的完整性直接挂钩。我给每次网关转发生成一个全局的requestId贯穿API网关、业务服务、模型调用三个环节。日志里记录请求路径、会话ID、业务标签、输入Token估算值、模型返回的实际Token用量、缓存是否命中、耗时、响应码。这套日志的价值在排查问题时最能体现。比如用户投诉同样的操作Token用得太快直接按会话ID查日志看哪一轮请求消耗了异常多的Token是历史堆积还是文本异常一目了然。再比如新功能上线之后成本波动按业务标签聚合对比很快就能锁定是哪条链路新增了成本。没有这套全链路日志面对成本问题就只能靠猜。5. 常见问题与排查技巧实录5.1 别把认证Token和LLM计费Token混为一谈排查这类问题第一个容易翻车的点是概念混淆。网关本身就有一套基于JWT的认证机制用户登录后拿到accessToken请求时带着它做身份校验。这个认证Token跟大模型计费的Token完全是两码事——一个是身份凭证一个是文本切分和计费的基本单位。我接手Openlaw时前任在网关日志里特意记录了请求头Authorization字段的长度标注说用户Token太大导致网关性能问题。实际上那是JWT一两个百字符很正常跟模型计费没有半毛钱关系。为了这个问题之前的人还专门给JWT做了压缩优化白忙活一场。所以无论是自己排查还是看网上的文章先分清楚说的到底是哪个Token否则方向就错了。5.2 网关统计与账单对不上三个原因另一个高频问题是网关自己统计的Token总量和模型供应商账单上的数字对不上而且通常是账单更多。原因主要有三个。第一重试没有被完整记录。网关侧一个转发动作可能触发多次模型调用但日志只记了第一次重试产生的消耗全部漏掉了。第二异步任务不在主链路里。Openlaw的部分文书解析任务在请求返回后异步调用模型做摘要这些消耗在网关主链路的日志里完全不体现。第三估算误差。网关用字符估算的Token数和模型实际计费存在偏差中英文混合、大量数字和标点的情况下偏差可以到15%以上。处理原则就一条以模型返回的usage字段为唯一计费依据。每次模型调用返回后把usage里的输入Token数和输出Token数写进网关日志用它去跟账单对账。网关自己估算的数值只用于预算拦截和实时判断不承担计费职责。5.3 优化效果反弹先看这两张图Token成本优化不是做一次就一劳永逸。业务每迭代一轮新的泄漏点就可能冒出来。我经历过一次典型的反弹语义缓存上线后效果很好但过了两个月产品在回复里新加了引用具体法条的能力每次模型都要基于最新法条库生成内容导致缓存命中率断崖式下跌。如果只盯总体成本趋势根本看不出原因把缓存命中率和业务变更记录一对照立刻就知道是功能改动引起的。所以后来我养成了一个习惯每次成本出现波动先看两样东西。第一业务变更记录——最近一周有没有人改提示词、接新功能、调模型参数第二Token转化率和缓存命中率——判断是输入膨胀还是输出增长是缓存失效还是重试激增。这套先看变更、再看指标的排查流程在Openlaw和另外几个项目里用了很久又快又准基本没失手过。这里也分享一个我自己的体会Token成本优化归根结底是九个字——先计量、再优化、后治理。计量不到位就动手优化大多时候是在瞎猜优化完不做持续监控问题过几个月必然复发。如果你的网关现在连每个请求的Token消耗都统计不出来第一件事不是写缓存而是先把Token计量补上。