上线前算过一次,不等于以后都划算
卷二给出两个数字:共享前缀的回本复用率是 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,867 | 99.86% | 3,872 | 4,339 | −10.76% | 60% | 3% | keep |
| 高失败 | 4,061 | 98.12% | 4,139 | 4,663 | −11.24% | 60% | 12% | watch |
| 低复用 | 4,362 | 99.86% | 4,368 | 4,339 | +0.67% | 20% | 3% | exit |
| 四节点 | 4,100 | 96.12% | 4,266 | 4,339 | −1.68% | 60% | 12% | exit |
| 质量溢价 | 4,600 | 99.50% | 4,623 | 4,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 当成生产数据。换一批流量与价格,四个读数都要重算。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。