《互联网产品的 AI 架构·推理性能与成本工程》13.2 优化方案与 ADR

拿到 13.1 的成本基线后,用架构决策记录在三个候选方案之间做取舍:稳定前缀缓存、经济档替代与输出上限,分别说明预期收益、质量代价、证据门槛、回退条件与退出条件,并给出可直接运行的 TypeScript 决策函数与可逐条复核的示例样本结果。

基线之后,三个方案同时出现

13.1 的基线给出一个明确信号:成本与延迟都在阈值内,唯一越线项是缓存命中率 3.75%。评审会上因此出现三个方案:做前缀缓存、把摘要能力切到经济档、给输出加硬上限。三个方案都自称能降本,也都有人支持。

如果按「谁降得多」投票,经济档会赢:它承诺降本 35%,是三者中最大的。但它同时承诺质量下降 2.8 个百分点,而且只有 120 条证据。前缀缓存降本 22%,质量不动;输出上限降本 12%,质量只降 0.4 个百分点。三个数字放在一起,已经不是一个投票问题,而是一个取舍问题。

取舍需要留下记录。架构决策记录(ADR)的作用不是把决定写得漂亮,而是让半年后的人能看懂:当时有哪些选项、为什么选这个、什么情况下应该撤。

文中收益、质量与回本期都来自 13.1 的同一组示例样本,不是生产实测;月请求量 300 万是用于计算的假设参数。

ADR 要回答的五个问题

一份能用的 ADR 至少要有五栏。缺任何一栏,决策都会退化成会议纪要。

栏目必须回答常见反例
背景基线数字、约束与时间窗口「最近成本有点高」
候选至少两个可执行选项只写最终方案
决策选哪个、预计收益、回本期「先试试看」
后果质量、延迟、运维与回退代价只写好处
状态提议、已采纳、已否决或已废弃没有状态,旧文档永远有效

「只写最终方案」是最常见的错误。没有候选,就没有比较;没有比较,回退时也不知道该退回哪里。ADR 的价值恰恰在于保留被否决的方案,以及它们被否决的理由。

三个候选方案

三个方案都从同一份基线出发,杠杆不同,代价也不同。

方案杠杆成本变化P95 变化质量变化证据风险实施成本
稳定前缀 + 前缀缓存上下文-22%-8%0240 条低8,000,000 微元
doc.summarize 切经济档模型档位-35%-25%-2.8 点120 条中12,000,000 微元
输出上限 + 停止序列输出长度-12%-10%-0.4 点240 条低3,000,000 微元

前缀缓存排除了提示改版带来的命中破坏:稳定前缀放在最前,可变内容追加在后。经济档直接换模型,降本最快,但质量代价最大。输出上限最便宜,收益也最小,适合作为叠加项而不是主方案。

判据:先护栏,再收益

决策不能把「降本」和「质量」放在一个加权公式里相加。权重一旦可调,结论就可以被调出来。更稳的做法是两道关:先过护栏,再比收益。

第一道:硬护栏

护栏阈值作用
质量回退不超过 1.5 个百分点防止用质量换短期账单
P95 变化不超过 +10%防止降本换来长尾恶化
证据量至少 200 条防止小样本上的偶然结论
可逆性必须有回退路径防止一步到位的单向门
高风险默认不允许高风险必须单独评审

经济档在两道护栏上同时失败:质量回退 2.8 点超过 1.5 点,证据 120 条低于 200 条。它降本最多,但不能进入候选排序。这不是说经济档永远不能用,而是说这批证据不足以支撑它。

第二道:收益与回本期

过护栏的方案按两个数字排序:月节省与回本期。

月节省 = 平均成本 × 月请求量 × 降本比例
回本期 = 实施成本 ÷ (月节省 ÷ 30)

按基线 104.28 微元、月请求 300 万计算,前缀缓存每月省 68,640,000 微元,回本期 4 天;输出上限每月省 37,440,000 微元,回本期 3 天。前缀缓存节省更多,输出上限回本更快。排序规则必须提前写死,否则两个方案各自都能找到支持自己的指标。

这里选择「先看月节省、再看风险等级、再看 P95」的字典序。理由是成本优化的主目标是长期账单,回本期只作为可行性检查;如果主目标是现金流,排序规则就应该反过来,但必须在 ADR 里写明。

回退条件与退出条件

这两个词经常被混用,实际含义相反。

