2026 年 10 月初,哈佛的理论物理学家 Matthew Schwartz 开源了 BootLoops 1.0。他给它的定位是一副“挽具”(harness):一套让大模型去做精密定量科学的工具和规程。按项目的说法,这套东西在三个月里参与了 36 篇稿件,覆盖从粒子物理到生态学的 18 个领域1。

真正让我想仔细读它的,是 Schwartz 在介绍文章里写的几段经历1。大模型报告“做完了,只有一个星号”,结果往往是根本没做完:一个证明“只差一个未证明的引理”,而那个引理就是整个证明;他禁止了未证引理,模型又报告做完了,这次多了“一条新公理”。工作了三天,模型写出“经过两年攻坚,公式终于解出”。项目官网把这类失败叫作“satisfiction”,意思是模型太想让人满意2。

在定量科学里,这种讨好最后会落到一句很具体的话上。比如 agent 告诉你:

这个积分等于 14(7ζ(3)−3πlog⁡2+G)\frac{1}{4}\left(7\zeta(3) - 3\pi\log 2 + G\right),和数值结果吻合 20 位,完成。

我们该不该信?这句话里至少藏着四个问题:

  1. 那个“数值结果”本身算对了吗?
  2. 就算数值是对的,“吻合 20 位”能说明闭式找对了吗?
  3. 什么样的检查才算数?执行检查的那段代码,会不会自己坏掉?
  4. 这些要求,怎样交给一个会遗忘、会讨好的 agent,让它每次都照做?

BootLoops 有 50 个工具包和 12 份技能文件,按目录介绍会很长,也看不出它为什么长成这样。本文换个顺序,顺着这四个问题往下走。我们会看到,它的每一层都是为了堵住其中一个漏洞才出现的。文中的输出都是我在本地实际跑出来的,脚本放在文末。


一、项目的来历与组成

先简单交代 BootLoops 是什么。

它起源于散射振幅的计算。粒子对撞的预言要算一类叫“圈积分”(Feynman 积分)的多维积分,答案常常是多重对数、椭圆函数这类特殊函数。名字里的 “Boot” 来自 S 矩阵 bootstrap,也就是从一般原理反推散射概率的思路。Schwartz 最初想做的,是把散落在各篇论文、各个公开和私有软件包里的方法移植到一个框架里,再让大模型为每个问题挑合适的工具2。

为什么从振幅开始?因为振幅可以用数字检查。一个积分可以用 AMFlow 这类数值方法算到很高精度,再和别的工具推出来的解析式比较;两边吻合 30 位,就很难是巧合了。项目据此定了一条 “BootLoops 标准”:一张图算“解决”,指它的完整函数形式已知,并且有一个 Python 脚本能在笔记本上把它算到任意精度。官网说得很直接,这条标准是被 AI 作弊逼出来的2。

开源出来的东西分两半,放在两个仓库里:

  • 工具箱 bootloops:tools/ 下 50 个 Python 包(少数带 Julia),从积分约化、奇点分析、微分方程输运,到球算术、整数关系搜索、贝叶斯证据。还有三个自研引擎和几个打过补丁的外部引擎 fork。
  • 技能 skills:12 份纯 Markdown 的规程,用 Agent Skills 格式写成,Claude Code、Codex、Cursor 都能读。它们规定的是“什么叫做完”。

使用方式也很朴素:在仓库里启动你的 agent,然后对它说3:

Read tools/README.md and the tool guides it points to, then propose a plan for this problem for me to approve.

代码是 Claude 在 Schwartz 指导下写的,仓库也声明它不是 Anthropic 的官方项目,由 Schwartz 本人维护3。

这里的 “harness” 容易引起误会。它和 Claude Code、Codex 这类常被称为 agent harness 的东西不是一回事,两者的关系和区别放在第七节讲,那时我们已经看过 BootLoops 的每一层。

了解了它是什么,回到开头那句话。第一个问题:那个“数值结果”本身靠得住吗?


二、数值本身的可信度

现象:三行代码,算错到 100

看一个经典例子,J.-M. Muller 构造的递推4:

xn+1=111−1130xn+3000xn xn−1x_{n+1} = 111 - \frac{1130}{x_n} + \frac{3000}{x_n\,x_{n-1}}

初值取 x0=2x_0 = 2、x1=−4x_1 = -4 时,这个序列的真实极限是 6。用 Python 的浮点数直接算:

float x10 = 6.274438662728116
float x20 = 99.8985692661829
float x30 = 99.99999999999993
float x100 = 100.0

前十步还像样,第二十步已经跑到 100 附近,最后稳稳停在 100.0。没有警告,没有 NaN,结果看起来还挺“收敛”。

原因:舍入误差唤醒了一个被压住的解

为什么会这样?这个递推的通解可以写成

xn=α⋅100n+1+β⋅6n+1+γ⋅5n+1α⋅100n+β⋅6n+γ⋅5nx_n = \frac{\alpha\cdot 100^{n+1} + \beta\cdot 6^{n+1} + \gamma\cdot 5^{n+1}}{\alpha\cdot 100^{n} + \beta\cdot 6^{n} + \gamma\cdot 5^{n}}

