《互联网产品的 AI 架构·推理性能与成本工程》11.1 质量-成本-延迟的权衡面

「回声」要在八个能力上同时选模型,加权总分却把取舍藏成一个数字。本节把质量、成本与延迟放回三维坐标,用支配关系求 Pareto 前沿,区分被支配区域与真正不可兼得的边界,再按能力护栏与边际成本选点;文中给出可运行的前沿模块与示例样本运行结果,说明最贵的不一定最好、最便宜的也不一定够用。

一个总分最高的方案,为什么上线后被换掉

「回声」的模型选型会上,团队把八个候选配置做成一张表,用加权总分排序:质量占 50%,成本占 30%,延迟占 20%。分数最高的是配置 G,大家准备按它上线。

两周后,配置 G 被换成了配置 C。原因不是 G 的质量不好,而是 G 在三条轴上都输给了 C:质量相同,C 更便宜,延迟也更低。加权总分却因为把三个不同量纲的数字先归一化再相加,让 G 的某一项被放大,最终排到了第一。

这类错误不是算错了小数,而是方法本身丢掉了一个关键问题:三个目标之间,哪些差异是“真的不能同时拿到”,哪些只是“没有被更好的点覆盖”。前者需要取舍,后者只需要换一个候选。

本节把候选放回三维坐标,用支配关系求 Pareto 前沿,再按能力护栏与边际成本选点。文中所有数字都来自 ai-lab 的示例样本运行结果,不是生产实测。

三条轴与一个证据门槛

三个目标必须先有统一口径,否则“质量高”只是形容词。

轴单位口径常见错误
质量通过率 0–1固定评测集上的通过比例用主观评分代替可复算的用例
成本微元/千次输入、输出与重试的整数微元之和只算成功请求,失败成本消失
延迟毫秒P95,最近秩法用平均值抹平长尾

样本量不是第四条轴,而是准入门槛。一个只有 40 条样本的 100% 通过率,不能和 600 条样本的 84% 放在同一张前沿图上比较。样本不足的坐标先被护栏拒绝,不参与支配关系。

成本用整数微元、延迟用毫秒,是为了让比较可复算。质量用通过率而不是“1–5 分”,是因为通过与否可以由评测脚本逐条重放;主观分一旦进入坐标,前沿就会随评审人变化。

支配:三条都不差,才叫更好

在三维空间里,点 A 支配点 B,当且仅当 A 在三条轴上都不比 B 差,且至少有一条严格更好。

A 支配 B  ⇔  qA ≥ qB  且  cA ≤ cB  且  lA ≤ lB
             且 (qA > qB 或 cA < cB 或 lA < lB)

被支配的点可以安全淘汰:一定存在另一个点,质量不低、成本不高、延迟不慢,而且至少一项更好。淘汰它不是“放弃一种取舍”,而是去掉一个永远不该被选的选项。

注意支配是偏序,不是全序。两个点可能互不支配:一个质量高但贵,一个便宜但慢。它们都可能留在前沿上,最终选哪个取决于业务护栏与偏好。

前沿与“不可兼得”的两种含义

Pareto 前沿是所有不被任何其他点支配的候选集合。它把平面分成两部分:前沿上的点,和前沿内部被支配的点。

这里有两个经常被混为一谈的“不可兼得”:

  • 被支配区域:其实可以兼得。存在一个点在三方面都不差,只是团队没有发现。这里的正确动作是换点,不是取舍。
  • 前沿上的移动:真的不能兼得。要提高质量,必须多付成本或接受更高延迟。这里的正确动作是定价取舍,不是继续找“完美方案”。

示例样本里,F、G、H 都落在被支配区域:F 被 B 支配,G 被 C 支配,H 被 D 支配。它们不是“另一种取舍”,而是更差的坐标。真正需要讨论的是前沿上的 A、B、E、C、D 五个点。

按能力设护栏,而不是全站一套阈值

同一个模型,对私信回复可能够用,对审核结论可能不够。护栏必须按能力设置:

能力最低质量成本上限(微元/千次)P95 上限最小样本
chat.reply0.78650650ms500
moderation.decide0.92900400ms800

