《互联网产品的 AI 架构·多能力编排与任务闭环》8.3 编排经济性复核

编排是否划算要持续复核,不是上线前算一次。本节用卷二 28.47% 的回本线与 13.02% 的失败预算复核三节点链路,跑出主场景、高失败、低复用、四节点与质量溢价五个样本,给出 keep、watch、exit 的判定与退出条件;复核要按链路、按窗口进行。

上线前算过一次,不等于以后都划算

卷二给出两个数字:共享前缀的回本复用率是 28.47%,三步链路的单步失败预算是 13.02%。上线评审时,团队用当时的流量、价格与缓存命中率算了一遍,结论是编排比独立调用便宜 10.71%,于是通过。半年后,模型价格变了,提示改版了,缓存命中率下降,链路还加了一个节点;账单仍然能跑,但「编排是否还划算」已经没人能回答。

经济性复核不是把成本模型再算一遍,而是拿四个读数去对照两条警戒线:复用率有没有跌破 28.47%,最差节点的失败率有没有越过 13.02%,每次成功交付是不是还比独立调用便宜,质量提升是否配得上溢价。四条中任何一条失守,编排就应该退出或改结构,而不是继续用「单次调用更便宜」解释账单。

本节用卷二的三节点样本做一次完整复核,并给出主场景、高失败、低复用、四节点与质量溢价五个样本的判定。所有数字都来自 ai-lab 示例样本上的运行结果,不是生产实测;复核对象是示例链路,不是线上流量。

四个复核读数

第一个读数是每次成功交付成本:期望成本除以整链成功率。它把重试与失败浪费算进去,回答「一条可用结果到底多少钱」。主场景的基础成本是 3,786 微元,含重试的期望成本是 3,867 微元,成功率 99.86%,每次成功交付 3,872 微元。

第二个读数是净成本:编排的每次成功交付成本减去独立调用的每次成功交付成本。主场景是 3,872 − 4,339 = −467 微元,相对独立调用便宜 10.76%。负数代表编排更便宜;正数代表编排更贵,需要质量提升来解释。

第三个读数是复用余量:实际复用率相对回本线的百分比。复用率 60%、回本线 28.47% 时,余量是 (60 − 28.47) ÷ 28.47 = 110.75%。余量越薄,缓存命中率的一次波动就越可能把编排推成亏损。

第四个读数是失败余量:单步失败预算与实际最差节点失败率的差。三步、每步 1 次重试、目标成功率 95% 时,单步预算 13.02%;最差节点 3% 时,余量 76.96%。余量低于 10% 时,链路已经贴着警戒线运行。

四个读数回答四个不同问题,不能互相替代。成本便宜但复用余量为负,说明这次便宜是靠别的因素撑住的;复用率很高但失败余量为负,说明省下的钱会被失败重试吃回去。

判定规则

复核输出三种判定。

keep 表示四条硬条件都满足,且余量充足:复用率高于回本线、最差节点失败率低于单步预算、每次成功交付不更贵(或有质量提升覆盖溢价)、不超过业务成本上限。

watch 表示硬条件暂时满足,但至少一条余量很薄:复用余量低于 10%、失败余量低于 10%,或成本优势小于 5%。这时不退出,但要把链路放进每周复核,并准备回退方案。

exit 表示至少一条硬条件失守。退出不是删掉编排,而是回到上一个可工作的结构:退回两节点链路、去掉可选节点、恢复独立调用,或者重新谈质量目标。退出条件必须在上线前写清楚,不能等账单出来再讨论。

质量溢价单独处理。编排可能比独立调用贵,但质量更高;这时用「成本溢价 ≤ 质量提升」作为宽松判据。示例中成本溢价 6.55%、质量提升 10 个百分点,判为可接受;如果质量只提升 2 个百分点,同样的溢价就应该退出。质量提升必须来自同一批评测,不能拿不同指标比较。

把复核写成函数

下面的实现把四个读数、三条硬条件与质量溢价收进一个函数。它不重新计算 token 成本,只接收已经算好的期望成本、成功率与失败率,因此可以复用卷二的成本模型,也可以接生产账单。

// file: src/orch/budget-economics.ts
// tier: L2
// verify: node --test test/orch/budget.test.ts

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