初值 2,−42, -4 恰好让 α=0\alpha = 0,所以极限是 6。但只要第一步有一点舍入误差,α\alpha 就不再严格为零;而 100n100^n 长得比 6n6^n 快得多,这点误差每一步被放大十几倍,几十步之后就把真解完全盖住了。

第一次尝试:加精度

最直觉的办法是把精度调高。我用 mpmath 换了几个十进制位数来算(这里用的是同一递推的另一组初值 11/2,61/1111/2, 61/11,真值同样趋向 6):

mpmath dps 50 -> 100.0
mpmath dps 100 -> 100.00000000000000001
mpmath dps 118 -> 78.936364783809585216
mpmath dps 120 -> -9.1516256868031901637
mpmath dps 150 -> 5.9999999879253266734

精度确实有用,150 位时终于对了。但问题在于:每一个错误答案都长得像正常数字,算的人并不知道该信哪一行。118 位给出 78.9,120 位给出 −9.15,没有任何一行告诉你“我错了”。把精度一路往上加,只是在赌下一次刚好够。

解决:让每个数自带误差半径

BootLoops 的回答是球算术(ball arithmetic)。每个数不再是一个点,而是“中心值 ± 半径”的一个球,每一步运算都严格地把舍入误差加进半径里。半径是被证明过的上界:真值一定在球里。tools/baller 在 FLINT/Arb 之上包了一层前门,用起来是这样的:

Python
import baller
f = lambda xp, x: 111 - 1130/x + 3000/(x*xp)
r = baller.run(f, x0=(2, -4), n=100, dps=50) # 固定 50 位
s = baller.solve(f, 16, x0=(2, -4), n=100) # 要 16 位可信数字,精度不够就自动加

输出:

run50 x10 6.2744385982163279138293784620714581132
run50 x20 6.036031881081856780011
run50 x30 6.0056
run50 x40 UNCERTIFIED[~6.0010875 +/- inf]
run50 x100 UNCERTIFIED[~100.00000 +/- inf]
SolveResult(6.0000000160995648890808742457068155406... +/- 1.87e-79, certified=79, dps=240)

同样是 50 位精度,球算术的表现完全不同。它打印的位数越来越少(x20 有 22 位,x30 只剩 5 位),因为半径在长;第 35 步半径变成无穷,之后的值一律标成 UNCERTIFIED。中心值其实也走到了 100,和浮点数一样,但它被明确标记为不可信,不会被当成答案打印出来。render 只打印被证明的数字,这是 Baller 刻意设计的“失败即关闭”。solve 则在半径爆掉时把精度翻倍重跑,一直到 240 位,拿到 79 位被证明的数字。

这一层解决的是“数值会悄悄算错”。但球算术有一个根本限制,官网也写得很清楚2:两个球不相交,可以证明两个数不相等;两个球重叠,却永远证明不了它们相等。而开头那句“这个积分等于 14(7ζ(3)−3πlog⁡2+G)\frac{1}{4}(7\zeta(3) - 3\pi\log 2 + G)”,恰恰是一个相等的断言。

数值可信了,接下来的问题是:从数值到闭式这一步,能不能被凑出来?


三、从数值到闭式

现象:随机数也能被“认出来”

从一串高精度数字反推闭式,标准工具是整数关系算法 PSLQ:给它目标值 xx 和一组候选常数 c1,…,ckc_1, \dots, c_k,它去找一组不全为零的整数 a0,…,aka_0, \dots, a_k,使得 a0x+a1c1+⋯+akck≈0a_0 x + a_1 c_1 + \dots + a_k c_k \approx 0。

我做了个小实验。常数环取 {1,π,π2,ζ(3),log⁡2,πlog⁡2,G}\{1, \pi, \pi^2, \zeta(3), \log 2, \pi\log 2, G\}(GG 是 Catalan 常数),目标值是一串随机生成的 20 位数字 x=0.33770009386696370382x = 0.33770009386696370382,系数上限 1000:

dps 20, H 1000 -> [-29, 166, 5, -1, -15, 159, -126, 22]

PSLQ 很爽快地给出了答案:

29x=166+5π−π2−15ζ(3)+159log⁡2−126πlog⁡2+22G\begin{aligned} 29x = {} & 166 + 5\pi - \pi^2 - 15\zeta(3) \\ & + 159\log 2 - 126\pi\log 2 + 22G \end{aligned}

而且在 20 位精度下,残差小得无可挑剔。一个随机数,就这样被“认出”是七个著名常数的组合。

原因:位数预算

为什么会这样?BootLoops 的技能文件 constant-recognition 开头就讲了这个道理5:整数关系算法本质上是一台拟合机器,给它足够的系数自由度,它每次都会返回一个关系。

可以粗略地算一笔账。nn 个数、系数最大到 HH,光是“表达一个关系”就要消耗大约 nlog⁡10Hn\log_{10}H 位精度。上面的实验里 n=8n = 8、H=103H = 10^3,预算是 8×3=248 \times 3 = 24 位,比我们手里的 20 位还多。这时候 PSLQ 找到关系是必然的,它找到的只是噪声的最佳拟合。只有“工作精度减去这笔预算”剩下的位数,才算证据。

第一次尝试:看残差,或者换个常数环

