454 微元省在哪里
「回声」把检索、摘要、回复三个能力串起来后,成本从 4,240 微元降到 3,786 微元,降幅 10.71%;延迟从 1,200ms 涨到 1,269ms,涨幅 5.79%。产品经理问了一个很具体的问题:省下的 454 微元,到底是哪个动作省出来的?如果只是把三个能力排了个序,为什么账单会变?
答案不在「排序」本身,而在上下文怎么到达每个节点。独立调用时,每个能力都要携带完整的 3,200 token 共享上下文;编排后,首跳仍带完整上下文,后续节点只传 820 token 的交接包,并按复用率共享前缀。省下的是重复上下文,付出的是交接包、交接延迟与失败传播。
本节把这三笔账拆开:一次交接到底传多少 token,复制、传递、共享三种模式各花多少钱,复用率低到什么程度就不如直接复制。文中数字来自 ai-lab 的示例样本运行结果,不是生产实测;单价与 token 数是用于演示的假设参数。
一次交接的 820 token 是什么
820 不是拍出来的。它由两部分组成:600 token 的增量上下文,加上 220 token 的上一跳结果。增量上下文包括本轮新增的指令、检索到的片段或用户补充的信息;上一跳结果是上游节点产出的摘要、分类标签或候选列表。
这两部分必须分开看。增量上下文随任务复杂度变化,结果 token 随上游输出长度变化。把它们合成一个「交接包」方便计费,但在设计时要知道哪一部分在涨。如果上游开始返回更长的结构化结果,820 会先变成 1,000,再变成 1,200,回本线也随之抬高。
| 组成 | 示例 token | 随什么变化 | 能不能压 |
|---|---|---|---|
| 增量上下文 | 600 | 任务复杂度、检索片段数 | 可以裁剪,但可能丢信息 |
| 上一跳结果 | 220 | 上游输出长度、结构化程度 | 可以要求上游输出更短 |
| 交接包合计 | 820 | 两者之和 | 不能只看总数 |
复制、传递、共享
同一个下游节点,可以用三种方式拿到上下文。
| 模式 | 每个下游节点拿到的 token | 信息完整性 | 成本特征 |
|---|---|---|---|
| copy | 完整共享上下文 3,200 | 最完整 | 每个节点都付全价 |
| pass | 只有交接包 820 | 依赖契约,可能缺共享块 | 最便宜,但可能不完整 |
| share | 交接包 820 + 重算的共享前缀 | 完整 | 复用率越高越省 |
在示例参数下,复用率 60%、缓存价是全价的 10%,share 模式每个下游节点需要重算的共享上下文是 3,200 × (1 − 0.6 × 0.9) = 1,472 token,加上 820 token 交接包,共 2,292 token。按输入 250 微元/千 token 计算:
| 模式 | 每跳 token | 每跳成本 | 两次交接 token | 两次交接成本 | 相对 copy |
|---|---|---|---|---|---|
| copy | 3,200 | 800 微元 | 6,400 | 1,600 微元 | 0% |
| pass | 820 | 205 微元 | 1,640 | 410 微元 | -74.375% |
| share | 2,292 | 573 微元 | 4,584 | 1,146 微元 | -28.375% |
share 相对 copy 省下 1,600 − 1,146 = 454 微元,正好等于整条链路省下的 454 微元。这不是巧合:示例里编排与独立调用的唯一差别就是上下文模式,其余输入、输出与单价完全相同。454 微元不是「编排费」,而是「共享上下文相对复制的净节省」。
pass 模式最便宜,但它只传 820 token 交接包。2.1 的 passSafe 判定说得很清楚:共享块不为空时,下游看不到系统政策或稳定指令,pass 模式不安全。省下的 1,190 微元(1,600 − 410)是用信息完整性换来的,不能直接写进方案。
回本线:复用率低于多少就不划算
share 模式每个下游节点的 token 是 820 + 3,200 × (1 − r × 0.9),copy 模式是 3,200。令两者相等:
820 + 3,200 × (1 − r × 0.9) = 3,200
820 = 3,200 × 0.9 × r
r = 820 ÷ (3,200 × 0.9) = 28.47%
复用率低于 28.47% 时,share 模式比 copy 模式更贵;高于它,share 才开始省。这个数字与单价无关,只与 token 结构有关:交接包越大,回本线越高;共享上下文越大,回本线越低;缓存折扣越小,回本线越高。
示例的复用率是 60%,远高于 28.47%,所以 share 模式在成功路径上省 454 微元。如果线上复用率掉到 20%,share 每个下游节点需要 820 + 3,200 × (1 − 0.18) = 3,444 token,反而比 copy 的 3,200 更贵。编排方案没有变,变的是缓存命中。
回本线随 token 结构变化
把 28.47% 当成通用常数,是这类分析最常见的错误。它只对「820 交接包 + 3,200 共享上下文 + 90% 缓存折扣」这组参数成立。换一组结构,回本线立刻移动。
| 结构变化 | 新的回本线 | 解释 |
|---|---|---|
| 交接包涨到 1,200 | 41.67% | 每次交接更贵,需要更高复用率才回本 |
| 共享上下文降到 2,000 | 45.56% | 可省的基数变小,回本更难 |
| 交接包涨到 3,000 | 不存在 | 交接成本超过全部可复用空间,share 永远更贵 |
这张表可以直接用于设计评审:如果上游准备把结果 token 从 220 涨到 600,交接包变成 1,200,回本线从 28.47% 抬到 41.67%。原来 35% 的复用率还能省钱,改完之后就不划算了。回本线不是财务指标,而是接口设计的约束。
实现
下面的模块把三种模式的 token 与成本、回本复用率、以及整条链路的期望成本放在一起。它同时计算成功路径的编排成本与失败路径的期望成本,避免只看成功路径得出「编排总是更省」的结论。
// file: src/orch/handoff-cost.ts
// tier: L2
// verify: node --test test/orch/handoff.test.ts
import { AiError } from "../core/errors.ts";
export type ContextMode = "copy" | "pass" | "share";
export interface HandoffStep {
id: string;
inputTokens: number;
outputTokens: number;
latencyMs: number;
/** 单次尝试的失败率;重试会把有效失败率降到它的幂。 */
failureRate: number;
}
export interface HandoffScenario {
steps: readonly HandoffStep[];
sharedContextTokens: number;
/** 每次交接新增的增量上下文。 */
handoffContextTokens: number;
/** 每次交接携带的上一跳结果。 */
resultTokensPerHop: number;
prefixReuseRatio: number;
/** 缓存输入价格占全价的比例,0.1 表示省 90%。 */
cachedInputPriceRatio: number;
retriesPerStep: number;
handoffLatencyMs: number;
/** 输入价格:微元 / 千 token。 */
inputMicrosPerKTok: number;
outputMicrosPerKTok: number;
}
export interface ModeCost {
mode: ContextMode;
contextTokensPerHop: number;
contextCostMicrosPerHop: number;
totalContextTokens: number;
totalContextCostMicros: number;
/** 相对 copy 模式的上下文成本变化;负数代表更省。 */
vsCopyPct: number;
}
export interface OrchestrationAnalysis {
steps: number;
standaloneCostMicros: number;
orchestratedCostMicros: number;
handoffTokensPerHop: number;
costDeltaPct: number;
retryCostMicros: number;
successProbability: number;
costPerSuccessMicros: number;
standaloneCostPerSuccessMicros: number;
latencyMs: number;
latencyTaxPct: number;
/** 复用率低于此值时,share 模式比 copy 模式更贵。 */
breakEvenReuseRatio: number | null;
maxStepFailureForTarget: number;
expectedWastedCostMicros: number;
modes: ModeCost[];
}
function fail(message: string): never {
throw new AiError("invalid-request", `handoff-cost: ${message}`);
}
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 cost(tokens: number, pricePerKTok: number): number {
return Math.ceil((tokens * pricePerKTok) / 1_000);
}
function validate(scenario: HandoffScenario): 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`);
if (!Number.isFinite(step.latencyMs) || step.latencyMs < 0) fail(`${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");
if (!Number.isFinite(scenario.handoffLatencyMs) || scenario.handoffLatencyMs < 0) fail("handoffLatencyMs 非法");
integer(scenario.inputMicrosPerKTok, "inputMicrosPerKTok", 1);
integer(scenario.outputMicrosPerKTok, "outputMicrosPerKTok", 1);
}
/** 每次交接的 token:增量上下文加上上一跳结果。 */
export function handoffTokensPerHop(scenario: HandoffScenario): number {
return scenario.handoffContextTokens + scenario.resultTokensPerHop;
}
/** share 模式下,一个下游节点实际需要重算的共享上下文 token。 */
export function shareContextTokens(scenario: HandoffScenario, reuseRatio = scenario.prefixReuseRatio): number {
ratio(reuseRatio, "reuseRatio");
return Math.ceil(scenario.sharedContextTokens * (1 - reuseRatio * (1 - scenario.cachedInputPriceRatio)));
}
export function contextTokensForMode(
mode: ContextMode,
scenario: HandoffScenario,
reuseRatio = scenario.prefixReuseRatio,
): number {
if (mode === "copy") return scenario.sharedContextTokens;
if (mode === "pass") return handoffTokensPerHop(scenario);
return handoffTokensPerHop(scenario) + shareContextTokens(scenario, reuseRatio);
}
/** 复制、传递、共享三种模式的 token 与现金成本;首跳没有交接成本。 */
export function compareContextModes(scenario: HandoffScenario): ModeCost[] {
validate(scenario);
const hops = scenario.steps.length - 1;
const copyCost = cost(contextTokensForMode("copy", scenario), scenario.inputMicrosPerKTok);
return (["copy", "pass", "share"] as const).map((mode) => {
const tokens = contextTokensForMode(mode, scenario);
const perHop = cost(tokens, scenario.inputMicrosPerKTok);
const baseline = copyCost * hops;
return {
mode,
contextTokensPerHop: tokens,
contextCostMicrosPerHop: perHop,
totalContextTokens: tokens * hops,
totalContextCostMicros: perHop * hops,
vsCopyPct: baseline === 0 ? 0 : ((perHop * hops - baseline) / baseline) * 100,
};
});
}
/**
* share 相对 copy 的回本复用率:
* handoff ÷ (共享上下文 × (1 − 缓存价比例))。
*/
export function breakEvenReuseRatio(scenario: HandoffScenario): number | null {
const handoff = handoffTokensPerHop(scenario);
if (handoff === 0) return 0;
if (scenario.sharedContextTokens === 0 || scenario.cachedInputPriceRatio === 1) return null;
const value = handoff / (scenario.sharedContextTokens * (1 - scenario.cachedInputPriceRatio));
return value <= 1 ? Number(value.toFixed(4)) : null;
}
/**
* 把独立调用与顺序编排放在同一套单价下比较。
* 编排默认使用 share 模式;失败路径按到达概率乘进每一步。
*/
export function analyzeOrchestration(scenario: HandoffScenario, successTarget = 0.95): OrchestrationAnalysis {
validate(scenario);
if (!(successTarget > 0 && successTarget < 1)) fail("successTarget 必须落在 (0, 1)");
const handoffTokens = handoffTokensPerHop(scenario);
const shareTokens = shareContextTokens(scenario);
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 =
cost(scenario.sharedContextTokens + step.inputTokens, scenario.inputMicrosPerKTok) +
cost(step.outputTokens, scenario.outputMicrosPerKTok);
standaloneCost += standaloneStep;
standaloneLatency += step.latencyMs;
const sharedCost = index === 0
? cost(scenario.sharedContextTokens, scenario.inputMicrosPerKTok)
: cost(shareTokens, scenario.inputMicrosPerKTok);
const handoffCost = index === 0 ? 0 : cost(handoffTokens, scenario.inputMicrosPerKTok);
const stepCost = sharedCost + handoffCost +
cost(step.inputTokens, scenario.inputMicrosPerKTok) +
cost(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 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: handoffTokens,
costDeltaPct: standaloneCost === 0 ? 0 : ((orchestratedCost - standaloneCost) / standaloneCost) * 100,
retryCostMicros: Math.round(expectedCost - expectedWithoutRetry),
successProbability: success,
costPerSuccessMicros: Math.round(expectedCost / success),
standaloneCostPerSuccessMicros: Math.round(standaloneFullCost / success),
latencyMs: Math.round(latency),
latencyTaxPct: standaloneLatency === 0 ? 0 : ((latency - standaloneLatency) / standaloneLatency) * 100,
breakEvenReuseRatio: breakEvenReuseRatio(scenario),
maxStepFailureForTarget: Number(maxStepFailure.toFixed(4)),
expectedWastedCostMicros: Math.round(expectedCost - success * fullCost),
modes: compareContextModes(scenario),
};
}
compareContextModes 只算上下文部分,不把各能力自身的输入输出混进来;analyzeOrchestration 才把三步的输入、输出、重试与到达概率全部计入。两个函数共用同一套 cost 与 shareContextTokens,所以模式比较与整链路分析不会出现口径漂移。
breakEvenReuseRatio 返回的是四舍五入到四位的小数。判定时不要拿它和复用率做严格相等比较,而应比较「低于」与「高于」两个区间;回本点本身对浮点误差不敏感,对 token 结构很敏感。
示例样本上的运行结果
主场景与高失败率变体的对比如下。高失败率变体把检索、摘要、回复的单次失败率分别改为 3%、12%、6%,其余参数不变。
| 指标 | 主场景 | 高失败率变体 |
|---|---|---|
| 独立调用成本 | 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 微元 | 58 微元 |
| 成功路径延迟 | 1,269ms | 约 1,262ms |
| 延迟变化 | +5.79% | +5.20% |
主场景省 454 微元,代价是 69ms 交接延迟。高失败率变体的编排成本几乎不变,但成功率从 99.86% 掉到 98.12%,每次成功交付成本从 3,872 涨到 4,139 微元,失败浪费从 4 涨到 58 微元。失败传播不改变价目表,它改变的是「一条可用结果到底多少钱」。
失败预算与回本线一起看
三步、每步 1 次重试、目标成功率 95% 时,单步失败率上限是 13.02%。高失败率变体的摘要是 12%,已经逼近上限;再加一个能力,上限会继续下降。回本线回答「省不省」,失败预算回答「省下来的钱能不能兑现」。两个判据必须一起过。
这也是编排最容易出错的地方:实验室只测成功路径,看到 454 微元就宣布方案有效;上线后失败重试把成本抬回去,成功交付成本反而高于独立调用。把失败概率乘进每一步的期望成本,才能看到完整账单。
小结
交接成本由三部分构成:820 token 交接包、共享上下文的重算部分、以及失败路径的重试与浪费。复制、传递、共享三种模式在不同复用率下互有胜负;示例中 share 在 60% 复用率下比 copy 省 454 微元,但复用率低于 28.47% 时就不如直接复制。
28.47% 不是通用常数,而是 820 ÷ (3,200 × 0.9) 这组 token 结构的产物。交接包变大、共享上下文变小或缓存折扣变差,回本线都会移动。下一节把视角从节点之间抬到编排层对外:输入输出契约长什么样,错误码怎么定,版本又该怎么演进。
阅读导航:上一节:2.1 能力之间的数据契约 · 下一节:2.3 编排层的输入输出与版本 。
常见错误与边界
- 把 454 微元当成「编排费」。它是共享上下文相对复制的净节省。
- 把 28.47% 当通用常数。token 结构一变,回本线就要重算。
- 只看交接包总数。增量上下文与上游结果的涨幅不同,必须分开看。
- 用 pass 模式省到极致。共享块不为空时,下游会丢失政策与系统约束。
- 只测成功路径。失败重试与浪费才是编排成本最容易失控的部分。
- 用单次调用成本评价编排。应看每次成功交付成本,把失败传播算进去。
- 复用率拍一个 100%。提示改版、租户隔离与缓存容量都会让它下降。
- 回本点用严格相等判断。浮点误差会让边界值抖动,应比较区间。
- 交接延迟不计入预算。每次 35ms,两次交接就是 70ms,接近示例的 69ms 增量。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。