测试通过、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,完整链路应能回答:
- 本地 diff 和测试是否基于
A对应的内容; - 远端目标分支是否真的指向
A; - PR 最终合并成哪个 revision
M; - CI 是否针对
M构建,并产生 artifact digestD; - deployment 是否把
D发布到了production,而不是 staging; - 运行时版本是否回报
D或M,关键业务路径是否呈现预期行为。
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 的整体状态、失败项和覆盖比例都要读回,不能只抽取第一条绿色结果。
异步状态被提前终结
queued、accepted、in_progress 都是中间状态。要轮询到终态,或诚实报告「已触发,尚未确认完成」。
失败后重试缺少幂等性
超时不代表请求没生效。没有 idempotency key 或前置读回的重试,可能把未知状态变成重复副作用。
诚实报告本身也是验证协议
最终汇报应该使用与证据层级一致的句子:
- 「已修改,尚未运行验证」;
- 「本地测试与 diff 已检查,未提交」;
- 「已提交到本地 commit
A,尚未 push」; - 「远端分支已回读为
A,PR 状态未核实」; - 「CI 对 revision
M为绿色,未发现部署证据」; - 「deployment 显示成功,但真实入口尚未回读」;
- 「production 已通过版本与业务路径回读,观察时间为
T」。
当权限、网络或环境限制让某一层不可验证时,报告 unknown、阻塞原因和已有证据。不要用「应该」「看起来」「大概率」把未观测事实包装成完成。
实施检查表
每次准备写下「完成」之前,逐项检查:
- 我的 claim 是什么,scope 和 target 是否明确?
- 证据来自哪个权威 source,发生在什么 time?
- 它是否晚于本次变更,能否绑定 revision、artifact 或 operation?
- 这条证据只支持哪一层,有没有越层推断?
- 外部操作是否完成了 Preview、Execute、Readback、Reconcile?
- 未验证、失败和未知状态是否被明确写出?
这套机制的核心不是制造更多报告,而是让每一个结论都能追溯到恰当的观察边界。OpenAI 的 Harness Engineering 实践也把快速反馈、机械化约束和仓库内的可验证知识视为提高 Agent 可靠性的基础。可靠交付不是「Agent 说它做完了」,而是人和系统都能指出:哪个版本,在什么范围,被哪一个权威来源,于什么时间证明到了哪一步。
相关文章
从 Workflow Harness 到轻量 Prompt:Codex 5.6 之后我删掉了什么
当模型的原生流程已经够强,Harness 会从效率放大器变成上下文负担。这次重写保留了安全边界,同时大幅精简 Prompt 和运行时。
补丁提升:为一次代码修改自建的一套运输协议
PatchBundleV1 将隔离环境中的候选文件状态封装为可验证工件,再由可信 Host 按日志步骤写入、回读并出具不可变回执。