《互联网产品的 AI 架构·多能力编排与任务闭环》13.2 一次编排方案评审

C 链路 95.00% 的成功率没到 97% 目标,团队提出给下单动作补幂等键与一次补偿重试。本节给出一份可归档的方案评审记录:方案、基线、改动、预期、条件、负责人、复查日期,逐项列出通过项与四条驳回项,并附可复用的记录模板与灰度条件说明。

评审会不是签字会

13.1 的基线表放在会议桌上,C 链路的 95.00% 很刺眼。产品希望下周就改,理由是「加一次重试而已」;工程希望先确认重试不会重复下单;合规只问一句:人工复核还在不在。三个角色看的是同一张表,担心的却不是同一个数字。

编排方案评审要解决的正是这件事:把「改什么、为什么改、改完预期多少、什么条件下才能上、谁负责、什么时候复查」写成一份可以归档、可以复查、可以被下一个团队接着用的记录。没有这份记录,灰度出问题时只能争论「当时说的是什么意思」。

本节是一次真实格式的评审记录。基线引用 13.1 的 echo-orch-baseline-v1,方案只动 C 链路的下单动作,预期与条件都写成可判定的句子。所有数字仍来自示例样本,不是生产实测。

评审记录的六个字段

一份能进入发布流程的评审记录,至少要有六个字段。缺任何一个,评审就退化成口头同意。

字段回答的问题不合格的表现
方案要改什么,改哪条链路、哪个节点「优化一下下单」
基线改动前的事实是什么只写「成功率不高」
改动具体参数从什么变成什么没有旧值与新值
预期改完哪些指标会变,变多少「应该会更好」
条件什么前提下才允许上线没有任何前置条件
负责人 / 复查日期谁对结果负责,什么时候回看只有会议纪要,没有责任人

这份记录不是审批单。审批单回答「能不能做」,评审记录回答「做完怎么判断对不对」。前者是一次性的,后者要被下一次评审、灰度报告和季度复盘反复引用。

方案与基线

方案编号 OR-2026-11-05-01,标题是「为 C 链路的下单动作补幂等键与一次补偿重试」。基线是 13.1 的三链路表,受影响链路只有 C。

C 的现状很清楚:240 条样本成功 228 条,成功率 95.00%;失败几乎全部落在 create-order,动作失败率 4%,没有重试。每次成功交付 2,429 微元,P95 793ms,成本与延迟都不是瓶颈。方案因此不碰模型节点,也不碰交接参数,只改最后一跳。

改动有三项:

  1. 下单请求携带幂等键,键由 runId 与订单业务键组成,服务端保留 24 小时。
  2. create-order 增加一次补偿重试,只对超时与 5xx 重试,4xx 直接失败。
  3. C 的发布门禁从「成功率不低于 95%」收紧到「不低于 97%」,与基线目标一致。

三项改动都指向同一个判断:C 的问题不是编排结构,而是动作节点的可靠性。加模型、加交接、加缓存都不会让 4% 的动作失败率下降。

改动与预期

评审记录把每一项改动写成「旧值 -> 新值」,预期写成可测的区间,而不是方向。

编号改动基线预期
C-1下单动作重试次数0 次1 次,仅超时与 5xx
C-2下单幂等键无runId + 订单业务键,保留 24h
C-3C 发布门禁成功率 ≥ 95%成功率 ≥ 97%

预期结果有四条:

  • C 的成功率从 95.00% 提升到不低于 97.4%。按单次动作失败率 4%、一次重试计算,理论有效失败率是 0.16%,保守取 97.4% 作为灰度门槛。
  • C 的每次成功交付成本从 2,429 微元增加到不超过 2,489 微元。重试只在失败时发生,按 4% 的触发率估算,增量约 60 微元。
  • 重复下单在 10,000 条回放样本中为 0。幂等键是这条预期的前提,不是可选项。
  • A、B 两条链路的基线不变。方案不改共享上下文、不改交接 token,也不改模型档位。

预期不是承诺。它给出的是「如果改动按设计生效,应该落在哪个区间」;灰度报告要拿真实结果与这四个区间逐条对照。

实现

下面的模块把评审记录结构化:必填字段校验、状态判定、文本渲染与归档前检查。它不判断方案好不好,只保证记录完整、可复查、不会因为缺字段而无法追责。

// file: src/orch/review.ts
// tier: L2
// verify: node --test test/orch/baseline.test.ts

import { AiError } from "../core/errors.ts";
import type { ChainId, Verdict } from "./baseline.ts";
import type { BaselineReport } from "./report.ts";

export interface ReviewChange {
  id: string;
  /** 改什么,必须指向具体节点或字段。 */
  target: string;
  baseline: string;
  expected: string;
}