示例样本只展示 chat.reply。在它的护栏下,A 因质量 0.72 被拒,E 因 P95 700ms 被拒,D 因成本 700 被拒,F 因样本 400 被拒。可行集只剩 B 与 C。

护栏的作用不是把前沿压成一个点,而是把“业务不能接受”的区域先切掉,再在剩余部分谈性价比。阈值本身要版本化:质量下限提高 0.02,可行集可能完全变样。

边际成本:沿质量阶梯给取舍定价

前沿按成本升序排列后,不是每个点都在“用成本换质量”。有些点只是用更高的成本换更低的延迟,例如 E:它比 B 贵 40 微元,质量高 0.03,但延迟从 460ms 升到 700ms。把 E 也算进质量阶梯,会把 C 的边际成本从 4000 抬到 12000。

因此只对“质量新高”的点计算边际成本:

marginal = (cost[i] - cost[prev]) / (quality[i] - quality[prev])

示例样本的完整前沿上,B 相对 A 是 1000 微元/质量点,E 相对 B 是 1333.33,C 相对 E 是 12000,D 相对 C 是 14000。这个阶梯说明:从 A 到 B 很便宜,从 E 到 C 很贵。

但 E 被延迟护栏淘汰后,可行阶梯只剩 B 与 C,C 的边际成本重算为 4000。这个差异不是实现细节,而是决策事实:被业务拒绝的中间点,不应该继续影响“下一档质量值多少钱”的判断。

三种选点方式

前沿本身不回答“选哪个”。至少需要三种显式策略:

  • max-quality:在可行集里选质量最高,适合审核、医疗、金融等错误代价高的能力。
  • min-cost:在可行集里选成本最低,适合草稿、摘要、内部工具等可容忍返工的能力。
  • knee:给定“每提升一个质量点最多愿意多付多少微元”,沿质量阶梯走到预算边界。

示例样本的护栏下,max-quality 选 C(质量 0.84,成本 420),min-cost 选 B(质量 0.80,成本 260)。knee 阈值取 5000 微元/质量点时选 C,因为 B→C 的边际成本是 4000;阈值降到 3000 时选 B。

三种策略没有哪个更“正确”。它们的区别是业务愿意为什么付费,必须写进决策记录,而不是藏进加权系数。

实现

下面的模块做四件事:校验坐标、求支配关系与前沿、在可行集上重算质量阶梯、按显式目标选点。它不访问模型,也不读取线上日志;输入是一组已经结算好的候选。

// file: src/perf/quality.ts
// tier: L1
// verify: node --test test/perf/quality.test.ts

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

export type SelectionObjective = "max-quality" | "min-cost" | "knee";

/** 一个候选点就是“某能力 + 某配置”在三条轴上的实测坐标。 */
export interface PerfCandidate {
  id: string;
  capability: string;
  quality: number;
  /** 每千次请求的整数微元成本。 */
  costMicrosPer1k: number;
  latencyP95Ms: number;
  /** 支撑该坐标的评测样本数;样本不足的坐标不能参与选点。 */
  sampleSize: number;
}

export interface CapabilityGuardrail {
  capability: string;
  minQuality: number;
  maxCostMicrosPer1k: number;
  maxLatencyP95Ms: number;
  minSampleSize: number;
}

export interface SelectionPolicy {
  objective: SelectionObjective;
  /** knee 模式:每提升 1 个质量点,最多愿意多付多少微元/千次。 */
  maxMarginalCostMicrosPerQualityPoint?: number;
}

export interface Rejection { id: string; reasons: string[] }

export interface FrontierPoint extends PerfCandidate {
  /** 相对上一个“质量新高”点的边际成本;不是新高时为 null。 */
  marginalCostMicrosPerQualityPoint: number | null;
  qualityLeader: boolean;
}

export interface FrontierSelection {
  chosen: string | null;
  feasible: string[];
  rejected: Rejection[];
  points: FrontierPoint[];
  rationale: string;
}

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