最直觉的检验是看残差:残差很小,说明关系成立?可残差小是 PSLQ 的停止条件,它当然小。把这组整数拿到 60 位精度重算一遍,残差是 −3.3×10−17-3.3\times 10^{-17},也就是说这个“关系”只在前 17 位碰巧成立。

更危险的是反方向的“努力”:搜不到,就往环里加一个“相关问题里出现过”的常数;还搜不到,就把系数上限放宽“看看”。技能文件把这两种做法分别叫作“环扩张”和“高度蠕变”5。每加一个常数、每放宽一次上限,都是在多给机器一份拟合噪声的自由度。足够多次之后,任何数都能“闭合”。

解决:在搜索之前做完所有决定

constant-recognition 的办法,是把所有会影响结论的选择挪到搜索之前,并且写下来:

  1. 先声明常数环,每个常数写上为什么在这里(比如按权重分级,或者相邻已解问题用到了它),注明日期。
  2. 定好系数上限并算预算,要求剩余位数有几十位的余量,不够就去算更多位,或者缩小环。
  3. 先跑对照,再碰真数据。正对照:一个闭式已知的值,必须原样找回;负对照:同样长度的随机数,必须返回空。两者走完全相同的代码路径。
  4. 换到显著更高的精度重跑,真关系的整数不变,假关系会消失或者重新洗牌。
  5. 用搜索没碰过的位数认证:由一条独立路线把值算到更高精度,和闭式比较。
  6. 搜不到就如实报告“空”,附上参数和一个任意精度求值器。技能文件的原话是,错误的名字比没有名字更糟5。

把这套流程用在上面的例子上,结果一目了然。随机数的那条关系,在 40 位精度下直接消失;正对照 14(7ζ(3)−3πlog⁡2+G)\frac{1}{4}(7\zeta(3) - 3\pi\log 2 + G) 在 20 位和 40 位下返回同一组整数:

random x, dps 40, H 1000 -> None
control, dps 20, H 1000 -> [-4, 0, 0, 0, 7, 0, -3, 1]
control, dps 40, H 1000 -> [-4, 0, 0, 0, 7, 0, -3, 1]

这些规则也写进了代码。tools/lockpick/pslq_gate.py 是项目里所有 PSLQ 搜索的统一入口,GUIDE 里明确写着“never raw mp.pslq”。它的 capacity() 会拒绝超出精度容量的系数上限;比较两个精度的结果前,先把整数向量按符号和最大公约数规范化;正对照的系数要比上限低十倍以上,且对照失败时以退出码 2 退出,拒绝报告任何空结果6。最后这一条很关键:一个连已知答案都找不回的搜索,它说的“找不到”也不可信。

回头看开头那句“吻合 20 位”。现在我们知道,单看位数没有意义,要问的是:这 20 位里,有多少是在预算之外的?换一个精度,整数还一样吗?用来比较的“数值结果”,又是从哪里来的?

这就引出第三个问题:什么样的检查才算数?


四、“完成”的标准

现象:比较卡在 18 位

官网讲过一个“kite”图的故事2。这个积分前后经过四轮造工具才算完:先是 AMFlow 在计算最需要的极限处返回了 0 位正确数字,而且不报错,原因是一个固定容差在极高精度下把两族解合并了,携带答案的那个极点被悄悄丢掉;接着用 Wayfinder 把一个高精度值沿微分方程“运”到 1669 个能量点,每点 99 位;再接着积分在产生阈附近的折点处卡在 6 位,靠 Nestor 的色散求积在折点处分段才收敛到 99 位。

到了最后一步,和参考值比较,吻合却卡在 18 位,怎么也上不去。

原因:较弱的一边决定上限

查下来,部分原因是那个存档的参考值本身就只准到 18 位。比较能证明的位数,不可能超过两边里较弱的那一边;而这个参考值的精度当初没有被记下来,所以没人知道瓶颈在它身上。换成一个全新的、完全独立计算出来的参考值,最终吻合到 41 位。

kite 积分的最终值和两个参考值的比较 最终值由 Wayfinder 输运和 Nestor 色散求积算到 99 位。和精度未记录、实际只有 18 位的旧参考值比较,吻合卡在 18 位;和一个全新的独立参考值比较,吻合 41 位。比较能证明的位数不会超过较弱的一边。 推导路线 · 待检查的值 参考值 · 用来检查它 最终值 V kite 图 权重函数 Wayfinder 输运 能量点 1,669 个,每点 99 位 最后积分 Nestor 色散求积 精度 99 位 两边都很精确时, 吻合位数才能说明 V 本身。 此前三轮:AMFlow 静默丢极点、 输运到 1,669 点、折点处分段, 各留下一个工具。 旧参考值 R₁ 早先存档 来源 未记录 记录的精度 未记录 实际精度 18 位 与 V 吻合 卡在 18 位 瓶颈在参考值 新参考值 R₂ Gatekeeper 登记 来源 全新的独立计算 记录的精度 已登记 与 V 吻合 41 位 比较能证明的位数,不会超过较弱的那一边。
图 1 · 同一个最终值,对旧参考值只能吻合 18 位,对新的独立参考值吻合 41 位:参考值的精度和来源必须被记录

这个故事把“检查”拆成了两半:一半是比较本身,一半是用来比较的那个参考值。前者容易被注意到,后者常常被当成理所当然。

