200 行代码的 LLM 智能路由:成本直降 41%
开发者用 TypeScript 实现模型选择路由器,通过智能调度不同 LLM,直接减少 41% 的 API 调用成本。
完整中文译文
原创技术 · 通过绕过包装器上下文冗余
我跟踪自己在三个项目中的 AI 支出。在三月份,增长最快的不是 Claude 或 GPT 调用。而是我的 Cursor 席位加上 Copilot 席位加上我从个人 CLI 调用的 Anthropic API。三个订阅、三个计费表,同一个 Opus token 被重复计费,因为 Cursor 将相同的上下文发送到它自己的后端,而我已经直接传给 Anthropic 了。
这些包装器不会宣传这个。路由代码不是它们的产品。产品是不用考虑哪个模型处理哪个提示的便利性。你用座位价格中烤入的利润支付了编排税。
我厌倦了支付这笔钱。所以我写了路由。它是 200 行 TypeScript。我的四月账单比三月低 41%,工作量基本相同。
这 41% 的节省来自一件事:停止让 Sonnet 处理 Haiku 能以十分之一成本完成的任务。大多数编码查询其实是伪装成问题的查找。按意图路由,而不是按习惯。
1. 编排税是真实存在的
每个包装器都做同样的权衡。它们为你选择一个模型,它们前置一个你无法编辑的系统提示,它们持有一个你无法检查的上下文窗口。作为回报,你不必考虑。
不思考的成本以两种方式出现:
包装器调用最贵的能满足其 SLA 的模型,因为这样更容易让演示看起来很好
包装器向你收费代表你发送的上下文,包括它自己的系统提示和工具定义
我在 Anthropic 仪表板中记录了 30 天的 Cursor 使用情况。Cursor 平均每次聊天回合发送 8,400 个输入 token。我对相同聊天的直接 API 调用平均 1,900。这 6,500 token 的差值是 Cursor 的框架、索引上下文和它的 agent 脚手架。有用,但不是免费的。
当你自己构建路由器时,你选择发送什么。这就是整个游戏。
2. 200 行路由器
这是这个文件。把它放到项目中,给它你的 API 密钥,它根据你控制的规则为每个请求选择一个模型。
// router.ts
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
type Intent = "trivial" | "code" | "plan" | "embed";
interface RouteRule {
match: (prompt: string) => boolean;
intent: Intent;
}
interface ModelConfig {
provider: "anthropic" | "openai";
model: string;
maxTokens: number;
}
const ROUTES: Record<Intent, ModelConfig> = {
trivial: { provider: "anthropic", model: "claude-haiku-4-5-20251001", maxTokens: 1024 },
code: { provider: "anthropic", model: "claude-sonnet-4-6", maxTokens: 4096 },
plan: { provider: "anthropic", model: "claude-opus-4-7", maxTokens: 8192 },
embed: { provider: "openai", model: "gpt-5-mini", maxTokens: 512 },
};
const RULES: RouteRule[] = [
{ intent: "trivial", match: (p) => p.length < 200 && /\?$/.test(p.trim()) },
{ intent: "trivial", match: (p) => /^(what is|define|fix typo|rename)/i.test(p) },
{ intent: "plan", match: (p) => /(refactor|design|architect|migrate|plan)/i.test(p) },
{ intent: "code", match: (p) => /(```|function |class |const |let )/i.test(p) },
{ intent: "embed", match: (p) => p.startsWith("CLASSIFY:") },
];
function pickIntent(prompt: string): Intent {
for (const rule of RULES) {
if (rule.match(prompt)) return rule.intent;
}
return "code";
}
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export interface RouteResult {
text: string;
model: string;
inputTokens: number;
outputTokens: number;
costUsd: number;
}
const PRICING: Record<string, { in: number; out: number }> = {
"claude-haiku-4-5-20251001": { in: 0.8, out: 4 },
"claude-sonnet-4-6": { in: 3, out: 15 },
"claude-opus-4-7": { in: 5, out: 25 },
"gpt-5-mini": { in: 0.5, out: 2 },
};
function priceCall(model: string, inTok: number, outTok: number): number {
const p = PRICING[model];
if (!p) return 0;
return (inTok * p.in + outTok * p.out) / 1_000_000;
}
export async function route(prompt: string): Promise<RouteResult> {
const intent = pickIntent(prompt);
const cfg = ROUTES[intent];
if (cfg.provider === "anthropic") {
const r = await anthropic.messages.create({
model: cfg.model,
max_tokens: cfg.maxTokens,
messages: [{ role: "user", content: prompt }],
});
const text = r.content
.filter((b) => b.type === "text")
.map((b) => (b as { text: string }).text)
.join("");
return {
text,
model: cfg.model,
inputTokens: r.usage.input_tokens,
outputTokens: r.usage.output_tokens,
costUsd: priceCall(cfg.model, r.usage.input_tokens, r.usage.output_tokens),
};
}
const r = await openai.chat.completions.create({
model: cfg.model,
max_tokens: cfg.maxTokens,
messages: [{ role: "user", content: prompt }],
});
const usage = r.usage ?? { prompt_tokens: 0, completion_tokens: 0 };
return {
text: r.choices[0]?.message?.content ?? "",
model: cfg.model,
inputTokens: usage.prompt_tokens,
outputTokens: usage.completion_tokens,
costUsd: priceCall(cfg.model, usage.prompt_tokens, usage.completion_tokens),
};
}
就这样。两个提供商、四个意图、五个规则和一个成本计算器。这样使用它:
import { route } from "./router";
const out = await route("rename this function from getUser to fetchUser");
console.log(out.model, out.costUsd.toFixed(5));
// claude-haiku-4-5-20251001 0.00012
这些规则故意很简单。长度加正则表达式覆盖了大约 70% 的路由决策。对于另外 30%,用前缀覆盖:
await route("[force:opus] design a permissions model for ...");
在 pickIntent 中添加一行来读取前缀。我为了保持例子简洁而省略了它。
3. 真正有效的路由规则
天真的方法是发送一个小分类器调用到便宜模型并让它选择路由。这听起来很聪明,但成本比节省更多,因为每个请求现在消耗两个 API 调用。pickIntent 的成本必须为零。
五个正则表达式规则覆盖了我的大部分工作量:
短且以问号结尾:trivial
以"what is"、"define"、"fix typo"、"rename"开头:trivial
包含"refactor"、"design"、"architect"、"migrate"、"plan":plan
包含代码围栏或 function 关键字:code
以"CLASSIFY:"前缀开头:embed(便宜分类器)
默认为 code。一个从 trivial 到 code 的错误路由在那一个请求上成本多 4 倍左右。一个从 code 到 opus 的错误路由成本多 1.6 倍。都不是灾难。要避免的 bug 是给 Haiku 发送它无法维持上下文的多步骤计划,这意味着要谨慎地默认。
我还记录每一次失误。两周后我有一个小的 CSV,记录"这个提示路由到 X 但应该是 Y"。我添加了两个正则表达式规则,失误率从 8% 降到 2% 以下。
4. 41% 的数字,详细分解
三月账单,没有路由器:
四月账单,有路由器(取消了 Cursor,仅保留 Copilot 用于 IDE 内联):
在总调用增加 30% 的情况下,低 41%。路由器将 62% 的调用转移到了 Haiku,这些调用原本由 Sonnet 处理。每次调用的平均成本从 $0.024 降到 $0.013。
Cursor 的取消做了主要的节省。路由器做了较小的、重复的、复合的节省。两者都来自同一个想法:包装器隐藏了你可以更好自己做的决策。
5. 这不做什么
这不是 agent 框架。它不流式传输。它不重试。它不缓存。它不处理速率限制。它不做工具使用。它不知道你的代码库。
添加任何这些都需要工作。流式传输需要两个改变。使用 Anthropic prompt cache 缓存是每次调用多一个 header。带指数退避的重试是 20 行。工具使用需要你无论如何都要写的架构管道。
如果你需要所有这些,使用真正的框架。如果你想停止在 80% 的调用上支付编排税,上面的 200 行会做到。在你实际遇到每个问题时添加其余部分。
包装器存在是因为路由 AI 调用很烦人。这也是你在自己的代码中能够拥有的最高杠杆的东西。上面的 200 行不是护城河。它们是周二下午。写它们的原因是你无法改进看不到的账单。
你当前的便宜模型调用与昂贵模型调用的比例是多少?如果你不知道,那是首先要修复的事情。在你设置路由器之前先设置成本日志。这些数字会让你惊讶。
GDS K S · thegdsks.com · building Glincker · follow on X @thegdsks
编排税是不会出现在定价页面上的 AI 账单的一部分。