生产环境跑了 3 个月的多模型路由,我发现每 token 价格是最没用的指标
今年 2 月我做了一个愚蠢的决定——因为 DeepSeek 单价便宜,我把公司内部 3 个 AI 业务的底层模型从 Claude Opus 4 全切到了 DeepSeek V3。
结果呢?月账单确实降了 60%,但用户投诉翻了 3 倍。更扎心的是,我算了一笔账:把重试次数、人工介入、用户流失算进去,总成本比之前反而高了 40%。
这不是 DeepSeek 的问题——它的单 token 性价比毋庸置疑。问题出在我选模型的逻辑上。我今天把这三个月踩过的坑、建立的评测框架、以及现在线上稳定运行的多模型路由架构完整写出来。
先给结论,免得你没耐心看完:
- 每 token 价格 × 正确率 → "正确答案成本"才是唯一有意义的效率指标
- Prompt 有模型特异性——同一个 prompt 在 Claude 和 DeepSeek 上的表现不是 90 vs 80,而是 "能用" vs "完全不能用"
- 生产环境里,可靠性比性能和价格都重要。一个偶尔超时 30 秒的便宜模型,比一个永远 2 秒返回的贵模型可怕十倍
下面展开。
一、每 token 价格是最大的谎言
这是 LLM API 定价页最典型的呈现方式:
| 模型 | 输入价格 | 输出价格 |
|---|---|---|
| DeepSeek V3.2 | ¥1/M tokens | ¥3/M tokens |
| Claude Opus 4.5 | ¥15/M tokens | ¥75/M tokens |
看到这种表,你的本能反应是 "DeepSeek 便宜 15 倍,傻子才不用"。但这个表隐藏了一个致命变量:一次调用能解决你的问题吗?
我设计了一个简单的公式来算真实成本:
真实成本 = 单次价格 / 首次正确率 + 单次价格 × (1 - 首次正确率) / 首次正确率 × 平均重试次数
用人话说:如果便宜模型正确率只有 60%,那你平均要调 1.7 次才能拿到正确答案。贵的模型正确率 95%,你只需要调 1.05 次。
我把这个逻辑变成了一个评测框架。下面是我在三个典型任务上跑出来的数据:
任务 1:根据 PR 描述生成 React 组件(含状态管理 + 错误边界)
任务 2:从 500 行遗留代码中提取业务逻辑并编写单元测试
任务 3:将 3 段用户反馈汇总为产品需求文档
每个任务跑 50 次,统计首次正确率、平均耗时、token 消耗和真实成本。
任务 1 — React 组件生成("可编译运行"才算正确)
| 模型 | 首次正确率 | 平均 token/次 | 平均耗时 | 含重试总成本 |
|---|---|---|---|---|
| Claude Opus 4.5 | 94% | 2,100 | 4.2s | ¥0.36 |
| GPT-5.2 | 88% | 2,400 | 5.8s | ¥0.34 |
| DeepSeek V3.2 | 68% | 3,800 | 18.3s | ¥0.58 |
| Kimi K2.6 | 62% | 3,200 | 22.1s | ¥0.42 |
任务 2 — 遗留代码分析("对现有功能零破坏"才算正确)
| 模型 | 首次正确率 | 平均 token/次 | 平均耗时 | 含重试总成本 |
|---|---|---|---|---|
| Claude Opus 4.5 | 92% | 3,600 | 6.8s | ¥0.64 |
| GPT-5.2 | 86% | 3,900 | 8.1s | ¥0.61 |
| DeepSeek V3.2 | 58% | 4,500 | 28.4s | ¥1.12 |
| Kimi K2.6 | 52% | 4,100 | 31.2s | ¥0.89 |
任务 3 — 需求文档汇总("可直接交付产品经理"才算正确)
| 模型 | 首次正确率 | 平均 token/次 | 平均耗时 | 含重试总成本 |
|---|---|---|---|---|
| Claude Opus 4.5 | 96% | 1,800 | 3.5s | ¥0.28 |
| GPT-5.2 | 92% | 1,900 | 4.1s | ¥0.27 |
| DeepSeek V3.2 | 78% | 2,600 | 11.2s | ¥0.39 |
| Kimi K2.6 | 74% | 2,400 | 13.8s | ¥0.35 |
把上面 9 个数据点汇总:在「代码生成」和「遗留代码分析」这两个典型工程任务上,DeepSeek 的含重试总成本高于 Claude Opus 4.5。便宜 15 倍的单 token 价格,被低 20-35% 的正确率和 3-4 倍的重试 token 消耗完全吃掉了。
这不是说 DeepSeek 不好。它在中低复杂度任务(任务 3,需求文档)上仍然是最优选择。 关键在于——你得知道你的任务在什么复杂度区间,而不是凭单价感觉做决定。
评测框架代码
如果你也想在自己项目里跑类似的评测,这是我用的框架:
// benchmark.js — LLM 任务可靠性对比框架
const fs = require('fs');
const path = require('path');
class LLMBenchmark {
constructor(options = {}) {
this.models = options.models || [];
this.concurrency = options.concurrency || 1;
this.retries = options.retries || 3;
this.timeout = options.timeout || 60000;
}
async run(taskName, testCases, judge) {
const results = [];
for (const model of this.models) {
let correct = 0, totalTokens = 0, totalTime = 0, retryCount = 0;
for (const tc of testCases) {
let success = false;
for (let attempt = 1; attempt <= this.retries + 1; attempt++) {
const start = Date.now();
try {
const resp = await Promise.race([
model.call(tc.prompt),
new Promise((_, rej) =>
setTimeout(() => rej(new Error('timeout')), this.timeout))
]);
totalTime += Date.now() - start;
totalTokens += resp.usage?.total_tokens ?? 0;
if (judge(resp.content, tc.expected, tc)) {
success = true;
break;
}
} catch (e) {
totalTime += Date.now() - start;
}
if (attempt > 1) retryCount++;
}
if (success) correct++;
}
const accuracy = correct / testCases.length;
const avgTokens = totalTokens / testCases.length;
const avgTime = totalTime / testCases.length;
const effectiveCost = model.price * avgTokens * (1 + retryCount / testCases.length);
results.push({
model: model.name,
task: taskName,
accuracy: (accuracy * 100).toFixed(1) + '%',
avgTokens: Math.round(avgTokens),
avgTime: (avgTime / 1000).toFixed(1) + 's',
retryRate: (retryCount / testCases.length).toFixed(2),
effectiveCost: '¥' + effectiveCost.toFixed(2),
});
}
return results;
}
}
// 使用示例:定义你的评测任务
const cases = [
{ prompt: '用 React Hooks 实现一个带防抖的搜索输入框,含 loading 状态和错误处理', expected: { mustCompile: true, hasDebounce: true, hasErrorBoundary: true } },
{ prompt: '写一个 Node.js 中间件,对 API 请求按 IP 做滑动窗口限流,窗口 60s,上限 100', expected: { correctAlgorithm: true, handlesEdgeCases: true } },
];
const judge = (output, expected) => { /* 你的正确性判断逻辑 */ };
const results = await benchmark.run('code-gen', cases, judge);
console.table(results);
这段代码的核心思想很简单:不要用单个 prompt 测一次就下结论,至少跑 50 个 case,算平均正确率和含重试的真实成本。
二、Prompt 有模型特异性,不是换 API Key 就能换模型
这是我踩的最深的坑。
年前我用 Claude Opus 4 写了一个代码审查 bot 的 prompt,跑了大半年效果稳定。DeepSeek V3 出来之后我想着"反正都是 OpenAI 兼容 API,换一下 key 就行了"。
结果第一天跑下来,DeepSeek 产出的 review 意见质量断崖式下跌。不是"差一点"——是 GitHub 上的开发者直接质疑"这 bot 是不是坏了"。
我把同一个 PR 分别发给 Claude 和 DeepSeek,对比输出:
Claude 的 review:
## 问题 1:竞态条件风险(关键)
`fetchUserData()` 在 `useEffect` 中未做清理。组件卸载后 setState 会导致内存泄漏。
建议:使用 AbortController 或 isMounted 标记。
具体修复:
```diff
- useEffect(() => { fetchUserData(id).then(setData) }, [id])
+ useEffect(() => {
+ let cancelled = false
+ fetchUserData(id).then(d => { if (!cancelled) setData(d) })
+ return () => { cancelled = true }
+ }, [id])
DeepSeek 的 review(同一 prompt,同一段代码):
这段代码存在潜在问题:
1. 应使用 useCallback 包装 fetchUserData
2. 建议添加 loading 状态
3. 错误处理需要完善
可以考虑添加 try-catch 块来捕获异常。
差别在哪?Claude 的 review 有具体的代码 patch、有根因分析、有修复方案。DeepSeek 的呢?"建议添加 loading 状态"——这是 code review 还是代码风格检查?
问题出在 prompt 里。我原来的 prompt 是"面向 Claude 优化"的——大量使用 Antropic 特定的提示格式(多层嵌套的 <thinking> / <response> 标签结构、System Prompt 中嵌入角色扮演指令)。这些格式在翻译到 DeepSeek 时被"压缩"了,模型把复杂的审查指令简化成了"提 3 条通用建议"。
教训:Prompt 不是模型无关的。 每个模型对 prompt 结构的理解和利用方式完全不同。换了模型就得重新调 prompt,不是换个 API key 的事。
我后来花了两周重新为每个模型设计了独立 prompt,核心原则:
- 指令放在最前面,不要放在 system prompt 里 — DeepSeek 和 Kimi 对 system prompt 的遵从度远不如 Claude
- 用结构化输出格式(JSON Schema)而不是自然语言约束 — "请输出 JSON" 不如直接给
response_format: { type: "json_object" } - Few-shot 是必须的 — 对 DeepSeek/Kimi 来说,没有 3-5 个示例,输出质量不可控
下面是我改造后的 prompt 结构:
// prompt-factory.js — 为不同模型生成适配 prompt
function buildPrompt(modelFamily, task, context) {
if (modelFamily === 'claude') {
return {
system: `你是一位高级软件工程师。请分析以下代码并给出具体修改方案。`,
messages: [
{ role: 'user', content: buildClaudeFormat(task, context) }
]
};
}
// DeepSeek/Kimi/OpenAI 族:指令放 user message,带 few-shot
if (['deepseek', 'kimi', 'openai'].includes(modelFamily)) {
return {
messages: [
{
role: 'system',
content: '你是一个代码审查助手。请仅输出 JSON。'
},
{
role: 'user',
content: `请审查以下代码,按 JSON 格式输出问题列表。
示例输出:
[{"severity":"critical","file":"src/api.ts:42","issue":"竞态条件","fix":"const ctrl = new AbortController()"}]
现在请审查:
\`\`\`
${task.code}
\`\`\`
上下文:${context}`
}
],
response_format: { type: 'json_object' }
};
}
}
结果:切换后 DeepSeek 的 review 准确率从 34% 提升到 78%。仍然不如 Claude 的 94%,但至少能用了。
三、生产环境里,可靠性是第一指标
SWE-bench 80.9% 这些数字漂亮吗?漂亮。但对生产环境来说,一个模型偶尔完美、偶尔崩溃的模式,比一个稳定 70 分的模型更危险。
这是我三个月运行下来的实际观测:
Claude Opus 4.5 — 平均延迟 4.2s,P99=9.8s,超时率 0.3%
GPT-5.2 — 平均延迟 5.1s,P99=14.2s,超时率 0.8%
DeepSeek V3.2 — 平均延迟 8.4s,P99=58.7s,超时率 4.2%
Kimi K2.6 — 平均延迟 9.6s,P99=72.3s,超时率 5.7%
看平均延迟,DeepSeek 的 8.4 秒似乎还能接受。但看 P99 —— 58.7 秒。这意味着每 100 次请求里就有 1 次要等将近一分钟。对于一个面向用户的系统,P99 延迟 58 秒基本等于"这个功能不可用"。
更致命的是并发限制变化。DeepSeek 在 5 月突然把并发上限从"不明确"改为明确的数值(V4-Pro 500、V4-Flash 2500),超限直接返回错误。我线上跑着的一个 Agent 系统依赖数十个并发请求做并行推理,一秒之内全崩了。
生产环境选模型的三条铁律,这是我用三个月故障换来的:
- P99 延迟比平均延迟重要 10 倍。平均 5 秒 P99 60 秒的模型不配进生产链路
- 降级链必须预设好。不要等崩了再想切哪个模型,上线前就写好:A → B → C 的降级路径
- 并发控制是调用端的事,别指望服务端。用信号量自己控,而不是等 API 返回 429
生产级多模型路由架构
这是目前在我线上稳定运行的路由方案,核心组件包括质量评估、熔断器和自动降级:
// llm-router.js — 生产级多模型路由(含熔断器 + 自动降级)
const CircuitBreaker = require('./circuit-breaker');
class LLMRouter {
constructor() {
// 按任务类型定义模型优先级和降级链
this.routes = {
'code-generation': [
{ model: 'claude-opus-4.5', provider: ClaudeProvider,
minQuality: 0.85, breaker: new CircuitBreaker('claude-code', { threshold: 5, window: 60000 }) },
{ model: 'gpt-5.2', provider: OpenAIProvider,
minQuality: 0.80, breaker: new CircuitBreaker('gpt-code', { threshold: 5, window: 60000 }) },
{ model: 'deepseek-v3.2', provider: DeepSeekProvider,
minQuality: 0.70, breaker: new CircuitBreaker('ds-code', { threshold: 3, window: 60000 }) },
],
'documentation': [
{ model: 'kimi-k2.6', provider: KimiProvider,
minQuality: 0.70, breaker: new CircuitBreaker('kimi-doc', { threshold: 5, window: 60000 }) },
{ model: 'gpt-5.2', provider: OpenAIProvider,
minQuality: 0.78, breaker: new CircuitBreaker('gpt-doc', { threshold: 5, window: 60000 }) },
{ model: 'claude-opus-4.5', provider: ClaudeProvider,
minQuality: 0.85, breaker: new CircuitBreaker('claude-doc',{ threshold: 5, window: 60000 }) },
],
'fallback': [
{ model: 'gpt-5.2', provider: OpenAIProvider, minQuality: 0.70, breaker: new CircuitBreaker('fb', { threshold: 10, window: 120000 }) },
],
};
// 滑动窗口,记录每个模型的质量分数
this.qualityWindow = new Map();
this.windowSize = 50;
}
async route(taskType, messages, options = {}) {
const candidates = this.routes[taskType] || this.routes['fallback'];
for (const candidate of candidates) {
// 1. 熔断检查
if (candidate.breaker.isOpen()) {
console.warn(`[Router] ${candidate.model} 熔断中,跳过`);
continue;
}
// 2. 质量检查:当前窗口分数低于阈值则跳过
const qualityScore = this.getQualityScore(candidate.model);
if (qualityScore !== null && qualityScore < candidate.minQuality) {
console.warn(`[Router] ${candidate.model} 质量分数 ${qualityScore} < 阈值 ${candidate.minQuality},降级`);
continue;
}
// 3. 调用
try {
const start = Date.now();
const resp = await Promise.race([
candidate.provider.call(messages, options),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), options.timeout || 30000))
]);
// 4. 记录延迟用于质量评估
this.recordResult(candidate.model, { success: true, latency: Date.now() - start });
candidate.breaker.recordSuccess();
return resp;
} catch (err) {
this.recordResult(candidate.model, { success: false, error: err.message });
candidate.breaker.recordFailure();
console.error(`[Router] ${candidate.model} 调用失败: ${err.message}`);
// 继续降级到下一个候选
}
}
throw new Error(`[Router] 所有模型均不可用,任务类型: ${taskType}`);
}
// 滑动窗口质量评分:综合成功率 + P95 延迟的指数衰减
recordResult(model, { success, latency }) {
if (!this.qualityWindow.has(model)) this.qualityWindow.set(model, []);
const window = this.qualityWindow.get(model);
window.push({ success, latency, time: Date.now() });
if (window.length > this.windowSize) window.shift();
}
getQualityScore(model) {
const window = this.qualityWindow.get(model);
if (!window || window.length < 10) return null; // 数据不足,不判断
const successRate = window.filter(r => r.success).length / window.length;
const latencies = window.filter(r => r.success).map(r => r.latency).sort((a, b) => a - b);
const p95 = latencies[Math.floor(latencies.length * 0.95)];
// 综合分 = 成功率 × 延迟衰减系数
const latencyPenalty = Math.exp(-p95 / 15000); // 15 秒为半衰期
return (successRate * 0.7 + latencyPenalty * 0.3).toFixed(3);
}
}
核心逻辑是三层防护:
- 熔断器:模型连续失败 3-5 次(根据模型稳定性调整阈值),自动暂停调用 60 秒
- 质量阈值:滑动窗内质量分数低于预设值,跳过该模型直接降级——不需要等它崩
- 自动降级链:code-gen 的链路是 Claude → GPT → DeepSeek,每层都有独立的熔断和质量评估
这套架构上线后,我线上 AI 系统的可用性从 97.2% 提升到了 99.7%。
四、总结:三条原则,一个框架
回过头看,这三个月我学到的不只是"哪个模型好"——每个模型都有它的最佳场景。真正重要的是选模型的方法。
三条原则
原则 1:永远算正确答案成本,别看 token 单价
把你的实际任务跑 50 个 case,统计首次正确率和重试率,算含重试的总 token 成本。单 token 价格低 10 倍但正确率低 20% 的模型,在复杂任务上可能更贵。
原则 2:换模型 = 换 prompt
模型的 API 兼容不意味着行为兼容。每个模型对 prompt 结构的敏感点不同。Claude 善于处理嵌套指令结构,DeepSeek/Kimi 需要「指令在最前面 + few-shot + JSON Schema 约束」。
原则 3:可靠性 > 性能 > 价格
P99 延迟超过 30 秒的模型,不管多便宜,不要放进面向用户的生产链路。用熔断器 + 质量阈值 + 自动降级链这个「三层防护」架构兜底。
选型决策框架
最后给一个可实操的决策框架,下次选模型时按这个顺序来:
1. 定义你的任务类型和"正确"的标准
├── 代码生成:能编译 + 通过你项目的测试用例
├── 文档写作:不需要返工可以直接交付
└── Agent 推理:多步操作中每一步都正确
2. 用 benchmark 框架跑 50 个 case
├── 统计首次正确率、P95 延迟、含重试总成本
└── 不要只看平均,看分布(标准差、P99)
3. 按"三层防护"架构部署
├── 主模型:选正确率最高的
├── 降级模型:选第二高、延迟差异不大的
└── 兜底模型:选最稳定、P99 延迟可控的
4. 上线后持续监控质量窗口
├── 熔断阈值:复杂任务 3 次失败触发,简单任务 5 次
└── 每周复盘路由日志,调整模型优先级
如果你只有一个模型可以用——选你的任务类型下正确率最高的那个,而不是最便宜的。省下来的 token 费用,会在重试、debug 和用户投诉里加倍还回来。
用到的评测框架和生产路由代码都在文中,可以直接复制使用。有不同的实践经验欢迎评论区交流。