测试通过、CI 绿色、线上正常,是三件不同的事

从本地测试到线上回读,每一层证据都必须绑定明确的结论范围,不能互相替代。

本页目录 · 18 节

工程任务里的「完成」不是一种感觉,而是一项必须由证据支撑的声明。Agent 最容易犯的错误并不是完全不验证,而是拿到了某一层的绿色结果,随后把结论扩张到了它没有观察过的层:本地测试通过,就说功能完成;CI 绿色,就说已经上线;部署任务结束,就说用户已经能正常使用。

成熟的 Verification & Readback 机制要解决两个问题:当前证据到底支持什么结论,以及下一层还缺什么证据。

先定义一条证据的最小结构

一条可用的工程证据至少包含五个字段:

Evidence = {
  claim,       // 准备证明的命题
  scope,       // 仓库、分支、服务、环境、用户路径或数据范围
  time,        // 观察发生的时间
  source,      // 命令、Provider API、流水线、日志或真实入口
  freshness    // 该观察是否晚于本次修改、发布或状态变化
}

例如「测试通过」不是完整证据。更准确的写法是:「在 commit A 的本地工作树中,于时间 T 运行 unit:test,覆盖目标模块的相关断言,退出码为 0。」它仍然不能证明生产正常,但它已经把结论边界说清楚了。

freshness 尤其重要。昨天的成功流水线、修改前的截图、上一次发布后的健康检查,即使来源真实,也不能证明这次变更。证据必须与本次目标通过 commit SHA、构建 ID、artifact digest、deployment ID 或发生时间建立关联。

证据阶梯:每一层只证明自己

层级 直接观察 可以支持的结论 不能替代
1. Local test 指定工作树中的测试、类型检查、构建结果 当前代码在当前环境满足这些断言 完整改动范围、远端状态、部署状态
2. Diff 工作树、暂存区或两个 commit 之间的差异 实际改了哪些文件和内容 行为正确、提交成功
3. Commit HEAD、commit tree 与目标文件回读 本地历史已经包含目标修改 远端已经收到该 commit
4. Remote Provider 上分支 ref 的 SHA 目标远端分支指向预期 commit PR 已创建或合并、CI 已通过
5. PR / Merge PR 状态、base/head、merge commit 评审对象和合并结果符合预期 构建成功、部署成功
6. CI workflow、job、check suite 对特定 SHA 的结果 自动化检查对该 revision 通过 artifact 已发布、运行环境已切换
7. Deploy deployment、release 与 artifact/commit 的绑定 指定产物已被部署到指定环境 流量已经使用它、业务行为正确
8. Runtime readback 真实入口、版本端点、指标、日志或业务查询 指定环境在观察时呈现预期状态 长期稳定性、未观测路径同样正确

这不是一条「越往下越高级」的排行榜,而是一条因果链。上一层回答「输入是什么」,下一层回答「这个输入是否真的抵达了新的边界」。Git 官方文档也把 git diff 定义为端点之间的差异展示;它是范围证据,不是正确性证明。

因此,证据之间不能做以下替换:

  • 测试结果不能代替 diff,因为测试可能没覆盖意外修改;
  • commit hash 不能代替远端回读,因为 push 可能失败或推错 remote;
  • PR 页面不能代替 merge readback,因为 PR 可能仍是 open;
  • CI 绿色不能代替 deployment,因为构建与发布是不同事件;
  • deployment success 不能代替 runtime readback,因为发布可能没有切流、读取了旧缓存,或只更新了部分实例。

风险比例验证:不是所有修改都跑同一套清单

验证强度应由「影响面、未知量、不可逆性」共同决定,而不是由文件数量决定。一个很小的权限改动可能比大段纯展示代码风险更高。

变更类型 最小验证面 需要补充的风险验证
文档与静态内容 链接、格式、构建、diff 导航可达性与真实页面渲染
纯函数或局部逻辑 聚焦单测、类型检查、diff 边界值、异常输入、回归用例
跨模块契约 两端测试、构建、schema/diff 向前兼容、向后兼容、旧数据
异步或事件链路 生产者与消费者测试 重试、重复、乱序、幂等、死信
数据迁移或权限变更 dry-run、精确范围、计划 回滚、最小权限、分批、结果回读
发布或外部写入 revision 与 artifact 绑定 环境保护、部署回执、运行时回读

高风险验证的重点不是「多跑几个命令」,而是增加相互独立的观察面:happy path 证明主路径,boundary case 证明边界,negative case 证明拒绝错误输入,retry/idempotency 证明重复执行不会放大副作用,compatibility 证明新旧版本交错时仍能工作。

如果某一类检查无法运行,也不能用另一类检查补位。例如集成环境不可用时,单元测试仍然有价值,但最终结论只能是「本地层已验证,集成层未验证」,不能改写成「已有充分覆盖」。

Provider 操作必须形成闭环

Git 平台、云平台、部署系统、工单系统和第三方 API 都有一个共同问题:命令成功只说明客户端收到了一个结果,不一定说明目标系统已经达到预期状态。对这些外部 Provider,验证采用四段式协议:

Preview -> Execute -> Readback -> Reconcile

