ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

多模型接入Token消耗优化:路由、上下文、重试与缓存实战

多模型接入Token消耗优化:路由、上下文、重试与缓存实战 1. 多模型接入的账本真相为什么你的 Token 花得比别人快三倍多模型接入这件事我从去年下半年开始正式在团队里推前后接入了 DeepSeek、GLM、Claude 这几家的 API中间还穿插着 Codex 和一些本地部署的模型做兜底。跑了大概大半年直到上个月财务把 API 账单拉出来对了一遍我才发现一个很扎心的事实同样的业务量我们的 Token 消耗比隔壁组高了将近三倍。不是模型贵是我们自己把 Token 当水喝了。这篇文章不聊虚的就把这大半年踩过的坑一个个摊开讲。核心关键词就几个多模型接入、Token 消耗、API 调用、DeepSeek、GLM。如果你正在做多模型路由、正在用 Claude Code 或者 Codex 这类工具同时配置多个模型、正在被 Token 用量和账单搞得头大那这篇内容基本就是给你写的。我会把每个坑的成因、排查过程、以及最后怎么改的说清楚包括具体的参数配置和代码层面的处理方式能直接抄作业的就直接抄。先说结论性的判断多模型接入的 Token 浪费八成不是模型本身的问题而是路由策略、上下文管理、重试机制、缓存设计这四个环节出了毛病。下面逐个拆。2. 坑一无脑路由把便宜模型当摆设贵模型当苦力2.1 路由策略缺失导致的成本失控最开始我们接入多模型的逻辑特别朴素哪个模型先配好就用哪个遇到报错就换下一个。结果就是 DeepSeek 和 GLM 基本没怎么被调用所有请求都堆到了 Claude 上。原因很简单Claude 的 SDK 我们最先调通路由表里它排第一位后面的模型只有在前面报错时才会被触发。这个问题的本质是没有按任务类型做路由。多模型接入最大的价值不是多一个备份而是让合适的模型干合适的活。比如简单的文本分类、意图识别、格式转换用 DeepSeek 或者 GLM 的轻量模型就够了成本可能只有旗舰模型的十分之一复杂的代码生成、长链推理才需要上 Claude 或者 DeepSeek 的推理模型纯翻译、摘要这类任务GLM 的性价比往往比通用大模型更高我们后来做了一张路由决策表按任务类型和输入长度两个维度来分任务类型输入长度推荐模型理由意图分类 500 tokenGLM 轻量版单次成本极低准确率够用文本摘要500-3000 tokenDeepSeek长文本处理性价比高代码生成任意Claude / DeepSeek 推理版代码质量优先格式转换 1000 tokenGLM结构化输出稳定复杂推理 3000 token旗舰模型需要强推理能力这张表不是拍脑袋定的是我们拿真实业务数据跑了 A/B 测试之后统计出来的。同样的任务用对模型之后单次调用成本平均降了 60% 以上。2.2 路由实现的关键代码逻辑路由层我们是用一个中间件做的核心逻辑大概长这样def route_model(task_type, input_tokens, prioritycost): routing_table { (classification, short): glm-light, (summarization, medium): deepseek-chat, (code_gen, any): claude-sonnet, (reasoning, long): deepseek-reasoner, } length_bucket short if input_tokens 500 else \ medium if input_tokens 3000 else long key (task_type, length_bucket) model routing_table.get(key) if not model: model routing_table.get((task_type, any), deepseek-chat) return model这段代码看着简单但关键在于task_type这个字段必须由上游业务方显式传入不能靠模型自己猜。我们一开始偷懒让路由层去猜任务类型结果猜错的概率不低反而导致重试和二次调用Token 消耗更高。注意路由决策一定要在调用之前完成不要等模型返回了再判断这个任务其实该用另一个模型那样等于白花了一次调用的钱。2.3 实操心得路由表要定期复盘路由表不是定完就完事了。我们现在的做法是每周拉一次调用日志统计各模型的实际调用量、平均 Token 消耗、失败率然后看有没有本该走便宜模型却走了贵模型的漏网之鱼。有一次发现某个批处理任务一直在走 Claude查下来是任务类型字段传错了改了一个枚举值一个月省了小两千块。3. 坑二上下文管理失控历史消息越滚越大3.1 多轮对话的 Token 黑洞多模型接入之后我们有个客服场景是多轮对话。最开始的做法是把完整的历史消息全部塞进 context 里发给模型想着这样模型能记住更多信息。结果就是对话到第十轮的时候单次请求的输入 Token 已经飙到八千多而其中大部分历史消息跟当前问题根本没关系。这个坑的隐蔽性在于它不会报错只会默默烧钱。你看着每次调用都成功但账单在涨。我们后来做了个统计发现多轮对话场景下平均有 65% 的输入 Token 是无效上下文——也就是对当前回答没有实质帮助的历史消息。3.2 上下文压缩的三种策略针对这个问题我们试了三种方案最后是组合使用的第一种是滑动窗口。只保留最近 N 轮对话更早的直接丢掉。简单粗暴但有效N 一般设 5 到 8 轮。缺点是如果用户突然问一个很早之前提过的东西模型就接不上了。第二种是摘要压缩。把超过窗口的历史消息用便宜模型比如 GLM 轻量版做一次摘要把摘要作为系统消息塞回去。这样既保留了关键信息又大幅压缩了 Token。摘要的 prompt 大概是请将以下对话历史压缩成不超过 200 字的摘要保留用户的核心诉求、已确认的关键信息、以及未解决的问题。不要添加任何推测内容。第三种是向量检索。把历史消息存进向量库每次只检索跟当前问题最相关的几条塞进 context。这个方案效果最好但实现成本最高适合对话轮次特别多的场景。我们最后用的是滑动窗口加摘要的组合窗口内保留原文窗口外用摘要。实测下来同样的对话轮次输入 Token 降了大概 55%。3.3 系统提示词的重复计费问题还有一个容易被忽略的点系统提示词每次调用都会重新计费。我们有个场景的系统提示词写得特别长有将近一千字包含各种规则和示例。每次调用都带上一天调用几千次光系统提示词就烧掉不少钱。后来我们把系统提示词做了精简把可以外置的规则挪到了后处理逻辑里只保留最核心的指令。同时对于固定不变的提示词利用部分模型提供的 prompt caching 能力如果 API 支持的话把重复部分缓存起来后续调用只计增量。这一项改下来系统提示词相关的 Token 消耗降了 70%。提示系统提示词能短则短能用代码做的规则校验就不要写进 prompt 里让模型判断。模型判断既费 Token 又不一定准。4. 坑三重试机制设计不当失败一次烧三次钱4.1 无脑重试的代价多模型接入之后我们给每个模型调用都加了重试逻辑想着提高成功率。但最开始的重试是原样重试——同样的请求同样的参数失败了就再发一次最多重试三次。问题在于很多失败是确定性失败比如参数格式不对、输入超长、模型不支持某个功能。这种失败你重试一百次也没用但每次重试都是一次完整的 Token 消耗。我们统计过重试产生的 Token 消耗占总消耗的将近 20%其中大部分是无效重试。4.2 区分可重试与不可重试错误正确的做法是先对错误分类只对瞬时性错误做重试错误类型是否重试处理方式429 限流是指数退避后重试500/502/503是退避后重试最多 2 次400 参数错误否直接抛出修参数401 鉴权失败否检查 API Key输入超长否截断或换模型超时是缩短输入后重试RETRYABLE_CODES {429, 500, 502, 503, 504} def should_retry(status_code, attempt): if status_code not in RETRYABLE_CODES: return False if attempt 2: return False return True def backoff_delay(attempt): return min(2 ** attempt random.uniform(0, 1), 30)这段逻辑加上之后无效重试基本消失了重试相关的 Token 消耗降到了 3% 以内。4.3 跨模型重试的陷阱多模型接入还有个特有的坑跨模型重试。比如 DeepSeek 失败了自动切到 GLM 重试。这个逻辑本身没问题但如果没做好去重可能出现DeepSeek 其实已经处理了但返回超时你又让 GLM 处理了一遍的情况等于同一件事付了两次钱。我们的处理方式是给每个请求加一个唯一的request_id在重试之前先查一下这个 id 有没有已经成功的记录。同时对于写操作类的请求比如生成内容后要落库重试前必须做幂等校验。注意跨模型重试时不同模型的输出格式可能不一样后处理逻辑要能兼容。我们踩过一次坑DeepSeek 返回的是 JSONGLM 返回的是带 markdown 代码块的 JSON解析直接挂了。5. 坑四缓存形同虚设同样的请求反复付费5.1 哪些请求值得缓存多模型接入的场景下有大量请求是重复或高度相似的。比如同样的系统提示词加同样的用户问题批量任务里重复出现的模板化输入开发调试阶段的反复测试请求我们最开始完全没做缓存每个请求都实打实地调 API。后来加了一层缓存命中率大概在 25% 左右直接省掉了四分之一的 Token 开销。5.2 缓存键的设计缓存的关键是缓存键怎么设计。太粗了会命中不该命中的太细了命中率又低。我们的做法是把以下字段拼起来做哈希def build_cache_key(model, system_prompt, user_message, temperature): raw f{model}|{system_prompt}|{user_message}|{temperature} return hashlib.sha256(raw.encode()).hexdigest()注意temperature也要进缓存键。如果 temperature 大于 0理论上同样的输入每次输出都可能不同这种就不适合缓存或者只能缓存很短的时间。我们一般只对temperature0的确定性请求做缓存。5.3 缓存的过期与失效缓存不能永久有效因为模型本身会更新业务规则也可能变。我们的策略是确定性请求缓存 24 小时模型版本变更时全量失效系统提示词变更时按前缀批量失效这套机制加上之后缓存命中率稳定在 30% 上下Token 成本又降了一截。6. 坑五监控缺失钱花在哪都不知道6.1 没有监控就没有优化前面四个坑能被发现全靠我们后来补上了监控。最开始的大半年我们只有一个总的账单数字根本不知道钱花在哪个模型、哪个业务、哪个用户身上。补监控之后才发现有一个内部测试账号一直在跑批量任务消耗了将近 15% 的总额度。监控要记的字段至少包括请求时间、request_id模型名称、任务类型输入 Token、输出 Token、总 Token调用耗时、是否成功、错误码业务标识、用户标识6.2 按维度拆解 Token 消耗有了数据之后按不同维度拆解问题就一目了然了维度用途按模型看各模型的实际成本和性价比按业务定位高消耗业务线按用户发现异常调用按任务类型验证路由策略是否合理按时间段发现峰值和异常波动我们现在的做法是每天出一份 Token 日报每周做一次复盘。日报里会标出消耗 Top 10 的请求来源如果某个来源突然涨了当天就能查到原因。6.3 设置预算告警监控之外还要有告警。我们给每个模型、每个业务线都设了日预算和月预算超过 80% 就发通知超过 100% 就自动降级到便宜模型或者直接限流。这个机制救过我们好几次有一次某个上游业务出了 bug 导致疯狂重试告警一响我们就切了限流避免了更大的损失。7. 多模型接入的配置实操以 DeepSeek 和 GLM 为例7.1 API 接入的基本配置多模型接入的第一步是把各家的 API 都调通。以 DeepSeek 和 GLM 为例基本的配置大概是这样import os from openai import OpenAI deepseek_client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com/v1 ) glm_client OpenAI( api_keyos.getenv(GLM_API_KEY), base_urlhttps://open.bigmodel.cn/api/paas/v4 )两家都兼容 OpenAI 的 SDK 格式所以可以用同一套调用代码只是 client 和 model 名称不同。DeepSeek 常用的模型名有deepseek-chat和deepseek-reasonerGLM 这边常用的有glm-4-flash和glm-4-plus。提示模型名称一定要以官方文档为准不同时期可能调整。调用前先用一个最小请求验证模型名是否有效避免因为模型名写错导致 400 错误。7.2 统一调用层的封装为了不让业务代码直接依赖某一家 SDK我们做了一层统一封装class ModelGateway: def __init__(self): self.clients { deepseek: deepseek_client, glm: glm_client, } self.model_map { deepseek-chat: deepseek, deepseek-reasoner: deepseek, glm-4-flash: glm, glm-4-plus: glm, } def call(self, model, messages, **kwargs): provider self.model_map[model] client self.clients[provider] start time.time() try: resp client.chat.completions.create( modelmodel, messagesmessages, **kwargs ) self._log(model, messages, resp, time.time() - start, None) return resp except Exception as e: self._log(model, messages, None, time.time() - start, str(e)) raise这层封装的好处是业务代码只认模型名不关心底层是哪家日志和监控统一在这里做重试和降级逻辑也集中在这一层。7.3 多模型同时配置的注意事项如果你是在 Claude Code、Codex 这类工具里同时配置多个模型有几个点要注意每个模型的 API Key 要分开管理不要混用工具的配置文件里模型名和 provider 要对应清楚切换模型时注意上下文格式是否兼容有些工具对消息格式有要求如果工具支持模型优先级配置把便宜模型放在前面做兜底我们团队内部用的一套配置模板把 DeepSeek 作为默认模型GLM 作为轻量任务模型Claude 只在代码生成场景启用。这样既保证了效果又把成本压了下来。8. 常见问题速查与避坑清单8.1 Token 异常消耗排查表现象可能原因排查方向账单突然翻倍某业务重试暴增查错误日志和重试次数单次调用 Token 很高上下文没压缩查输入消息长度便宜模型没被调用路由表配置错误查路由决策日志缓存命中率低缓存键设计太细检查键的组成字段某模型成本占比过高路由策略不合理按任务类型重新分配8.2 实操避坑清单路由决策必须在调用前完成不要事后补救系统提示词能精简就精简能外置的规则不要写进 prompt重试只针对瞬时错误确定性错误直接抛出跨模型重试要做幂等校验避免重复计费缓存只对确定性请求开启temperature 大于 0 的慎用监控字段要全至少覆盖模型、业务、用户、任务类型四个维度预算告警必须设超限自动降级或限流模型名称以官方文档为准调用前先验证8.3 几个容易被忽略的细节第一个是流式输出的 Token 统计。流式返回的时候很多 SDK 不会在最后一个 chunk 里带 usage 信息需要额外处理。我们一开始没注意导致流式请求的 Token 统计全是零监控数据不准。第二个是图片和多模态输入的计费。如果接了多模态模型图片是按分辨率折算成 Token 计费的一张高清图可能顶几千个文本 Token。我们有个场景传了原图后来改成压缩后再传成本降了一大截。第三个是并发控制。多模型接入之后如果并发没控制好可能同时触发多个模型的限流导致大量重试。我们后来加了令牌桶做并发限制每个模型独立限流稳定性好了很多。9. 我个人的几点体会这套东西跑下来最大的感受是多模型接入的成本优化技术含量不在接了多少个模型而在怎么管住每一次调用。模型本身的价格差异是明牌但路由、上下文、重试、缓存、监控这五个环节的优化空间远比选哪个模型大得多。我们现在的状态是同样的业务量Token 成本比最开始降了大概 65%而且稳定性还提升了。靠的不是换了更便宜的模型而是把每一分 Token 都花在了该花的地方。如果你刚开始做多模型接入我的建议是先把监控和日志做起来哪怕先不优化至少要知道钱花在哪。有了数据之后上面这些坑一个个填成本自然就下来了。别一上来就追求智能路由自动降级这些花活基础的路由表加缓存加限流已经能解决大部分问题。最后分享一个小技巧定期拿一批真实请求做回放测试对比不同模型在同样任务上的输出质量和成本用数据来调整路由表。我们每个月做一次路由策略就是这么一点点磨出来的。
返回列表