/** 一次经济性复核的输入;成本用整数微元,比例用 0–1。 */
export interface EconomicsInput {
  name: string;
  /** 编排后的期望成本,含重试与失败浪费。 */
  orchestratedCostMicros: number;
  successProbability: number;
  /** 独立调用(或旧方案)每次成功交付的成本,用作对照。 */
  standaloneCostPerSuccessMicros: number;
  /** 复核窗口内按 token 加权的共享前缀复用率。 */
  reuseRatio: number;
  sharedContextTokens: number;
  handoffTokens: number;
  /** 缓存输入占全价的比例;0.1 表示 90% 折扣。 */
  cachedInputPriceRatio: number;
  /** 每个节点的单次失败率,不含重试。 */
  stepFailureRates: readonly number[];
  retriesPerStep: number;
  /** 整链目标成功率,用于反推单步失败预算。 */
  targetSuccess: number;
  /** 编排相对独立调用的质量提升,百分点;没有提升填 0。 */
  qualityLiftPct: number;
  maxCostPerSuccessMicros?: number;
}

export interface EconomicsCheck { name: string; passed: boolean; detail: string }

export interface EconomicsReview {
  name: string;
  costPerSuccessMicros: number;
  netCostMicros: number;
  netCostPct: number;
  breakEvenReuseRatio: number;
  reuseMarginPct: number;
  maxStepFailureRate: number;
  worstStepFailureRate: number;
  failureHeadroomPct: number;
  costPremiumPct: number;
  qualityJustified: boolean;
  checks: EconomicsCheck[];
  exitReasons: string[];
  verdict: "keep" | "watch" | "exit";
}

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

function nonNegative(value: number, field: string): number {
  if (!Number.isFinite(value) || value < 0) fail(`${field} must be a finite number >= 0`);
  return value;
}

function ratio(value: number, field: string, allowZero = true): number {
  if (!Number.isFinite(value) || value < (allowZero ? 0 : Number.MIN_VALUE) || value > 1) {
    fail(`${field} must be in ${allowZero ? "[0, 1]" : "(0, 1]"}`);
  }
  return value;
}

function validate(input: EconomicsInput): void {
  if (input.name.trim() === "") fail("name 不能为空");
  nonNegative(input.orchestratedCostMicros, "orchestratedCostMicros");
  ratio(input.successProbability, "successProbability", false);
  nonNegative(input.standaloneCostPerSuccessMicros, "standaloneCostPerSuccessMicros");
  ratio(input.reuseRatio, "reuseRatio");
  nonNegative(input.sharedContextTokens, "sharedContextTokens");
  nonNegative(input.handoffTokens, "handoffTokens");
  ratio(input.cachedInputPriceRatio, "cachedInputPriceRatio");
  if (input.stepFailureRates.length === 0) fail("至少需要一个节点的失败率");
  for (const rate of input.stepFailureRates) ratio(rate, "stepFailureRate");
  nonNegative(input.retriesPerStep, "retriesPerStep");
  ratio(input.targetSuccess, "targetSuccess", false);
  nonNegative(input.qualityLiftPct, "qualityLiftPct");
  if (input.maxCostPerSuccessMicros !== undefined) {
    nonNegative(input.maxCostPerSuccessMicros, "maxCostPerSuccessMicros");
  }
}

/** 共享前缀相对全量复制的回本线;分母为 0 时返回 null。 */
export function breakEvenReuseRatio(
  sharedContextTokens: number, handoffTokens: number, cachedInputPriceRatio: number,
): number | null {
  nonNegative(sharedContextTokens, "sharedContextTokens");
  nonNegative(handoffTokens, "handoffTokens");
  ratio(cachedInputPriceRatio, "cachedInputPriceRatio");
  const saved = sharedContextTokens * (1 - cachedInputPriceRatio);
  if (saved <= 0) return null;
  const value = handoffTokens / saved;
  return value <= 1 ? Number(value.toFixed(4)) : null;
}

/**
 * 单步失败预算:给定节点数与每步重试次数,要让整链成功率达到目标,
 * 单个节点的单次失败率最多是多少。重试把有效失败率降到 f^(r+1)。
 */
export function maxStepFailureRate(steps: number, retriesPerStep: number, targetSuccess: number): number {
  if (!Number.isInteger(steps) || steps < 1) fail("steps 必须是 >= 1 的整数");
  nonNegative(retriesPerStep, "retriesPerStep");
  ratio(targetSuccess, "targetSuccess", false);
  const perStep = 1 - Math.pow(targetSuccess, 1 / steps);
  return Number(Math.pow(perStep, 1 / (1 + retriesPerStep)).toFixed(6));
}