条件回答的问题例子
回退条件出什么问题就撤缓存命中率连续 3 天低于 10%
退出条件什么时候停止投入、转为常态月成本连续两个月下降 ≥15%

回退条件是保护机制,触发后回到基线;退出条件是收尾机制,达成后把实验转成日常配置。只写回退,方案会永远停留在灰度;只写退出,出问题时没有人知道该撤。

每个候选方案都必须同时具备两类条件。经济档的回退条件是质量跌破 0.80、长文档摘要出现结构性缺失;退出条件是质量回退稳定在 1 个点以内、月成本下降至少 25%。这两组条件不成立,方案就不该进入候选表。

实现

下面的模块把护栏判断、收益排序与 ADR 生成放在一起。输入是 13.1 的基线摘要和候选方案,输出是每个候选的可行性、节省、回本期,以及一份可直接归档的 ADR。

// file: src/perf/adr.ts
// tier: L1
// verify: node --test test/perf/adr.test.ts

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

export type RiskLevel = "low" | "medium" | "high";

/** ADR 的输入必须是 13.1 报告里的同一组数字,不能用另一套口径。 */
export interface AdrBaseline {
  avgCostMicros: number;
  e2eP95Ms: number;
  qualityMean: number;
  monthlyRequests: number;
}

export interface AdrCandidate {
  id: string;
  title: string;
  levers: string[];
  /** 负数表示降本;正数表示会涨价。 */
  costDeltaPct: number;
  p95DeltaPct: number;
  /** 质量分变化,单位是百分点;-1.5 表示从 0.882 降到 0.867。 */
  qualityDeltaPts: number;
  implementationCostMicros: number;
  evidenceSamples: number;
  risk: RiskLevel;
  reversible: boolean;
  rollback: string[];
  exit: string[];
}

export interface AdrPolicy {
  maxQualityDropPts: number;
  maxP95IncreasePct: number;
  minEvidenceSamples: number;
  allowHighRisk: boolean;
}

export const DEFAULT_ADR_POLICY: AdrPolicy = {
  maxQualityDropPts: 1.5,
  maxP95IncreasePct: 10,
  minEvidenceSamples: 200,
  allowHighRisk: false,
};

export interface CandidateAssessment {
  id: string;
  title: string;
  eligible: boolean;
  reasons: string[];
  savingsMicrosPerMonth: number;
  qualityAfter: number;
  p95AfterMs: number;
  paybackDays: number;
  reversibility: "reversible" | "one-way";
  risk: RiskLevel;
}

export interface AdrAlternative {
  id: string;
  verdict: "accepted" | "rejected";
  reason: string;
}

export interface AdrRecord {
  id: string;
  title: string;
  status: "accepted" | "rejected";
  context: string;
  decision: string;
  consequences: string[];
  alternatives: AdrAlternative[];
}

export interface AdrDecision {
  status: "accepted" | "rejected";
  chosen: string | null;
  assessments: CandidateAssessment[];
  adr: AdrRecord;
}

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

