评审会不是签字会
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,成本与延迟都不是瓶颈。方案因此不碰模型节点,也不碰交接参数,只改最后一跳。
改动有三项:
- 下单请求携带幂等键,键由
runId与订单业务键组成,服务端保留 24 小时。 create-order增加一次补偿重试,只对超时与 5xx 重试,4xx 直接失败。- C 的发布门禁从「成功率不低于 95%」收紧到「不低于 97%」,与基线目标一致。
三项改动都指向同一个判断:C 的问题不是编排结构,而是动作节点的可靠性。加模型、加交接、加缓存都不会让 4% 的动作失败率下降。
改动与预期
评审记录把每一项改动写成「旧值 -> 新值」,预期写成可测的区间,而不是方向。
| 编号 | 改动 | 基线 | 预期 |
|---|---|---|---|
| C-1 | 下单动作重试次数 | 0 次 | 1 次,仅超时与 5xx |
| C-2 | 下单幂等键 | 无 | runId + 订单业务键,保留 24h |
| C-3 | C 发布门禁 | 成功率 ≥ 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。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。