《互联网产品的 AI 架构·推理性能与成本工程》6.3 模型级联与成本收益

40 条样本上,按 0.7375 盈亏平衡点级联总成本 7670 微元,比常驻大模型省 730,代价是准确率从 100% 降到 97.5%。本节推导阈值公式、量化误判成本,并给出三策略对比与阈值扫描方法,说明级联并非无条件更省,质量下限也会改变结论。

先小后大不是默认正确

「回声」上线了模型级联:小模型先答,置信度低再升级大模型。上线一个月,账单确实降了,但错误率也涨了。复盘发现两个问题:阈值是拍脑袋定的 0.6,升级后的请求把两个模型的成本都付了一遍,团队却只按“大模型单价”估算收益。

级联不是一条自然更优的路径,它是一笔用准确率换成本的交易。交易是否划算,取决于三个数:小模型一次多少钱、升级一次多花多少钱、一次错误答案的业务损失多少钱。三个数都写清楚,阈值才有出处。

本节用 40 条确定性样本比较三种策略:常驻小模型、常驻大模型、按盈亏平衡点级联。文中所有数字均为「示例样本上的运行结果」,成本单位为整数微元,延迟单位为毫秒。

级联的成本结构

路径计算成本延迟结果来自
小模型直接采纳小模型成本小模型延迟小模型
升级小模型成本 + 大模型成本小模型延迟 + 大模型延迟大模型

升级路径的成本不是“大模型成本”,而是“两次调用之和”。小模型已经跑完,钱已经花了。很多级联估算只算大模型单价,结果高估收益。

延迟同理:升级请求要等两次。示例里小模型 P95 122ms、大模型 P95 600ms,全量升级的 P95 变成 706ms;按盈亏平衡点级联因为只升级一部分,P95 是 692ms。级联省的是未升级请求的钱,不是所有请求的延迟。

阈值从哪来

设小模型置信度为 p,一次错误成本为 E,升级成本为 L。采纳小模型的边际期望成本是 (1-p)×E,升级的边际成本是 L。两者相等时:

p* = 1 - L / E

示例 L=210、E=800,得到 p* = 1 - 210/800 = 0.7375。置信度低于 0.7375 时升级更划算,高于它则采纳更省。

这个公式只回答成本,不回答质量。它假设置信度是校准过的:说 0.8 的时候确实有八成正确。如果模型“自信地答错”,阈值会把错误答案留在小模型侧。示例特意保留了一条置信度 0.95 但答错的样本,用来提醒这一点。

当 L ≥ E 时,公式给出非正阈值,含义是升级永远不会在成本上回本。这时是否升级只能由质量要求决定,不能伪装成省钱。

误判成本怎么定

误判成本不是模型指标,是业务数字。可以用三类损失相加:一次人工纠正的工时、一次退款或补偿的金额、一次用户流失的期望损失。三者都换成微元,写进配置并注明来源。

示例取 800 微元,约为一次大模型调用成本的 3.8 倍。这个比例决定了级联是否有空间:错误成本越高,越值得升级;错误成本低于升级成本时,常驻大模型反而更省。

误判成本也不能一刀切。代码评审的错误可能是线上故障,闲聊的错误可能只是一句不准确。按能力分别配置 E,比全局一个数字更接近真实。

示例样本上的运行结果

40 条样本,小模型成本 30 微元、大模型成本 210 微元、错误成本 800 微元,盈亏平衡点 0.7375。

策略准确率升级率计算成本错误成本总成本平均成本P95 延迟
常驻小模型32.5%0%12002160022800570.00122ms
常驻大模型100%—840008400210.00600ms
盈亏平衡级联97.5%67.5%68708007670191.75692ms

级联比常驻大模型省 730 微元,准确率低 2.5 个百分点;比常驻小模型省 15130 微元,准确率高 65 个百分点。它落在中间,不是无条件最优。

按 0.01 步长扫描 101 个阈值,纯成本最优点是 0.71,总成本 7460,比盈亏平衡点再省 210。原因是那条 0.72 置信度的样本恰好答对,而公式只按概率算,不知道这一条的真值。扫描结果会过拟合验收集,不能直接当成线上阈值。

如果业务要求准确率不低于 99%,可选阈值跳到 0.96 以上:所有请求都升级,总成本 9600,比常驻大模型还贵 1200,因为每个请求都先付了一次小模型成本。这说明“级联满足质量下限”不等于“级联比常驻大模型便宜”。

实现

模块包含盈亏平衡推导、单阈值模拟、三策略对比与阈值扫描。所有成本都是整数微元,错误成本按“错误条数 × 单次错误成本”结算。

// file: src/perf/cascade.ts
// tier: L2
// verify: node --test test/perf/cascade.test.ts