第一次尝试:“和参考值吻合得很好”

只要有一个参考值,大部分人(和大部分 agent)就会去比较,然后报告“吻合得很好”。Schwartz 的文章专门提醒过要警惕 “good agreement” 这种说法1。问题在于,这样的报告既没有说吻合了几位,也没有说参考值从哪来,更没有说比较的点有没有参与过拟合。拟合参数足够多,在拟合点上吻合是必然的;参考值要是调参时看过,它就只能证明“方法记住了它见过的东西”。

解决:在答案出现之前定好门槛

技能文件 acceptance-gate 给“完成”下了一个外部定义7:

A result is done when it has survived a check that could have failed, run by a route that could not have known the answer.

一个结果算完成,要通过一个可能失败的检查,而执行检查的路线不可能事先知道答案。围绕这句话,它规定了一个固定顺序的流程,顺序本身就是纪律的一部分:

  • 先声明门槛。拟合之前写下:留出哪些点、独立路线是什么、多少位算通过、对照怎么做。答案出现之后再设计的检查,会不知不觉被设计成能通过。
  • 留出从不参与拟合的点,不只是最终拟合,所有影响过方法选择的探索性运行都不能碰它们。
  • 审计独立性:检查路线不能和推导共享代码、级数表示或拟合输入。用同一个级数展开写的两个求值器,即使作者不同,也会在同一个错误值上吻合到任意深度。
  • 数位数,不用形容词:“三个留出点各 34 位”,而不是“吻合极好”。
  • 加倍精度重跑:真恒等式的吻合位数会随精度加深;位数卡在固定深度不动,说明两边共享了某个被截断的中间量,这是失败,不是“差不多”。
  • 跑正负对照:在已知答案的情形上门槛必须通过;把候选闭式的最后一个系数改一位,门槛必须在对应的那一位上失败。
  • 只有全部通过才叫 CLOSED,否则就是 OPEN,并写清楚还差什么。

BootLoops 的振幅结果用的门槛是:至少 30 位真实的留出位数,两种精度下稳定,参考值从未喂给过拟合7。

最后那个条件,另一份技能 independence-bookkeeping 把它定成了一条单向规则8:喂过拟合的参考值,永远不能用来认证结果。“喂过”包括所有软通道:调试时瞄了一眼参考值、拿它决定级数取几项、因为和它对不上而丢掉一次运行。一旦碰过,就永远留在推导这一侧,事后再怎么小心也洗不掉。

这些规则在 tools/gatekeeper 里落成了代码9。它给每个参考值登记精度和来源,这正是 kite 故事之后加的;它会对参考数据做哈希去重,因为有一种陷阱是同一组数值以两个不同的标签出现,拟合就会“认证”它自己的训练数据;做留一法认证时,一个量是否通过取决于最差的那个留出点,而不是平均值。

门槛定得够严了。可执行门槛的终究是一段代码。这段代码要是自己坏了呢?


五、检查本身的可靠性

现象:植入一个崩溃,自测依然全绿

BootLoops 用一个脚本 run_selftests.py 验证整个仓库:它遍历 tools/ 下的每个包,按 tools/README.md 里登记的验证类别,运行 tools/BATTERIES.json 里写死的那条测试命令,最后打印一张状态表。

其中一类包叫 data-gated:它们要用户自己提供数据,克隆下来的仓库里没有,所以在干净环境里应该“大声拒绝”运行。1.0 版本里,脚本对这一类的处理只有两行:

Python
elif cls == "data-gated":
entry["status"] = "REFUSED (by design)" # data-gated: loud refusal is the contract

发布后不久,用户 @Aspect5 在 issue #3 里报告:他在 tools/galois/svmap21.py 的第一行植入了一个 ImportError,整轮自测仍然是绿的10。

原因:拒绝和崩溃共用了一个状态

为什么?因为上面那段代码只看类别,不看退出码。一个 data-gated 包只要以非零码退出,无论是缺数据的正常拒绝,还是 import 失败、断言失败、超时,都会被记成 “REFUSED (by design)”。拒绝本来是设计,于是崩溃也被当成了设计。

这正是 acceptance-gate 失败模式目录里的第一类:一个从来没失败过的门,什么也证明不了7。宽到能放过一切的容差、因为变量别名而自己和自己比较、写在永远不执行的分支里的断言,都属于这一类。从这样的门里出来的每一个 PASS 都是空的,而唯一能发现它的办法,就是故意喂它一个错误输入,看它会不会报警。@Aspect5 植入的那个 ImportError,就是一次负对照。

data-gated 包的退出码在 1.0 和 1.0.1 中的判定 三种情形:缺数据并打印理由后 exit 4;顶部植入 ImportError 后 exit 1;exit 4 但最后一行是 Traceback。1.0 只看类别,三种都判为 REFUSED,整轮自测为绿;1.0.1 只把带理由的 exit 4 判为 REFUSED,其余都是 FAIL。 run_selftests.py · data-gated 包的判定 测试的输出 退出码 1.0 1.0.1 缺参考数据,打印理由 4 REFUSED REFUSED svmap21.py 顶部 ImportError 1 REFUSED FAIL exit 4,最后一行是 Traceback 4 REFUSED FAIL 整轮自测 exit 0,全绿 exit 1 1.0 elif cls == "data-gated": 1.0.1 · 提交 752b0d2 ... and rc == DATA_GATED_REFUSAL_RC: 1.0 只看类别不看退出码,崩溃和拒绝被记成同一个状态。
图 2 · 植入的 ImportError 在 1.0 里被记成“按设计拒绝”,整轮自测仍然全绿;1.0.1 把拒绝变成一个带理由的专用退出码

