《互联网产品的 AI 架构·平台化与治理》7.3 优化优先级与投资回报

优化清单按节省额排序,往往把预算压在一个回本慢、风险高的项目上。本节把节省额、回本期、质量风险与实施成本放进同一套门槛和评分,先判断能不能做,再决定先做哪个,并用示例样本上的 1 亿实施预算说明最大节省项为何被推迟、被拒绝与推迟又有何不同。

省得最多的项目,为什么没有先做

季度成本评审前,平台团队列了五条优化:三能力编排合并,每次成功交付从 4,240 微元降到 3,786 微元,省 454 微元,按每月 12 万次成功交付计,每月省 5,448 万微元;离线批处理迁移,每月省 1,800 万;提示缓存命中率提升,每月省 1,200 万;关闭前沿档兜底路由,每月省 900 万;摘要输出长度上限,每月省 240 万。

如果按节省额排序,编排合并毫无疑问排第一。但它的实施成本是 1.8 亿微元,需要改三条链路、重跑评测、迁移调用方,回本期 3.89 个月,质量风险中等。摘要输出长度上限只省 240 万,实施成本 120 万,回本期 0.53 个月,质量风险低。预算只有 1 亿时,先做哪个?

本节给出一套可复算的排序方法:先过门槛,再算分数,最后在预算内做组合选择。所有数字都来自示例样本上的运行结果,不是生产实测。

只看节省额会漏掉什么

按节省额排,五条优化的顺序是:编排合并 5,448 万、离线批处理 1,800 万、缓存命中 1,200 万、关闭兜底 900 万、输出长度 240 万。这个顺序至少漏掉三件事。

第一,实施成本不同。编排合并要 1.8 亿,输出长度只要 120 万;同样是“每月省 1 微元”,投入的本金差 150 倍。第二,回本期不同。编排合并 3.89 个月,输出长度 0.53 个月;前者占用预算的时间是后者的七倍。第三,质量风险不同。关闭兜底路由省 900 万,但会把一部分请求从高质量档位赶到标准档,属于高风险改动,必须先过评测门禁。

只看节省额,还会把“节省”当成已经落袋的钱。节省额是预期值,要乘置信度才是可比较的期望收益。编排合并的置信度 85%,期望月节省是 4,630.8 万;离线批处理置信度 60%,期望月节省是 1,080 万,和缓存命中持平。排序前先做这一步,结论会变。

四个输入,一个派生量

排序需要四个输入:月节省额、实施成本、收益置信度、质量风险。回本期不是输入,它由实施成本除以期望月节省得到。

输入单位回答的问题常见错误
月节省额微元/月做完能省多少把重叠项目的节省额直接相加
实施成本微元要做多少工程与迁移只算研发人日,不算迁移与评测
收益置信度基点这个节省有多确定所有项目都填 100%
质量风险低/中/高做完会不会掉质量用“应该没事”代替评测结论

实施成本要包含研发、迁移、评测和回滚准备。只算研发人日,会把跨团队项目算便宜;等到上线时才发现调用方迁移和回归评测才是大头。示例把实施成本统一折算成微元,便于和月节省额相除。

质量风险不是扣分项,而是准入门槛。中高风险项目必须先过评测门禁,高风险项目在示例策略里直接不允许进入本季度组合。把风险只当分数折扣,会出现“分数很高但根本不该做”的项目。

先过门槛,再算分

示例策略有四个门槛:月节省额必须大于 0;置信度不低于 50%;质量风险不高于“中”;回本期不超过 6 个月。四条全过,才进入评分。

门槛的顺序有讲究。先看有没有节省,避免为“优化”而优化;再看置信度,避免拿一个拍出来的收益数排队;再看质量风险,避免把不可接受的改动算进组合;最后看回本期,避免用短期预算养一个长期项目。顺序反过来,会让高风险项目先拿到高分,再被人工否决,评审就失去可复算性。

门槛也会产生“看起来可惜”的结果。关闭前沿档兜底路由每月省 900 万,回本期 4.77 个月,单看数字都合格;但质量风险是高,示例策略直接拒绝。拒绝不是永远不做,而是先补评测、补回滚方案,等风险降到中再重新排队。

评分公式