import { AiError } from "../core/errors.ts";
import { percentile } from "../observe/baseline.ts";

/** 一条级联样本同时记录两个模型的成本、延迟与正确性,阈值才能被逐条复算。 */
export interface CascadeSample {
  id: string;
  /** 难度 0 到 1,用于解释置信度分布,不参与阈值计算。 */
  difficulty: number;
  /** 小模型对这条样本的置信度,0 到 1。 */
  smallConfidence: number;
  smallCorrect: boolean; largeCorrect: boolean;
  smallCostMicros: number; largeCostMicros: number;
  smallLatencyMs: number; largeLatencyMs: number;
}

export interface CascadePolicy {
  /** 低于该置信度就升级;可由 breakEvenConfidence 推出,也可按质量要求手动收紧。 */
  confidenceThreshold: number;
  /** 一次错误答案的业务成本,用整数微元表示。 */
  errorCostMicros: number;
}

export interface ThresholdPoint {
  threshold: number; escalationRate: number; accuracy: number;
  computeCostMicros: number; errorCostMicros: number; totalCostMicros: number;
  avgCostMicros: number; p95LatencyMs: number;
}

export interface CascadeResult extends ThresholdPoint {
  /** 小模型被采纳的样本数。 */
  accepted: number; escalated: number; errors: number;
}

export interface BreakEven {
  /** 小模型置信度高于该值时,采纳它比升级更省。 */
  threshold: number;
  /** 升级成本高于一次错误成本时,纯经济口径下永远不值得升级。 */
  escalationPaysOff: boolean; explanation: string;
}

function fail(message: string): never {
  throw new AiError("invalid-request", `cascade: ${message}`);
}

function rate(numerator: number, denominator: number): number {
  return denominator === 0 ? 0 : numerator / denominator;
}

function validateSample(sample: CascadeSample): void {
  if (sample.id.trim() === "") fail("样本 id 不能为空");
  if (!(sample.smallConfidence >= 0 && sample.smallConfidence <= 1)) {
    fail(`样本 ${sample.id} 的 smallConfidence 必须落在 [0, 1]`);
  }
  if (!(sample.difficulty >= 0 && sample.difficulty <= 1)) {
    fail(`样本 ${sample.id} 的 difficulty 必须落在 [0, 1]`);
  }
  for (const field of ["smallCostMicros", "largeCostMicros"] as const) {
    if (!Number.isInteger(sample[field]) || sample[field] < 0) {
      fail(`样本 ${sample.id} 的 ${field} 必须是非负整数`);
    }
  }
  for (const field of ["smallLatencyMs", "largeLatencyMs"] as const) {
    if (!Number.isFinite(sample[field]) || sample[field] < 0) {
      fail(`样本 ${sample.id} 的 ${field} 必须是非负数`);
    }
  }
}

/**
 * 盈亏平衡推导只看边际成本:
 * 采纳小模型 = 小模型成本 + (1-p) × 错误成本;升级 = 小模型成本 + 大模型成本。
 * 两者相等时 p* = 1 - 大模型成本 / 错误成本。
 */
export function breakEvenConfidence(
  smallCostMicros: number,
  largeCostMicros: number,
  errorCostMicros: number,
): BreakEven {
  if (!Number.isInteger(smallCostMicros) || smallCostMicros < 0) fail("smallCostMicros 必须是非负整数");
  if (!Number.isInteger(largeCostMicros) || largeCostMicros < 0) fail("largeCostMicros 必须是非负整数");
  if (!Number.isInteger(errorCostMicros) || errorCostMicros <= 0) fail("errorCostMicros 必须是正整数");
  if (largeCostMicros >= errorCostMicros) {
    return { threshold: 0, escalationPaysOff: false,
      explanation: `升级成本 ${largeCostMicros} 不低于一次错误成本 ${errorCostMicros},纯经济口径下不应升级` };
  }
  const threshold = 1 - largeCostMicros / errorCostMicros;
  return { threshold, escalationPaysOff: true,
    explanation: `置信度低于 ${threshold.toFixed(4)} 时,期望错误成本高于升级成本` };
}