function validate(candidate: PerfCandidate): void {
  for (const field of ["id", "capability"] as const) {
    if (typeof candidate[field] !== "string" || candidate[field].trim() === "") fail(`${field} is required`);
  }
  if (!Number.isFinite(candidate.quality) || candidate.quality < 0 || candidate.quality > 1) {
    fail(`quality of "${candidate.id}" must be between 0 and 1`);
  }
  for (const field of ["costMicrosPer1k", "latencyP95Ms"] as const) {
    if (!Number.isInteger(candidate[field]) || candidate[field] < 0) fail(`${field} must be an integer >= 0`);
  }
  if (!Number.isInteger(candidate.sampleSize) || candidate.sampleSize < 1) {
    fail(`sampleSize of "${candidate.id}" must be an integer >= 1`);
  }
}

/** 三条轴都不差,且至少一条严格更好;只有被支配的点才可安全淘汰。 */
export function dominates(left: PerfCandidate, right: PerfCandidate): boolean {
  const noWorse =
    left.quality >= right.quality &&
    left.costMicrosPer1k <= right.costMicrosPer1k &&
    left.latencyP95Ms <= right.latencyP95Ms;
  const strict =
    left.quality > right.quality ||
    left.costMicrosPer1k < right.costMicrosPer1k ||
    left.latencyP95Ms < right.latencyP95Ms;
  return noWorse && strict;
}

function sameObjectives(left: PerfCandidate, right: PerfCandidate): boolean {
  return left.quality === right.quality &&
    left.costMicrosPer1k === right.costMicrosPer1k &&
    left.latencyP95Ms === right.latencyP95Ms;
}

/** Pareto 前沿只保留非支配点,并按成本升序排列。 */
export function paretoFrontier(candidates: readonly PerfCandidate[]): PerfCandidate[] {
  if (candidates.length === 0) fail("at least one candidate is required");
  const capability = candidates[0].capability;
  const seen = new Set<string>();
  for (const candidate of candidates) {
    validate(candidate);
    if (candidate.capability !== capability) fail(`mixed capabilities: ${capability} / ${candidate.capability}`);
    if (seen.has(candidate.id)) fail(`duplicate candidate "${candidate.id}"`);
    seen.add(candidate.id);
  }
  const nonDominated = candidates.filter(
    (candidate) => !candidates.some((other) => other.id !== candidate.id && dominates(other, candidate)),
  );
  const unique: PerfCandidate[] = [];
  for (const candidate of [...nonDominated].sort((left, right) => left.id.localeCompare(right.id))) {
    if (!unique.some((kept) => sameObjectives(kept, candidate))) unique.push(candidate);
  }
  return unique.sort((left, right) =>
    left.costMicrosPer1k - right.costMicrosPer1k ||
    left.latencyP95Ms - right.latencyP95Ms ||
    right.quality - left.quality ||
    left.id.localeCompare(right.id));
}

/**
 * 三维前沿投影到“成本-质量”平面后,只有不断刷新质量高点的点才构成质量阶梯。
 * 其余点用成本换延迟,不能用来计算“每提升一个质量点多少钱”。
 */
export function annotateFrontier(frontier: readonly PerfCandidate[]): FrontierPoint[] {
  const sorted = [...frontier].sort((left, right) =>
    left.costMicrosPer1k - right.costMicrosPer1k ||
    left.latencyP95Ms - right.latencyP95Ms ||
    right.quality - left.quality ||
    left.id.localeCompare(right.id));
  const points: FrontierPoint[] = [];
  let bestQuality = -1;
  let previous: PerfCandidate | null = null;
  for (const candidate of sorted) {
    const leader = candidate.quality > bestQuality;
    let marginal: number | null = null;
    if (leader) {
      if (previous !== null) {
        marginal = (candidate.costMicrosPer1k - previous.costMicrosPer1k) / (candidate.quality - previous.quality);
      }
      bestQuality = candidate.quality;
      previous = candidate;
    }
    points.push({ ...candidate, qualityLeader: leader, marginalCostMicrosPerQualityPoint: marginal });
  }
  return points;
}

