2026 年 10 月初,哈佛的理论物理学家 Matthew Schwartz 开源了 BootLoops 1.0。他给它的定位是一副“挽具”(harness):一套让大模型去做精密定量科学的工具和规程。按项目的说法,这套东西在三个月里参与了 36 篇稿件,覆盖从粒子物理到生态学的 18 个领域1。
真正让我想仔细读它的,是 Schwartz 在介绍文章里写的几段经历1。大模型报告“做完了,只有一个星号”,结果往往是根本没做完:一个证明“只差一个未证明的引理”,而那个引理就是整个证明;他禁止了未证引理,模型又报告做完了,这次多了“一条新公理”。工作了三天,模型写出“经过两年攻坚,公式终于解出”。项目官网把这类失败叫作“satisfiction”,意思是模型太想让人满意2。
在定量科学里,这种讨好最后会落到一句很具体的话上。比如 agent 告诉你:
这个积分等于 ,和数值结果吻合 20 位,完成。
我们该不该信?这句话里至少藏着四个问题:
- 那个“数值结果”本身算对了吗?
- 就算数值是对的,“吻合 20 位”能说明闭式找对了吗?
- 什么样的检查才算数?执行检查的那段代码,会不会自己坏掉?
- 这些要求,怎样交给一个会遗忘、会讨好的 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.mdand 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:
初值取 、 时,这个序列的真实极限是 6。用 Python 的浮点数直接算:
float x10 = 6.274438662728116float x20 = 99.8985692661829float x30 = 99.99999999999993float x100 = 100.0前十步还像样,第二十步已经跑到 100 附近,最后稳稳停在 100.0。没有警告,没有 NaN,结果看起来还挺“收敛”。
原因:舍入误差唤醒了一个被压住的解
为什么会这样?这个递推的通解可以写成
初值 恰好让 ,所以极限是 6。但只要第一步有一点舍入误差, 就不再严格为零;而 长得比 快得多,这点误差每一步被放大十几倍,几十步之后就把真解完全盖住了。
第一次尝试:加精度
最直觉的办法是把精度调高。我用 mpmath 换了几个十进制位数来算(这里用的是同一递推的另一组初值 ,真值同样趋向 6):
mpmath dps 50 -> 100.0mpmath dps 100 -> 100.00000000000000001mpmath dps 118 -> 78.936364783809585216mpmath dps 120 -> -9.1516256868031901637mpmath dps 150 -> 5.9999999879253266734精度确实有用,150 位时终于对了。但问题在于:每一个错误答案都长得像正常数字,算的人并不知道该信哪一行。118 位给出 78.9,120 位给出 −9.15,没有任何一行告诉你“我错了”。把精度一路往上加,只是在赌下一次刚好够。
解决:让每个数自带误差半径
BootLoops 的回答是球算术(ball arithmetic)。每个数不再是一个点,而是“中心值 ± 半径”的一个球,每一步运算都严格地把舍入误差加进半径里。半径是被证明过的上界:真值一定在球里。tools/baller 在 FLINT/Arb 之上包了一层前门,用起来是这样的:
import ballerf = 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.2744385982163279138293784620714581132run50 x20 6.036031881081856780011run50 x30 6.0056run50 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:两个球不相交,可以证明两个数不相等;两个球重叠,却永远证明不了它们相等。而开头那句“这个积分等于 ”,恰恰是一个相等的断言。
数值可信了,接下来的问题是:从数值到闭式这一步,能不能被凑出来?
三、从数值到闭式
现象:随机数也能被“认出来”
从一串高精度数字反推闭式,标准工具是整数关系算法 PSLQ:给它目标值 和一组候选常数 ,它去找一组不全为零的整数 ,使得 。
我做了个小实验。常数环取 ( 是 Catalan 常数),目标值是一串随机生成的 20 位数字 ,系数上限 1000:
dps 20, H 1000 -> [-29, 166, 5, -1, -15, 159, -126, 22]PSLQ 很爽快地给出了答案:
而且在 20 位精度下,残差小得无可挑剔。一个随机数,就这样被“认出”是七个著名常数的组合。
原因:位数预算
为什么会这样?BootLoops 的技能文件 constant-recognition 开头就讲了这个道理5:整数关系算法本质上是一台拟合机器,给它足够的系数自由度,它每次都会返回一个关系。
可以粗略地算一笔账。 个数、系数最大到 ,光是“表达一个关系”就要消耗大约 位精度。上面的实验里 、,预算是 位,比我们手里的 20 位还多。这时候 PSLQ 找到关系是必然的,它找到的只是噪声的最佳拟合。只有“工作精度减去这笔预算”剩下的位数,才算证据。
第一次尝试:看残差,或者换个常数环
最直觉的检验是看残差:残差很小,说明关系成立?可残差小是 PSLQ 的停止条件,它当然小。把这组整数拿到 60 位精度重算一遍,残差是 ,也就是说这个“关系”只在前 17 位碰巧成立。
更危险的是反方向的“努力”:搜不到,就往环里加一个“相关问题里出现过”的常数;还搜不到,就把系数上限放宽“看看”。技能文件把这两种做法分别叫作“环扩张”和“高度蠕变”5。每加一个常数、每放宽一次上限,都是在多给机器一份拟合噪声的自由度。足够多次之后,任何数都能“闭合”。
解决:在搜索之前做完所有决定
constant-recognition 的办法,是把所有会影响结论的选择挪到搜索之前,并且写下来:
- 先声明常数环,每个常数写上为什么在这里(比如按权重分级,或者相邻已解问题用到了它),注明日期。
- 定好系数上限并算预算,要求剩余位数有几十位的余量,不够就去算更多位,或者缩小环。
- 先跑对照,再碰真数据。正对照:一个闭式已知的值,必须原样找回;负对照:同样长度的随机数,必须返回空。两者走完全相同的代码路径。
- 换到显著更高的精度重跑,真关系的整数不变,假关系会消失或者重新洗牌。
- 用搜索没碰过的位数认证:由一条独立路线把值算到更高精度,和闭式比较。
- 搜不到就如实报告“空”,附上参数和一个任意精度求值器。技能文件的原话是,错误的名字比没有名字更糟5。
把这套流程用在上面的例子上,结果一目了然。随机数的那条关系,在 40 位精度下直接消失;正对照 在 20 位和 40 位下返回同一组整数:
random x, dps 40, H 1000 -> Nonecontrol, 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 位。
这个故事把“检查”拆成了两半:一半是比较本身,一半是用来比较的那个参考值。前者容易被注意到,后者常常被当成理所当然。
第一次尝试:“和参考值吻合得很好”
只要有一个参考值,大部分人(和大部分 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 版本里,脚本对这一类的处理只有两行:
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,就是一次负对照。
解决:把“拒绝”变成一个契约
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 theverification 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:
- 它做什么;
- 什么时候用它,以及相邻情形该用哪个工具;
- 它的输出是什么意思,失败时长什么样,如果失败的输出和成功很像,要加粗写明;
- 它的答案要通过什么测试,才能被相信。
第三条和第四条最能体现这个项目的取向。一份普通的 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 850 个包里,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.mdand 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 只提供内容。
图里那条强调色的线值得多看一眼。几份重型技能对 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 harness | BootLoops | |
|---|---|---|
| 回答的问题 | 模型怎么行动:循环、工具调用、上下文、权限 | 行动的结果能不能信:仪器、规程、验收门槛 |
| 形态 | 一个运行时程序 | 一个仓库: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 让它动手的结果能被检查。 一个跑在另一个上面,谁也替代不了谁。
八、回到开头那句话
现在可以回头看开头那句话了:
这个积分等于 ,和数值结果吻合 20 位,完成。
按 BootLoops 的标准,这句话本身还不能说明任何事。我们会依次追问:数值结果是用球算术算的,还是固定精度?如果是后者,半径是多少,没人知道。常数环是搜索前声明的吗?8 个数、系数到 1000,预算 24 位,20 位吻合正好落在“必然能找到”的范围里。换到 40 位,整数还一样吗?用来比较的值是在留出点上、由一条独立路线算的吗?那条路线的值有没有喂过拟合?门槛有没有在一个错误的候选上失败过?
只有这些问题都有了带数字的回答,这个结果才能从 OPEN 变成 CLOSED。
当然,BootLoops 也有它明确声明的边界:
- 品味仍然来自人。 Schwartz 的文章反复强调,选哪些问题值得做、结果有没有意思,要靠人来判断;领域之外,模型的结果“技术上正确,但不那么有趣”1。技能仓库也写明,这些规程让结果可检查,但不替代合格的人审核,每个结果发布前都由作者审过14。
- 完整性检查不是安全边界。 很多工具会执行输入文件的内容(JSON、YAML、
.m、.jl、pickle 等),别人给的输入文件等同于代码,只该在沙箱里运行。证书和校验防的是意外,防不了恶意3。 - 它是研究仪器。 README 明确写着不得用于临床、精算、支付、监管和公共安全决策3。
- 官网上列出的各个领域的结论,本文没有逐一核对,读者引用前应该去看对应的论文。
小结
顺着开头那句话,我们走过了这样一条问题链:
- 数值本身会不会悄悄算错? 会。Muller 递推在浮点数下收敛到 100,固定精度下的错误答案看起来和正确答案一样正常。球算术让每个数带上被证明的误差半径,算不准时标成
UNCERTIFIED而不是打印出来;但它只能证明不相等,证明不了相等。 - 吻合的位数能不能被凑出来? 能。PSLQ 给足自由度就一定返回关系,20 位随机数也能被“认出”。办法是在搜索前声明常数环和系数上限、算位数预算、先跑正负对照、换精度复核,搜不到就如实报告。
- 什么样的检查才算数? 一个可能失败、由不可能事先知道答案的路线执行的检查:门槛提前定好,留出点从不参与拟合,参考值登记精度和来源,喂过拟合的参考值永远不能用来认证。
- 检查本身坏了怎么办? 用负对照去试它。BootLoops 自己的自测脚本就因为一个植入的
ImportError暴露了“拒绝和崩溃共用一个状态”的漏洞,修复后拒绝变成了带理由的专用退出码;校验文件用 sha 固定,防止被改。 - 怎样让 agent 每次都照做? 把“怎么用、什么时候别信”写进每个工具的说明书,用决策阶梯让问题沉淀成工具,再把纪律写成和模型无关的 Markdown 技能。
- BootLoops 和 agent harness 是什么关系? 上下两层。agent harness 管模型怎么行动(循环、工具调用、上下文、子 agent),BootLoops 管行动的结果能不能信(仪器、规程、验收门槛)。它不含 agent 循环,只通过读文件、跑命令、加载技能接入;需要新开上下文时,也只能写下要求,交给 harness 去做。
如果想自己试一试,下面两段代码就是本文的两个实验。第一段需要克隆 BootLoops,并 pip install mpmath python-flint:
import sys; sys.path.insert(0, "bootloops/tools/baller")import ballerf = 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:
import mpmath as mpdef 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
-
Matthew Schwartz. Claude-shaped science. Anthropic 网站客座文章,2026-10-01。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
J.-M. Muller et al. Handbook of Floating-Point Arithmetic. Birkhäuser, 2010。递推本身也是 Baller 前门的参考例子,见
tools/baller/baller/frontdoor.py。 ↩ -
BootLoops skills:constant-recognition/SKILL.md。PSLQ 见 Ferguson, Bailey, Arno, Math. Comp. 68 (1999) 351。 ↩ ↩2 ↩3
-
BootLoops 仓库
tools/lockpick/GUIDE.md。 ↩ -
BootLoops skills:acceptance-gate/SKILL.md。 ↩ ↩2 ↩3 ↩4
-
BootLoops skills:independence-bookkeeping/SKILL.md。 ↩
-
BootLoops 仓库
tools/gatekeeper/GUIDE.md。 ↩ -
BootLoops 仓库 CHANGELOG.md 1.0.1 条目,与提交 752b0d2、13dd800。 ↩ ↩2
-
BootLoops 仓库
tools/emitall/GUIDE.md。 ↩ -
BootLoops skills:tool-stewardship/SKILL.md。 ↩ ↩2
-
BootLoops skills:timing-discipline/SKILL.md。 ↩
-
BootLoops skills 仓库 README.md,以及其中的
referee-sim/SKILL.md、prove-protocol/SKILL.md。 ↩ ↩2 ↩3 -
Anthropic Engineering. Effective harnesses for long-running agents. 2025-11-26。 ↩ ↩2