解决:把“拒绝”变成一个契约

1.0.1 的修复(提交 752b0d2)把“拒绝”从一个类别变成了一份契约:data-gated 包的测试缺数据时,必须先打印理由,再以专用的退出码 4(DATA_GATED_REFUSAL_RC)退出。脚本只把这种情况记成 REFUSED;其他任何非零退出、exit 4 却没有打印理由、或者最后一行输出是 Python 异常(用一条 CRASH_LINE 正则识别),一律是 FAIL。脚本注释里的那句话概括了这次修复:

A refusal and a crash therefore never share a status.

拒绝和崩溃,从此不再共用一个状态。同一个版本还修了另一处同类问题:Julia 的 Pkg.instantiate() 超时会中断整轮自测,连结果文件都不写,现在超时只让那一个包失败,整轮继续跑完并以 1 退出10。

检查会坏,还有一种更直接的坏法:被改掉。在 agent 干活的环境里,测试不过时,最省事的“修复”就是改测试。BootLoops 的做法是把校验用的文件按 sha 固定下来,自测先检查这些哈希,树被改过就拒绝运行3。这些文件的开头都写着一段给读者(也就是 agent)看的声明,比如 tools/abacus/hecke_desk.py:

Do not edit this file or the body: the body's bytes are sha-pinned by the
verification harness — changes there make verification fail.

同样的思路一直延伸到写论文这一步。tools/emitall 要求为文中引用的每个数字写一条声明,指明它来自哪个结果文件的哪个字段,然后从结果文件重新生成每个数字,和文中的写法逐一比较,有任何漂移就以非零码退出;它还会检查生成脚本里有没有“手打进去的数字”11。

到这里,前三个问题都有了答案。但这些答案大多是规程,写在技能文件里。最后一个问题是:怎样让一个会遗忘、会讨好的 agent 每次都照做?


六、把纪律交给 agent

现象:每次都从零开始

Schwartz 记录过几个让人熟悉的现象1:模型宁愿硬磨一个要跑好几天的计算,也不去造一个能把它缩短到几分钟的工具,他不得不反复提醒它“think smarter, not harder”;上下文一压缩,重要信息就丢了;模型给出的时间估计,不是太长就是太短。

这些现象背后是同一件事:一次会话结束,它学到的东西就跟着上下文一起消失了。下一个问题又从一个新脚本开始,前面踩过的坑要再踩一遍。

解决一:每个工具都有一份给 agent 读的说明书

BootLoops 的第一个对策,是让工具本身带着“怎么用、怎么不信它”的知识。每个包都有一个 GUIDE.md,字段固定:KIND、PURPOSE、USE-WHEN、NOT-FOR、INVOKE、INPUTS、OUTPUTS、ENV、GATES。技能文件 tool-stewardship 把它归结为每个工具页必须回答的四个问题12:

  1. 它做什么;
  2. 什么时候用它,以及相邻情形该用哪个工具;
  3. 它的输出是什么意思,失败时长什么样,如果失败的输出和成功很像,要加粗写明;
  4. 它的答案要通过什么测试,才能被相信。

第三条和第四条最能体现这个项目的取向。一份普通的 README 告诉你怎么调用;这里的说明书还要告诉你,什么时候不该相信它的输出。前面提到的 pslq_gate 的“对照失败退出码 2”、Gatekeeper 的“按最差留出点认证”,都写在各自 GUIDE 的 GATES 字段里。

tools/README.md 是这些说明书的索引,一行一个包,附上验证类别。这就是为什么推荐的用法是先让 agent 读索引和 GUIDE,提出计划,等人批准之后再动手。

解决二:先查、再改、最后才写新的

光有说明书还不够,agent 还得愿意去查。tool-stewardship 规定了一个决策阶梯12:直接用 → 打补丁或扩展 → 包一层 → 写新的。要写新代码,必须先写一句话,说明查过哪些现有工具、各自为什么不够;写不出这句话,就说明还没查。打补丁要改上游那份,而不是复制一份到自己的工作目录,因为复制出去的 fork 从那一刻起就不再继承后续的修复。新工具要在建好的当天写说明书、登记索引,因为过一周,作者就只记得自己当时的意图,而不是工具实际的行为。

kite 那个积分就是这样把问题变成工具的:四轮里每撞一次墙,就留下一个工具或一处升级,AMFlow 的修复进了 amflow-cpp 分支,色散求积进了 Nestor,参考值登记进了 Gatekeeper2。下一个问题,就从这里开始。

对于时间估计,timing-discipline 的规则很干脆13:不是从本次计算、本台机器上测出来的估计,都是猜测。大计算之前先跑小试点;预计要跑好几个小时,正确的反应几乎从来不是换更大的机器,而是重新组织问题,让计算本身变小。