通过门槛的项目按下面的分数排序:

期望月节省 = 月节省额 × 置信度
回本期(月×100) = 实施成本 × 100 ÷ 期望月节省
分数(基点) = 期望月节省 × 12 × 10,000 × 100 ÷ (实施成本 × 风险因子)

风险因子低风险为 100、中风险为 150、高风险为 300。分数的含义是“每投入 1 微元实施成本,一年能带回多少风险调整后的节省”,单位是基点。它不追求绝对精确,只要求同一套输入下可复现、可比较。

为什么不直接用“节省额除以实施成本”?因为那会把质量风险排除在外。高风险项目的分母应该更大,相当于要求更高的回报才值得做。示例里编排合并的分数是 20,581 基点,输出长度是 228,000 基点;后者每微元投入的回报是前者的十一倍。

分数也不是越高越该做。分数高只说明“单位投入的回报高”,还要看预算能不能装下、项目之间是否重叠、团队有没有并行实施能力。分数是排序依据,不是决策本身。

示例样本上的组合

五条优化在示例策略下的评分与决策如下。策略是实施预算 1 亿微元、回本期上限 6 个月、允许中风险、最低置信度 50%。

项目月节省实施成本置信度风险回本期分数决策
输出长度上限240 万120 万95%低0.53 月228,000资助
缓存命中提升1,200 万1,800 万90%低1.67 月72,000资助
编排合并5,448 万1.8 亿85%中3.89 月20,581推迟
离线批处理1,800 万6,000 万60%中5.56 月14,400资助
关闭兜底路由900 万3,000 万70%高4.77 月8,400拒绝

按分数排序是输出长度、缓存命中、编排合并、离线批处理;按节省额排序是编排合并、离线批处理、缓存命中、关闭兜底、输出长度。两个顺序几乎相反,这不是公式在唱反调,而是四个输入本来就不一样。

组合选择按分数贪心:先资助输出长度 120 万,再资助缓存命中 1,800 万,剩余 8,080 万;编排合并需要 1.8 亿,装不下,推迟;离线批处理 6,000 万能装下,资助;最后剩余 2,080 万。被拒绝的关闭兜底不占预算,但也不计入节省。

为什么最大节省被推迟

编排合并被推迟,不是因为它不好,而是因为它太贵。资助完前两个项目后只剩 8,080 万,装不下 1.8 亿。如果把它排到第一,预算会直接变成负数,后面两个高回报项目一个也做不了。

这里有一个反直觉的结论:在预算约束下,“最大节省”与“最高回报”是两种不同的目标。前者适合预算充足、追求绝对金额的场景;后者适合预算有限、需要先把钱用在刀刃上的场景。示例是后者,所以推迟编排合并是合理选择,而不是对它价值的否定。

推迟也不等于放弃。报告里要写清楚:它需要 1.8 亿,当前预算 1 亿,若下季度预算增加到 1.9 亿,按同样排序会重新进入资助名单。这样团队知道下一步该争取什么,而不是只看到一个“未通过”。

关闭兜底路由是另一种情况:它不是被预算挡住,而是被质量风险挡住。即使预算无限,示例策略也不会资助它,除非先补评测与回滚方案,把风险降到中。两类“没做”的原因必须分开写,否则会被误读成同一个优先级问题。

组合层面的三个数

组合选完后,至少报告三个数:计划实施成本 7,920 万,期望月节省 2,388 万,组合回本期 3.32 个月。这三个数回答“这季度花多少、能省多少、多久回本”。

还要报告两个“没做”的数:推迟项目的期望月节省 4,630.8 万,拒绝项目的期望月节省 630 万。它们不能加进“已实现节省”,但必须让评审看到,否则组合看起来只省了 2,388 万,像是漏掉了最大的机会。

组合回本期用总成本除以总期望节省,不是各项目回本期的平均。平均回本期会给小项目过高权重,掩盖大项目占用预算的事实。示例的组合回本期 3.32 个月,比编排合并单独的 3.89 个月短,因为资助的项目整体回报更高。

节省额不能简单相加

组合选择最容易犯的错,是把各项目的节省额直接相加。编排合并省 5,448 万,缓存命中省 1,200 万,如果两者都减少同一批 token,实际总节省会小于 6,648 万。

