1. 为什么“最便宜模型”反而让账单更贵:一个 Agent 场景的真实成本账
先说结论:在 Agent 工作流里,模型单价只是成本公式里的一个变量,另一个被大多数人忽略的变量是缓存命中率。当你的 Agent 反复发送同一份 System Prompt、Tool Definition、仓库上下文和对话历史时,真正决定每次请求花多少钱的,是“有多少 Token 走了缓存价”而不是“每百万 Token 标价多少”。
我拿一个具体场景算给你看。假设一个 Coding Agent 的上下文稳定在 100,000 输入 Token,其中 90,000 Token 已经在模型 A 上建立了热缓存。模型 A 的未缓存输入价是每百万 2.5 美元,缓存命中价是每百万 0.2 美元。那么下一次请求的输入成本是:10,000 未缓存 Token × 2.5/1M + 90,000 缓存 Token × 0.2/1M = 0.025 + 0.018 = 0.043 美元。
现在你看到模型 B 的未缓存输入价只要每百万 0.7 美元,心想“这不是便宜三倍多吗”,于是把请求切过去。问题是切换模型意味着原有缓存全部失效,模型 B 必须重新 Prefill 全部 100,000 Token。成本变成 100,000 × 0.7/1M = 0.07 美元。比继续用“标价更贵”的模型 A 贵了约 1.6 倍。
这就是缓存感知要解决的核心问题:模型选择不是单次请求的局部最优,而是整个 Agent Session 的全局最优。一次破坏缓存的切换,Prefill 的 Prompt 数量可能变成原来的 10 倍,Time to First Token 也会明显上升——虽然不一定线性放大 10 倍,因为不同模型的 Prefill 性能不同,但方向是确定的。
哪些人最该关心这件事?三类:一是把 Agent 跑在生产环境、Token 用量指数增长的团队;二是已经在用多模型池、但发现账单没降下来的开发者;三是正在做模型路由选型、纠结“要不要为了省钱换模型”的技术负责人。如果你属于其中任何一类,下面的配置和验证步骤可以直接跟做。
需要说明的是,本文所有价格和数字都是示例,用于说明成本结构,实际以你所用平台发布时为准。不同工作负载的缓存命中率差异很大,编程类负载在部分平台上能到 90% 以上,但通用对话可能低得多,这个前提必须先建立。
2. 推理路由器的缓存感知前置:Affinity 与 Routing Budget 是什么
在讲配置之前,得先把两个机制讲清楚,否则你复制了参数也不知道在控制什么。
第一个机制是显式模型亲和性(Model Affinity)。已经自己维护 Session ID 或 Task ID 的应用,可以在每次请求里带一个 Affinity Key。推理路由器第一次收到这个 Key 时,按你配置的 Task、Model Pool 和 Routing Preference 选模型;之后只要请求携带相同的 Affinity 值,路由器就把它们视为同一个工作单元,保留 Session 与模型的绑定,继续复用已缓存的上下文。Affinity Identifier 应该对应真正有意义的工作单元——一次 Coding Session、一个 Research Task、一段 Support Conversation,或者单独一次 Agent Run。当应用真正开始新任务时,生成新的 Identifier,让路由器重新选模型。
第二个机制是Routing Budget Policy。它解决的是“什么时候放弃当前模型和缓存、承担额外成本才值得”。当系统判断有必要切换模型时,会先计算放弃当前 Session 热缓存所增加的额外成本:比较继续用当前模型的 Cached Input 成本,与在候选模型上重建全部上下文的 Uncached Input 成本,再把这部分额外费用和整个 Session 已累计的切换支出做比较。你可以通过 Maximum Switching Budget 定义这个取舍,基准是“如果整个 Session 一直留在当前模型上本来会花多少钱”。比如设置 20%,表示累计切换成本最多只能比这个基准高 20%。
模型能力判断和切换成本判断是分开的:路由器先选出更适合当前任务的模型,再由 Routing Budget 判断为了切过去而承担额外输入成本是否值得。这个设计很关键,它避免了两个极端——既不会因为缓存热就死守一个已经不适合任务的模型,也不会为了追新模型而无脑破坏缓存。
如果你没有显式提供 Identifier,路由器也能自行推断 Affinity:从多轮请求中保持稳定的上下文生成 Session Key,包括 System Instruction、Developer Instruction、Tool Definition 和第一条 User Message。如果这部分稳定 Prefix 发生变化,路由器就把缓存视为冷缓存,建立新的模型绑定,新 Session 重新分配模型并从头积累热缓存。
理解这两点后,你就能明白为什么“最便宜的模型不一定最省钱”:单价低但破坏缓存,等于把缓存带来的折扣全部吐回去,还额外付了 Prefill 的延迟代价。
3. 可复制的路由配置:Affinity Header 与 Budget 参数怎么写
这一节给你可以直接复制的配置片段。先看 Python 侧怎么带 Affinity Header,这是最常用的接入方式。
import os import uuid from openai import OpenAI client = OpenAI( api_key=os.environ["MODEL_ACCESS_KEY"], base_url="https://inference.do-ai.run/v1/", ) session_id = str(uuid.uuid4()) messages = [] user_turns = [ "Help me debug this failing test.", "Here's the stack trace, what's causing it?", "That fixed it, now can you also add a regression test?", ] for user_turn in user_turns: messages.append({"role": "user", "content": user_turn}) response = client.chat.completions.create( model="router:<your-router-name>", messages=messages, extra_headers={ "X-Model-Affinity": session_id, }, ) assistant_reply = response.choices[0].message.content messages.append({"role": "assistant", "content": assistant_reply})关键点有三个:base_url指向推理服务端点;model用router:前缀加你的路由器名;extra_headers里带X-Model-Affinity,值在整个 Session 内保持不变。第一次请求按你的 Task 和 Model Pool 选模型,后续请求因为 Affinity 相同,会保留模型绑定并复用缓存。
如果你不想改应用代码,可以用 Routing Budget 让路由器自己判断。下面这个 curl 示例展示了如何设置切换预算:
curl -i "https://inference.do-ai.run/v1/chat/completions" \ -H "Authorization: Bearer $MODEL_ACCESS_KEY" \ -H "Content-Type: application/json" \ -H "X-Routing-Max-Switch-Spend-Pct: 20" \ -d '{ "model": "router:software-engineering", "messages": [ {"role": "user", "content": "Summarize this in 2 sentences."} ] }'X-Routing-Max-Switch-Spend-Pct: 20表示累计模型切换成本最多比“一直留在当前模型”的基准高 20%。注意这里没有显式设置X-Model-Affinity,路由器会自行推断 Affinity。如果你既想显式控制工作单元,又想限制切换成本,两个 Header 可以同时带。
如果你用的是配置文件驱动的工具链,比如把路由参数写进 settings 或 TOML,核心字段就是三个:Base URL、Key、Model ID。以 JSON 形式表达如下:
{ "base_url": "https://inference.do-ai.run/v1/", "api_key_env": "MODEL_ACCESS_KEY", "model": "router:software-engineering", "headers": { "X-Model-Affinity": "${SESSION_ID}", "X-Routing-Max-Switch-Spend-Pct": "20" } }这里model必须是完整的 Model ID,不能只写模型名。Base URL、Key、Model ID 三件套缺一不可,很多接入失败都是因为 Model ID 写成了裸模型名而不是router:前缀的路由器名。
配置写完后,建议先在低风险任务上跑一轮,观察 Analyze 页面里的缓存效率和切换次数,再决定是否放大到生产流量。
4. 验证请求与成功结果:怎么确认缓存真的被复用了
配置写完不等于生效,必须验证。验证分两步:先确认请求成功,再确认缓存命中。
第一步,发一个最小请求确认链路通。用上面的 curl,把 messages 换成一句简单的话,观察返回。成功的话你会拿到标准的 chat completion 响应,choices[0].message.content有内容。如果这一步就失败,先看第 5 节的报错排查。
第二步,连续发多轮请求,观察缓存效率。这里的关键是:第一轮请求一定是冷缓存,从第二轮开始才可能命中。所以不要拿单次请求判断缓存是否生效。你可以这样构造验证:
import os from openai import OpenAI client = OpenAI( api_key=os.environ["MODEL_ACCESS_KEY"], base_url="https://inference.do-ai.run/v1/", ) session_id = "verify-cache-session-001" messages = [{"role": "system", "content": "You are a coding assistant. " * 200}] for i in range(5): messages.append({"role": "user", "content": f"Turn {i}: explain step {i} briefly."}) resp = client.chat.completions.create( model="router:software-engineering", messages=messages, extra_headers={"X-Model-Affinity": session_id}, ) usage = resp.usage print(f"turn={i} prompt={usage.prompt_tokens} " f"cached={getattr(usage, 'prompt_tokens_details', None)}") messages.append({"role": "assistant", "content": resp.choices[0].message.content})跑完后看输出:prompt_tokens会随轮次增长,但缓存相关的明细字段应该显示大部分 Token 走了缓存。不同 SDK 版本字段名可能不同,有的在prompt_tokens_details.cached_tokens,有的在顶层。如果字段拿不到,就去 Analyze 页面看 Cache Efficiency 趋势。
Analyze 页面能回答几个关键问题:整体缓存效率是多少?有多少请求发生了模型切换?有多少请求为了保持热缓存而继续留在原模型上?延迟表现如何?还能下钻到具体模型和 Task,看流量组成,发现某些模型上的流量热点,验证你的模型选择策略是否合理。Cache Efficiency 趋势追踪可以从 Request 和 Token 两个层面观察缓存效率随时间的变化,这样更容易发现 Cache Regression,验证 Prompt 和 Cache 调优到底有没有效果。
一个实测经验:如果你发现缓存命中率始终上不去,先检查稳定 Prefix 是不是被破坏了。System Instruction、Tool Definition、第一条 User Message 这三部分只要有一处在每轮请求里变化,路由器就会判定为冷缓存,重新建立绑定。很多人把时间戳、随机 ID 塞进 System Prompt,缓存直接归零。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来对照,都是接入推理路由器时高频踩的坑。
401 Unauthorized。最常见的原因是 Key 没读到或读错了环境变量。检查MODEL_ACCESS_KEY是否真的导出到了当前 shell,Python 里os.environ["MODEL_ACCESS_KEY"]如果 Key 不存在会直接抛 KeyError,而不是 401,所以如果你看到的是 401,说明 Key 读到了但值不对。确认 Key 没有多余空格、没有换行、没有把别的服务的 Key 贴进来。另外确认 Authorization Header 格式是Bearer <key>,中间一个空格。
local proxy failed。这个报错通常出现在你本地网络环境有额外转发层的时候。先确认base_url写的是推理服务端点,而不是某个本地地址。如果你在容器里跑,检查容器网络是否能直连外网。这个报错和模型选择无关,纯粹是网络链路问题,先把链路打通再谈缓存。
reading choices 相关报错。典型表现是response.choices为空或索引越界。原因通常是请求被路由到了某个返回结构不同的端点,或者你用了流式返回但按非流式解析。检查model字段是不是router:前缀的完整 Model ID。如果用了stream=True,要按流式 chunk 解析,不能直接取choices[0].message.content。还有一种情况是请求被拒绝但返回体结构异常,打印完整响应体再定位。
OAuth 相关报错。如果你用的是需要 OAuth 的工具链,注意推理路由器的鉴权是 API Key 模式,不是 OAuth 授权码模式。把 OAuth 流程和 API Key 混用会导致鉴权失败。正确做法是在工具配置里填 Base URL、Key、Model ID 三件套,不要走 OAuth 回调。如果你用的是 Claude Code 这类工具,确认它的配置项里填的是 API Key 而不是登录态。
缓存命中率异常低。这不是报错但比报错更隐蔽。排查顺序:先确认 Affinity 值在整个 Session 内是否稳定;再确认稳定 Prefix 有没有被动态内容污染;最后看 Routing Budget 是不是设得太宽松,导致路由器频繁切换模型破坏缓存。把X-Routing-Max-Switch-Spend-Pct调低一点,观察切换次数是否下降、缓存效率是否回升。
切换过于频繁导致延迟抖动。如果你发现 TTFT 忽高忽低,去 Analyze 页面看模型切换次数。切换本身不是坏事,但如果切换发生在 Session 中途且没有预算约束,每次切换都要重建缓存,延迟自然抖。给 Session 加 Affinity,或者设置合理的切换预算,通常能稳住。
6. 从成本对比到长期策略:把缓存感知接进你的 Agent 工作流
验证通过后,下一步是把它变成长期策略。这里给几条可执行的建议。
第一,先建立基线再优化。在你接入缓存感知之前,记录一周的 Token 用量、缓存命中率、模型切换次数和账单。没有基线,你无法判断优化是否有效。Analyze 页面的 Cache Efficiency 趋势就是干这个的。
第二,区分“该省的”和“不该省的”。便宜的默认模型适合简单任务,但复杂任务需要质量达标的模型。只根据 Benchmark 选模型不够,因为模型真实表现取决于它所在的应用环境——System Prompt、Tool Definition、Context、Output Constraint、Conversation History,以及什么才算成功。换个 Agent Harness,模型排名可能就变了。一个在独立 Coding Benchmark 里最好的模型,放进需要处理大型仓库、几十个 Tool 和长对话历史的 Agent 里,不一定还是最佳选择。
第三,缓存感知不是“死守热缓存”。如果当前模型已经不适合任务,该切还是要切,Routing Budget 就是用来量化这个取舍的。真正的目标不是最大化 Token 数量,也不是盲目把单价降到最低,而是在满足质量、延迟和可靠性的前提下,最大化每一美元的有效智能产出。
第四,把 Affinity 的粒度设计对。一次 Coding Session、一个 Research Task、一段 Support Conversation、一次 Agent Run,各自应该是独立的 Affinity 单元。粒度太粗会导致不相关任务共享模型绑定,粒度太细会导致缓存无法积累。判断标准是:这些请求是否共享同一份稳定上下文。
如果你还在选型阶段,建议先用模型对话页面手动对比几个模型在同一份上下文下的表现和成本,再决定路由器里放哪些模型。需要长期跑编码或 Agent 任务的话,Coding Plan 更适合按周期评估成本结构。接入过程中遇到鉴权或配置问题,直接查接入文档和 API Keys 页面,比在社区里翻旧帖快得多。
最后提醒一句:所有成本数字都依赖你的实际缓存命中率和模型价格,本文示例只用于说明结构。上线前务必用自己的真实流量跑一轮对比,把“看起来便宜”和“实际便宜”分开验证。