function guardrailReasons(candidate: PerfCandidate, guardrail: CapabilityGuardrail): string[] {
  const reasons: string[] = [];
  if (candidate.capability !== guardrail.capability) reasons.push(`capability ${candidate.capability} != ${guardrail.capability}`);
  if (candidate.quality < guardrail.minQuality) reasons.push(`quality ${candidate.quality.toFixed(3)} < ${guardrail.minQuality.toFixed(3)}`);
  if (candidate.costMicrosPer1k > guardrail.maxCostMicrosPer1k) reasons.push(`cost ${candidate.costMicrosPer1k} > ${guardrail.maxCostMicrosPer1k}`);
  if (candidate.latencyP95Ms > guardrail.maxLatencyP95Ms) reasons.push(`latency ${candidate.latencyP95Ms} > ${guardrail.maxLatencyP95Ms}`);
  if (candidate.sampleSize < guardrail.minSampleSize) reasons.push(`sample ${candidate.sampleSize} < ${guardrail.minSampleSize}`);
  return reasons;
}

function pick(feasible: readonly FrontierPoint[], policy: SelectionPolicy): FrontierPoint | null {
  if (feasible.length === 0) return null;
  if (policy.objective === "max-quality") {
    return [...feasible].sort((left, right) =>
      right.quality - left.quality ||
      left.costMicrosPer1k - right.costMicrosPer1k ||
      left.latencyP95Ms - right.latencyP95Ms ||
      left.id.localeCompare(right.id))[0];
  }
  if (policy.objective === "min-cost") {
    return [...feasible].sort((left, right) =>
      left.costMicrosPer1k - right.costMicrosPer1k ||
      left.latencyP95Ms - right.latencyP95Ms ||
      right.quality - left.quality ||
      left.id.localeCompare(right.id))[0];
  }
  const threshold = policy.maxMarginalCostMicrosPerQualityPoint;
  if (threshold === undefined || !Number.isFinite(threshold) || threshold <= 0) {
    fail("knee objective requires a positive maxMarginalCostMicrosPerQualityPoint");
  }
  const leaders = feasible.filter((point) => point.qualityLeader);
  const affordable = leaders.filter((point) =>
    point.marginalCostMicrosPerQualityPoint !== null &&
    point.marginalCostMicrosPerQualityPoint <= threshold);
  return affordable.length > 0 ? affordable[affordable.length - 1] : leaders[0] ?? null;
}

/**
 * 先按能力护栏过滤,再在前沿上按显式目标选点。
 * 被支配且可行与被护栏拒绝的原因分开记录,避免把“更差”说成“不可行”。
 */
export function selectOnFrontier(
  candidates: readonly PerfCandidate[],
  guardrail: CapabilityGuardrail,
  policy: SelectionPolicy,
): FrontierSelection {
  const frontier = paretoFrontier(candidates);
  const points = annotateFrontier(frontier);
  const feasible = points.filter((point) => guardrailReasons(point, guardrail).length === 0);
  // 边际成本必须在“过滤后的可行集”上重算:被护栏淘汰的中间点不该抬高阶梯价格。
  const ladder = annotateFrontier(feasible);

  const feasibleIds = new Set(feasible.map((point) => point.id));
  const frontierIds = new Set(frontier.map((point) => point.id));
  const rejected: Rejection[] = [];
  for (const candidate of candidates) {
    if (feasibleIds.has(candidate.id)) continue;
    const reasons = guardrailReasons(candidate, guardrail);
    if (reasons.length > 0) {
      rejected.push({ id: candidate.id, reasons });
      continue;
    }
    if (!frontierIds.has(candidate.id)) {
      const dominator = candidates.find((other) => other.id !== candidate.id && dominates(other, candidate));
      rejected.push({
        id: candidate.id,
        reasons: [dominator ? `dominated by "${dominator.id}"` : "equivalent to another frontier point"],
      });
    }
  }

  const chosen = pick(ladder, policy);
  const rationale = chosen
    ? `objective=${policy.objective}; chosen "${chosen.id}" (quality ${chosen.quality.toFixed(3)}, cost ${chosen.costMicrosPer1k}, latency ${chosen.latencyP95Ms}); feasible=${feasible.length}, rejected=${rejected.length}`
    : `objective=${policy.objective}; no feasible candidate; rejected=${rejected.length}`;
  return { chosen: chosen?.id ?? null, feasible: feasible.map((point) => point.id), rejected, points, rationale };
}

