
先说结论Openlaw 网关某一周 Token 消耗环比涨了 4.2 倍账单金额直逼当月预算的 87%。我盯着监控面板看了十分钟第一反应不是模型变贵了而是网关层某个开关失效了。做过 AI 应用的人应该都懂网关一旦开始无脑转发请求Token 就像漏水的龙头关不住也堵不上最后只能看着成本曲线在报表里画出一条垂直向上的线。这里先解释一下背景。Openlaw 是一个面向法律场景的 AI 平台主要做合同审查、法规问答、裁判文书结构化这类事情。法律文本本来就长一份合同动辄两三万字监管规定又常带着几十个条款所以在 Openlaw 的调用链路上Token 天然就是敏感指标。我负责的是 AI 网关层所有业务方请求大模型都必须经过这一层也是在这里统一做鉴权、限流、计量和计费。因此当成本超标这件事发生的时候第一现场不在模型服务商也不在某个业务代码而就在网关的访问日志和用量明细里。这篇文章算是我对这个问题的完整复盘包括 Token 激增的根因分析、成本归因方法、优化方向和落地细节。如果你也在做 AI 网关、模型代理层或聚合 API 平台或者你只是被大模型账单吓到过我相信这里面的方法和踩坑记录都能直接用上。1. 问题背景先从一次“Token 成本超标”说起1.1 Openlaw 网关在这条链路里到底做了什么Openlaw 的架构并不复杂核心链路是客户端请求进入业务服务业务服务拼装 Prompt然后通过内部网关统一转发到多个大模型接口。网关在这里承担的不只是反向代理它还会做三件重要的事第一把不同模型提供商的接口格式统一成内部标准第二把每个请求的模型名、业务线、用户维度、Token 用量记录下来第三控制并发、超时和重试策略。听起来很常规但问题恰好出在“统一”和“控制”上。一旦业务方认为网关会自动处理重试、会自动裁剪上下文、会自动切换模型他们就会在代码里少写很多防御逻辑。法律业务的需求千奇百怪有的业务方会把整部法规塞进 Prompt有的会一口气发起 20 个审查子任务还有的在模型返回超时后立刻再来一次相同请求。这些做法在业务侧很难直观感受到成本压力因为对业务方来说只是“多调用了一次接口”但在网关侧每一次转发都是在燃烧 Token。而且法律场景有它的特殊性文本必须精读问题通常需要很长的上下文。同一个用户在多轮问答中既往对话可能积累几万 Token同一个合同审查请求可能需要把合同全部输入之外还要追加法条、公司风险偏好、行业规范。这种“大上下文 长会话 多子任务”的组合是 Token 激增的天然温床。1.2 Token 激增的现场数据本次问题是在一个周三下午被成本告警触发发现的。告警规则是“当日 Token 消耗超过 7 日均值 2 倍”时发送通知而实际上周五当日消耗已经飙到 7 日均值的 4.6 倍。从网关日志里抽几组典型数据看会更直观单次对话类请求的平均 Token 消耗从优化前的 1.2 万涨到 3.8 万某个合同审查入口一次请求最高消耗 12.7 万 Token重试请求占总请求量的比例从 2% 升到了 9%同一用户的并发会话数没有明显增加说明不存在“真实用户暴涨”的合理性4 个模型接口中有 3 个的输入 Token 占比超过 95%输出 Token 几乎没有异常。最后一组数据非常关键。输出 Token 正常说明不是模型在“长篇大论”而是我们送进模型的上下文被无限放大了。结合业务时间线发现最近上线了一个“法规知识库增强”功能用户提问后会把检索到的法条片段、司法解释、相似裁判文书全部拼装到 Prompt 里。出发点是好的但没有任何上限控制最极端的请求拼装了 6 万字材料。再加上多轮历史对话本身已经很长最终一个普通问题也可能触发几万 Token 的输入成本。1.3 为什么说网关是排查成本问题的最佳观测点刚开始业务团队觉得是不是模型服务商计费出错了我直接把网关里的 JSON 日志翻出来按 request_id 还原了每一次完整请求。相比直接在模型服务商后台看账单网关日志有不可替代的优势它记录了业务请求的原文、路由信息、Token 用量、响应码和耗时。只要网关在设计之初就把 usage 字段原样记录你就能回答“哪些业务线烧钱”“哪种请求烧钱”“是输入贵还是输出贵”这三个核心问题。所以遇到 Token 成本激增永远不要急着去调模型参数先把网关日志当作第一手数据库来查。数据不会骗人具体怎么查下一个章节详细讲。2. 核心归因Token 激增背后的五个“吃钱黑洞”2.1 先把 Token 到账单的换算规则说清楚大模型计费的传统是按 Token 单价乘以用量输入和输出往往价格不同。以一个常见模型举例假设输入每百万 Token 5 美元输出每百万 Token 15 美元。那么一次消耗 1 万输入 Token 的请求成本是 0.05 美元但如果这次请求因为超时被自动重试了 3 次成本就直接变成 0.2 美元而业务侧看到的“失败返回”数量可能只有一条。如果再把这种重复放大到整个网关一个 6 万输入 Token 的重型请求每次成本可能在 0.3 美元以上。重试三次就是 0.9 美元1000 个用户同时触发就是 900 美元。换算成人民币之后这就是一笔完全不该花的钱。所以在做成本优化时我习惯把 Token 账拆成四块真实业务消耗、重试导致的重叠消耗、上下文无增长带来的无效消耗、以及模型选择不当造成的高单价消耗。这四块分别对应四种不同的优化手段不能混在一起谈。2.2 现场拆解五个最可能的激增源第一个黑洞是没有设置历史对话上限。Openlaw 的法规问答场景里用户经常会连续追问比如先问“合同违约金上限多少”再问“那定金呢”最后问“两个能并用吗”。这些单轮问题看似很轻但如果网关在拼装 Prompt 时把用户进来之后的所有历史消息都带上了三轮、十轮之后上下文就会不断膨胀。法律用户在真实场景中甚至能连续问几十轮最夸张的一个会话历史超过了 50 万 Token模型光读上下文就要花掉十几秒费用当然压不住。第二个黑洞是失败重试没有退避策略。网关里配置了重试但很多团队第一次做的时候只会配一个简单的“失败就重试”。尤其是遇到上游模型接口返回 429限流或 503服务暂不可用时重试如果立即执行不仅会继续被限流还会把每次失败请求的上下文重复发送出去。Openlaw 曾经发生过一次上游服务抖动10 分钟内网关自动重试了 6400 次而这 6400 次里面超过 70% 请求的输入内容完全相同Token 消耗直接翻倍。第三个黑洞是系统提示词和指令模板越来越长。为了优化输出质量团队会不断往 System Prompt 里加规则比如“你是一名资深法律顾问”“回答必须引用具体条文”“禁止编造法条”“如果信息不足要明确说明”。单条规则不贵但累计起来很容易超过 2000 Token。这个值乘以每天几十万次请求就是每月的固定浪费。更隐蔽的是有些模板会包含大量示例Few-shot 示例一多系统提示词甚至能到 5000 Token。第四个黑洞是 RAG 召回结果不做去重和裁剪。知识库增强功能上线后网关每次会把检索到的所有片段全部拼进 Prompt。问题是检索算法基本不会只返回 3 条为了追求召回率通常返回 10 条甚至 20 条而这些片段之间存在大量重叠和重复。更夸张的是有些片段是整页裁判文书明明有效信息只有“法院认为合同有效”这一句话却把前后三页都带了进来。第五个黑洞是批处理类任务没有流控。Openlaw 有一个老合同批量审查功能用户上传一个 Excel 表格系统会对每一份合同调用一次模型。这个功能本意是好的但业务方会在同一个请求里并发 50 份合同每份合同 8000 Token一次性产生 40 万 Token 的用量。如果这种批量任务密集出现网关又没做排队就会在几分钟内烧掉平时一天的成本。2.3 实际排查方法从 Trace 到 SQL 的完整链路排查 Token 激增不能靠猜要做一次数据还原。我在这次问题中使用了如下方法你完全可以直接复制。首先确认总趋势。从网关在 MongoDB 里的调用日志表查出按小时聚合的 Token 消耗SELECT date_trunc(hour, request_time) AS hour, model_name, COUNT(*) AS request_count, SUM(input_tokens) AS total_input_tokens, SUM(output_tokens) AS total_output_tokens, SUM(input_tokens) SUM(output_tokens) AS total_tokens FROM gateway_request_log WHERE request_time now() - interval 7 day GROUP BY 1, 2 ORDER BY total_tokens DESC通常这条 SQL 就能看出成本大头集中在哪个模型、哪个小时段。第二步按业务线下钻。保证每个请求都带有 business_line 和 user_id 标签是网关设计的底线。然后继续查业务线占比SELECT business_line, COUNT(*) AS cnt, SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens, AVG(input_tokens) AS avg_input_tokens FROM gateway_request_log WHERE request_time now() - interval 7 day GROUP BY business_line ORDER BY input_tokens DESC第三步查重试比例。这一步最关键。我先找到所有 retry_count 0 的请求再关联到原始请求的 prompt_hash。prompt_hash 是网关在转发前对完整 Prompt 内容做的哈希值它让我能用 SQL 找出“完全相同的请求被发送了多少次”。查出来的结果触目惊心某个 2 万 Token 的重型请求在 3 分钟内被原样重试了 18 次原因是下游服务偶发超时而网关的重试指数退避配置没有生效。为了长期监控这个指标建议建一张“Token 异常波动”的汇总表保留每日数据。以后再做优化时直接盯这张表的趋势就够了。3. 优化方向设计怎么把成本重新装回笼子里3.1 上下文瘦身从源头控制输入 Token成本超标问题爆发后我们立刻开始设计上下文治理方案原则只有一个能用得越少就绝不多送一个 Token 给模型。第一步给多轮会话加硬上限。网关不再无条件拼接所有历史消息而是采用滑动窗口。比如普通问答最多保留最近 6 轮对话如果整体上下文超过 12000 Token就把更早的历史消息改写成摘要。这个摘要可以由一次轻量模型调用生成也可以直接用简单的截断策略。对很多法律咨询场景“把前几轮问题压缩成一句主题”完全够用不需要保留原文逐字信息。第二步对上下文做分段定位。合同审查类任务并不需要一次把整份合同全部塞进模型。可以先把合同按条款切块每块配合特定问题独立请求模型。比如“审查违约责任条款”只传入违约相关的 3 个条款而不是整个合同全文。这样即使合同有 5 万字单次请求的 Token 也能控制在 5000 以内。第三步在 RAG 侧增加召回结果裁剪。这里推荐“先粗排后精排再拼接”的做法检索器先召回 20 条重排器取 top5最后按内容去重并限制每个片段最大加入 800 Token。如果超过总量上限按相关度从高到低截断。另外要对检索片段做冗余检测两段文本若相似度超过阈值只保留更完整的那个。实现上上下文管理不应该散落在业务代码里而应该在网关层做统一。网关收到业务方的请求时检查请求里的 messages 结构如果超出限额就自动调用摘要模型或直接执行裁剪策略。这样业务方不需要关心底层细节模型服务的质量也更容易稳定。3.2 Prompt 工程和输出层的成本治理上下文瘦身解决的是“历史消息太长”和“原始材料太多”的问题但还有一类浪费来自我们自己的指令。系统提示词需要做一次“减脂”。我在优化时建了一个提示词资产库把每条指令拆成“必要性标签”分成核心规则、安全规则、示例、格式要求四类。核心规则和安全规则必须保留但示例尽量精简到 1 个格式要求能用一句话说清的就不写五句话。删完后模型响应质量没有变化但固定成本下降了约 25%。另一个很值得做的优化是要求模型只输出增量信息。过去业务方会要求“把修改后的完整合同返回”这会带来大量输出 Token而输出 Token 往往比输入贵 3 倍。改成返回结构化修改建议比如只输出“修改位置、原文引用、修改后内容、修改理由”输出量能缩减到原来的五分之一。同时打开模型的 JSON 输出模式网关在拿到结果后再转换成业务需要的格式。还要注意 Stop Token 和最大输出长度限制。有些模型默认最大输出 4096 Token如果业务逻辑只需要 200 Token 的结论就应该在请求参数里把 max_tokens 拉低。Openlaw 问答场景里我们统一将绝大多数接口的 max_tokens 设为 800把超长输出能力留给少数确实需要生成完整文书的接口。3.3 网关侧能做的事限流、缓存和模型路由网关不仅可以做被动转发还可以主动承担成本治理职责。限流是防止突发批处理用量的最重要手段。我们不能阻止业务方上传批量任务但可以在网关层给每个用户或每个业务线配置 QPS 上限和 Token 每分钟消耗上限。一个很实用的做法是 Token Bucket 算法每个用户每分钟允许消耗的总 Token 数固定超过后排队等待。这样即使有用户一次上传 100 份合同系统也会匀速处理不会在 5 分钟内把成本烧穿。缓存是本文成本优化里性价比最高的手段。语义相同或接近的请求不需要每次都去调用大模型。比如法律问答里用户经常问“合同违约金的上限是多少”换成问题“民法典关于违约金的规定”本质查询目标相同。网关层可以对用户输入做 embedding然后存到向量数据库当相似度超过 0.95 时直接返回缓存结果。更简单的场景是相同 prompt_hash 的请求只要有滚动窗口缓存就直接命中不需要再消耗任何 Token。模型路由则是“把不同类型的任务送到不同价格的模型”。Openlaw 内部将任务分成三类复杂合同分析、标准法规问答、简单格式抽取。第一类使用最强的大模型第二类使用中等性能且便宜的模型第三类直接用轻量模型。网关在路由时根据业务方声明的任务等级或根据 Prompt 预算自动选择模型。测试结果显示在不牺牲关键业务质量的前提下整体成本下降了约 40%。3.4 建立成本基线和自动熔断机制优化只做一次是不够的业务迭代随时可能让 Token 用量再次失控。所以必须建立一套成本控制机制。首先是成本预警。网关层每天统计每个模型接口的输入/输出 Token 用量并与最近 14 天基线做对比。若单日消耗超过基线 1.5 倍触发黄色告警超过 2 倍时触发红色告警自动发送到值班群。其次是自动熔断。网关在转发请求前会先计算当前时间窗口内已经消耗的成本和单次请求的预估成本。如果单次请求的预估成本超过 2 美元并且用户没有申请特殊权限则直接拒绝请求并返回“上下文过长请精简后重试”。这个阈值一开始可以设高一点运营一段时间后再调低。最后是成本归因报表。每个月初我会从网关导出一张按业务线聚合的成本表发给各业务负责人。这其实就是把成本意识下放到每个团队。事实证明业务方看到自己的请求量不过几百成本却有几千美元就会主动去优化 Prompt。比我们在后台反复推动有效得多。4. 核心实现网关层的关键代码与配置思路4.1 标准化 Token 打点逻辑网关层实现成本优化的前提是日志里有足够完整的数据。以常见的 Python/FastAPI 网关为例在调用大模型接口后必须把响应的 usage 对象保存下来并给请求补上 request_id、business_line、user_id、prompt_hash 等维度。下面是我实现的接入层核心片段import hashlib import time import json from datetime import datetime def build_request_meta(request_body: dict, business_line: str, user_id: str): messages request_body.get(messages, []) prompt_text json.dumps(messages, ensure_asciiFalse) prompt_hash hashlib.md5(prompt_text.encode(utf-8)).hexdigest() return { request_id: f{int(time.time()*1000)}-{user_id}, business_line: business_line, user_id: user_id, prompt_hash: prompt_hash, request_time: datetime.utcnow().isoformat(), model_name: request_body.get(model, ), input_estimation: estimate_tokens(prompt_text) } def log_usage(record: dict, usage: dict, elapsed_ms: int): record[input_tokens] usage.get(prompt_tokens, 0) record[output_tokens] usage.get(completion_tokens, 0) record[total_tokens] usage.get(total_tokens, 0) record[elapsed_ms] elapsed_ms save_to_mongo(record)在写入日志之前做一次 prompt_hash比后续离线分析时再去对全文做哈希高效太多。另外每次写入日志时记录预估输入 Token 的好处是即使上游没有返回 usage也能通过离线公式估算成本。Openlaw 很多模型接口在异常返回时不会带 usage但请求内容已经发送出去了这部分成本如果不记录会变成“隐形 Token”。4.2 成本预估公式与多模型定价表成本预估这件事很多人想复杂了。本质上就是 Token 量乘以对应模型的单价公式如下请求成本 (输入 Token / 1_000_000) × 输入单价 (输出 Token / 1_000_000) × 输出单价但实际应用里输出 Token 往往不会在请求前就知道。所以网关层需要在转发前先用规则做“最坏情况”预估假设输出会达到 max_tokens 上限。这样算出来的成本虽然偏保守但用于限额控制是安全的。我用下面的方法维护动态价格表MODEL_PRICE { claude-sonnet: {input: 5.0, output: 15.0}, # 美元/百万 Token gpt-4o-mini: {input: 1.5, output: 6.0}, local-small: {input: 0.3, output: 1.2}, } def predict_cost(model: str, input_tokens: int, max_output_tokens: int): if model not in MODEL_PRICE: # 如果遇到未知模型按最高价拦截宁可多拦不能漏拦 return (input_tokens / 1e6) * 10 (max_output_tokens / 1e6) * 30 price MODEL_PRICE[model] input_cost (input_tokens / 1e6) * price[input] output_cost (max_output_tokens / 1e6) * price[output] return input_cost output_cost在网关的拦截逻辑中先执行 predict_cost再根据当前时间窗口内的剩余预算决定是放行还是排队def check_budget(record: dict, cost_limit_per_req: float 2.0): predicted predict_cost( record[model_name], record[input_estimation], 2048 # 统一按最大输出预期计算 ) if predicted cost_limit_per_req: raise RequestTooExpensive( fpredicted cost {predicted:.3f} USD exceeds limit {cost_limit_per_req} )这套逻辑上线后无数“把整部法规塞进 Prompt”的请求被拦截在了网关之前。业务方只需要看一眼系统提示就能明白自己写的请求为什么被拒。4.3 优化前与优化后的效果对比把上下文截断、提示词精简、缓存和模型路由全部落地后我统计了完整两周的数据。第一周是所有优化上线前的基线第二周是优化上线后的稳定期。结果如下指标优化前优化后变化日均请求量18.2 万19.7 万8.2%日均输入 Token6.4 亿2.1 亿-67.2%日均输出 Token0.42 亿0.27 亿-35.7%平均单请求成本0.023 美元0.007 美元-69.6%周成本占预算比例87%26%大幅下降请求量上涨但成本下降的事实证明之前烧掉的钱大部分都是工程浪费。缓存命中率在开通后稳定在 31% 左右这部分请求完全没有调用模型。RAG 裁剪策略上线后长尾请求的平均输入 Token 从 1.5 万降到 6000业务答复准确率还小幅度提升了因为多余的噪声文本被清理后模型更聚焦于核心问题。这里要特别说明63.7% 的成本下降不能只归功于某一项优化。缓存节省的是重复请求成本上下文裁剪节省的是长上下文成本模型路由节省的是“过度使用贵模型”的成本。如果只做其中一项效果都会大打折扣。5. 常见问题与排查技巧实录5.1 “改了上下文策略后模型回答变差了怎么办”这是做得最多的争议。业务方可能会反馈说之前能记住 20 轮前的信息现在只保留 6 轮连续追问时上下文就不连贯了。我的建议是把“上下文窗口”和“长期记忆”分开。不要指望把全部历史都塞进 Prompt而是定期把重要信息抽取成摘要作为系统提示词的一部分注入。Openlaw 里我会在每 10 轮对话后调用一次轻量模型生成一个包含“用户核心诉求、关键事实、待确认事项”的会话摘要后续请求都携带这个摘要。这样一来模型还是能回答“前面提到的那个合同”但不用携带全部原文。另外一个回退手段是支持业务方显式标记“这条请求必须全量上下文”。如果业务侧确实需要完整阅读一份长合同就不做截断而是把该请求路由到支持较长上下文的模型。网关的价值正是把这种例外控制在一个可控范围内而不是全面放开。5.2 “缓存命中率很低为什么会这样”工程同学经常提出缓存命中率不到 10%怀疑缓存没用。我讲一个真实案例Openlaw 最初做缓存时用完整的 messages 数组做 key结果完全相同的问题带上不同的历史背景哈希值就变了缓存几乎永远不命中。后来我给缓存 key 增加了一层抽象只对“当前用户问题 当前系统提示词版本 知识库检索结果的文档 ID 列表”做哈希而不是把完整 prompt 所有字段都纳进去。这样只要用户问题相同、引用材料相同哪怕历史上下文有细微差异也能命中缓存。如果连这个命中率都低那说明业务场景本身重复请求较少。这种情况下可以考虑语义缓存通过 embedding 计算相似度但要注意延迟和误判风险。语义缓存更适合“不同问法、相似意图”的咨询场景对合同审查这类强精确匹配场景帮助不大。5.3 “重试逻辑为什么会让成本翻倍”很多团队的网关配置了重试但没有区分错误类型。如果上游返回 400客户端参数错误重试永远不会成功只会重复消耗无效 Token如果上游返回 429重试必须带退避否则会加剧限流如果上游返回 5xx可以重试但次数上限一般在 2 到 3 次内。我的排查技巧是在网关日志里记录每次重试是第几次尝试并将重试请求关联同一个 trace_id。只要 trace_id 相同离线分析时就可以把重试请求合并成一组。问题发生时能立刻算出“这组请求因为重试额外消耗了多少 Token”。5.4 “模型返回内容开始重复或走样”有些团队优化过头把上下文裁到 3000 Token导致模型缺少必要的背景信息。针对这个问题我建议守住两条底线一是必须保留系统提示词中与安全相关的内容即使它会占掉一些 Token二是关键定义和术语不能砍比如用户已经明确了合同标的、价款、管辖法院这类信息绝不因为“超长”而丢弃。如果上下文真的不够不要硬裁而是拆成多个子任务分别调用模型最后再汇总。这样每轮请求都只带和该子任务相关的内容准确率和成本往往都能优化。6. 写在最后如果再来一次我会先做什么这次成本超标问题的完整处理过程让我对“模型调用网关成本治理”有了很深刻的体会。如果时间倒流让我回到最初做 Openlaw 网关的设计阶段我会把下面三件事放在最高优先级第一从第一天就在网关层强制统计 Token而不是等账单出来再倒推。早期只记录了请求数和延迟导致成本异常发生时无法快速定位是哪些业务线烧钱。这个教训很贵。第二把“上下文上限”做成默认参数而不是让每个业务方自己处理。用户不会主动控制 Prompt 长度业务方也不会为成本负责。只有网关兜底限制才是真正的防线。第三优化成本时不要只盯着“便宜”。法律场景对准确率要求极高如果为了省 Token 把关键上下文砍掉最后模型回答出错带来的业务损失可能远超省下的一点模型成本。正确的思路是在保证质量的前提下减少无用 Token、合理路由模型。最后再分享一个可以直接落地的小技巧给网关的每个请求加上 estimated_cost 字段然后再配一个“当日成本 Top50 请求列表”的定时任务。每天看一眼这个列表你不需要依靠任何复杂报表就能第一时间发现异常调用模式。这比在模型服务商后台看账单直观得多也是这次优化过程中我认为投入产出比最高的一件事。