生产环境跑了 3 个月的多模型路由,我发现每 token 价格是最没用的指标

5 阅读12分钟

生产环境跑了 3 个月的多模型路由,我发现每 token 价格是最没用的指标

今年 2 月我做了一个愚蠢的决定——因为 DeepSeek 单价便宜,我把公司内部 3 个 AI 业务的底层模型从 Claude Opus 4 全切到了 DeepSeek V3。

结果呢?月账单确实降了 60%,但用户投诉翻了 3 倍。更扎心的是,我算了一笔账:把重试次数、人工介入、用户流失算进去,总成本比之前反而高了 40%

这不是 DeepSeek 的问题——它的单 token 性价比毋庸置疑。问题出在我选模型的逻辑上。我今天把这三个月踩过的坑、建立的评测框架、以及现在线上稳定运行的多模型路由架构完整写出来。

先给结论,免得你没耐心看完:

  1. 每 token 价格 × 正确率 → "正确答案成本"才是唯一有意义的效率指标
  2. Prompt 有模型特异性——同一个 prompt 在 Claude 和 DeepSeek 上的表现不是 90 vs 80,而是 "能用" vs "完全不能用"
  3. 生产环境里,可靠性比性能和价格都重要。一个偶尔超时 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.594%2,1004.2s¥0.36
GPT-5.288%2,4005.8s¥0.34
DeepSeek V3.268%3,80018.3s¥0.58
Kimi K2.662%3,20022.1s¥0.42

任务 2 — 遗留代码分析("对现有功能零破坏"才算正确)

模型首次正确率平均 token/次平均耗时含重试总成本
Claude Opus 4.592%3,6006.8s¥0.64
GPT-5.286%3,9008.1s¥0.61
DeepSeek V3.258%4,50028.4s¥1.12
Kimi K2.652%4,10031.2s¥0.89

任务 3 — 需求文档汇总("可直接交付产品经理"才算正确)

模型首次正确率平均 token/次平均耗时含重试总成本
Claude Opus 4.596%1,8003.5s¥0.28
GPT-5.292%1,9004.1s¥0.27
DeepSeek V3.278%2,60011.2s¥0.39
Kimi K2.674%2,40013.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,核心原则:

  1. 指令放在最前面,不要放在 system prompt 里 — DeepSeek 和 Kimi 对 system prompt 的遵从度远不如 Claude
  2. 用结构化输出格式(JSON Schema)而不是自然语言约束 — "请输出 JSON" 不如直接给 response_format: { type: "json_object" }
  3. 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 系统依赖数十个并发请求做并行推理,一秒之内全崩了。

生产环境选模型的三条铁律,这是我用三个月故障换来的:

  1. P99 延迟比平均延迟重要 10 倍。平均 5 秒 P99 60 秒的模型不配进生产链路
  2. 降级链必须预设好。不要等崩了再想切哪个模型,上线前就写好:A → B → C 的降级路径
  3. 并发控制是调用端的事,别指望服务端。用信号量自己控,而不是等 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);
  }
}

核心逻辑是三层防护:

  1. 熔断器:模型连续失败 3-5 次(根据模型稳定性调整阈值),自动暂停调用 60 秒
  2. 质量阈值:滑动窗内质量分数低于预设值,跳过该模型直接降级——不需要等它崩
  3. 自动降级链: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 和用户投诉里加倍还回来。


用到的评测框架和生产路由代码都在文中,可以直接复制使用。有不同的实践经验欢迎评论区交流。