一次实时验证运行了两分钟,最后只留下一行:timeout。
这行结果足以让验证门禁(gate)变红,却不足以支持下一轮开发。接手的智能体不知道系统走到了哪里:模型是否选择了工具,工具是否开始执行,外部请求是否返回,结果是否已经保存,还是应用只差最后一个终态事件。它只能重新翻阅日志,然后猜一个修改位置。
最容易出现的反应,是增加超时时间、补一次重试,或者修改一个看起来最可疑的组件。但这些改动也许与真正的问题无关。更糟的是,它们可能改变原本正确的产品行为,只为了让下一次测试更容易通过。
我后来意识到,这道门禁只完成了一半工作。它阻止了未经证明的结果继续发布,却没有为下一轮开发留下足够证据。
在大语言模型智能体(LLM agent)参与的开发循环中,后一半工作越来越重要。智能体可以快速阅读代码、修改实现和重新运行测试。它下一轮做得好不好,很大程度上取决于上一道门禁除了 PASS 或 FAIL,还说明了什么。
一个更完整的循环应该是:
实现
→ 门禁执行
→ 契约判决 + 过程观察
→ 确定最后可证阶段
→ 找到下一个调查边界
→ 下一轮窄修改
这套方法可以压缩成一句话:
Observe semantics; assert contracts; diagnose trajectories.
门禁不只是判定器
这套方法不是减少 hard gate。接口不允许出现的内容仍然必须被拒绝,要求完成的生命周期仍然必须到达终态,发布所用的源码、制品和校验和也不能因为系统具有不确定性而放松。
真正需要避免的是权威来源(authority)混乱。一个成熟的门禁至少要分清三种职责,但它们只是方法论底座,不是完整的实践清单。
| 职责 | 权威来源 | 结果 |
|---|---|---|
| 契约断言 | 拥有该语义的产品、协议、发布或测试契约 | 违反时 hard fail |
| 诊断观察 | 运行时事件和已有生命周期接口 | 默认只解释次数、顺序、耗时和阶段 |
| 判定充分性 | 验证器自身的证据契约 | 证据不足时不得给出确定根因 |
契约断言决定 GREEN 或 RED。诊断观察(observation)说明这次运行实际发生了什么。判定充分性则约束验证器:它不能只凭一个超时或意外计数,就把原因归给某个组件。
三者可以放进同一条证据流:
┌─────────────────────────┐
│ owning product contract │
└────────────┬────────────┘
│
▼
runtime events → observations → semantic assertions
│ │ │
│ │ └─ GREEN / RED
│ │
│ └─ counts / timings / ordering
│
└─ existing lifecycle/read surfaces
on RED:
observations
↓
oracle sufficient?
├─ no → root cause INCONCLUSIVE
└─ yes → classify narrow failure boundary
release:
only semantic GREEN evidence is authoritative
diagnostic RED evidence is retained but never promoted to release proof
这个图也说明了 observation 的位置。它不是较弱的 assertion,也不是越多越好的日志。原始事件需要先去重、关联和解释,才能成为下一轮智能体可以使用的语义阶段。发布系统可以保留 RED 的诊断依据,却只能把 semantic GREEN 当成正式发布证据。
协作系统需要生命周期,而不只是计数
设想一个父智能体可以创建子任务。代表性测试要求它创建一个目标子任务,取得结果,然后完成父任务。
为了让模拟脚本容易编写,测试可能预设一条理想轨迹:创建请求只能出现一次,父任务产生三次模型响应,子任务产生一次响应。真实运行如果出现两个创建请求和第四次父任务响应,门禁就报告数量不符。
这个结果对下一轮开发的帮助很小。两个创建请求只能证明模型提出了两次请求,不能证明运行时实际创建了两个子任务。第四次响应也可能是重复创建、合法的工具续写,或另一轮模型后续生成。只有继续观察生命周期,才能区分这些情况。
模型请求创建
< 运行时完成创建调用
< 子任务获得身份并开始运行
< 子任务进入终态
< 父任务取得结果
< 父任务完成目标
越靠后的证据,证明力越强。次数描述频率;身份关联和生命周期建立因果。
因此,协作门禁的硬断言应关注目标子任务是否实际创建、是否完成,以及父任务是否取得结果并完成目标。同时,它可以输出创建请求数、实际子任务数、等待次数、父子任务状态和相对时间。
这些观察会把下一轮修改导向不同边界。如果模型请求两次,但运行时只创建一个子任务,产品语义可能正确;若要减少成本,应检查提示或模型规划。如果创建请求已经出现,但运行时没有完成创建,应检查工具适配。如果子任务已经完成,父任务却没有继续,应检查等待结果或父任务续写。
即使本次语义验收为绿色,这些观察也有价值。额外请求和多余续写可以成为下一轮效率优化的依据,只是不能被伪装成当前产品正确性的失败。
并发顺序也是同一类问题。不同任务的请求可能合法交错。如果系统只保证每个任务内部的顺序,测试就不应要求所有请求按照一个全局脚本到达。门禁可以在内部使用任务身份做关联,对外只报告局部因果关系是否成立。这样,智能体看到的不是“第 4 个请求不符合预期”,而是“两个任务的局部顺序正确,测试匹配器发生重叠”。修复位置自然落在测试,而不是产品调度器。
超时应该留下阶段快照
再看开头的长耗时操作。一次完整执行可能经过这些阶段:
模型选择工具
→ 工具请求出现
→ 操作开始
→ 外部请求返回
→ 结果完成处理和保存
→ 模型根据工具结果继续生成
→ 应用发布最终终态
外层截止时间只证明要求的结果没有在验证窗口内得到证明。它不能单独证明外部服务卡住,也不能说明工具是否启动。
截止时间到达时,门禁应停止等待,读取已有状态,并保存一个保密且有界的阶段快照。例如:
semantic_acceptance: not_proven
last_proven_stage: operation_completed
next_unproven_stage: post_tool_continuation
root_cause: inconclusive
oracle_completeness: sufficient
这个结果仍然是红灯。但下一轮智能体已经知道操作本身完成了,应该先检查工具后的模型续写,而不是重写外部客户端。
相对时间也可以帮助判断:工具请求何时出现,操作何时开始和完成,父任务回复与最终终态是否出现。这些时间默认是诊断数据,不是产品不变量。它们的价值在于显示停滞发生在哪一段。
这里的命名会直接影响后续判断。无论外层窗口是 120 秒还是 180 秒,它首先是验证器的 timeout observation,是为了限制本次实验。除非拥有该行为的产品契约明确给出服务等级目标(SLA),否则不能把这个数字写成产品必须遵守的响应时间。
还要为诊断收尾留出时间。if: always() 只有在 runner 能继续执行后续步骤时才有作用;如果 job 自己先被强制终止,证据仍然无法上传。更安全的关系是:
scenario deadline
+ state snapshot / evidence flush / upload margin
< job timeout
同理,验证器可以 hard assert runner_turn_submission_count = 1,因为它自己控制提交次数;一次 Turn 内模型调用多少次后端、产生多少次续写,则属于产品执行轨迹。含义模糊的 operation_count 很容易把两者混为一谈。
如果门禁只能确认截止时间已经到达,就应诚实输出 oracle_completeness: insufficient。语义验收仍然失败,但现有证据不能支持更具体的根因分类。失败结果与根因结论必须分开。
这也意味着,诊断能力本身可以成为门禁的硬要求。观察的具体数值通常不决定产品成败,但一道关键门禁在失败后必须留下足以支持下一轮判断的阶段证据。完整诊断不能把红灯变成绿灯;它只阻止团队在证据不足时改错地方。
实践中需要检查哪些关注点
不是每个测试都需要阶段观察。编译错误、格式错误和直接的纯函数断言,通常已经能指出修改位置。观察更适合加入跨越多个所有权边界的关键门禁:模型决策、工具调用、并发任务、外部服务、长耗时操作和发布状态变更(mutation)。
真正实施时,我会检查下面这些关注点。
第一,在代码和证据格式中分开断言与观察。 最简单的做法,是分别维护 required_assertions 和 diagnostic_observations,或为每个场景编写显式的语义验证函数。前者决定成败;后者只检查类型、边界和保密性。不要让一个字典的精确相等检查同时承担产品契约、运行预算和轨迹计数。
第二,确保 RED 也会留下 evidence。 GitHub Actions 最容易出现的反模式是:场景成功后才上传证据,上传步骤没有 if: always()。真正需要诊断的 RED 运行反而没有构建产物(artifact)。更稳妥的顺序是:
run scenario
↓
helper writes diagnostic envelope on GREEN / RED
↓
upload diagnostics if: always()
↓
explicitly require semantic GREEN
↓
all semantic groups GREEN → build authoritative release evidence
诊断包(diagnostic envelope)应足够小,只记录场景、语义状态、判定充分性、失败类型、最后阶段和有界观察。同时,它需要用 schema_version、source_revision、validator_revision 和 artifact_digest 绑定被验证对象。否则,下一轮智能体可能用旧验证器产生的 observation 解释新实现。诊断包不是发布证明;只有全部语义断言为 GREEN,聚合器才生成可以进入发布制品的正式 evidence。
第三,只拆真正独立的语义组(semantic group)。 如果供应商投影、默认控制、智能体协作和媒体路径互不依赖,就让它们分别运行,并在最后统一聚合失败集合。在 CI 中,可以只对这些独立组使用 continue-on-error,再用最后一道 required aggregate gate 决定整体成败。最终聚合门禁不仅要检查已经产生的结果,还要确认每个预期组都提交了状态;缺少整个语义组同样应 hard fail,并标为 oracle insufficient。这样,一个组 RED 不会阻止其他组产生观察,缺失结果也不会被误读成没有失败。但不要走向“一项测试一个 job”的矩阵,也不要为了多收集结果而重复运行相同编译或检查。
第四,让 timeout 对应阶段,而不是只对应秒数。 外层 180 秒可以限制实验,却不能自动成为产品 SLA。长耗时请求至少应区分请求已发出、响应头已收到、首字节已收到、正文完成、结果已规范化和制品已保存。只有这些部分阶段观察在异常路径也能保存,下一轮智能体才知道应检查传输、解析还是后续处理。
第五,区分验证器控制的计数与产品内部轨迹。 runner_turn_submission_count 可以是硬断言,因为验证器控制它;后端调用次数、轮询次数和模型续写次数通常只是观察。相反,如果调用者明确请求返回 n 个结果,那么结果数量属于 API 语义契约,可以硬断言。关键不在于“计数能不能断言”,而在于谁拥有这个计数的语义。
第六,用身份和局部因果代替全局顺序。 内部标识可以用于去重和关联,但公开 evidence 只保留结构结果。并发场景应验证必要的前置关系,不应把偶然的事件到达顺序冻结成产品契约。
第七,把发布证据与 RED 诊断分开。 发布只能消费 semantic GREEN。RED 的诊断产物应保留,但不能在后续文档中被提升为发布通过的证明。发布动作发生后如果上传中断,可以用 if: always() 执行只读检查,记录标签、发布目标和已有制品;不要自动删除、重试或补传。部分 mutation 需要明确的人类恢复决策。
第八,保持观察非干扰且保密。 验证器不应为了得到完整数据而重试模型操作、重新提交 Turn,或在客户端取消后继续读取(drain)。缺失值应保持 unknown。提示词、回复、参数、凭据、原始流量和内部身份也不应进入公开 evidence。
最后,先组合已有的工具开始、工具完成、任务终态和状态读取接口,再考虑增加埋点。判断一个 observation 是否值得加入,可以问:它会不会改变下一轮智能体选择的调查边界?如果不会,它通常只是噪声。
这些规则都在限制实验和判决,而不是缩窄产品能力。可以限制任务范围、外层时间、证据保留和外部副作用;不能因为测试需要确定性,就擅自要求产品只能调用一次工具、只能产生三次响应,或只能使用一个并发任务。
把方法写进编程智能体 Skill
如果这套方法只存在于一次代码审查或一个测试文件中,后续智能体很容易重新回到“看到异常计数就修改产品”的旧路径。一个编程智能体技能(coding-agent Skill)可以把它变成可重复的开发习惯。
这个 Skill 不应定义新的产品契约。它的职责是连接契约、门禁证据和下一轮修改,并把前面的关注点变成一次可重复审计。对每个独立 semantic group,Skill 至少应产出下面这组关系:
semantic group
→ owning authority
→ hard assertions
→ diagnostic observations
→ RED evidence path
→ next investigation boundary
执行时,智能体需要依次确认:assertions 与 observations 是否在实现中分开;RED 时 diagnostic envelope 是否仍会生成和上传;timeout 属于 runner budget 还是产品 SLA;并发关系是否通过身份和生命周期证明;mutation 失败后是否有只读状态快照;最终 release 是否只消费 semantic GREEN。
Skill 的结果不应只有一份测试结论。它还应形成一个小型证据包:哪些契约已经满足,哪些仍未证明,最后可证阶段是什么,根因分类是否充分,以及下一步应调查哪里。对于 GitHub Actions,它还应检查 diagnostic upload 是否具有 if: always(),以及 aggregate 是否会把 RED diagnostics 错当成正式 evidence。
这不是让 Skill 代替工程判断。相反,它限制智能体只能在证据支持的最小边界内行动。如果一次超时没有阶段信息,下一步应先补门禁观察,而不是直接修改产品。如果一个 semantic group RED 使其他独立组没有运行,下一步应调整 gate topology,而不是盲目扩大测试矩阵。如果局部因果正确,只有全局顺序不符,下一步应修复测试匹配器。
在文档组织上,根级智能体指引只需提供触发词和路由。完整方法可以放进 Skill;具体断言仍由产品、协议和验证文档拥有。Skill 连接这些信息,但不成为产品语义的新来源。
门禁是开发循环的传感器
在 Harness Engineering:模型之外的工程 中,我把 harness 描述为模型行动的外部结构。门禁是这套结构中的判定器,也是传感器。
判定器只对明确契约做硬判决。传感器记录系统自然运行时的语义阶段、次数、时序和终态。两者共同组成适合 LLM agent 开发循环的反馈接口。
Observe semantics:让门禁看见系统完成了什么。
Assert contracts:只让明确契约决定红灯和绿灯。
Diagnose trajectories:让过程证据指导下一轮窄修改。
只会说“不”的门禁是一道关卡。能够说明系统走到哪里、哪些仍然未知,以及下一步该检查什么的门禁,才真正成为开发循环的一部分。