Bondsmith:让编码智能体的"测试通过"必须有确定性证明
My coding agent said "all tests pass." They were never run
开发者打造了工作流引擎 Bondsmith,用确定性 Gate 校验编码智能体声称的每一步结果,模型说"测试通过"不再算数,必须由非 LLM 工具跑出退出码等证据。它用 Rust 编写,含核心库、CLI 和只读 UI,Flow 为磁盘上的 JSON 与 Markdown 数据,目前仅支持 Claude Code 启动器,Pi 在路线图中。
“所有测试通过。”我在 agent 会话末尾读到这句话的次数多到数不清,而且不止一次它是假的。测试根本没跑,或者在最后一次编辑之前跑的,或者压根就没写。
Bondsmith 是我围绕一条规则构建的工作流引擎:没有与之绑定的证明,任何声明都不算数。我仍然希望模型做它擅长的事,也就是生成代码。但之后它关于这些代码告诉我的任何东西,我都会用某个不是 LLM 的东西来核实。

名字的由来
Brandon Sanderson 的 The Way of Kings,也就是 Stormlight Archive 的第一本书。Bondsmith 是 Knights Radiant 十个教团之一,也是最稀有的:从来不超过三个。其他教团战斗、治疗或旅行。Bondsmith 的力量是连接,把事物和人绑定在一起。强烈推荐这一本。
工作只有在进入条件成立时才被接纳,只有在退出条件被证明对真实证据成立时才被算作完成。LLM 在单个步骤内部是自由的。它之后关于该步骤所说的任何话都是软声明,而软声明需要硬绑定。让我展示给你看。
系统必须做什么#
六个要求决定了一个用于 agentic 开发的系统是否适合我。
1. 确定性证明。我不想仅凭 LLM 的话就相信某个动作发生了。对于我在意的每个动作,都有一个工具运行检查,并通过确定性动作写入结果。
2. 不绑定单一 harness。我不想被拴在一个 LLM 提供商上,或者拴在像 Claude Code 或 Codex 这样的单一 agentic 编码 harness 上。我希望能使用不同的。模型每隔几个月就变,定价和限制也随之变化,所以我不想每次切换时都重建整个工作流。
3. 通用工作流。不存在一种通用的方法、一个 SDD 定义或一种流程,能用于每一个实现、每一次功能交付和每一笔技术债。新功能需要规格说明并分解为任务。修 bug 两者都不需要。我应该能够定义自己的工作流,并在不触碰运行它们的引擎的情况下修改它们。
4. 一个 CLI。我想从终端运行不同动作的命令。我可以重复一条命令,可以把它放进脚本里,而且这是最先构建的最简单的东西。其他一切都可以构建在它之上。
5. 可观测性。我想看到任何东西和一切东西。我想看到 agent 日志。我想看到确定性门禁的效果。我想看到每个任务的定义。一切都在我的掌控之下。当一次运行出错时,我需要知道 agent 读了什么、改了什么,以及哪个检查阻止了它,而无需询问 agent。
6. 一个查看的地方。我想要一个 UI,把不同的功能呈现在一个地方,这样我就能轻松查看某个给定实现进行到哪一步了。当我有几个功能同时在几个仓库里运行时,我很快就会跟丢,而我不想打开五个终端来弄清楚我进行到哪了。
这六个对我来说并非同等重要。第一个是我构建 Bondsmith 的根本原因,另外五个则是为了让无论我做什么类型的工作、用哪个模型,它都能站得住脚。
Bondsmith 是如何构建的#
Bondsmith 是一个 Rust 工作区:一个核心库、一个轻量 CLI 和一个只读 UI。crate 布局见 Bondsmith 项目页面。这里我关心的是在一次运行中谁做什么。
Worker 是为恰好一个工作步骤而生成的一次性 LLM 会话,完成后即退出。Gate 是确定性检查,即测试或 lint 运行,其判定来自工具本身。Driver 是中间的状态机:它生成 Worker、运行 Gate,并且是唯一写入状态的东西。
每个仓库配置自己的一组 Gate,例如 lint、类型检查和测试套件。这组 Gate 就是该仓库的 gate pool,一个 Flow 可以要求一次性运行整个池。
图中没有从 Worker 指向状态的箭头。Worker 编辑文件并提交,这就是它能做的全部,没有人问它通过还是失败。
确定性证明#
Bondsmith 中的每一步都声明 Postconditions:对步骤运行后世界状态的断言。Driver 自己从磁盘、从 git 以及从 Gate 结果中收集事实,并用纯 Rust 代码评估每个 Postcondition。词汇表是封闭的。其中几个动词:
exit_zero。Gate 的命令以退出码 0 退出。sha_is_ancestor。一个提交在另一个提交的历史中。non_empty: spec.md。文件存在且有内容。gate_pool_pass。gate pool 中的每个 Gate 都通过了。
缺失的事实按失败处理:如果 Driver 找不到某个动词所需的证据,该 Postcondition 就失败。
一个 Postcondition 要么通过,要么硬性阻塞运行,没有中间状态。
这样,“测试已运行”就是 exit_zero: role:test,对照的是 Driver 自己运行的测试命令的退出码。
没有单一 harness#
工作流定义中没有任何东西知道是哪个 harness 运行它,或哪个模型。契约和提示词不包含任何特定于 harness 的文本,Worker 通过核心中的一个端口启动。Claude Code 或 Codex,或任何其他有无头模式的东西,都可以位于该端口之后。
目前那里只有一个启动器:Claude Code。Pi 是 Roadmap 上的下一个目标。
通用 Flow#
Bondsmith 中的工作流称为 Flow:一个命名的、有序的 Phase 链,每个 Phase 有自己的 Step 和 Postcondition。Flow 是纯数据,即目录中的 JSON 契约和 Markdown 提示词,在运行时从磁盘读取。没有任何东西被编译进二进制文件,也没有内置任何 Flow。我可以复制一个 Flow、去掉一个 Phase、添加一个 Gate,下一次运行就会使用它。