/**
 * 复核不是重算一遍成本,而是回答四个是非题:
 * 复用率过线没有、失败率守预算没有、每次成功交付是否更便宜、质量提升是否配得上溢价。
 * 任何一条硬条件不满足就退出;都满足但余量很薄则进入观察。
 */
export function reviewEconomics(input: EconomicsInput): EconomicsReview {
  validate(input);

  const costPerSuccess = Math.round(input.orchestratedCostMicros / input.successProbability);
  const netCost = costPerSuccess - input.standaloneCostPerSuccessMicros;
  const netCostPct = input.standaloneCostPerSuccessMicros === 0
    ? 0 : Number(((netCost / input.standaloneCostPerSuccessMicros) * 100).toFixed(4));

  const breakEven = breakEvenReuseRatio(
    input.sharedContextTokens, input.handoffTokens, input.cachedInputPriceRatio);
  const reuseMarginPct = breakEven === null || breakEven === 0
    ? 0 : Number((((input.reuseRatio - breakEven) / breakEven) * 100).toFixed(4));

  const maxStep = maxStepFailureRate(
    input.stepFailureRates.length, input.retriesPerStep, input.targetSuccess);
  const worstStep = Math.max(...input.stepFailureRates);
  const failureHeadroomPct = maxStep === 0
    ? 0 : Number((((maxStep - worstStep) / maxStep) * 100).toFixed(4));

  const costPremiumPct = netCostPct;
  const qualityJustified = netCost <= 0 || input.qualityLiftPct >= costPremiumPct;

  const checks: EconomicsCheck[] = [
    {
      name: "复用率过回本线",
      passed: breakEven !== null && input.reuseRatio >= breakEven,
      detail: breakEven === null
        ? "缓存折扣为 0,共享前缀无法回本"
        : `复用率 ${(input.reuseRatio * 100).toFixed(2)}%,回本线 ${(breakEven * 100).toFixed(2)}%`,
    },
    {
      name: "单步失败率守预算",
      passed: worstStep <= maxStep,
      detail: `最差节点 ${(worstStep * 100).toFixed(2)}%,预算 ${(maxStep * 100).toFixed(2)}%`,
    },
    {
      name: "每次成功交付不更贵",
      passed: netCost <= 0 || qualityJustified,
      detail: `编排 ${costPerSuccess} 微元,独立 ${input.standaloneCostPerSuccessMicros} 微元,差 ${netCostPct.toFixed(2)}%`,
    },
  ];

  if (input.maxCostPerSuccessMicros !== undefined) {
    checks.push({
      name: "不超过业务成本上限",
      passed: costPerSuccess <= input.maxCostPerSuccessMicros,
      detail: `每次成功 ${costPerSuccess} 微元,上限 ${input.maxCostPerSuccessMicros} 微元`,
    });
  }

  const exitReasons: string[] = [];
  if (breakEven === null) exitReasons.push("缓存折扣为 0,共享前缀无法回本");
  else if (input.reuseRatio < breakEven) {
    exitReasons.push(`复用率 ${(input.reuseRatio * 100).toFixed(2)}% 低于回本线 ${(breakEven * 100).toFixed(2)}%`);
  }
  if (worstStep > maxStep) {
    exitReasons.push(`最差节点失败率 ${(worstStep * 100).toFixed(2)}% 超过单步预算 ${(maxStep * 100).toFixed(2)}%`);
  }
  if (netCost > 0 && !qualityJustified) {
    exitReasons.push(
      `每次成功交付贵 ${netCostPct.toFixed(2)}%,质量提升 ${input.qualityLiftPct.toFixed(2)} 个百分点不足以覆盖`);
  }
  if (input.maxCostPerSuccessMicros !== undefined && costPerSuccess > input.maxCostPerSuccessMicros) {
    exitReasons.push(`每次成功交付 ${costPerSuccess} 微元超过业务上限 ${input.maxCostPerSuccessMicros} 微元`);
  }

  const thin = reuseMarginPct < 10 || failureHeadroomPct < 10 || (netCostPct > -5 && netCostPct < 0);
  const verdict = exitReasons.length > 0 ? "exit" : thin ? "watch" : "keep";

  return {
    name: input.name,
    costPerSuccessMicros: costPerSuccess,
    netCostMicros: netCost,
    netCostPct,
    breakEvenReuseRatio: breakEven ?? 0,
    reuseMarginPct,
    maxStepFailureRate: maxStep,
    worstStepFailureRate: worstStep,
    failureHeadroomPct,
    costPremiumPct,
    qualityJustified,
    checks,
    exitReasons,
    verdict,
  };
}

