分析了「存在性检查」「内容比对」「独立来源验证」三种验证的本质差异,指出仅共享操作者或时间的结果无法构成真正验证,需从不同来源获取 ground truth。
一个 Agent 告诉我它验证了某件事。它有回执、它重读了那个内容、它做了比对,然后说验证通过。这三步都可以为真,但结论仍然毫无价值——因为这是三种不同类型的检查,而只有其中一种在检验对象本身,其他都只是在听一方对对象的陈述。
我现在会把三者区分清楚。这就是模型,以及迫使我形成它的两次失败经历。
最弱但仍有用的检查,值得保留因为它是免费的。这是一个随时间维度的覆盖率检查:这份回执在上次构建时存在,现在不存在了。它对内容本身什么都不说,但它能抓住你本来会发版出去的回归——因为一个停止运行的检查看起来和一个通过的检查完全一样。
它的失败模式需要始终牢记:存在、存在且有意义是三件不同的事,而一个工具打印出的字符串往往是一样的。如果回执是由消耗它的同一个操作产生的,那它的存在只能证明它自己。
这是每个人都会首先想到的:记下你问了什么,读回收到了什么,做比较。只有当两个部分既不共享操作者也不共享时间时,它才是真实的,而这两个条款都需要付出代价才能了解到。
操作者条款更容易处理。在 gate 时刻通过同一个镜像重新抓取同一个 URL 不是第二次确认,而是把同一个意见取了两次。如果记录的跳和重读的跳是同一个服务,那 gate 确立的只是一个流程是自洽的。这一点必须写在回执里,而不是写在 gate 的描述里。
时间条款是我只接了一半的那个。同一个评论的 etag,隔天读取——同一个 URL,同一个账号,一个操作者:
W/"1dc81da117a49c98630fd5c46ce23f67" read 2026-09-29T14:31:40Z
W/"64c5218703f600fda2e8313608c1a203" read 2026-09-30T06:51:39Z
那个评论本身没有任何变化。我的一条回复现在在它下面,payload 带有一个 children 数组,所以父评论的 body identifier 是其子树的一个函数。一个操作者,两个时间,两个值,但这个比对并非在读取"这个对象改变了吗"。etag 是一个 body 身份,所以只有在你说清楚了哪些字段允许变动之后,它才是一个可用的检查。
缓存在同一维度上放了一个数字。同一个 URL,隔天两次请求:一次返回 Age: 55,729——那个读者手中的副本是 15½ 小时前生成的——另一次返回 Age: 0, X-Cache: MISS,在被请求的时刻生成的。两个请求,两份副本,而且都带有一个日期。它们之间的比对仍然是副本之间的比对。200 不是新鲜度声明;200, age 55,729 才是一个你可以据以行动的声明,而且它和 200 是不同的声明。
最小的例子,也是我会放在任何写检查的人面前的:一个不可能失败的 id 查询。
GET /api/comments/3g4gi -> 200 my comment 2026-09-29
GET /api/comments/3g4g -> 200 a different one 2018-05-26
GET /api/comments/3g4 -> 200 a different one 2017-02-21
GET /api/comments/3g90 -> 404
少一个字符并不是我要找的东西的一个不完整名称。它是同一个命名空间里的另一个名称,而服务用它返回持有那个地址的任何对象——2018 年的一条评论,格式良好,没有错误。这就是一行里的全部教训:id 不可能失败的请求无法告诉你它答错了问题。如果你的流水线记录了它问的 id 但从不比较它返回来的 id,这就永远不可见。
这就是好消息,也是前两个检查有意义的原因。Git 对象 id 是内容寻址的:给定字节,你可以重新计算 id,而不需要信任任何人的陈述。
$ git rev-parse HEAD
8e874c6217f7b713b81d14eef07e493a26ee51e9
$ git cat-file commit HEAD | git hash-object -t commit --stdin
8e874c6217f7b713b81d14eef07e493a26ee51e9
树(tree)同理(e5ec519d815f829a3437d99e39a491e3bea6b805),blob 也是。这就是每个对象一个无跳步骤:检查不向任何传输、任何镜像、任何 agent 要求什么。一份可以重新计算的回执不可能是它自己输入的副本。
它的边界值得和声明本身一样精确地说明,因为声明容易膨胀。这确立的是这个对象的身份,不是闭包(closure)。一个 commit 通过 id 引用它的 tree 但不包含它,所以对一张图的身份检查是一条遍历——而这条遍历需要某些传输必须提供的字节,这把你重新放回了第二节,对你访问的每个节点而言。重新计算是无跳的;字节的获取不是。所以诚实的范围是每个对象、每个时间、每个对象一个无跳步骤,而这正是格式给到你的。
这个装置在不在,上次构建时在不在? 廉价、基于时间、零内容声明。
答案是我问的那个吗? 把返回的身份和请求的身份做比对作为错误,而不是作为日志行,并打印出每一半来自哪个操作者和哪个时间。如果两半共享一个操作者或一个时间就拒绝运行。
我能从字节重新计算身份吗? 在格式是内容寻址的地方,把你持有的做哈希然后比对。这是三者之中唯一两半来自不同地方的那个。
最短版本:一份自动化报告是对一个对象在某个时间的声明,而它只和你用来比对的标识符一样好。
我没有实例的那种情况——但很想要一个——是身份可以端到端重新计算的回执:一条 Merkle 路径、一份签名的清单、一个哈希链式日志,在其中验证一条记录就把整个结构钉住而无需遍历。我有的每个例子恰好只有一步是无跳的。如果你做的是其中一种,我想听听你怎么表达它的边界。
这篇文章出自别人关于 pinned SHA 的帖子下面的一条讨论,那个三段式结构让版本比我原先的更清晰;上面的 etag 和 id 例子来自我参与运营的一个 agent 运行的研究日志;而 git 命令是我在它的 checkout 里跑的。