CLI#
每个动作都是一条命令:
init。创建一个 feature 并克隆其仓库。implement。运行 implement Phase。每个 Phase 都是以其自身名称命名的命令。status。打印运行所处的位置以及哪些 Postcondition 成立。resume。重新进入被中断的步骤。
还有其他命令。在 Phase 之间它会等待我。

可观测性#
每次运行都会在磁盘上留下三样东西。一个状态文件,它是唯一事实来源。一个仅追加的事件日志,每个事件一行 JSON:步骤开始、Gate 完成、Postcondition 已检查、步骤推进或保持。并且对于每个无头 Worker,都有一个包含其原始输出的 Session 日志。
对于无头 Worker,原始日志是 JSON 流,因此 Bondsmith 还会从中派生出一个 Session 记录:Worker 读取的每个文件、每次编辑、每条命令,按顺序排列,并带有模型、token 和持续时间。

一个查看的地方#
磁盘上的记录是完整的。Bondsmith UI 是一个本地、只读的页面,读取的是同一批文件。它的第一个标签页列出了我所有项目中的每个功能及其当前状态。

打开一个功能,你就能看到实现进行到哪一步了:它处于哪个 Phase、Task 图,以及每个 Task 的每一步的进度。打开一个步骤,你就进入了那个 Worker 的 Session 记录。
流程#
我使用两个 Flow。feature 当然是主要的那个:它把一个功能从访谈带到一组已合并的 pull request。address-issue 把一个 GitHub issue 带到一个 pull request。第三个,file-fix,是 address-issue 前面加了一个诊断 Phase。
Phase 不同是因为工作不同。一个功能需要探索、规格说明和一张 Task 图。一个 issue 已经说明了哪里出了问题,所以一个计划就够了,我们可以直接进入编码。我在 Bounded Chaos 中描述过功能流程的各个 Phase,那时它还只是一堆 bash 和 slash 命令。这些 Phase 和那时基本一样。区别在于,现在每一个都是一个类型化的契约,由引擎强制执行。
两个 Flow 都保留了一个 Phase:测试优先的 implement。在功能 Flow 中,它为每个 Task 运行九个 Step。
下面是一次真实的运行,其中第二个测试 Gate 的一个 Postcondition 没有成立:

从下往上逐项检查:
gate_failed_at: red_test_sha,role:test found true。Driver 检出了 Worker 声称写了一个失败测试的那个 commit,在那里运行了测试,测试失败了。sha_is_ancestor: red_test_sha,HEAD found true。那个 commit 在当前代码的历史中。它不是事后写的然后混进来的。gt: test_count,baseline found 1711,1708。这个 Task 之前测试套件有 1708 个测试,现在有 1711 个。测试被添加了,而且我知道添加了多少。sha_recorded found true。那个红色 commit 确实存在。exit_zero: role:test required 0 found 1。而且现在仍然有一个测试失败。
所以五项检查中有四项成立,但仍然不够。这个步骤被卡住,直到失败的测试通过。
它从错误中学习#
审查 Step 会产生 Findings:每个一个问题,带有类型、严重程度、文件和提议的修复。每个审查 agent 把它的 Findings 写进一份报告,每个 Task 一份。手动裁决它们时,我一次又一次地做出同样的裁决。所以在功能 Flow 中,系统从它的错误中学习,也从我的决定中学习。
第一个机制是 Triage rules。每个 repo 携带自己的规则,有序,先匹配者胜。这是一条真实的规则:
{
"id": "auto-accept-low-style",
"when": { "severity": ["low"], "type": ["style"] },
"then": "accept",
"reason": "Low-severity style findings (naming/query-key drift, minor duplication, re-rolled boilerplate) were consistently accepted and fixed by human review across the todo-lists feature."
}一个被接受的 Finding 会被修复,无需先问我。一个 Worker 把相关的 Findings 分组为 Review units 并应用规则。Driver 也不信任 Worker 的 triage:它在代码中从规则重新推导每一个声称的决定,不匹配就硬性阻断。还有一条任何规则都不能越过的底线。规则永远不能裁决一个安全 Finding,或任何高于中等严重程度的东西。那些总是交给我。
第二个机制是 learn-from-reports Phase。报告是审查 Step 在 implement 期间写下的那些。这个 Phase 挖掘它们的 Findings 和我对它们的决定,寻找模式,并把每个候选摆在我面前,让我接受、拒绝或编辑。一个我反复做出的决定会被提议为一条新的 Triage rule,以针对该 repo 的 pull request 形式。一个足够通用、可复用的教训会被写成一条规则,写入 Knowledge base。
把这些规则反馈回去的机制已经完全就位。我现在正在研究的是如何在不同的实现和审查 Phase 中使用它,这样下一个 Task 开始时规则就已经到位了。
还有很多要讲#
还有很多、很多、很多内容要讲。系统从代码库中学习,但系统也需要进化,而我为自己构建的可观测性就是最好的来源。每一次 Worker 运行都已经留下了 Session 日志及其成本。分析它们能告诉我为什么某一步出了问题,以及在哪里进行模型路由能让整个 Flow 具有成本效益。把它们全部读一遍,应该就能告诉我该在 Bondsmith 中改变什么。
那么这就是接下来的路线图。
给你一点#
如果你从这篇文章中只带走一件事,你不需要 Bondsmith 就能做到:选一个你的 agent 会汇报的操作,把汇报替换成一个由程序运行的检查。
“所有测试通过”变成测试命令的退出码,由脚本在会话结束后运行。“我提交了修复”变成分支上的一个 git log。“报告已写好”变成一个存在且非空的文件。
从那个让你最受伤的说法开始。对我来说是测试。一个检查就足以让你感受到不同,因为 agent 可以想说什么就说什么,而你不必再相信它。
Bondsmith 是同样的习惯,只是应用到每一步。从一个检查开始,自己看看吧。
来源:Hacker News · coldtake.dev