export interface RejectedItem {
  id: string;
  proposal: string;
  reason: string;
}

export interface ReviewInput {
  id: string;
  date: string;
  owner: string;
  proposal: string;
  baseline: BaselineReport;
  chains: readonly ChainId[];
  changes: readonly ReviewChange[];
  expected: readonly string[];
  conditions: readonly string[];
  recheckDate: string;
  rejected: readonly RejectedItem[];
}

export interface ReviewRecord {
  id: string;
  date: string;
  owner: string;
  proposal: string;
  baselineVersion: string;
  baselineVerdict: Verdict;
  chains: readonly ChainId[];
  changes: readonly ReviewChange[];
  expected: readonly string[];
  conditions: readonly string[];
  recheckDate: string;
  rejected: readonly RejectedItem[];
  status: "accepted" | "conditional" | "rejected";
}

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

/** 评审记录必须能回答六个问题:方案、基线、改动、预期、条件、复查。 */
export function buildReviewRecord(input: ReviewInput): ReviewRecord {
  for (const field of ["id", "date", "owner", "proposal", "recheckDate"] as const) {
    if (input[field].trim() === "") fail(`${field} 不能为空`);
  }
  if (input.chains.length === 0) fail("至少指定一条受影响链路");
  if (input.changes.length === 0) fail("改动清单不能为空");
  if (input.expected.length === 0) fail("预期结果不能为空");
  if (input.recheckDate <= input.date) fail("复查日期必须晚于评审日期");
  const changed = new Set(input.chains);
  for (const change of input.changes) {
    if (!/^[ABC]$/.test(change.target.slice(0, 1))) fail(`改动 ${change.id} 未指向 A/B/C 链路`);
    if (!changed.has(change.target.slice(0, 1) as ChainId)) fail(`改动 ${change.id} 指向未受影响链路`);
  }
  const status = input.conditions.length > 0 ? "conditional" : "accepted";
  return {
    id: input.id,
    date: input.date,
    owner: input.owner,
    proposal: input.proposal,
    baselineVersion: input.baseline.sampleVersion,
    baselineVerdict: input.baseline.overall.verdict,
    chains: input.chains,
    changes: input.changes,
    expected: input.expected,
    conditions: input.conditions,
    recheckDate: input.recheckDate,
    rejected: input.rejected,
    status,
  };
}

/** 渲染成可归档的评审文本;字段顺序固定,空条件显式写「无」。 */
export function renderReviewRecord(record: ReviewRecord): string {
  const lines: string[] = [];
  lines.push(`# 编排方案评审 ${record.id}`);
  lines.push("");
  lines.push(`- 日期:${record.date}`);
  lines.push(`- 负责人:${record.owner}`);
  lines.push(`- 方案:${record.proposal}`);
  lines.push(`- 基线:${record.baselineVersion}(${record.baselineVerdict})`);
  lines.push(`- 影响链路:${record.chains.join("、")}`);
  lines.push(`- 状态:${record.status}`);
  lines.push("");
  lines.push("## 改动");
  for (const change of record.changes) {
    lines.push(`- ${change.id} ${change.target}:${change.baseline} -> ${change.expected}`);
  }
  lines.push("");
  lines.push("## 预期");
  for (const item of record.expected) lines.push(`- ${item}`);
  lines.push("");
  lines.push("## 条件");
  if (record.conditions.length === 0) lines.push("- 无");
  for (const item of record.conditions) lines.push(`- ${item}`);
  lines.push("");
  lines.push(`## 复查日期`);
  lines.push(record.recheckDate);
  lines.push("");
  lines.push("## 驳回项");
  if (record.rejected.length === 0) lines.push("- 无");
  for (const item of record.rejected) lines.push(`- ${item.id} ${item.proposal}:${item.reason}`);
  return `${lines.join("\n")}\n`;
}

/** 归档前的最小校验:缺字段的记录不能进入发布流程。 */
export function assertReviewComplete(record: ReviewRecord): void {
  if (record.changes.length === 0) fail("评审记录缺少改动");
  if (record.expected.length === 0) fail("评审记录缺少预期");
  if (record.recheckDate <= record.date) fail("评审记录缺少有效的复查日期");
  if (record.owner.trim() === "") fail("评审记录缺少负责人");
}

buildReviewRecord 做三件事:检查六个字段非空、检查改动指向受影响链路、根据是否存在条件决定状态是 accepted 还是 conditional。renderReviewRecord 的输出顺序固定,空条件显式写「无」,避免归档后看不出是「没有条件」还是「忘了写」。

assertReviewComplete 是发布流程的最后一道检查。它不替代评审,只阻止缺改动、缺预期或缺复查日期的记录进入灰度。

一次评审记录