/** 按给定阈值跑一遍级联:小模型先答,低于阈值才升级。 */
export function simulateCascade(samples: readonly CascadeSample[], policy: CascadePolicy): CascadeResult {
  if (samples.length === 0) fail("至少需要一条样本");
  if (!(policy.confidenceThreshold >= 0 && policy.confidenceThreshold <= 1)) fail("confidenceThreshold 必须落在 [0, 1]");
  if (!Number.isInteger(policy.errorCostMicros) || policy.errorCostMicros <= 0) fail("errorCostMicros 必须是正整数");
  for (const sample of samples) validateSample(sample);

  let accepted = 0;
  let escalated = 0;
  let errors = 0;
  let computeCostMicros = 0;
  const latencies: number[] = [];
  for (const sample of samples) {
    const useSmall = sample.smallConfidence >= policy.confidenceThreshold;
    const correct = useSmall ? sample.smallCorrect : sample.largeCorrect;
    if (useSmall) accepted += 1;
    else escalated += 1;
    computeCostMicros += sample.smallCostMicros + (useSmall ? 0 : sample.largeCostMicros);
    latencies.push(sample.smallLatencyMs + (useSmall ? 0 : sample.largeLatencyMs));
    if (!correct) errors += 1;
  }
  const errorCostMicros = errors * policy.errorCostMicros;
  const totalCostMicros = computeCostMicros + errorCostMicros;
  return {
    threshold: policy.confidenceThreshold, accepted, escalated, errors,
    escalationRate: rate(escalated, samples.length),
    accuracy: rate(samples.length - errors, samples.length),
    computeCostMicros, errorCostMicros, totalCostMicros,
    avgCostMicros: rate(totalCostMicros, samples.length), p95LatencyMs: percentile(latencies, 0.95),
  };
}

export interface StrategyComparison {
  alwaysSmall: ThresholdPoint; alwaysLarge: ThresholdPoint; cascade: ThresholdPoint;
  breakEven: BreakEven;
  /** 级联相对常驻大模型节省的总成本,可为负。 */
  savingVsAlwaysLargeMicros: number;
}

/** 常驻策略不经过阈值判断,单独计算,避免用 1.0001 这类哨兵值污染阈值口径。 */
function simulateFixed(samples: readonly CascadeSample[], useSmall: boolean, errorCostMicros: number): CascadeResult {
  let errors = 0;
  let computeCostMicros = 0;
  const latencies: number[] = [];
  for (const sample of samples) {
    const correct = useSmall ? sample.smallCorrect : sample.largeCorrect;
    computeCostMicros += useSmall ? sample.smallCostMicros : sample.largeCostMicros;
    latencies.push(useSmall ? sample.smallLatencyMs : sample.largeLatencyMs);
    if (!correct) errors += 1;
  }
  const errorCost = errors * errorCostMicros;
  return {
    threshold: useSmall ? 0 : 1, accepted: useSmall ? samples.length : 0,
    escalated: useSmall ? 0 : samples.length, errors, escalationRate: useSmall ? 0 : 1,
    accuracy: rate(samples.length - errors, samples.length),
    computeCostMicros, errorCostMicros: errorCost, totalCostMicros: computeCostMicros + errorCost,
    avgCostMicros: rate(computeCostMicros + errorCost, samples.length), p95LatencyMs: percentile(latencies, 0.95),
  };
}

/** 三种策略放在同一批样本上比较:常驻小、常驻大、按盈亏平衡点级联。 */
export function compareStrategies(samples: readonly CascadeSample[], errorCostMicros: number): StrategyComparison {
  if (samples.length === 0) fail("至少需要一条样本");
  for (const sample of samples) validateSample(sample);
  const smallCost = Math.round(samples.reduce((sum, item) => sum + item.smallCostMicros, 0) / samples.length);
  const largeCost = Math.round(samples.reduce((sum, item) => sum + item.largeCostMicros, 0) / samples.length);
  const breakEven = breakEvenConfidence(smallCost, largeCost, errorCostMicros);
  const alwaysSmall = simulateFixed(samples, true, errorCostMicros);
  const alwaysLarge = simulateFixed(samples, false, errorCostMicros);
  const cascade = simulateCascade(samples, {
    confidenceThreshold: breakEven.escalationPaysOff ? breakEven.threshold : 1,
    errorCostMicros,
  });
  const pick = (result: CascadeResult): ThresholdPoint => ({
    threshold: result.threshold,
    escalationRate: result.escalationRate,
    accuracy: result.accuracy,
    computeCostMicros: result.computeCostMicros,
    errorCostMicros: result.errorCostMicros,
    totalCostMicros: result.totalCostMicros,
    avgCostMicros: result.avgCostMicros,
    p95LatencyMs: result.p95LatencyMs,
  });
  return {
    alwaysSmall: pick(alwaysSmall),
    alwaysLarge: pick(alwaysLarge),
    cascade: pick(cascade),
    breakEven,
    savingVsAlwaysLargeMicros: alwaysLarge.totalCostMicros - cascade.totalCostMicros,
  };
}

export interface ThresholdSweep {
  points: ThresholdPoint[]; best: ThresholdPoint;
  /** 同时满足准确率下限的最低成本点;没有满足的点时为 null。 */
  qualityFloor: ThresholdPoint | null;
}