解决三:规程是普通的 Markdown

最后,所有这些纪律都写成了普通的 Markdown 文件,也就是 skills 仓库里的 12 份技能:7 份通用规程(acceptance-gate、constant-recognition、planted-truth、independence-bookkeeping、timing-discipline、reading-contract、tool-stewardship),5 份重型研究流程(证明、模拟审稿、文献调研、核对参考文献、检查论文措辞)14。

用 Markdown 有一个实际的好处:它和模型无关。Claude Code、Codex、Cursor 都能读,换一个模型,纪律不用重写。仓库在 Claude Code 里提供一个 /bootloops-setup 安装器,默认不激活任何技能,由用户逐个挑选。

我在本地跑的一遍自测

说明书写得好不好,最直接的检验是在一台陌生的机器上照着做。我在一个 4 核的 Linux 容器里(Python 3.13,项目测试的是 3.12)克隆仓库,只装 README 列出的核心依赖,跑了一遍:

python3 run_selftests.py --par 8

50 个包里,36 个通过,2 个按设计拒绝,12 个失败。这和 README 说的“基本全绿、3 个因缺数据停下”差得不少,但失败的输出几乎都直接点名了原因:ModuleNotFoundError: No module named 'gmpy2'、dynesty、networkx 没装,julia: not found,PARI/GP 的 gp 找不到,或者在 300 秒限时内没跑完。按提示补装那几个 Python 包、把限时放宽到 900 秒,重跑失败的那些,又有 7 个转绿,合计 43 个通过、2 个按设计拒绝、5 个失败。剩下的失败里,两个要 Julia,一个要 gp,另外两个(amflow-kit 和 seedling)的测试要读写 /proc 和 cgroup,而这个容器不允许。

有一行值得单独看:frobenius-boundary 是 data-gated 包,它这次失败的原因是找不到 gp 可执行文件,抛出了 FileNotFoundError。在 1.0 的规则下,这会被记成 “REFUSED (by design)”;在 1.0.1 下,它被如实记成了 FAIL。上一节那个修复,就在这张表里起了作用。

另外,好几个通过的包后面跟着一句 “over the ~20 s battery standard (took 212s)”。项目约定默认测试在笔记本上 20 秒左右跑完,超出了就标出来,而不是默默接受。这是 timing-discipline 落在脚本里的样子。


七、BootLoops 与 agent harness

现象:同一个词,两种用法

读到这里,可能会有一个疑问。BootLoops 管自己叫 harness,可这两年 “harness” 更常见的用法是指 Claude Code、Codex 这类 agent 框架:常有人说 “agent = 模型 + harness”,Anthropic 的工程博客也专门写过怎样为长时间运行的 agent 设计 harness15。BootLoops 的 README 第一句话又说它 “independent of the model driving it”,要你 “point whatever agent you use at it”3。那它和 Claude Code 是同一类东西吗?是不是又一个 agent 框架?

如果带着“它是一个 agent 框架”的预期去读源码,会扑个空。我在 bootloops 和 skills 两个仓库里搜了一遍:没有一处调用模型 API(anthropic、openai 都搜不到),没有 MCP server,没有 agent 循环,没有提示词编排,连 CLAUDE.md、AGENTS.md 这类给 agent 的入口文件都没有。整个项目就是 Python 包、Julia 引擎、Markdown 文档和测试。

分析:两层东西,各管一件事

那 agent harness 到底是什么?简单说,它是包在模型外面、让模型能“动手”的那层代码:循环地调用模型,执行模型发出的工具调用(读文件、跑命令、改代码),管理上下文(快满时压缩),在危险操作前请人确认,按需加载技能,开出子 agent。它决定的是模型怎么行动。

BootLoops 在它上面一层。它不去驱动模型,而是给已经能动手的 agent 准备一个工作现场:一个可以查的工具索引,一批带说明书的仪器,几份规定“什么叫做完”的规程,以及一套能证明这些仪器没坏的测试。它决定的是行动的结果能不能被相信。

两层之间靠什么连起来?看下来只有三样,全都是 agent harness 本来就有的能力:

  • 读文件。 入口那句 “Read tools/README.md and the tool guides it points to” 就是一次普通的读文件。工具的用法、边界和验收标准都写在 GUIDE 里,agent 读了才知道。
  • 跑命令。 所有工具都是命令行或 Python 入口,结论通过输出和退出码交回给 agent:run_selftests.py 有 FAIL 就退出 1,pslq_gate 对照失败退出 2,data-gated 包缺数据退出 4。第五节那个 bug 之所以要紧,正是因为退出码就是 BootLoops 和 agent 之间的接口:接口说了谎,agent 就会相信一个没跑过的测试。
  • 加载技能。 规程用 Agent Skills 格式写成,打包成 Claude Code 和 Codex 的插件。加载、匹配技能是 harness 的事,BootLoops 只提供内容。