按上面的结构渲染,这次评审的记录如下:

  • 日期:2026-11-05
  • 负责人:编排组 / 林澈
  • 方案:为 C 链路的下单动作补幂等键与一次补偿重试
  • 基线:echo-orch-baseline-v1(conditional)
  • 影响链路:C
  • 状态:conditional

改动三项:C-1 重试 0 -> 1;C-2 幂等键从无到有;C-3 门禁从 95% 收紧到 97%。预期四条:成功率 ≥ 97.4%、每次成功交付 ≤ 2,489 微元、重复下单 0、A/B 不变。条件是下单服务支持 24 小时幂等窗口、只对可重试错误重试、灰度 5% 观察 7 天、重复下单出现一例即回滚。复查日期是 2026-11-12。

记录的状态是 conditional,不是 accepted。原因不是方案有问题,而是它依赖三个外部条件:下单服务的幂等能力、错误分类的准确性、灰度窗口的长度。条件没有满足之前,方案不能全量。

驳回项

评审记录同样要记录「什么没通过」。只写通过项,下一次会有人把驳回的方案重新提一遍。

编号提议驳回理由
R-1下单动作重试 3 次重试次数越多,重复下单与尾部延迟的风险越高;一次补偿足够覆盖超时与瞬时 5xx
R-2取消人工复核,改为模型自动通过内容合规要求人工复核不可绕开,成本再低也不能动
R-3去掉 820 token 交接,只传订单号C 的复用率 72%,交接成本占比 17.91%;省下的钱不足以抵消上下文缺失带来的错误下单
R-4把 C 的成本预算提高到 3,500 微元成本不是 C 的瓶颈,提高预算不会让 95.00% 变成 97%

四条驳回项里,R-1 与 R-2 是风险类,R-3 是成本与质量的交换,R-4 是「用预算掩盖问题」。评审记录把驳回理由写清楚,下一次提案就能直接引用,而不是重新争论一遍。

R-3 值得多解释一句。C 的交接成本占比 17.91%,看起来有压缩空间;但交接传的是上一跳的重排结果与用户意图,去掉之后下单动作只能拿到订单号,模型无法判断「买哪个规格」。省 820 token 的代价是错误下单,这笔账不划算。

条件与复查

四个条件里,前两个是技术前提,后两个是发布纪律。

第一,下单服务必须支持幂等键,且窗口不短于 24 小时。没有这个前提,重试就是重复下单的入口。

第二,错误分类必须准确。只有超时与 5xx 可以重试;4xx 重试没有意义,还会放大无效请求。

第三,灰度 5%,观察 7 天。观察指标不是只有成功率,还包括重复下单数、P95 延迟与每次成功交付成本。

第四,出现一例重复下单立即回滚。回滚不需要再次评审,这是一票否决条件。

复查日期是 2026-11-12。复查时要回答三个问题:成功率是否达到 97.4%、成本是否落在 2,489 微元以内、四条驳回项是否仍然成立。如果成功率达标但成本超了,方案仍然算未通过,因为预期是同时成立的。

小结

一次编排方案评审要把六个字段写全:方案、基线、改动、预期、条件、负责人与复查日期。这次评审的对象是 C 链路的下单动作:基线 95.00%、2,429 微元、P95 793ms;改动是幂等键加一次补偿重试;预期是成功率不低于 97.4%、每次成功交付不超过 2,489 微元、重复下单为 0;条件是幂等窗口、错误分类、5% 灰度与一票回滚。

评审还驳回了四项提议:三次重试、取消人工复核、去掉交接上下文、提高成本预算。驳回理由与通过项一样重要,它们构成下一次提案的边界。评审记录不是签字,而是把「改完怎么判断对不对」提前写死。

阅读导航:上一节:13.1 回声的编排基线 · 下一节:13.3 进入卷四:平台化与治理 。

常见错误与边界

  • 评审只写方案,不写基线。没有改动前的数字,预期无法验证。
  • 改动不写旧值。只写「增加重试」,复查时分不清是从 0 到 1 还是从 1 到 3。
  • 预期写成方向而不是区间。「应该会更好」无法判定通过与否。
  • 条件写成愿望。「尽量保证幂等」不是条件,「幂等窗口不少于 24 小时」才是。
  • 驳回项不记录。被否的方案会反复出现,评审成本翻倍。
  • 把评审记录当审批单。它解决的是可复查,不是一次性签字。
  • 灰度只看成功率。重复下单、成本与 P95 任何一项越界都应触发回滚。
  • 复查日期不写或写成评审当天。复查必须晚于灰度窗口,否则没有新数据可看。
  • 用示例样本的 97.4% 当生产承诺。它是灰度门槛,不是对用户的 SLA。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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