单能力都优化完了,然后呢
「回声」的三个能力各自完成了优化:检索重排换了经济档,摘要加了前缀缓存,回复加了输出上限。单看每个能力,成本都降了,延迟都稳了。于是产品提出下一步:把三个能力串起来,用户问一个问题,系统先检索、再摘要、最后生成回复。
上线前,团队按单能力的成本相加,预估每条请求 4,240 微元。实际跑起来,成本确实低了一点,但延迟涨了,失败时的表现也变得难以解释:检索失败后,摘要还要不要跑?摘要跑了一半失败,回复能不能用部分结果?这些问题在单能力视角下根本不存在。
多能力编排不是把三个单能力相加。它会引入四类新成本:跨能力调用、上下文复用、结果传递与失败传播。前两类影响钱,后两类影响钱和体验。本节用一个三能力场景把这四类成本量化,并给卷三留下三个问题。
文中成本、延迟与成功率都来自 ai-lab 的示例样本运行结果,不是生产实测;单价与 token 数是用于演示的假设参数。
四个新成本
| 新成本 | 具体表现 | 单能力视角为什么看不见 |
|---|---|---|
| 跨能力调用 | 每次交接都要一次新请求、一次新计费 | 单能力只算自己那一次 |
| 上下文复用 | 共享上下文重复传还是按前缀复用 | 单能力没有「共享」概念 |
| 结果传递 | 上一跳结果变成下一跳输入,token 重新计费 | 单能力没有上游产物 |
| 失败传播 | 上游失败导致下游跳过或重试,成本与成功率一起变 | 单能力的失败是局部失败 |
四类成本里,只有第一类容易被看见,因为它对应一笔明确的调用费。后三类都藏在 token 与概率里:上下文复用决定省多少,结果传递决定加多少,失败传播决定这些节省能不能兑现。
一个三能力场景
场景由三个顺序步骤组成。每个能力独立调用时都要携带完整的 3,200 token 共享上下文;编排时,首跳携带完整上下文,后续只传 600 token 增量上下文与 220 token 上一跳结果。
| 步骤 | 输入 token | 输出 token | 延迟 | 单次失败率 |
|---|---|---|---|---|
| search.rerank | 700 | 80 | 160ms | 1% |
| doc.summarize | 2,600 | 420 | 620ms | 3% |
| chat.reply | 1,100 | 240 | 420ms | 2% |
计价参数:输入 250 微元/百万 token,输出 1,000 微元/百万 token;前缀复用率 60%,缓存输入单价为全价的 10%;每步允许 1 次重试;每次交接额外 35ms。
前缀复用率不是拍脑袋的数字。它来自第 3 章的前缀缓存条件:稳定前缀越长、提示改版越少,复用率越高。60% 意味着共享上下文里仍有 40% 需要全价重算,这已经比「每次复制完整上下文」便宜。
实现
下面的模块把「独立调用」与「顺序编排」放在同一套单价下比较。它计算两件事:编排省下多少重复上下文,以及交接、重试与失败传播又加回多少成本。
// file: src/perf/handoff.ts
// tier: L1
// verify: node --test test/perf/handoff.test.ts
import { AiError } from "../core/errors.ts";
export interface OrchestrationStep {
id: string;
inputTokens: number;
outputTokens: number;
latencyMs: number;
/** 单次尝试的失败率;重试会把有效失败率降到它的幂。 */
failureRate: number;
}
/**
* 多能力编排的最小成本模型:共享上下文在首跳全价,后续按前缀复用计价;
* 每次交接额外传递上一跳结果与增量上下文。
*/
export interface OrchestrationScenario {
steps: readonly OrchestrationStep[];
sharedContextTokens: number;
handoffContextTokens: number;
resultTokensPerHop: number;
prefixReuseRatio: number;
cachedInputPriceRatio: number;
retriesPerStep: number;
handoffLatencyMs: number;
inputMicrosPerKTok: number;
outputMicrosPerKTok: number;
}
export interface HandoffAnalysis {
steps: number;
standaloneCostMicros: number;
orchestratedCostMicros: number;
handoffTokensPerHop: number;
/** 相对“每个能力各自携带完整上下文”的成本变化;负数代表更省。 */
costDeltaPct: number;
retryCostMicros: number;
successProbability: number;
costPerSuccessMicros: number;
standaloneCostPerSuccessMicros: number;
/** 编排后成功路径的累计延迟,包含每次交接开销。 */
latencyMs: number;
latencyTaxPct: number;
/** 复用率低于此值时,共享上下文不如直接复制。 */
breakEvenReuseRatio: number | null;
/** 要让整体成功率达到目标,单步失败率的上限。 */
maxStepFailureForTarget: number;
expectedWastedCostMicros: number;
}
export const HANDOFF_QUESTIONS = [
"跨能力调用:哪些上下文必须随调用传递,哪些可以只传引用、摘要或游标?",
"上下文复用:共享前缀的复用率低于多少时,传递成本会超过复制成本?",
"失败传播:上游失败后,下游应该跳过、重试,还是用部分结果继续?",
] as const;
function fail(message: string): never {
throw new AiError("invalid-request", `handoff: ${message}`);
}
function finite(value: number, field: string, min = 0): number {
if (!Number.isFinite(value) || value < min) fail(`${field} must be a finite number >= ${min}`);
return value;
}
function integer(value: number, field: string, min = 0): number {
if (!Number.isInteger(value) || value < min) fail(`${field} must be an integer >= ${min}`);
return value;
}
function ratio(value: number, field: string): number {
if (!Number.isFinite(value) || value < 0 || value > 1) fail(`${field} must be in [0, 1]`);
return value;
}
function inputCost(tokens: number, pricePerKTok: number, factor = 1): number {
return Math.ceil((tokens * pricePerKTok * factor) / 1_000);
}
function outputCost(tokens: number, pricePerKTok: number): number {
return Math.ceil((tokens * pricePerKTok) / 1_000);
}
function validate(scenario: OrchestrationScenario): void {
if (scenario.steps.length === 0) fail("至少需要一个能力步骤");
const seen = new Set<string>();
for (const step of scenario.steps) {
if (step.id.trim() === "") fail("step.id 不能为空");
if (seen.has(step.id)) fail(`重复的 step.id ${step.id}`);
seen.add(step.id);
integer(step.inputTokens, `${step.id}.inputTokens`);
integer(step.outputTokens, `${step.id}.outputTokens`);
finite(step.latencyMs, `${step.id}.latencyMs`);
ratio(step.failureRate, `${step.id}.failureRate`);
}
integer(scenario.sharedContextTokens, "sharedContextTokens");
integer(scenario.handoffContextTokens, "handoffContextTokens");
integer(scenario.resultTokensPerHop, "resultTokensPerHop");
ratio(scenario.prefixReuseRatio, "prefixReuseRatio");
ratio(scenario.cachedInputPriceRatio, "cachedInputPriceRatio");
integer(scenario.retriesPerStep, "retriesPerStep");
finite(scenario.handoffLatencyMs, "handoffLatencyMs");
integer(scenario.inputMicrosPerKTok, "inputMicrosPerKTok", 1);
integer(scenario.outputMicrosPerKTok, "outputMicrosPerKTok", 1);
}
/**
* 把“独立调用”与“顺序编排”放在同一套 token 单价下比较。
* 编排省下的是重复上下文,付出的是交接 token、交接延迟与失败传播。
*/
export function analyzeHandoff(
scenario: OrchestrationScenario,
successTarget = 0.95,
): HandoffAnalysis {
validate(scenario);
if (!(successTarget > 0 && successTarget < 1)) fail("successTarget 必须落在 (0, 1)");
const reuseFactor = 1 - scenario.prefixReuseRatio * (1 - scenario.cachedInputPriceRatio);
const handoffTokensPerHop = scenario.handoffContextTokens + scenario.resultTokensPerHop;
let standaloneCost = 0;
let standaloneFullCost = 0;
let orchestratedCost = 0;
let fullCost = 0;
let expectedCost = 0;
let expectedWithoutRetry = 0;
let reach = 1;
let success = 1;
let latency = 0;
let standaloneLatency = 0;
scenario.steps.forEach((step, index) => {
const standaloneStep =
inputCost(scenario.sharedContextTokens + step.inputTokens, scenario.inputMicrosPerKTok) +
outputCost(step.outputTokens, scenario.outputMicrosPerKTok);
standaloneCost += standaloneStep;
standaloneLatency += step.latencyMs;
const sharedCost = index === 0
? inputCost(scenario.sharedContextTokens, scenario.inputMicrosPerKTok)
: inputCost(scenario.sharedContextTokens, scenario.inputMicrosPerKTok, reuseFactor);
const handoffCost = index === 0
? 0
: inputCost(handoffTokensPerHop, scenario.inputMicrosPerKTok);
const stepCost = sharedCost + handoffCost +
inputCost(step.inputTokens, scenario.inputMicrosPerKTok) +
outputCost(step.outputTokens, scenario.outputMicrosPerKTok);
orchestratedCost += stepCost;
const attemptFactor = 1 + scenario.retriesPerStep * step.failureRate;
const effectiveFailure = Math.pow(step.failureRate, 1 + scenario.retriesPerStep);
standaloneFullCost += standaloneStep * attemptFactor;
fullCost += stepCost * attemptFactor;
expectedCost += reach * stepCost * attemptFactor;
expectedWithoutRetry += reach * stepCost;
latency += reach * (step.latencyMs + (index === 0 ? 0 : scenario.handoffLatencyMs));
reach *= 1 - effectiveFailure;
success *= 1 - effectiveFailure;
});
const costPerSuccess = success === 0 ? Number.POSITIVE_INFINITY : expectedCost / success;
const standaloneCostPerSuccess = success === 0 ? Number.POSITIVE_INFINITY : standaloneFullCost / success;
const breakEven = handoffTokensPerHop === 0
? 0
: scenario.sharedContextTokens === 0 || scenario.cachedInputPriceRatio === 1
? null
: handoffTokensPerHop / (scenario.sharedContextTokens * (1 - scenario.cachedInputPriceRatio));
const maxStepFailure = Math.pow(
1 - Math.pow(successTarget, 1 / scenario.steps.length),
1 / (1 + scenario.retriesPerStep),
);
return {
steps: scenario.steps.length,
standaloneCostMicros: standaloneCost,
orchestratedCostMicros: orchestratedCost,
handoffTokensPerHop,
costDeltaPct: standaloneCost === 0 ? 0 : ((orchestratedCost - standaloneCost) / standaloneCost) * 100,
retryCostMicros: Math.round(expectedCost - expectedWithoutRetry),
successProbability: success,
costPerSuccessMicros: Math.round(costPerSuccess),
standaloneCostPerSuccessMicros: Math.round(standaloneCostPerSuccess),
latencyMs: Math.round(latency),
latencyTaxPct: standaloneLatency === 0 ? 0 : ((latency - standaloneLatency) / standaloneLatency) * 100,
breakEvenReuseRatio: breakEven !== null && breakEven <= 1 ? Number(breakEven.toFixed(4)) : null,
maxStepFailureForTarget: Number(maxStepFailure.toFixed(4)),
expectedWastedCostMicros: Math.round(expectedCost - success * fullCost),
};
}
analyzeHandoff 的关键不是把三项成本相加,而是把「到达概率」乘进每一步:第 i 步只有在前面都成功时才会发生,所以它的成本要乘以 reach。失败传播因此不是一句定性描述,而是一个会改变期望成本的乘数。
breakEvenReuseRatio 回答的是「复用率低到什么程度,共享上下文还不如直接复制」。它由交接 token 与可复用上下文的比例决定,不依赖具体单价;单价只影响绝对金额,不影响回本线。
示例样本上的运行结果
把三能力场景输入分析函数,主场景与一个高失败率变体的结果如下:
| 指标 | 主场景 | 高失败率变体 |
|---|---|---|
| 独立调用成本 | 4,240 微元 | 4,240 微元 |
| 编排后成本 | 3,786 微元 | 3,786 微元 |
| 成本变化 | -10.71% | -10.71% |
| 每次交接 token | 820 | 820 |
| 重试成本 | 82 微元 | 293 微元 |
| 成功率 | 99.86% | 98.12% |
| 每次成功交付成本 | 3,872 微元 | 4,139 微元 |
| 独立调用每次成功成本 | 4,339 微元 | 4,663 微元 |
| 成功路径延迟 | 1,269ms | 1,262ms |
| 延迟变化 | +5.79% | +5.20% |
| 失败浪费成本 | 4 微元 | 58 微元 |
主场景的结论是:编排省下 454 微元(-10.71%),代价是 69ms 交接延迟(+5.79%)。这个交换在延迟预算内是划算的。但「每次成功交付成本」比「每次调用成本」更值得看:独立调用是 4,339,编排是 3,872;前者比 4,240 高,是因为失败重试也被计入了成本。
高失败率变体把摘要步骤的失败率从 3% 提到 12%,回复步骤从 2% 提到 6%。成本几乎没变,成功率从 99.86% 掉到 98.12%,每次成功交付成本从 3,872 涨到 4,139,失败浪费从 4 涨到 58。失败传播不会改变单次调用的价目表,但它会改变「一条可用结果到底多少钱」。
复用回本线与失败预算
两个判据可以直接写进编排设计。
第一,复用回本线。交接 token 为 820,共享上下文为 3,200,缓存折扣为 90%,回本复用率是 820 ÷ (3,200 × 0.9) = 28.47%。复用率低于 28.47% 时,共享上下文不如每次复制;高于它,编排才开始省 token。这个数字与单价无关,只与 token 结构有关。
第二,失败预算。三步、每步 1 次重试、目标成功率 95% 时,单步失败率上限是 13.02%。超过这个值,整条链路的成功率就守不住 95%。高失败率变体的摘要是 12%,已经逼近上限;如果再加一个能力,上限会继续下降。
两个判据合起来解释了一个常见现象:编排在实验室里总是更省,因为实验室只测成功路径;上线后可能更贵,因为失败路径的重试与浪费没有进模型。
留给卷三的三个问题
卷三讨论模型组合与编排。单能力优化回答「怎么让一个能力更便宜」,卷三要回答「多个能力怎么协同完成复杂任务」。三个问题必须先回答,否则编排只会把单能力的成本优势吃掉。
第一,跨能力调用:哪些上下文必须随调用传递,哪些可以只传引用、摘要或游标?传得越多,越不容易丢信息,也越贵。
第二,上下文复用:共享前缀的复用率低于多少时,传递成本会超过复制成本?本节给出的 28.47% 是一个可计算的起点,不是一个通用常数。
第三,失败传播:上游失败后,下游应该跳过、重试,还是用部分结果继续?跳过省钱但可能没有结果,重试提高成功率但放大成本,部分结果则需要定义「部分可用」的验收标准。
这三个问题没有统一答案,但它们有共同的判断方式:把每一次交接的 token、延迟与失败概率写进模型,再决定编排结构。卷三会沿着这个方向展开。
小结
单能力优化之后,多能力编排带来四类新成本:跨能力调用、上下文复用、结果传递与失败传播。三能力示例中,编排把成本从 4,240 降到 3,786 微元(-10.71%),但把延迟从 1,200 拉到 1,269ms(+5.79%);高失败率变体让成功率从 99.86% 降到 98.12%,每次成功交付成本从 3,872 涨到 4,139 微元。
编排是否值得,不取决于单次调用便宜多少,而取决于复用率是否越过 28.47% 的回本线、单步失败率是否守得住 13.02% 的预算。这三个数字,连同三个待回答的问题,就是卷二交给卷三的输入。
阅读导航:上一节:13.2 优化方案与 ADR · 下一节:目录与学习路径 。
常见错误与边界
- 把编排成本当成各能力成本之和。交接、结果传递与重试都会新增计费。
- 只测成功路径。失败路径的重试与浪费才是编排最容易失控的部分。
- 共享上下文每次完整复制。复用率低于回本线时,复制更便宜;高于时,编排才划算。
- 忽略交接延迟。每次 35ms 看似很小,三次交接就是 105ms。
- 用单次调用成本评价编排。应看每次成功交付成本,把失败传播算进去。
- 失败后一律重试。重试会同时放大成本与延迟,必须有全局预算。
- 复用率拍一个 100%。提示改版、租户隔离与缓存容量都会让它下降。
- 把示例样本的 28.47% 当成通用常数。token 结构一变,回本线就要重算。
- 编排没有验收「部分可用」。上游失败后下游拿到半截结果,用户看到什么必须提前定义。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。