处理办法是给每个项目声明“基线假设”和“重叠项目”。两个项目若作用于同一批调用,只能按边际节省计入组合;边际节省要在方案评审时用同一份样本测算。示例为了聚焦排序,假设五个项目的节省额互不重叠;真实组合必须做这一步校验,否则会高估回报。

另一个边界是“节省归谁”。编排合并由平台实施,节省落在三条链路的调用方;如果 7.2 的分摊规则不变,平台承担实施成本,团队享受账单下降,平台就没有动力做。治理上要么把节省按约定分享,要么把实施成本计入共享池再分摊。只算总账不分账,优化会停在报告里。

实现

下面的模块只做三件事:按门槛给每个项目定性、按公式算分、在预算内做贪心组合。它不读账单,也不替代方案评审。

// file: src/gov/portfolio.ts
// tier: L2
// verify: node --test test/gov/portfolio.test.ts

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

/** 质量风险不是扣分项,而是准入门槛与折现因子。 */
export type RiskBand = "low" | "medium" | "high";

export const RISK_ORDER: Record<RiskBand, number> = { low: 0, medium: 1, high: 2 };

/** 风险折现:高风险项目的预期收益按 3 倍实施成本计入分母。 */
export const RISK_FACTOR_PCT: Record<RiskBand, number> = { low: 100, medium: 150, high: 300 };

export interface OptimizationItem {
  id: string;
  title: string;
  ownerTeam: string;
  savingsMicrosPerMonth: number;
  implementationCostMicros: number;
  /** 收益置信度,单位基点;80% 记为 8000。 */
  confidenceBps: number;
  qualityRisk: RiskBand;
}

export interface PortfolioPolicy {
  budgetMicros: number;
  /** 回本期上限,单位“月 × 100”;6 个月记为 600。 */
  maxPaybackMonthsX100: number;
  allowedRisk: RiskBand;
  minConfidenceBps: number;
}

export type RejectReason = "no-saving" | "low-confidence" | "quality-risk" | "payback-too-long";

export interface ScoredItem {
  item: OptimizationItem;
  /** 按置信度折算后的月节省额。 */
  expectedSavingMicros: number;
  /** 回本期,单位“月 × 100”;收益为 0 时没有回本期。 */
  paybackMonthsX100: number | null;
  /** 年化风险调整收益 / 实施成本,单位基点。 */
  scoreBps: number;
  rejection: RejectReason | null;
  /** 非低风险项目必须先过评测门禁。 */
  requiresEval: boolean;
}

export interface PortfolioTotals {
  fundedCount: number;
  plannedCostMicros: number;
  expectedMonthlySavingMicros: number;
  paybackMonthsX100: number | null;
  deferredMonthlySavingMicros: number;
  rejectedMonthlySavingMicros: number;
}

export interface PortfolioPlan {
  ranked: ScoredItem[];
  funded: ScoredItem[];
  deferred: ScoredItem[];
  rejected: ScoredItem[];
  totals: PortfolioTotals;
}

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

function text(value: string, field: string): string {
  if (typeof value !== "string" || value.trim() === "") fail(`${field} is required`);
  return value;
}

function integer(value: number, field: string, min = 0): number {
  if (!Number.isSafeInteger(value) || value < min) fail(`${field} must be an integer >= ${min}`);
  return value;
}

function checkPolicy(policy: PortfolioPolicy): void {
  integer(policy.budgetMicros, "policy.budgetMicros", 1);
  integer(policy.maxPaybackMonthsX100, "policy.maxPaybackMonthsX100", 1);
  integer(policy.minConfidenceBps, "policy.minConfidenceBps", 0);
  if (policy.minConfidenceBps > 10_000) fail("policy.minConfidenceBps must not exceed 10000");
  if (RISK_ORDER[policy.allowedRisk] === undefined) fail(`unknown risk ${String(policy.allowedRisk)}`);
}

/**
 * 先过门槛,再算分数。门槛回答“能不能做”,分数回答“先做哪个”。
 * 分数 = 年化风险调整节省额 / 实施成本,避免“省得最多”直接等于“最该先做”。
 */
