省得最多的项目,为什么没有先做
季度成本评审前,平台团队列了五条优化:三能力编排合并,每次成功交付从 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 被省两次,组合回报虚高。
- 只算总节省不分账。平台出钱、团队受益,优化没有推动者。
- 预算装不下就否定项目价值。推迟项要写清重新进入的条件。
- 贪心组合当最优解。它可解释,但不保证全局最优,边界要写明。
- 用新数据重算上季度组合。已关闭账期的决策不能追溯改写。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。