/**
 * 阈值扫描不是“挑一个最省钱的点”,而是把所有候选点的成本、准确率与升级率摊开,
 * 再由业务决定质量下限。返回的 best 只代表纯成本最优。
 */
export function sweepThresholds(
  samples: readonly CascadeSample[],
  errorCostMicros: number,
  options: { step?: number; minAccuracy?: number } = {},
): ThresholdSweep {
  if (samples.length === 0) fail("至少需要一条样本");
  const step = options.step ?? 0.05;
  if (!(step > 0 && step <= 0.5)) fail("step 必须落在 (0, 0.5]");
  const points: ThresholdPoint[] = [];
  for (let threshold = 0; threshold <= 1 + 1e-9; threshold += step) {
    const rounded = Math.min(1, Math.round(threshold * 1000) / 1000);
    const result = simulateCascade(samples, { confidenceThreshold: rounded, errorCostMicros });
    points.push({
      threshold: rounded,
      escalationRate: result.escalationRate,
      accuracy: result.accuracy,
      computeCostMicros: result.computeCostMicros,
      errorCostMicros: result.errorCostMicros,
      totalCostMicros: result.totalCostMicros,
      avgCostMicros: result.avgCostMicros,
      p95LatencyMs: result.p95LatencyMs,
    });
  }
  const best = points.reduce((left, right) => {
    if (right.totalCostMicros !== left.totalCostMicros) {
      return right.totalCostMicros < left.totalCostMicros ? right : left;
    }
    return right.accuracy > left.accuracy ? right : left;
  });
  const minAccuracy = options.minAccuracy;
  const qualified =
    minAccuracy === undefined
      ? []
      : points.filter((point) => point.accuracy >= minAccuracy);
  const qualityFloor = qualified.length === 0
    ? null
    : qualified.reduce((left, right) =>
        right.totalCostMicros < left.totalCostMicros ? right : left,
      );
  return { points, best, qualityFloor };
}

breakEvenConfidence 只做边际比较,不预测准确率。simulateCascade 在升级时累加两次成本与两段延迟,避免把升级请求算成单次调用。compareStrategies 的常驻策略单独计算,不用 1.0001 之类的哨兵阈值。

扫描函数返回全部候选点,而不是只返回一个“最佳阈值”。最佳点只代表样本内的成本最优;是否采用,还要看准确率下限、P95 延迟与线上校准。

测试与运行结果

测试覆盖盈亏平衡公式、升级不划算的边界、阈值 0 与 1 的常驻等价、级联总成本、三策略对比、101 点扫描与 99% 准确率下限。执行:

node --test test/perf/cascade.test.ts

本机一次运行的真实输出摘要如下,耗时随机器变化:

ℹ tests 8
ℹ pass 8
ℹ fail 0

上线顺序

第一步是离线配对:同一批线上请求同时送小模型与大模型,记录置信度、正确性与两次成本,先把校准曲线画出来。第二步用盈亏平衡点做影子级联,不影响用户,只比较预估账单。第三步按能力放量,每个能力单独配置错误成本与质量下限。

回滚条件提前写:准确率低于下限、P95 延迟超过交互预算、或级联总成本高于常驻大模型,任何一条触发就关闭级联。级联的收益很薄,一次错误的成本参数就能把它变成负收益。

小结

级联的收益来自“大部分请求不需要大模型”,成本来自“升级请求付两次钱”和“错误答案的业务损失”。阈值不该拍脑袋,而应由 1 - 升级成本 / 错误成本 推出;示例样本上的盈亏平衡点是 0.7375,级联总成本 7670,比常驻大模型省 730,准确率低 2.5 个百分点。

级联也不是终点。下一章把在线与离线的边界讲清楚:有些工作根本不必在请求路径上做,离线批处理能把单位成本压到在线级联做不到的水平。

阅读导航:上一节:6.2 蒸馏与小模型替代 · 下一节:7.1 在线与离线的边界 。

常见错误与边界

  • 把升级成本只算成大模型一次调用。小模型已经花掉的钱不会退回。
  • 用单一错误成本覆盖所有能力。代码故障与闲聊错误的代价不在一个量级。
  • 阈值凭直觉设定。没有成本公式,无法解释为什么是 0.6 或 0.8。
  • 假设置信度天然校准。自信答错的样本会把错误留在小模型侧。
  • 只看平均成本,不看 P95 延迟。升级请求要等两次调用。
  • 用扫描出的最低成本点直接上线。它可能过拟合验收集。
  • 把“满足准确率下限”当成“比常驻大模型便宜”。全量升级仍付两次成本。
  • 不设回滚条件。错误成本估高一倍,级联就从省钱变成烧钱。
  • 把示例 mock 数字写成生产账单。所有结论都要在真实分布上复核。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查