export function scoreItem(item: OptimizationItem, policy: PortfolioPolicy): ScoredItem {
  checkPolicy(policy);
  text(item.id, "item.id");
  text(item.title, "item.title");
  text(item.ownerTeam, "item.ownerTeam");
  integer(item.savingsMicrosPerMonth, "item.savingsMicrosPerMonth");
  integer(item.implementationCostMicros, "item.implementationCostMicros", 1);
  integer(item.confidenceBps, "item.confidenceBps");
  if (item.confidenceBps > 10_000) fail("item.confidenceBps must not exceed 10000");
  if (RISK_ORDER[item.qualityRisk] === undefined) fail(`unknown risk ${String(item.qualityRisk)}`);

  const expectedSavingMicros = Math.floor(
    (item.savingsMicrosPerMonth * item.confidenceBps) / 10_000,
  );
  const paybackMonthsX100 = expectedSavingMicros === 0
    ? null
    : Math.ceil((item.implementationCostMicros * 100) / expectedSavingMicros);
  const scoreBps = expectedSavingMicros === 0
    ? 0
    : Math.floor(
      (expectedSavingMicros * 12 * 10_000 * 100) /
      (item.implementationCostMicros * RISK_FACTOR_PCT[item.qualityRisk]),
    );

  let rejection: RejectReason | null = null;
  if (expectedSavingMicros === 0) rejection = "no-saving";
  else if (item.confidenceBps < policy.minConfidenceBps) rejection = "low-confidence";
  else if (RISK_ORDER[item.qualityRisk] > RISK_ORDER[policy.allowedRisk]) rejection = "quality-risk";
  else if (paybackMonthsX100 !== null && paybackMonthsX100 > policy.maxPaybackMonthsX100) {
    rejection = "payback-too-long";
  }

  return {
    item: { ...item },
    expectedSavingMicros,
    paybackMonthsX100,
    scoreBps,
    rejection,
    requiresEval: item.qualityRisk !== "low",
  };
}

function compareEligible(left: ScoredItem, right: ScoredItem): number {
  if (right.scoreBps !== left.scoreBps) return right.scoreBps - left.scoreBps;
  const leftPayback = left.paybackMonthsX100 ?? Number.MAX_SAFE_INTEGER;
  const rightPayback = right.paybackMonthsX100 ?? Number.MAX_SAFE_INTEGER;
  if (leftPayback !== rightPayback) return leftPayback - rightPayback;
  const byRisk = RISK_ORDER[left.item.qualityRisk] - RISK_ORDER[right.item.qualityRisk];
  return byRisk !== 0 ? byRisk : left.item.id.localeCompare(right.item.id);
}

/**
 * 预算内按分数贪心选择。贪心不保证组合最优,
 * 但它可解释、可复算,适合按季度公开评审;最优解留到预算更紧时再引入。
 */
export function planPortfolio(
  items: readonly OptimizationItem[],
  policy: PortfolioPolicy,
): PortfolioPlan {
  checkPolicy(policy);
  const seen = new Set<string>();
  const scored = items.map((item) => {
    if (seen.has(item.id)) fail(`duplicate item ${item.id}`);
    seen.add(item.id);
    return scoreItem(item, policy);
  });

  const eligible = scored.filter((item) => item.rejection === null).sort(compareEligible);
  const rejected = scored
    .filter((item) => item.rejection !== null)
    .sort((left, right) => right.item.savingsMicrosPerMonth - left.item.savingsMicrosPerMonth);

  let remaining = policy.budgetMicros;
  const funded: ScoredItem[] = [];
  const deferred: ScoredItem[] = [];
  for (const item of eligible) {
    if (item.item.implementationCostMicros <= remaining) {
      funded.push(item);
      remaining -= item.item.implementationCostMicros;
    } else {
      deferred.push(item);
    }
  }

  const plannedCostMicros = funded.reduce((sum, item) => sum + item.item.implementationCostMicros, 0);
  const expectedMonthlySavingMicros = funded.reduce((sum, item) => sum + item.expectedSavingMicros, 0);
  const totals: PortfolioTotals = {
    fundedCount: funded.length,
    plannedCostMicros,
    expectedMonthlySavingMicros,
    paybackMonthsX100: expectedMonthlySavingMicros === 0
      ? null
      : Math.ceil((plannedCostMicros * 100) / expectedMonthlySavingMicros),
    deferredMonthlySavingMicros: deferred.reduce((sum, item) => sum + item.expectedSavingMicros, 0),
    rejectedMonthlySavingMicros: rejected.reduce((sum, item) => sum + item.expectedSavingMicros, 0),
  };

  return {
    ranked: [...funded, ...deferred, ...rejected],
    funded,
    deferred,
    rejected,
    totals,
  };
}

