如何评估操作规范与其执行之间的一致性
在 我上一篇文章 中,我探讨了执行器独立性(Executor Independence),即不同的执行机制应当能够满足同一份操作规范。
所谓执行器独立性,指的是即便执行具体任务的技术手段发生了变更,该操作规范依然保持相对稳定。
由此,我们就能构建出一个实用架构:
规范
↓
执行器
↓
证据
但它留下一个关键问题尚未解决:如何判断执行过程中产生的证据,是否真正符合规范要求?
这是一个一致性(Conformance)问题。
从执行器的角度看,一次部署可能成功完成,却仍然违反了操作契约。
一个修复代理可以完成其计划内的所有动作,系统却可能依然处于可接受范围之外。
一个恢复操作可能没有报错结束,但要求恢复的数据却依然缺失。
因此,仅有一个通用的状态标识:
success = true
是远远不够的。
我们需要一种方法,将“本应发生的事”与“实际观察到的现象”进行对比,得出一个明确、可解释且实用的结果。
在本文中,我将展示如何对操作一致性进行建模,如何评估单个约束条件,如何区分通过、失败和未知结果,如何处理容差以及应对证据缺失、分配严重度等级、计算有用的一致性摘要,并避免将一致性评估简化为一个具有误导性的分数。
文中的示例使用 TypeScript 并设定了一个部署场景,但同样的方法也适用于基础设施变更、事件响应、备份恢复、数据管道以及由 AI 驱动的系统。
核心观点如下:一致性,并不意味着执行器声称成功了。一致性,是指观察到的证据满足了操作规范。
前置知识
你应当熟悉以下内容:
核心软件架构概念
TypeScript 或类似的语言
基础测试
可观测性(Observability)
CI/CD 概念
操作规范(Operational Specifications)
执行器独立性(Executor Independence)
你无需特定的部署平台,文中的示例也特意保持精简,以便评估模型更加直观清晰。
目录
此处的“合规”含义
在日常用语中,合规指遵守规则、标准或要求。
在这里,我把这个词的含义限定得更具体一些:
运行符合性(Operational conformance)是指评估观测到的执行证据是否、以及在哪些地方满足了运行规范所定义的要求。
在这个语境下,证据是从系统中采集的一组可观测事实,用来和规范进行比对。证据本身不是符合与否的判定结论,而是得出这个结论所依据的数据。
这个定义包含三个部分。
1. 规范(Specification)
定义什么状态才是应当成立的。
例如:
version = v42
available replicas >= 3
error rate <= 1%
p95 latency <= 400 ms
2. 证据(Evidence)
描述实际观测到的情况。
例如:
version = v42
available replicas = 3
error rate = 0.4%
p95 latency = 280 ms
3. 评估(Evaluation)
把证据和规范逐项比对。
例如:
version PASS
available replicas PASS
error rate PASS
p95 latency PASS
因此,符合性并不是执行器本身的属性,而是用证据对照规范进行评估后得出的结果。
概念上可以表示为:
Specification
+
Evidence
↓
Conformance Evaluation
↓
Result
从规范和证据出发
假设我们有这样一份部署规范:
type DeploymentSpec = {
version: string;
minReplicas: number;
maxErrorRate: number;
maxP95LatencyMs: number;
};
const spec: DeploymentSpec = {
version: "v42",
minReplicas: 3,
maxErrorRate: 0.01,
maxP95LatencyMs: 400,
};
再定义证据:
type DeploymentEvidence = {
version: string;
replicas: number;
errorRate: number;
p95LatencyMs: number;
};
const evidence: DeploymentEvidence = {
version: "v42",
replicas: 3,
errorRate: 0.004,
p95LatencyMs: 280,
};
最简单的评估器可以返回一个布尔值:
function conforms(
spec: DeploymentSpec,
evidence: DeploymentEvidence
): boolean {
return (
evidence.version === spec.version &&
evidence.replicas >= spec.minReplicas &&
evidence.errorRate <= spec.maxErrorRate &&
evidence.p95LatencyMs <= spec.maxP95LatencyMs
);
}
这能用,但有个问题。
如果结果是:
false
你就不知道原因。这对运维工作来说远远不够。
逐项评估约束条件
不要只返回一个布尔值,而是单独评估每一个约束条件。
type ConstraintResult = {
name: string;
passed: boolean;
};
function evaluateDeployment(
spec: DeploymentSpec,
evidence: DeploymentEvidence
): ConstraintResult[] {
return [
{
name: "version",
passed:
evidence.version === spec.version,
},
{
name: "replicas",
passed:
evidence.replicas >= spec.minReplicas,
},
{
name: "error-rate",
passed:
evidence.errorRate <= spec.maxErrorRate,
},
{
name: "latency",
passed:
evidence.p95LatencyMs <= spec.maxP95LatencyMs,
},
];
}
ConstraintResult 为每个检查项提供名称和布尔结果。evaluateDeployment() 函数逐一将观测值与规格中的对应规则进行比较。它检查部署版本是否精确匹配、副本数是否足够、以及错误率和延迟是否保持在限制范围内。
函数不再把所有检查项归结为一个 true 或 false,而是为每个约束条件返回一个结果。这让调用方能清楚看到哪条要求通过,哪条失败。
此时输出可以是:
version PASS
replicas PASS
error-rate PASS
latency PASS
或者:
version PASS
replicas FAIL
error-rate PASS
latency FAIL
这样实用得多。失败的操作变得可解释。
不要将一切简化为通过或失败
有时二元评估仍然过于简单。
假设规格要求:
error rate <= 1%
但你的可观测性系统不可用。
结果应该是什么?
PASS 显然是错的。
FAIL 也可能具有误导性,因为它混淆了两种不同情况:
系统违反了约束条件
以及:
我们不知道系统是否违反了约束条件
更好的模型至少使用三种状态:
PASS
FAIL
UNKNOWN
例如:
type ConstraintStatus =
| "PASS"
| "FAIL"
| "UNKNOWN";
未知证据绝不应静默地变为成功。
将缺失证据视为未知
假设证据变得不完整。换句话说,系统可能只返回部分观测结果,而非全部。部署记录可能告知我们当前运行的版本和可用的副本数量,而错误率指标暂时不可用。
我们可以通过将每个证据字段设为可选来表示这种情况:
type DeploymentEvidence = {
version?: string;
replicas?: number;
errorRate?: number;
p95LatencyMs?: number;
};
然后,评估器需要区分真正的违规和缺失的观测:
function evaluateErrorRate(
spec: DeploymentSpec,
evidence: DeploymentEvidence
): ConstraintStatus {
if (
evidence.errorRate === undefined
) {
return "UNKNOWN";
}
return (
evidence.errorRate <= spec.maxErrorRate
)
? "PASS"
: "FAIL";
}
第一个分支检查是否观测到错误率值。如果没有,评估器返回 UNKNOWN 而不是进行猜测。如果该值存在,函数会将其与允许的最大错误率进行比较,并返回 PASS 或 FAIL。
这将产生以下结果:
errorRate = 0.004 → PASS
errorRate = 0.03 → FAIL
errorRate missing → UNKNOWN
这种区分至关重要。在这里,遥测数据缺失意味着我们期望观测到的某个指标或运营信号不可用。例如,因为监控系统未报告该执行时间窗口内的错误率值。如果将遥测数据缺失视为成功,合规性将变得过于乐观且存在危险。
在精确比较不适用时使用容差
某些约束应使用精确相等。
例如:
deployed version must equal v42
其他约束则不应如此。
数值系统可能需要容差,因为即使两个值在运营上是等效的,测量或计算值也可能存在微小差异。浮点算术、采样、舍入和测量噪声都可能产生不应仅因不完全相等就导致约束失败的值。
function withinTolerance(
observed: number,
expected: number,
tolerance: number
): boolean {
return (
Math.abs(observed - expected)
<= tolerance
);
}
然后调用:
withinTolerance(
1.9999999,
2,
0.0001
);
返回 true。
这个函数计算观测值和期望值之差的绝对值,再判断该差值是否在允许的容差范围内。当我们预期数值会有细微偏差、而严格相等判定会造成误报失败时,这种方式很有用。
但容差必须来自实际业务领域,不能仅仅为了让失败消失而随意引入。
给约束添加严重级别
不同约束在运维上的重要程度并不相同。
假设有两个约束失败了:
p95 latency = 405 ms
replicas = 0
两者都是失败,但严重程度完全不同。
延迟略微超标可能值得关注,但未必需要立即回滚;而可用副本数为零则可能意味着服务已经不可用,必须立即触发恢复。引入严重级别,可以让评估结果保留这种运维层面的差异,而不是把所有失败一视同仁。
type Severity =
| "INFO"
| "WARNING"
| "CRITICAL";
type ConstraintResult = {
name: string;
status: ConstraintStatus;
severity: Severity;
};
这里,status 表示约束是通过、失败还是无法评估,severity 则表示该结果在运维上的重要程度。
这是两个独立的维度:同一个失败约束,可能是 WARNING 级别,也可能是 CRITICAL 级别。
于是:
latency
FAIL
WARNING
replicas
FAIL
CRITICAL
在决定是继续、暂停、回滚还是升级处理时,这种区分非常关键。
区分硬性约束与建议性约束
有些需求决定了操作本身是否可接受,另一些则只是有用的信号,并不一定会阻塞执行。
这与严重性(severity)不同。严重性描述的是某个结果的影响程度,而约束模式(constraint mode)则决定该规则是否参与最终的合规性判定。`REQUIRED` 约束必须通过,操作才算合规;`ADVISORY` 约束即使未通过,操作整体仍可被视为合规,但该失败仍需被报告。
type ConstraintMode =
| "REQUIRED"
| "ADVISORY";
现在,结果可以包含以下字段:
type ConstraintResult = {
name: string;
status: ConstraintStatus;
severity: Severity;
mode: ConstraintMode;
expected?: unknown;
observed?: unknown;
};
例如:
version
REQUIRED
PASS
latency-target
ADVISORY
FAIL
在这种情况下,操作整体上仍可能是合规的,但附带警告。这比用一个全局布尔值来表达要丰富得多。
构建可复用的合规性评估器
目前,示例都绑定于单一部署和少量硬编码检查。下一步是将评估机制与具体规则解耦,使同一引擎能够评估不同的运营规范(operational specifications)。
为此,我们将每条规则表示为一个 `Constraint
type Constraint<T> = {
name: string;
severity: Severity;
mode: ConstraintMode;
evaluate(
evidence: T
): ConstraintStatus;
};
type OperationalSpec<T> = {
name: string;
constraints: Constraint<T>[];
};
接下来:
function evaluateConformance<T>(
spec: OperationalSpec<T>,
evidence: T
): ConstraintResult[] {
return spec.constraints.map(
(constraint) => ({
name: constraint.name,
status:
constraint.evaluate(evidence),
severity:
constraint.severity,
mode:
constraint.mode,
})
);
}
规范负责定义需要评估的内容。
评估器负责收集结果。它无需理解每个约束的含义,只需执行每个约束的评估函数,并保留生成的状态、严重性和模式。这种方式既保持了评估引擎的通用性,又让规范继续承担运营规则的定义职责。
在这个最小示例中,expected 和 observed 字段是可选的,但在生产系统中我会将它们填充完整,或者存储对底层证据的引用,以确保最终判定结果可解释且可审计。
仔细计算合规性摘要
单项结果很有用,但有时你也需要一个汇总。
type ConformanceSummary = {
total: number;
passed: number;
failed: number;
unknown: number;
conformant: boolean;
};
如下所示:
function summarize(
results: ConstraintResult[]
): ConformanceSummary {
const required =
results.filter(
(result) =>
result.mode === "REQUIRED"
);
const passed =
results.filter(
(result) =>
result.status === "PASS"
).length;
const failed =
results.filter(
(result) =>
result.status === "FAIL"
).length;
const unknown =
results.filter(
(result) =>
result.status === "UNKNOWN"
).length;
const conformant =
required.every(
(result) =>
result.status === "PASS"
);
return {
total: results.length,
passed,
failed,
unknown,
conformant,
};
}
现在,整体合规性要求所有必需约束都必须通过。
注意这对 UNKNOWN 意味着什么:由于 every() 要求必需约束的状态必须为 PASS,如果某个必需约束缺乏证据,整体结果会被标记为不合规,而不是静默通过。
建议性约束仍会出现在报告中。它们只是不会决定最终判定。
为什么单一分数可能具有误导性
创建一个单一分数很有吸引力:
conformance = 92%
但这可能具有误导性。
假设:
9 个低风险的约束通过
1 个关键的安全约束失败
一个简单的分数会显示:
90% 合规
在操作上,这可能是完全不可接受的。
如果你计算分数,应该保留:
约束级别的结果
严重程度
必需/建议性状态
未知证据
失败原因
分数可以汇总信息,但不应抹去证据。
保留判定背后的证据
一致性检查的结果不能只报一个 FAIL,还应该保留得出这个结论的依据。
{
"constraint": "minimum-replicas",
"expected": ">= 3",
"observed": 2,
"status": "FAIL",
"severity": "CRITICAL"
}
这样人们就能回答这些问题:
What did we expect?
What did we observe?
Which rule was applied?
Why did it fail?
当一致性判断会触发自动化恢复时,这一点尤其重要。
一致性应该可复现
假设某个操作在 10:00 执行。
10:01 时,错误率是:
0.6%
到了 14:00,仪表盘上显示的是:
2.1%
如果你用当前的数据去重新评估这个早已完成的操作,结果可能就不一样了。
因此,一致性评估应该基于执行窗口内的证据。我说的执行窗口,指的是操作运行及其直接效果被验证的那段时间。比如一次在 10:00 完成的部署,你应该用 10:00 到 10:05 之间采集的数据来评估健康状态,而不是几小时后仪表盘上碰巧显示的数值。
关键在于:把判定结果绑定到那次具体执行时可用的证据上。否则,系统后续的变化会追溯性地改变一个已经发生的操作的表面结论。
一条可复现的记录可能包含:
specification version
execution ID
evidence snapshot
evaluation rules
timestamp
例如:
{
"executionId": "deploy-812",
"specVersion": "3",
"evaluatedAt": "2026-10-06T10:01:00Z",
"evidence": {
"version": "v42",
"replicas": 3,
"errorRate": 0.006,
"p95LatencyMs": 290
}
}
这样,事后就能还原当时的判定过程。
多个执行器场景下的应用
假设两个执行器运行同一个规范。
执行器 A:
version PASS
replicas PASS
error-rate PASS
latency PASS
执行器 B:
version PASS
replicas FAIL
error-rate PASS
latency PASS
此时问题不再是:
哪个执行器更好?
而是要首先回答:
哪次执行满足了规范?
你可以对每个执行器产生的证据运行同一个合规性评估器来回答这个问题。规范和评估规则保持不变,只有证据会不同。
在上方的例子中,执行器 A 满足规范,因为其所有必需约束均评估为 PASS。执行器 B 则不满足,因为其副本约束评估为 FAIL。这为比较提供了共同基准,而不要求两个执行器使用相同的内部步骤。
随着时间推移,可以通过合规率、故障类别、延迟、成本、恢复频率和未知证据率来比较执行器性能。但合规性始终是契约层面的评估。
这在 AI Agent 中如何应用
对于 AI Agent,显式的合规性评估尤为重要。
针对同一目标,Agent 可能会选择不同的计划。
Agent 执行 1:
scale
deploy
observe
route
Agent 执行 2:
deploy canary
observe
expand
Agent 执行 3:
create parallel environment
switch traffic
由于 Agent 每次都可能选择不同计划,逐步比较执行过程并没有太大意义。某次运行可能采用金丝雀发布,另一次则创建并行环境,两者都可以是达成同一目标的有效方式。
保持稳定的是规范:目标、约束和证据要求不会因 Agent 选择不同路径而改变。
每次运行后,系统会收集关于结果状态的证据。合规性评估器随后将该证据与用于所有其他运行的同一规范进行对比。这使得 Agent 可以灵活调整执行计划,同时将成功的定义权保留在 Agent 之外。
Specification
↓
AI Agent
↓
Dynamic Plan
↓
Execution
↓
Evidence
↓
Conformance
Agent 可以提出操作建议,但它不应是判断这些操作是否达成预期结果的唯一权威。
一个小的端到端示例
首先定义证据:
type DeploymentEvidence = {
version?: string;
replicas?: number;
errorRate?: number;
p95LatencyMs?: number;
};
然后定义约束:
const constraints:
Constraint<DeploymentEvidence>[] = [
{
name: "version",
severity: "CRITICAL",
mode: "REQUIRED",
evaluate: (evidence) => {
if (!evidence.version) {
return "UNKNOWN";
}
return (
evidence.version === "v42"
)
? "PASS"
: "FAIL";
},
},
{
name: "replicas",
severity: "CRITICAL",
mode: "REQUIRED",
evaluate: (evidence) => {
if (
evidence.replicas === undefined
) {
return "UNKNOWN";
}
return (
evidence.replicas >= 3
)
? "PASS"
: "FAIL";
},
},
{
name: "error-rate",
severity: "CRITICAL",
mode: "REQUIRED",
evaluate: (evidence) => {
if (
evidence.errorRate === undefined
) {
return "UNKNOWN";
}
return (
evidence.errorRate <= 0.01
)
? "PASS"
: "FAIL";
},
},
{
name: "latency-target",
severity: "WARNING",
mode: "ADVISORY",
evaluate: (evidence) => {
if (
evidence.p95LatencyMs === undefined
) {
return "UNKNOWN";
}
return (
evidence.p95LatencyMs <= 350
)
? "PASS"
: "FAIL";
},
},
];
创建规范:
const deploymentSpec:
OperationalSpec<DeploymentEvidence> = {
name: "deploy-orders-v42",
constraints,
};
执行评估:
const evidence:
DeploymentEvidence = {
version: "v42",
replicas: 3,
errorRate: 0.004,
p95LatencyMs: 370,
};
const results =
evaluateConformance(
deploymentSpec,
evidence
);
const summary =
summarize(results);
可能的结果如下:
version PASS REQUIRED
replicas PASS REQUIRED
error-rate PASS REQUIRED
latency-target FAIL ADVISORY
最终状态:
conformant = true
所有必需约束均通过。
建议性的延迟目标未达标,因此该操作仍应发出警告。
实用的合规评估工作流
若要向现有系统引入合规评估,步骤如下:
选定一个操作。
定义规范。
把约束区分为必需或建议。
设定严重级别。
定义证据模型。
决定缺失证据如何变成
UNKNOWN。逐条评估约束。
生成汇总结果。
保留证据快照。
为规范做版本管理。
对产生意外失败或未知结果的规则进行复盘。
不要为了让结果通过就轻易放宽规范。
符合性不能证明什么
符合性只能告诉你:
观测到的证据满足规范。
它无法自动告诉你:
规范本身是正确的。
规范可能有错,证据也可能有错。
所以符合性依赖于:
specification quality
evidence quality
evaluation correctness
它是一种验证机制,不是绝对真理。
从执行成功到可验证的操作
传统自动化往往止步于此:
execute
↓
success / failure
基于符合性的模型增加了更多结构:
Specification
↓
Executor
↓
Execution
↓
Evidence
↓
Constraint Evaluation
↓
Conformance
现在你可以提出这些问题:
哪些要求满足了?
哪些失败了?
哪些无法评估?
结论背后有哪些证据支撑?
如果执行完成后系统立即符合规范,你就拥有了一个基线。
以这个基线为起点,你可以持续观察系统随时间的变化。
然后追问:
当操作已经成功,而现实开始偏离规范时,会发生什么?
这就是运维漂移。
结语
执行器成功还不够,流水线全绿不够,agent 计划完成也不够。
真正重要的是,最终得到的系统是否满足运维规范。
这需要三样东西:
Specification
Evidence
Evaluation
一个实用的符合性模型应当保留:
约束级结果
通过 / 不通过 / 未知
严重程度
强制规则与参考性规则
观测值
期望值
证据快照
规范版本
执行者负责执行工作,证据描述实际发生了什么,而规范定义本应发生什么。
接着,一致性评估器将两者进行比较。
这为传统自动化以及日益自主的系统奠定了更坚实的基础。
但某一时刻的一致性并非故事的终点。系统当前可能符合规范,但日后可能会产生漂移。
接下来的问题是:
我们如何检测到观测现实不再匹配操作意图的时刻?
这正是我接下来要探讨的方向。