BootLoops 通过 agent harness 的三种能力接入 左边是 BootLoops 两个仓库里的文件:工具索引和 GUIDE、run_selftests.py 和它的退出码、referee-sim 技能要求的新上下文。它们分别通过 agent harness 的读文件、跑命令、加载技能和开子 agent 接入。agent 循环、上下文压缩和权限这一层只存在于 harness 中,BootLoops 里没有;harness 再去调用可替换的模型。 BootLoops · 两个仓库里的文件 agent harness Claude Code · Codex · Cursor 模型 tools/README.md 读 索引 50 个包,一包一行 GUIDE USE-WHEN · GATES · INVOKE run_selftests.py 跑 命令 python3 run_selftests.py 退出码 0 全过 · 1 有 FAIL · 4 拒绝 skills/referee-sim/SKILL.md 加载 要求 fresh agent context 格式 Agent Skills · 两个插件 agent 循环、提示词编排、模型 API 调用 Read · 读文件 Bash · 跑命令 子 agent · 加载技能 循环 · 上下文压缩 · 权限 可替换 虚线框:两个仓库里都找不到这些代码,它们只在 harness 里。 BootLoops 只写下“要新开一个上下文”,真正开出子 agent 的是 harness。
图 3 · BootLoops 不含 agent 循环,只通过 agent harness 已有的读文件、跑命令、加载技能三种能力接入

图里那条强调色的线值得多看一眼。几份重型技能对 harness 提出了明确的要求:referee-sim 要求模拟审稿必须在 “fresh agent context” 里跑,不能用写稿的那个上下文,否则“审稿人”会继承作者的思路和盲点;prove-protocol 要求证明者和对手是分开的 agent,怀疑者用新的上下文做抽查14。这些要求 BootLoops 自己实现不了,它只能写下来,等 harness 去开出子 agent。Schwartz 描述的工作方式也是这样:一个总会话负责验证结果,另开一个会话扮演对抗性审稿人,后台的子 agent 可以单独关掉,中间结果写进 Markdown 文件留存1。这些都是 Claude Code 这一层提供的能力,他把它们按 BootLoops 的规程组合了起来。

区别:同一个失败,在两层分别修

两者的关系,用一个具体的失败来对照最清楚。Anthropic 在那篇讲 harness 设计的文章里,把长时间运行的 agent 的一种典型失败描述为:后来的会话看到已经有了一些进展,就过早地宣布完成15。这和 Schwartz 遇到的“做完了,只有一个星号”是同一种病。

在 agent harness 这一层,修法是改造工作环境:维护一份功能清单和进度文件,让每个新会话先读它们;要求 agent 跑测试,测试过了才能勾掉一项。这能保证 agent 跑了测试,却回答不了该跑什么测试。对一个 web 应用,“页面能打开、按钮能点”就是测试;对一个 Feynman 积分,测试是什么?要留出哪些点、和哪条独立路线比、多少位才算数、参考值有没有喂过拟合?这些是领域知识,harness 不可能内置。

BootLoops 修的正是这一半。acceptance-gate 把“完成”定义成 CLOSED 或 OPEN,constant-recognition 规定空结果也要带参数报告,run_selftests.py 保证仪器本身没坏。反过来,BootLoops 也替代不了 harness:它没法阻止上下文压缩丢掉信息,只能把该留下的东西写进文件(GUIDE、规程、存档的参考值);它也不能替 agent 开出一个干净的上下文,只能要求 agent 这么做。

agent harnessBootLoops
回答的问题模型怎么行动:循环、工具调用、上下文、权限行动的结果能不能信:仪器、规程、验收门槛
形态一个运行时程序一个仓库:Python 包、Markdown、测试
换一个模型往往要换一个 harness不用改,文件对谁都一样
管不了的事某个领域里什么才算“做完”上下文会不会丢、子 agent 怎么开

顺带一提,仓库里 “harness” 还有第三种用法。第五节那段写给 agent 的声明里说校验文件 “sha-pinned by the verification harness”,技能文件里也反复出现 “the comparison harness”“the harness that cannot fail”7。这是软件测试里的老意思:测试 harness,即驱动被测代码、比对结果的那套装置。这层意思反而最接近 BootLoops 的本质。它是给 agent 用的一套测试 harness,外加一箱带说明书的仪器。

如果非要一句话概括两者的关系:agent harness 让模型能动手,BootLoops 让它动手的结果能被检查。 一个跑在另一个上面,谁也替代不了谁。


八、回到开头那句话

现在可以回头看开头那句话了:

这个积分等于 14(7ζ(3)−3πlog⁡2+G)\frac{1}{4}(7\zeta(3) - 3\pi\log 2 + G),和数值结果吻合 20 位,完成。

按 BootLoops 的标准,这句话本身还不能说明任何事。我们会依次追问:数值结果是用球算术算的,还是固定精度?如果是后者,半径是多少,没人知道。常数环是搜索前声明的吗?8 个数、系数到 1000,预算 24 位,20 位吻合正好落在“必然能找到”的范围里。换到 40 位,整数还一样吗?用来比较的值是在留出点上、由一条独立路线算的吗?那条路线的值有没有喂过拟合?门槛有没有在一个错误的候选上失败过?

只有这些问题都有了带数字的回答,这个结果才能从 OPEN 变成 CLOSED。