scoreItem 把门槛与评分放在同一个函数里,避免调用方只取分数、忘记门槛。planPortfolio 的贪心不保证组合最优,但它可解释、可复算;预算更紧或项目更多时,再考虑整数规划,而不是一开始就上优化器。

示例样本上的运行结果

测试覆盖评分、节省额排序与分数排序的差异、质量风险门槛、预算组合、回本期门槛、置信度门槛、零节省与非法输入。执行 node --test test/gov/portfolio.test.ts,示例样本上的运行结果为 8 项通过、0 项失败。

关键输出:输出长度分数 228,000、缓存命中 72,000、编排合并 20,581、离线批处理 14,400、关闭兜底 8,400;资助输出长度、缓存命中、离线批处理,推迟编排合并,拒绝关闭兜底。计划成本 79,200,000,期望月节省 23,880,000,组合回本期 332(3.32 个月)。

这些数字来自示例样本上的运行结果,不是生产实测。换一组节省额、成本或策略,排序会变,但“先门槛、后分数、再组合”的顺序不变。

与 7.2 的衔接

7.2 决定成本如何分到团队,本节决定优化先做哪个。两者必须一起看:一个优化的节省额落在哪个团队,实施成本由谁承担,决定了它有没有人愿意做。

示例里编排合并由平台实施,节省落在内容、客服、交易三条链路。如果平台承担 1.8 亿而账单下降全归团队,平台没有财务动机;反过来,如果实施成本全部分摊给团队,团队会倾向于选自己就能做的小项目。治理会要在立项时同时确认“谁出钱、谁受益、节省怎么记”,否则组合排序只是一张好看的纸。

小结

优化排序不能只看节省额。四个输入是月节省额、实施成本、收益置信度、质量风险;回本期由前两者与置信度派生。先过四道门槛:有节省、置信度够、风险可接受、回本期不超限;再按“年化风险调整节省额除以实施成本”算分。

示例样本上,输出长度上限每月只省 240 万,却因成本低、回本快、风险低排第一;编排合并每月省 5,448 万,却因 1.8 亿实施成本在 1 亿预算下被推迟;关闭兜底路由省 900 万,因高风险被拒绝。组合计划成本 7,920 万,期望月节省 2,388 万,组合回本期 3.32 个月。

三个边界必须记住:重叠项目的节省额不能直接相加;推迟与拒绝是两种不同原因;节省归谁要与 7.2 的分摊规则一起定。

阅读导航:上一节:7.2 成本分摊与内部计价 · 下一节:8.1 数据分级与出域 。

常见错误与边界

  • 只按节省额排序。大项目会挤掉高回报的小项目,预算利用率下降。
  • 把节省额当已落袋收益。不乘置信度,排序会被乐观估计主导。
  • 实施成本只算研发。迁移、评测、回滚准备往往才是大头。
  • 质量风险只当扣分项。高风险项目应先过门槛,而不是靠高分抵消。
  • 所有项目置信度都填 100%。没有依据的确定性,等于没有置信度。
  • 用平均回本期代表组合。小项目权重过高,掩盖大项目占用预算。
  • 把推迟和拒绝混为一谈。一个缺预算,一个缺风险方案,动作完全不同。
  • 重叠项目的节省额直接相加。同一批 token 被省两次,组合回报虚高。
  • 只算总节省不分账。平台出钱、团队受益,优化没有推动者。
  • 预算装不下就否定项目价值。推迟项要写清重新进入的条件。
  • 贪心组合当最优解。它可解释,但不保证全局最优,边界要写明。
  • 用新数据重算上季度组合。已关闭账期的决策不能追溯改写。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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