reviewEconomics 的输出里,checks 是给人看的四条是非题,exitReasons 是给自动化用的退出原因。两者不能只留一个:评审会上需要看每条检查的细节,回退脚本需要知道具体哪条失守。

breakEvenReuseRatio 与 maxStepFailureRate 被单独导出,是为了让它们能进入别的工具:前者可以贴到缓存看板,后者可以贴到节点 SLO。把公式埋在复核函数里,别的系统就只能复制一份,迟早出现两个版本。

qualityJustified 的判据是 净成本 ≤ 0 或 质量提升 ≥ 成本溢价。这是一个显式的、可争议的规则,不是真理;它的价值在于让「贵但值得」变成可审计的判断,而不是一句主观结论。

示例样本上的运行结果

五个样本的复核结果如下。成本单位是微元,成功率与复用率是比例。

样本期望成本成功率每次成功独立对照净变化复用率最差失败率判定
主场景3,86799.86%3,8724,339−10.76%60%3%keep
高失败4,06198.12%4,1394,663−11.24%60%12%watch
低复用4,36299.86%4,3684,339+0.67%20%3%exit
四节点4,10096.12%4,2664,339−1.68%60%12%exit
质量溢价4,60099.50%4,6234,339+6.55%60%3%keep

主场景是健康状态:复用余量 110.75%,失败余量 76.96%,每次成功交付比独立调用便宜 467 微元。它说明编排在当前结构与流量下仍然成立,但成立的前提是复用率维持在 60% 附近、最差节点失败率维持在 3% 附近。

高失败样本把摘要失败率从 3% 提到 12%、回复从 2% 提到 6%。成本只涨到 4,061,每次成功交付 4,139,仍然比独立调用的 4,663 便宜 11.24%;但最差节点 12% 已经逼近 13.02% 的预算,失败余量只剩 7.83%。判定是 watch,不是 exit:它还在赚钱,但没有承受一次波动的空间。

低复用样本把复用率降到 20%。编排成本涨到 4,362,每次成功交付 4,368,比独立调用贵 0.67%;复用余量变成 −29.75%。两条硬条件同时失守,判定 exit。这个样本说明「编排便宜」不是结构自带的属性,而是复用率高于回本线时才成立的结果。

四节点样本在原来的三步之后加了一个失败率 12% 的节点。成本 4,100、每次成功 4,266,看起来仍然比独立调用便宜 1.68%;但节点数从三变四,单步失败预算从 13.02% 降到 11.29%,12% 已经越线,失败余量 −6.31%。判定 exit。这是最容易被忽略的退出原因:账单更便宜,可靠性却已经不合格。

质量溢价样本每次成功交付 4,623,比独立调用贵 6.55%,但质量提升 10 个百分点,覆盖了溢价,判定 keep。它不是「成本可接受」,而是「用 6.55% 的成本换 10 个百分点的质量,业务认为值」。如果质量提升只有 2 个百分点,同样的成本就应该退出。

回本线要看窗口,不看单次

28.47% 是一条线,但单次请求的复用率没有统计意义。一次请求可能完全命中缓存,复用率 100%;下一次提示改版,复用率 0%。复核必须用窗口内的加权复用率:按 token 加权,而不是按请求数平均,否则长上下文请求会被短请求稀释。

示例建议用 7 天滚动窗口,并设置最小样本量。样本不足时,判定应该是 watch 而不是 keep:不是链路有问题,而是证据不够。连续两个窗口低于回本线,才触发退出评审;单窗口跌破可能只是一次提示发布或缓存故障。

失败率同理。单次请求的失败率只有 0 或 1,没有信息量;复核要用窗口内的节点失败次数除以尝试次数,并区分「首次失败」与「重试后失败」。把重试后的成功率当成节点成功率,会低估失败预算的消耗。

节点数一加,失败预算就降