paretoFrontier 先校验“同一能力、唯一 id、坐标合法”,再淘汰被支配点。相等坐标只保留字典序最小的 id,避免同一组数字在前沿上出现两次。

annotateFrontier 用 qualityLeader 标记质量新高点,只有这些点参与边际成本计算。这个标记让“用成本换延迟”的点不会被误算成“用成本换质量”。

selectOnFrontier 先按护栏过滤,再对可行集重新标注一次。示例中 C 在完整前沿上的边际成本是 12000,在可行集上是 4000;如果不重算,knee 会错误地停在 B。

示例样本上的运行结果

在实验工程根目录执行:

node examples/perf/quality-frontier.ts

本机一次运行的真实输出如下:

frontier=a,b,e,c,d
a quality=0.72 cost=180 latency=420 marginal=null
b quality=0.80 cost=260 latency=460 marginal=1000.00
e quality=0.83 cost=300 latency=700 marginal=1333.33
c quality=0.84 cost=420 latency=520 marginal=12000.00
d quality=0.86 cost=700 latency=610 marginal=14000.00
objective=max-quality chosen=c feasible=b,c rejected=6
objective=min-cost chosen=b feasible=b,c rejected=6
objective=knee chosen=c feasible=b,c rejected=6
feasible-ladder b marginal=null
feasible-ladder c marginal=4000.00

这组结果给出三个判断。第一,前沿只有五个点,八个候选里有三个是被支配的,选型会不需要讨论它们。第二,最贵的 D 质量最高但成本 700,超过护栏,贵不等于可上线。第三,C 在完整前沿上的边际成本 12000,在可行集上只有 4000;同一份数据,过滤前后必须重算,否则 knee 会给出相反结论。

落地顺序

第一步先固定评测集与结算口径。质量通过率、成本微元与 P95 延迟必须来自同一批请求,否则三条轴不在同一时空。

第二步按能力写护栏。先写“绝对不能接受什么”,再写“愿意为质量付多少”。顺序反了,团队会先陷入性价比争论,却说不清底线。

第三步才是求前沿与选点。前沿的价值是缩小讨论范围:把“八个方案哪个好”变成“两个可行方案愿意为 0.04 的质量差付 160 微元吗”。

最后把选点结果与阈值写进 ADR。半年后模型价格、评测集与业务护栏都会变,没有记录的选点无法复核。

小结

质量、成本与延迟构成三维权衡面。支配关系淘汰“三条都不如别人”的点;Pareto 前沿保留真正需要取舍的点;能力护栏切掉业务不能接受的区域;边际成本给“再提升一点质量”定价。示例样本的前沿是 A、B、E、C、D,护栏后只剩 B 与 C,max-quality 与 knee 选 C,min-cost 选 B。

下一节把“质量”从一次选型变成持续门禁:评测集怎样构建、怎样分层抽样、阈值怎样带置信度,以及新模型为什么会在整体指标持平的情况下被拦下。

阅读导航:上一节:10.3 结果解读与回归风险 · 下一节:11.2 评测集与回归门禁 。

常见错误与边界

  • 用加权总分选模型。归一化与权重把取舍藏起来,被支配点也可能排第一。
  • 把被支配点当成一种取舍。它只是三条都不如别人,正确动作是换点。
  • 只画成本-质量二维图。延迟可能让一个“更便宜”的点根本不可用。
  • 全站共用一套护栏。审核与草稿对质量、延迟的要求不同。
  • 忽略样本量。40 条样本的 100% 通过率不能和 600 条样本的 84% 同图比较。
  • 对前沿上所有点计算边际成本。用成本换延迟的点会污染质量阶梯。
  • 在完整前沿上算完边际成本后直接过滤。可行集变了,阶梯必须重算。
  • 把最贵等同于最好。超过成本或延迟护栏的点不可上线。
  • 把示例样本结果写成生产实测。数字必须标注来源与样本量。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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