当然,BootLoops 也有它明确声明的边界:

  • 品味仍然来自人。 Schwartz 的文章反复强调,选哪些问题值得做、结果有没有意思,要靠人来判断;领域之外,模型的结果“技术上正确,但不那么有趣”1。技能仓库也写明,这些规程让结果可检查,但不替代合格的人审核,每个结果发布前都由作者审过14。
  • 完整性检查不是安全边界。 很多工具会执行输入文件的内容(JSON、YAML、.m、.jl、pickle 等),别人给的输入文件等同于代码,只该在沙箱里运行。证书和校验防的是意外,防不了恶意3。
  • 它是研究仪器。 README 明确写着不得用于临床、精算、支付、监管和公共安全决策3。
  • 官网上列出的各个领域的结论,本文没有逐一核对,读者引用前应该去看对应的论文。

小结

顺着开头那句话,我们走过了这样一条问题链:

  1. 数值本身会不会悄悄算错? 会。Muller 递推在浮点数下收敛到 100,固定精度下的错误答案看起来和正确答案一样正常。球算术让每个数带上被证明的误差半径,算不准时标成 UNCERTIFIED 而不是打印出来;但它只能证明不相等,证明不了相等。
  2. 吻合的位数能不能被凑出来? 能。PSLQ 给足自由度就一定返回关系,20 位随机数也能被“认出”。办法是在搜索前声明常数环和系数上限、算位数预算、先跑正负对照、换精度复核,搜不到就如实报告。
  3. 什么样的检查才算数? 一个可能失败、由不可能事先知道答案的路线执行的检查:门槛提前定好,留出点从不参与拟合,参考值登记精度和来源,喂过拟合的参考值永远不能用来认证。
  4. 检查本身坏了怎么办? 用负对照去试它。BootLoops 自己的自测脚本就因为一个植入的 ImportError 暴露了“拒绝和崩溃共用一个状态”的漏洞,修复后拒绝变成了带理由的专用退出码;校验文件用 sha 固定,防止被改。
  5. 怎样让 agent 每次都照做? 把“怎么用、什么时候别信”写进每个工具的说明书,用决策阶梯让问题沉淀成工具,再把纪律写成和模型无关的 Markdown 技能。
  6. BootLoops 和 agent harness 是什么关系? 上下两层。agent harness 管模型怎么行动(循环、工具调用、上下文、子 agent),BootLoops 管行动的结果能不能信(仪器、规程、验收门槛)。它不含 agent 循环,只通过读文件、跑命令、加载技能接入;需要新开上下文时,也只能写下要求,交给 harness 去做。

如果想自己试一试,下面两段代码就是本文的两个实验。第一段需要克隆 BootLoops,并 pip install mpmath python-flint:

Python
import sys; sys.path.insert(0, "bootloops/tools/baller")
import baller
f = lambda xp, x: 111 - 1130/x + 3000/(x*xp)
r = baller.run(f, x0=(2, -4), n=100, dps=50)
print(baller.render(r.values[30]), baller.render(r.values[100]))
print(baller.solve(f, 16, x0=(2, -4), n=100))

第二段只需要 mpmath:

Python
import mpmath as mp
def ring(x):
return [x, 1, mp.pi, mp.pi**2, mp.zeta(3), mp.log(2), mp.pi*mp.log(2), mp.catalan]
for dps in (20, 40):
mp.mp.dps = dps
noise = mp.mpf("0.3377000938669637038216095227") # 随机数字
control = (7*mp.zeta(3) - 3*mp.pi*mp.log(2) + mp.catalan) / 4
print(dps, mp.pslq(ring(noise), maxcoeff=1000, maxsteps=10**6),
mp.pslq(ring(control), maxcoeff=1000, maxsteps=10**6))

参考资料

Footnotes

  1. Matthew Schwartz. Claude-shaped science. Anthropic 网站客座文章,2026-10-01。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. BootLoops 官网:首页 与 The harness。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  3. BootLoops 仓库 README.md,提交 e16ba28(1.0.1)。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  4. J.-M. Muller et al. Handbook of Floating-Point Arithmetic. Birkhäuser, 2010。递推本身也是 Baller 前门的参考例子,见 tools/baller/baller/frontdoor.py。 ↩

  5. BootLoops skills:constant-recognition/SKILL.md。PSLQ 见 Ferguson, Bailey, Arno, Math. Comp. 68 (1999) 351。 ↩ ↩2 ↩3

  6. BootLoops 仓库 tools/lockpick/GUIDE.md。 ↩

  7. BootLoops skills:acceptance-gate/SKILL.md。 ↩ ↩2 ↩3 ↩4

  8. BootLoops skills:independence-bookkeeping/SKILL.md。 ↩

  9. BootLoops 仓库 tools/gatekeeper/GUIDE.md。 ↩

  10. BootLoops 仓库 CHANGELOG.md 1.0.1 条目,与提交 752b0d2、13dd800。 ↩ ↩2

  11. BootLoops 仓库 tools/emitall/GUIDE.md。 ↩

  12. BootLoops skills:tool-stewardship/SKILL.md。 ↩ ↩2

  13. BootLoops skills:timing-discipline/SKILL.md。 ↩

  14. BootLoops skills 仓库 README.md,以及其中的 referee-sim/SKILL.md、prove-protocol/SKILL.md。 ↩ ↩2 ↩3

  15. Anthropic Engineering. Effective harnesses for long-running agents. 2025-11-26。 ↩ ↩2