三个节点、每步 1 次重试、目标 95% 时,单步失败预算 13.02%。四个节点时,同样的目标与重试,预算降到 11.29%。这不是线性下降,而是指数摊薄:整链成功率是各步成功率的乘积,节点越多,每一步允许的失败率越低。

这条规律对编排设计有两个含义。

第一,加节点之前先算失败预算。新节点的失败率如果高于新的预算,加它不会提升能力,只会把整链成功率拉下来。四节点样本就是这样:成本只多了 233 微元,成功率从 99.86% 掉到 96.12%,判定从 keep 变成 exit。

第二,提高重试次数可以换回预算,但代价是成本与延迟。每步 2 次重试时,三步链路的单步预算会上升,但最坏路径的尝试次数也上升,重试成本与尾延迟一起涨。失败预算、重试预算与成本预算是同一个三角,不能只动一角。

退出条件与退出动作

复核的退出条件应该写成可执行的四条。

第一,窗口加权复用率低于 28.47%,连续两个窗口。动作:检查提示改版、缓存容量与租户隔离;如果无法恢复,退回全量复制或独立调用。

第二,最差节点失败率高于当前节点数对应的单步预算。动作:先修节点,再谈编排;修不好就减少节点或提高该节点的重试预算,并同步重算成本。

第三,每次成功交付成本高于独立调用,且质量提升不足以覆盖溢价。动作:回到两节点链路,或去掉可选节点,重新跑一轮对照。

第四,每次成功交付成本超过业务上限。动作:这一条与对照方案无关,是硬约束;即使编排比独立调用便宜,也必须降档、裁剪或退出。

退出不是失败,而是把「编排」从默认选项降级为需要持续证明的选项。一个健康的编排系统应该能回答:当前这条链路为什么还值得保留?如果答案只能是「一直都这么做」,那它已经在靠惯性运行。

复核节奏

建议按链路、按周复核,而不是按月看总账单。总账单会把三条链路混在一起,掩盖某一条的亏损;按链路复核才能定位到具体节点与具体失败率。

复核时要保证三个「同」:同一批价格、同一版提示、同一套评测。价格变了要重算,提示改版要重新采样复用率,评测集换了要重新标定质量提升。三个「同」不满足时,数字之间的差异不能归因于编排本身。

最后,复核结果要进入变更流程,而不是只留在报表里。watch 应该触发一次结构评审,exit 应该触发回退工单;如果复核结果没人消费,它就只是另一张好看的图。

小结

编排经济性复核用四个读数对照两条警戒线:每次成功交付成本、净成本、复用余量、失败余量。示例样本上,主场景每次成功 3,872 微元、比独立调用便宜 10.76%,判定 keep;高失败样本 4,139 微元、失败余量只剩 7.83%,判定 watch;低复用样本 4,368 微元、复用余量 −29.75%,判定 exit;四节点样本虽然便宜 1.68%,但失败预算从 13.02% 降到 11.29%,判定 exit。

退出的四条条件是复用率跌破 28.47%、最差节点失败率越过单步预算、每次成功交付更贵且质量不配、超过业务成本上限。复核要按链路、按窗口、按同一批价格与提示进行;结果必须进入变更流程,否则只是报表。

阅读导航:上一节:8.2 超支时的裁剪顺序 · 下一节:9.1 跨能力的权限传播 。

常见错误与边界

  • 用单次请求的复用率做判定。单次只有 0 或 100%,必须用窗口内按 token 加权的复用率。
  • 把总账单当成编排账单。三条链路混在一起,会掩盖某一条的亏损。
  • 只看成本,不看失败余量。四节点样本更便宜,但失败预算已经失守。
  • 认为加节点只增加成本。节点数会摊薄单步失败预算,可靠性也被改变。
  • 把「贵但质量高」当成无条件合理。质量提升必须来自同一批评测,并且覆盖溢价。
  • 复用率刚跌破回本线就立刻退出。单窗口跌破可能是一次提示发布或缓存故障,应连续两个窗口确认。
  • 样本不足时判 keep。证据不够应该是 watch,而不是乐观。
  • 用重试后的成功率当节点成功率。它会低估失败预算的消耗。
  • 复核结果不进变更流程。watch 没有结构评审、exit 没有回退工单,复核就没有作用。
  • 把示例样本的 3,872/4,139/4,368 当成生产数据。换一批流量与价格,四个读数都要重算。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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