1. Preview:固定即将发生的事实

预览至少要确定目标 Provider、账户或组织、仓库/服务、分支或环境、动作、关键参数、前置状态、预期结果和可恢复方式。对于有副作用的动作,还要生成可比较的 payload 摘要。预览不是执行证明,但它让后续读回有了明确的 expected state。

2. Execute:只记录 Provider 真正接受的请求

执行结果应绑定请求 ID、目标、时间、revision 或 payload digest。HTTP 200、CLI 退出码 0 只能证明调用阶段没有显式失败;异步 Provider 返回 queued、pending 或 operation ID 时,状态仍是「已受理」,不是「已完成」。

3. Readback:从权威边界重新读取

读回要绕开本地推测,从负责持久化状态的 Provider 获取结果:push 后读远端 ref,PR 创建后读 PR 的 base/head/state,部署后读 deployment 对应的 artifact 和 environment。涉及用户可见行为时,还要从真实入口观察版本和业务响应,不能只读部署控制台。

4. Reconcile:比较 expected 与 observed

最终状态只有三种:

  • verified:目标、版本、范围和预期结果全部一致;
  • mismatch:Provider 有结果,但与预期不一致;
  • unknown:请求超时、读回不可用或证据无法绑定本次操作。

unknown 不是成功的委婉说法。它意味着停止扩大结论,保留 operation ID,再通过幂等查询或人工检查继续对账。重试写操作之前先读回,可以避免「第一次其实成功了,第二次又执行一遍」造成重复发布、重复消息或重复扣费。

一次部署如何串起整条证据链

假设目标是把 commit A 发布到 production,完整链路应能回答:

  1. 本地 diff 和测试是否基于 A 对应的内容;
  2. 远端目标分支是否真的指向 A
  3. PR 最终合并成哪个 revision M
  4. CI 是否针对 M 构建,并产生 artifact digest D
  5. deployment 是否把 D 发布到了 production,而不是 staging;
  6. 运行时版本是否回报 DM,关键业务路径是否呈现预期行为。

GitHub 的部署文档把 workflow、environment、protection rules、deployment history 分成不同控制面,正说明「流水线 job 结束」与「目标环境已接收发布」不是同一个事实。SLSA 的 artifact verification进一步强调:消费者要把 provenance 与自己对包和来源的期望进行比较。这里借用的是「产物身份必须与预期绑定」这一原则,并不表示项目仅凭记录一个 SHA 就已经符合 SLSA。

最常见的失真方式

证据陈旧

测试或页面确实成功过,但发生在本次修改之前。修复方式是把证据绑定到 revision,并记录观察时间。

目标读错

命令查看了本地 main,结论却写成 origin/main;读了 staging,结论却写成 production。修复方式是在每条证据中显式写 target,而不是依赖当前上下文。

缓存制造假阳性

CDN、浏览器、服务缓存或最终一致性存储可能返回旧值。需要版本端点、cache-busting、权威存储读回或等待窗口,并明确说明一致性模型。

部分成功被写成全部成功

多区域、多实例或批量操作只成功了一部分。Provider 的整体状态、失败项和覆盖比例都要读回,不能只抽取第一条绿色结果。

异步状态被提前终结

queuedacceptedin_progress 都是中间状态。要轮询到终态,或诚实报告「已触发,尚未确认完成」。

失败后重试缺少幂等性

超时不代表请求没生效。没有 idempotency key 或前置读回的重试,可能把未知状态变成重复副作用。

诚实报告本身也是验证协议

最终汇报应该使用与证据层级一致的句子:

  • 「已修改,尚未运行验证」;
  • 「本地测试与 diff 已检查,未提交」;
  • 「已提交到本地 commit A,尚未 push」;
  • 「远端分支已回读为 A,PR 状态未核实」;
  • 「CI 对 revision M 为绿色,未发现部署证据」;
  • 「deployment 显示成功,但真实入口尚未回读」;
  • 「production 已通过版本与业务路径回读,观察时间为 T」。

当权限、网络或环境限制让某一层不可验证时,报告 unknown、阻塞原因和已有证据。不要用「应该」「看起来」「大概率」把未观测事实包装成完成。

实施检查表

每次准备写下「完成」之前,逐项检查:

  1. 我的 claim 是什么,scope 和 target 是否明确?
  2. 证据来自哪个权威 source,发生在什么 time?
  3. 它是否晚于本次变更,能否绑定 revision、artifact 或 operation?
  4. 这条证据只支持哪一层,有没有越层推断?
  5. 外部操作是否完成了 Preview、Execute、Readback、Reconcile?
  6. 未验证、失败和未知状态是否被明确写出?

这套机制的核心不是制造更多报告,而是让每一个结论都能追溯到恰当的观察边界。OpenAI 的 Harness Engineering 实践也把快速反馈、机械化约束和仓库内的可验证知识视为提高 Agent 可靠性的基础。可靠交付不是「Agent 说它做完了」,而是人和系统都能指出:哪个版本,在什么范围,被哪一个权威来源,于什么时间证明到了哪一步。

相关文章

在做类似的事情?

很乐意就分布式系统、交付流程和应用 AI 交换意见。

[email protected]