function fail(message: string): never {
  throw new AiError("invalid-request", `adr: ${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 validateCandidate(candidate: AdrCandidate): void {
  for (const field of ["id", "title"] as const) {
    if (candidate[field].trim() === "") fail(`${field} is required`);
  }
  if (candidate.levers.length === 0) fail(`${candidate.id}: levers must not be empty`);
  integer(candidate.evidenceSamples, `${candidate.id}.evidenceSamples`, 1);
  integer(candidate.implementationCostMicros, `${candidate.id}.implementationCostMicros`);
  if (!Number.isFinite(candidate.costDeltaPct)) fail(`${candidate.id}: costDeltaPct must be finite`);
  if (candidate.rollback.length === 0) fail(`${candidate.id}: rollback conditions are required`);
  if (candidate.exit.length === 0) fail(`${candidate.id}: exit conditions are required`);
}

function assess(
  baseline: AdrBaseline,
  candidate: AdrCandidate,
  policy: AdrPolicy,
): CandidateAssessment {
  validateCandidate(candidate);
  const savingsMicrosPerMonth = Math.round(
    baseline.avgCostMicros * baseline.monthlyRequests * (-candidate.costDeltaPct / 100),
  );
  const qualityAfter = Number((baseline.qualityMean + candidate.qualityDeltaPts / 100).toFixed(3));
  const p95AfterMs = Math.round(baseline.e2eP95Ms * (1 + candidate.p95DeltaPct / 100));
  const dailySavings = savingsMicrosPerMonth / 30;
  const paybackDays = savingsMicrosPerMonth <= 0
    ? Number.POSITIVE_INFINITY
    : Math.ceil(candidate.implementationCostMicros / dailySavings);

  const reasons: string[] = [];
  if (candidate.evidenceSamples < policy.minEvidenceSamples) {
    reasons.push(`evidence ${candidate.evidenceSamples} < ${policy.minEvidenceSamples}`);
  }
  if (-candidate.qualityDeltaPts > policy.maxQualityDropPts) {
    reasons.push(`quality drop ${(-candidate.qualityDeltaPts).toFixed(2)} > ${policy.maxQualityDropPts}`);
  }
  if (candidate.p95DeltaPct > policy.maxP95IncreasePct) {
    reasons.push(`p95 increase ${candidate.p95DeltaPct}% > ${policy.maxP95IncreasePct}%`);
  }
  if (candidate.risk === "high" && !policy.allowHighRisk) {
    reasons.push("high risk is not allowed by policy");
  }
  if (!candidate.reversible) reasons.push("candidate is one-way; no rollback path");
  if (savingsMicrosPerMonth <= 0) reasons.push("no positive monthly saving");

  return {
    id: candidate.id,
    title: candidate.title,
    eligible: reasons.length === 0,
    reasons,
    savingsMicrosPerMonth,
    qualityAfter,
    p95AfterMs,
    paybackDays,
    reversibility: candidate.reversible ? "reversible" : "one-way",
    risk: candidate.risk,
  };
}

function pick(assessments: readonly CandidateAssessment[]): CandidateAssessment | null {
  const eligible = assessments.filter((item) => item.eligible);
  if (eligible.length === 0) return null;
  return [...eligible].sort((left, right) =>
    right.savingsMicrosPerMonth - left.savingsMicrosPerMonth ||
    RISK_ORDER[left.risk] - RISK_ORDER[right.risk] ||
    left.p95AfterMs - right.p95AfterMs ||
    left.id.localeCompare(right.id))[0];
}

/**
 * 决策函数只做三件事:按护栏淘汰、按收益排序、把结果写成 ADR。
 * 回退条件与退出条件必须分开:前者是失败时怎么撤,后者是什么时候停止投入。
 */
export function decideAdr(
  baseline: AdrBaseline,
  candidates: readonly AdrCandidate[],
  policy: AdrPolicy = DEFAULT_ADR_POLICY,
): AdrDecision {
  if (candidates.length === 0) fail("至少需要一个候选方案");
  integer(baseline.monthlyRequests, "baseline.monthlyRequests", 1);
  if (!Number.isFinite(baseline.avgCostMicros) || baseline.avgCostMicros < 0) fail("baseline.avgCostMicros 非法");
  if (!Number.isFinite(baseline.e2eP95Ms) || baseline.e2eP95Ms < 0) fail("baseline.e2eP95Ms 非法");
  if (!Number.isFinite(baseline.qualityMean) || baseline.qualityMean < 0 || baseline.qualityMean > 1) {
    fail("baseline.qualityMean 必须落在 [0, 1]");
  }

  const assessments = candidates.map((candidate) => assess(baseline, candidate, policy));
  const chosen = pick(assessments);
  const byId = new Map(candidates.map((candidate) => [candidate.id, candidate]));
  const alternatives: AdrAlternative[] = assessments.map((item) => ({
    id: item.id,
    verdict: item.id === chosen?.id ? "accepted" : "rejected",
    reason: item.id === chosen?.id
      ? `savings ${item.savingsMicrosPerMonth} micros/month, payback ${item.paybackDays} days`
      : item.reasons.length > 0 ? item.reasons.join("; ") : "lower saving than the accepted option",
  }));

  const context =
    `baseline: ${baseline.avgCostMicros} micros/request, p95 ${baseline.e2eP95Ms}ms, ` +
    `quality ${baseline.qualityMean.toFixed(3)}, ${baseline.monthlyRequests} requests/month; ` +
    `policy: quality drop <= ${policy.maxQualityDropPts}pts, p95 increase <= ${policy.maxP95IncreasePct}%, ` +
    `evidence >= ${policy.minEvidenceSamples}.`;
  const consequences = chosen === null
    ? ["没有候选方案同时满足收益与护栏;保持基线,补充证据后再决策"]
    : [
      ...byId.get(chosen.id)!.exit.map((condition) => `退出条件:${condition}`),
      ...byId.get(chosen.id)!.rollback.map((condition) => `回退条件:${condition}`),
      `预期质量 ${chosen.qualityAfter.toFixed(3)},P95 ${chosen.p95AfterMs}ms,回本期 ${chosen.paybackDays} 天`,
    ];

  return {
    status: chosen === null ? "rejected" : "accepted",
    chosen: chosen?.id ?? null,
    assessments,
    adr: {
      id: "ADR-013",
      title: chosen === null ? "本轮不采纳成本优化方案" : `采纳「${chosen.title}」`,
      status: chosen === null ? "rejected" : "accepted",
      context,
      decision: chosen === null
        ? "所有候选方案至少违反一项护栏,暂不改变基线。"
        : `采纳「${chosen.title}」,预计每月节省 ${chosen.savingsMicrosPerMonth} 微元,` +
          `回本期 ${chosen.paybackDays} 天。`,
      consequences,
      alternatives,
    },
  };
}

decideAdr 先对每个候选跑同一组护栏,再在可行集里按「月节省、风险、P95」排序。被拒绝的方案不会被删除,而是进入 adr.alternatives,保留原因。这样半年后回看,能区分「当时没选」和「当时没考虑」。

paybackDays 用 Math.ceil,因为回本期按完整天数表达;savingsMicrosPerMonth 用 Math.round,保持整数微元口径。两个舍入方向不同,是为了让回本期不被低估、节省不被高估。

示例样本上的运行结果

把 13.1 的基线摘要与三个候选输入决策函数,得到:

方案可行月节省(微元)回本期质量后P95 后
稳定前缀 + 前缀缓存是68,640,0004 天0.869767ms
doc.summarize 切经济档否109,200,0004 天0.841626ms
输出上限 + 停止序列是37,440,0003 天0.865751ms

最终采纳前缀缓存:预计每月节省 68,640,000 微元,回本期 4 天,质量不变,P95 从 834ms 降到 767ms。ADR 的 alternatives 里,经济档被标记为「证据 120 < 200;质量回退 2.80 > 1.5」,输出上限被标记为「节省低于被采纳方案」。

这个结果值得多看一眼:节省最大的方案被否决,节省更小的方案被采纳。这不是自相矛盾,而是护栏在起作用。如果只看节省,经济档会赢;但它的质量代价没有被证据支撑,赢了也不可持续。

决策之后:把 ADR 变成门禁

ADR 写完不等于执行。三个数字应该进入发布流程:月节省进入预算基线,回本期进入项目排期,回退条件进入监控规则。

ADR 字段落地位置验证方式
月节省 68,640,000下季度预算账单连续两个月对比
回本期 4 天项目计划实施成本与节省对齐
缓存命中率 <10% 回退监控告警注入低命中样本,告警必须触发
质量回退 ≤0.3 点回归门禁评测集跑批,超线自动阻断

门禁要能失败。只把条件写进文档,下一次发布仍然会绕过;只有一条会变红的测试,才能让回退条件真正生效。

小结

这一节把基线变成了一个可执行的决策:三个候选方案先过质量、延迟、证据与可逆性护栏,再按月节省与回本期排序。经济档降本最多,却因质量回退 2.8 点、证据只有 120 条被否决;前缀缓存以降本 22%、质量不变、回本期 4 天被采纳;输出上限作为低风险叠加项保留。

ADR 的价值不在结论本身,而在它保留了被否决的方案与理由。下一节走出单能力优化,讨论多能力编排会带来哪些新的成本。

阅读导航:上一节:13.1 推理成本基线 · 下一节:13.3 进入卷三 。

常见错误与边界

  • 只写最终方案。没有候选与否决理由,回退时不知道退回哪里。
  • 把降本和质量放进一个加权分。权重可调,结论就可以被调出来。
  • 用节省最大的方案直接上线。收益越大,越要先看护栏。
  • 证据不足就下结论。120 条样本上的 35% 降本只是方向,不是承诺。
  • 把回退条件写成「效果不好就撤」。没有可观测阈值,等于没有条件。
  • 只有回退、没有退出。方案会永远停在灰度,无法转成常态。
  • 回本期用四舍五入。少算一天,排期就会过于乐观。
  • ADR 写完不进门禁。文档里的条件不会自动生效。
  • 把示例收益写成生产收益。基线、请求量与价格一变,数字全部要重算。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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