﻿---
title: "怎样相信大模型算出来的数字：拆解 BootLoops"
description: "日常程序里 16 位浮点数足够用，科学里的精密计算却要几十上百位，还得证明每一位可信。大模型说“这个积分算出来了，和数值吻合 20 位”，我们该不该信？顺着这句话往下追：数值本身会不会悄悄算错，吻合的位数能不能被凑出来，什么样的检查才算数，检查本身坏了怎么办，以及这些纪律怎样交给一个会遗忘、会讨好的 agent，最后看它和 Claude Code 这类 agent harness 是什么关系。BootLoops 的每一层，都是为了堵住其中一个漏洞。"
pubDate: 2026-10-15
updatedDate: 2026-10-17
tags: ["BootLoops", "AI for Science", "大模型", "数值计算", "开源项目"]
clc: "O24"
hero: "./hero.svg"
refs:
  - id: "1"
    title: "Claude-shaped science"
    url: "https://www.anthropic.com/research/claude-shaped-science"
    collect: "anthropic-claude-shaped-science"
    site: "Anthropic"
    author: "Matthew Schwartz"
    date: "2026-10-01"
    note: "Anthropic 网站客座文章"
  - id: "2"
    title: "首页"
    url: "https://www.bootloops.ai"
    collect: "bootloops"
    site: "BootLoops 官网"
  - id: "2b"
    title: "The harness"
    url: "https://www.bootloops.ai/harness.html"
    collect: "bootloops-harness-html"
    site: "BootLoops 官网"
  - id: "3"
    title: "README.md"
    url: "https://github.com/BootLoops-ai/bootloops/blob/main/README.md"
    collect: "github-bootloops-ai-bootloops-readme-md"
    site: "BootLoops 仓库"
    note: "提交 `e16ba28`（1.0.1）"
  - id: "4"
    title: "Handbook of Floating-Point Arithmetic"
    site: "Birkhäuser"
    author: "J.-M. Muller et al."
    date: "2010"
    note: "递推本身也是 Baller 前门的参考例子，见 `tools/baller/baller/frontdoor.py`。"
  - id: "5"
    title: "constant-recognition/SKILL.md"
    url: "https://github.com/BootLoops-ai/skills/blob/main/skills/constant-recognition/SKILL.md"
    collect: "github-bootloops-ai-skills-skills-constant-recog"
    site: "BootLoops skills"
    note: "PSLQ 见 Ferguson, Bailey, Arno, *Math. Comp.* 68 (1999) 351。"
  - id: "6"
    title: "tools/lockpick/GUIDE.md"
    site: "BootLoops 仓库"
  - id: "7"
    title: "acceptance-gate/SKILL.md"
    url: "https://github.com/BootLoops-ai/skills/blob/main/skills/acceptance-gate/SKILL.md"
    collect: "github-bootloops-ai-skills-skills-acceptance-gat"
    site: "BootLoops skills"
  - id: "8"
    title: "independence-bookkeeping/SKILL.md"
    url: "https://github.com/BootLoops-ai/skills/blob/main/skills/independence-bookkeeping/SKILL.md"
    collect: "github-bootloops-ai-skills-skills-independence-b"
    site: "BootLoops skills"
  - id: "9"
    title: "tools/gatekeeper/GUIDE.md"
    site: "BootLoops 仓库"
  - id: "10"
    title: "CHANGELOG.md"
    url: "https://github.com/BootLoops-ai/bootloops/blob/main/CHANGELOG.md"
    collect: "github-bootloops-ai-bootloops-changelog-md"
    site: "BootLoops 仓库"
    note: "1.0.1 条目，与提交 752b0d2、13dd800。"
  - id: "11"
    title: "tools/emitall/GUIDE.md"
    site: "BootLoops 仓库"
  - id: "12"
    title: "tool-stewardship/SKILL.md"
    url: "https://github.com/BootLoops-ai/skills/blob/main/skills/tool-stewardship/SKILL.md"
    collect: "github-bootloops-ai-skills-skills-tool-stewardsh"
    site: "BootLoops skills"
  - id: "13"
    title: "timing-discipline/SKILL.md"
    url: "https://github.com/BootLoops-ai/skills/blob/main/skills/timing-discipline/SKILL.md"
    collect: "github-bootloops-ai-skills-skills-timing-discipl"
    site: "BootLoops skills"
  - id: "14"
    title: "README.md"
    url: "https://github.com/BootLoops-ai/skills/blob/main/README.md"
    collect: "github-bootloops-ai-skills-readme-md"
    site: "BootLoops skills 仓库"
    note: "以及其中的 `referee-sim/SKILL.md`、`prove-protocol/SKILL.md`。"
  - id: "15"
    title: "Effective harnesses for long-running agents"
    url: "https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents"
    collect: "anthropic-engineering-effective-harnesses-for-lo"
    site: "Anthropic Engineering"
    date: "2025-11-26"
---

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

要理解它为什么要这样设计，得先看清科学里的精密计算和我们平时的数值计算有什么不同。

我们平时写程序算数，很少会怀疑数字本身。在 Python 里敲 `0.1 + 0.2`，得到 `0.30000000000000004`，大家笑一笑也就过去了。双精度浮点数有 53 位二进制尾数，大约是 16 位十进制有效数字。算账、画图、写物理引擎、训练神经网络，这点误差都绰绰有余：误差藏在第 16 位，而结果最后总会被现实检验。账要和银行对得上，画面歪了一眼就能看出来，模型好不好看测试集上的准确率。

科学里的精密计算不是这样。先看一个很小的例子：用泰勒级数 $e^x = \sum_k x^k/k!$ 算指数函数，加 200 项，和标准库的 `math.exp` 比一比：

```text
x = -1    泰勒级数 0.36787944117144245     math.exp 0.36787944117144233
x = -30   泰勒级数 6.1030424788918156e-06  math.exp 9.357622968840175e-14
```

$x = -1$ 时两者只在第 16 位上有差别，这就是日常计算里熟悉的那种误差。$x = -30$ 时，同一个公式、同一段代码，结果错了八个数量级，而且没有任何报错。原因在于级数的中间项：$30^k/k!$ 最大能到 $7.8 \times 10^{11}$，正负交替，彼此抵消，最后只剩下 $10^{-13}$ 量级的答案。最大的那几项在第 16 位上的舍入误差就有 $10^{-5}$ 量级，比答案本身大得多。抵消掉的是数值，留下来的是误差。

这个小例子背后，是日常计算和科学精密计算的三处根本差别。

第一，**要的位数不一样**。日常计算问的是“大约多少”，16 位远远够用。科学计算常常要回答的是“它是不是**恰好等于**某个表达式”，比如一个积分是不是 $\zeta(3)$、$\pi^2$、$\log 2$ 这些常数的某个整数组合。要从数字里认出这样的组合，光是表达组合本身就要消耗二十多位精度（第三节会细算这笔账），而双精度一共只有 16 位。BootLoops 的标准是至少 30 位独立吻合，有些方法要用到上千位[^1]。

第二，**中间过程不一样**。像 $e^{-30}$ 这样巨大的中间量相互抵消、最后剩下一个很小的结果，在 Feynman 积分这类计算里是常态。所以很多步骤干脆不用浮点数，而是在有理数上做精确运算，系数太大时先在有限域上采样，再把有理函数重建出来。

第三，**检验方式不一样**。没有人见过这个积分的正确答案。结果错了八个数量级，也不会有账单对不上、画面歪掉这样的信号提醒你。唯一的检验，是用另一条完全独立的路线再算一遍，看两边能吻合多少位。

| | 日常数值计算 | 科学精密计算 |
|---|---|---|
| 要回答的问题 | 大约多少 | 是否恰好等于某个表达式 |
| 需要的精度 | 双精度约 16 位就够 | 30 位起，有时上千位 |
| 误差的来源 | 舍入，通常留在最后几位 | 抵消和放大，可能吞掉全部有效数字 |
| 怎么发现算错了 | 现实会暴露：对账、看图、测试集 | 只能靠另一条独立路线 |

**这就是为什么需要专用工具。** 通用的数值工具是为表里“日常数值计算”那一列设计的。浮点数算得快，但只有 16 位，而且不会告诉你其中还剩几位可信。mpmath 这样的任意精度库能多算很多位，可精度要多少得你自己猜，猜少了照样悄悄出错（第二节会看到一个例子）。而真正能算 Feynman 积分的那些专业引擎，散落在各篇论文和各个软件包里，不少还绑定在 Mathematica 上，agent 很难驱动。BootLoops 的工具箱补的就是这些：带误差证明的任意精度运算，有理数和有限域上的精确运算，有纪律的整数关系搜索，几条彼此独立的数值路线；它还把 SOFIA、Blade、SubTropica 这类引擎移植成了不需要 Wolfram 内核的 Python 和 Julia 版本[^3]。

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

在精密计算里，这种讨好最后会落到一句很具体的话上。比如 agent 告诉你：

> 这个积分等于 $\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][^2b]。

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

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

- **工具箱** [`bootloops`](https://github.com/BootLoops-ai/bootloops)：`tools/` 下 50 个 Python 包（少数带 Julia），从积分约化、奇点分析、微分方程输运，到球算术、整数关系搜索、贝叶斯证据。还有三个自研引擎和几个打过补丁的外部引擎 fork。
- **技能** [`skills`](https://github.com/BootLoops-ai/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

开头那个 $e^{-30}$ 的问题，换个算法就能绕开：先算 $e^{30}$，再取倒数。可很多时候我们并不知道哪种算法会出事，也没有现成的“好算法”可换。再看一个经典例子，J.-M. Muller 构造的递推[^4]：

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

初值取 $x_0 = 2$、$x_1 = -4$ 时，这个序列的真实极限是 6。用 Python 的浮点数直接算：

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

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

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

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

$$
x_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, -4$ 恰好让 $\alpha = 0$，所以极限是 6。但只要第一步有一点舍入误差，$\alpha$ 就不再严格为零；而 $100^n$ 长得比 $6^n$ 快得多，这点误差每一步被放大十几倍，几十步之后就把真解完全盖住了。

### 第一次尝试：加精度

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

```text
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 位可信数字，精度不够就自动加
```

输出：

```text
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][^2b]：两个球不相交，可以证明两个数**不相等**；两个球重叠，却永远证明不了它们**相等**。而开头那句“这个积分**等于** $\frac{1}{4}(7\zeta(3) - 3\pi\log 2 + G)$”，恰恰是一个相等的断言。

**数值可信了，接下来的问题是：从数值到闭式这一步，能不能被凑出来？**

---

## 三、从数值到闭式

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

从一串高精度数字反推闭式，标准工具是整数关系算法 PSLQ：给它目标值 $x$ 和一组候选常数 $c_1, \dots, c_k$，它去找一组不全为零的整数 $a_0, \dots, a_k$，使得 $a_0 x + a_1 c_1 + \dots + a_k c_k \approx 0$。

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

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

PSLQ 很爽快地给出了答案：

$$
\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]：整数关系算法本质上是一台拟合机器，给它足够的系数自由度，它**每次**都会返回一个关系。

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

### 第一次尝试：看残差，或者换个常数环

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

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

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

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

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

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

```text
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][^2b]。这个积分前后经过四轮造工具才算完：先是 AMFlow 在计算最需要的极限处返回了 0 位正确数字，而且不报错，原因是一个固定容差在极高精度下把两族解合并了，携带答案的那个极点被悄悄丢掉；接着用 Wayfinder 把一个高精度值沿微分方程“运”到 1669 个能量点，每点 99 位；再接着积分在产生阈附近的折点处卡在 6 位，靠 Nestor 的色散求积在折点处分段才收敛到 99 位。

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

### 原因：较弱的一边决定上限

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

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 388" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="bl-kite-title bl-kite-desc">
<title id="bl-kite-title">kite 积分的最终值和两个参考值的比较</title>
<desc id="bl-kite-desc">最终值由 Wayfinder 输运和 Nestor 色散求积算到 99 位。和精度未记录、实际只有 18 位的旧参考值比较，吻合卡在 18 位；和一个全新的独立参考值比较，吻合 41 位。比较能证明的位数不会超过较弱的一边。</desc>
<defs>
<marker id="bl-kite-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
<marker id="bl-kite-arrow-acc" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="388" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="320" height="356" rx="8"/>
<text class="t-zone" x="32" y="44">推导路线 · 待检查的值</text>
<rect class="dd-zone" x="352" y="16" width="432" height="356" rx="8"/>
<text class="t-zone" x="368" y="44">参考值 · 用来检查它</text>
<path class="dd-edge-acc" d="M304 120 H328 Q336 120 336 128 V136 Q336 144 344 144 H366" marker-end="url(#bl-kite-arrow-acc)"/>
<path class="dd-edge" d="M304 160 H316 Q324 160 324 168 V262 Q324 270 332 270 H366" marker-end="url(#bl-kite-arrow)"/>
<rect class="dd-card" x="32" y="72" width="272" height="136" rx="6"/>
<path class="dd-card-head" d="M32.6 104 V78 Q32.6 72.6 38 72.6 H298 Q303.4 72.6 303.4 78 V104 Z"/>
<line class="dd-edge" x1="32" y1="104" x2="304" y2="104"/>
<text class="t-name" x="48" y="93">最终值 V</text>
<text class="t-note" x="288" y="93" text-anchor="end">kite 图</text>
<line class="dd-hair" x1="48" y1="130" x2="288" y2="130"/>
<line class="dd-hair" x1="48" y1="156" x2="288" y2="156"/>
<line class="dd-hair" x1="48" y1="182" x2="288" y2="182"/>
<text class="t-mono" x="48" y="121">权重函数</text>
<text class="t-val" x="128" y="121">Wayfinder 输运</text>
<text class="t-mono" x="48" y="147">能量点</text>
<text class="t-val" x="128" y="147">1,669 个，每点 99 位</text>
<text class="t-mono" x="48" y="173">最后积分</text>
<text class="t-val" x="128" y="173">Nestor 色散求积</text>
<text class="t-mono" x="48" y="199">精度</text>
<text class="t-val" x="128" y="199">99 位</text>
<text class="t-say" x="32" y="248">两边都很精确时，</text>
<text class="t-say" x="32" y="268">吻合位数才能说明 V 本身。</text>
<text class="t-note" x="32" y="308">此前三轮：AMFlow 静默丢极点、</text>
<text class="t-note" x="32" y="328">输运到 1,669 点、折点处分段，</text>
<text class="t-note" x="32" y="348">各留下一个工具。</text>
<rect class="dd-card" x="368" y="72" width="400" height="136" rx="6"/>
<path class="dd-card-head" d="M368.6 104 V78 Q368.6 72.6 374 72.6 H762 Q767.4 72.6 767.4 78 V104 Z"/>
<line class="dd-edge" x1="368" y1="104" x2="768" y2="104"/>
<text class="t-name" x="384" y="93">旧参考值 R₁</text>
<text class="t-note" x="752" y="93" text-anchor="end">早先存档</text>
<line class="dd-hair" x1="384" y1="130" x2="752" y2="130"/>
<line class="dd-hair" x1="384" y1="156" x2="752" y2="156"/>
<line class="dd-hair" x1="384" y1="182" x2="752" y2="182"/>
<text class="t-mono" x="384" y="121">来源</text>
<text class="t-val" x="464" y="121">未记录</text>
<text class="t-mono" x="384" y="147">记录的精度</text>
<text class="t-val" x="464" y="147">未记录</text>
<text class="t-mono" x="384" y="173">实际精度</text>
<text class="t-val" x="464" y="173"><tspan class="t-acc">18 位</tspan></text>
<text class="t-mono" x="384" y="199">与 V 吻合</text>
<text class="t-val" x="464" y="199"><tspan class="t-acc">卡在 18 位</tspan></text>
<text class="t-note" x="752" y="199" text-anchor="end">瓶颈在参考值</text>
<rect class="dd-card" x="368" y="224" width="400" height="110" rx="6"/>
<path class="dd-card-head" d="M368.6 256 V230 Q368.6 224.6 374 224.6 H762 Q767.4 224.6 767.4 230 V256 Z"/>
<line class="dd-edge" x1="368" y1="256" x2="768" y2="256"/>
<text class="t-name" x="384" y="245">新参考值 R₂</text>
<text class="t-note" x="752" y="245" text-anchor="end">Gatekeeper 登记</text>
<line class="dd-hair" x1="384" y1="282" x2="752" y2="282"/>
<line class="dd-hair" x1="384" y1="308" x2="752" y2="308"/>
<text class="t-mono" x="384" y="273">来源</text>
<text class="t-val" x="464" y="273">全新的独立计算</text>
<text class="t-mono" x="384" y="299">记录的精度</text>
<text class="t-val" x="464" y="299">已登记</text>
<text class="t-mono" x="384" y="325">与 V 吻合</text>
<text class="t-val" x="464" y="325">41 位</text>
<text class="t-say" x="368" y="360">比较能证明的位数，不会超过较弱的那一边。</text>
</svg>
</div>
<figcaption>图 1 · 同一个最终值，对旧参考值只能吻合 18 位，对新的独立参考值吻合 41 位：参考值的精度和来源必须被记录</figcaption>
</figure>

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

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

只要有一个参考值，大部分人（和大部分 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](https://github.com/BootLoops-ai/bootloops/issues/3) 里报告：他在 `tools/galois/svmap21.py` 的第一行植入了一个 `ImportError`，整轮自测仍然是绿的[^10]。

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

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

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

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="bl-exit-title bl-exit-desc">
<title id="bl-exit-title">data-gated 包的退出码在 1.0 和 1.0.1 中的判定</title>
<desc id="bl-exit-desc">三种情形：缺数据并打印理由后 exit 4；顶部植入 ImportError 后 exit 1；exit 4 但最后一行是 Traceback。1.0 只看类别，三种都判为 REFUSED，整轮自测为绿；1.0.1 只把带理由的 exit 4 判为 REFUSED，其余都是 FAIL。</desc>
<defs>
<marker id="bl-exit-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="360" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="768" height="328" rx="8"/>
<text class="t-zone" x="32" y="44">run_selftests.py · data-gated 包的判定</text>
<rect class="dd-card" x="32" y="64" width="736" height="162" rx="6"/>
<path class="dd-card-head" d="M32.6 96 V70 Q32.6 64.6 38 64.6 H762 Q767.4 64.6 767.4 70 V96 Z"/>
<line class="dd-edge" x1="32" y1="96" x2="768" y2="96"/>
<text class="t-name" x="48" y="85">测试的输出</text>
<text class="t-name" x="384" y="85">退出码</text>
<text class="t-name" x="480" y="85">1.0</text>
<text class="t-name" x="624" y="85">1.0.1</text>
<line class="dd-hair" x1="48" y1="122" x2="752" y2="122"/>
<line class="dd-hair" x1="48" y1="148" x2="752" y2="148"/>
<text class="t-mono" x="48" y="113">缺参考数据，打印理由</text>
<text class="t-val" x="384" y="113">4</text>
<text class="t-val" x="480" y="113">REFUSED</text>
<text class="t-val" x="624" y="113">REFUSED</text>
<text class="t-mono" x="48" y="139"><tspan class="t-acc">svmap21.py 顶部 ImportError</tspan></text>
<text class="t-val" x="384" y="139"><tspan class="t-acc">1</tspan></text>
<text class="t-val" x="480" y="139"><tspan class="t-acc">REFUSED</tspan></text>
<text class="t-val" x="624" y="139">FAIL</text>
<text class="t-mono" x="48" y="165">exit 4，最后一行是 Traceback</text>
<text class="t-val" x="384" y="165">4</text>
<text class="t-val" x="480" y="165">REFUSED</text>
<text class="t-val" x="624" y="165">FAIL</text>
<line class="dd-edge" x1="32" y1="182" x2="768" y2="182"/>
<text class="t-mono" x="48" y="208">整轮自测</text>
<text class="t-val" x="480" y="208"><tspan class="t-acc">exit 0，全绿</tspan></text>
<text class="t-val" x="624" y="208">exit 1</text>
<rect class="dd-card" x="32" y="242" width="360" height="62" rx="6"/>
<text class="t-note" x="48" y="264">1.0</text>
<text class="t-mono" x="48" y="290">elif cls == "data-gated":</text>
<rect class="dd-card" x="408" y="242" width="360" height="62" rx="6"/>
<text class="t-note" x="424" y="264">1.0.1 · 提交 752b0d2</text>
<text class="t-mono" x="424" y="290">... and rc == DATA_GATED_REFUSAL_RC:</text>
<path class="dd-edge" d="M392 273 H406" marker-end="url(#bl-exit-arrow)"/>
<text class="t-say" x="32" y="332">1.0 只看类别不看退出码，崩溃和拒绝被记成同一个状态。</text>
</svg>
</div>
<figcaption>图 2 · 植入的 ImportError 在 1.0 里被记成“按设计拒绝”，整轮自测仍然全绿；1.0.1 把拒绝变成一个带理由的专用退出码</figcaption>
</figure>

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

1.0.1 的修复（提交 [752b0d2](https://github.com/BootLoops-ai/bootloops/commit/752b0d278d4fb4f425126270b8b2bf9b10ff0f5d)）把“拒绝”从一个类别变成了一份契约：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`：

```text
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，参考值登记进了 Gatekeeper[^2][^2b]。下一个问题，就从这里开始。

对于时间估计，`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 列出的核心依赖，跑了一遍：

```text
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 设计 harness[^15]。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 只提供内容。

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 476" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="bl-stack-title bl-stack-desc">
<title id="bl-stack-title">BootLoops 通过 agent harness 的三种能力接入</title>
<desc id="bl-stack-desc">左边是 BootLoops 两个仓库里的文件：工具索引和 GUIDE、run_selftests.py 和它的退出码、referee-sim 技能要求的新上下文。它们分别通过 agent harness 的读文件、跑命令、加载技能和开子 agent 接入。agent 循环、上下文压缩和权限这一层只存在于 harness 中，BootLoops 里没有；harness 再去调用可替换的模型。</desc>
<defs>
<marker id="bl-stack-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
<marker id="bl-stack-arrow-acc" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="476" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="368" height="400" rx="8"/>
<text class="t-zone" x="32" y="44">BootLoops · 两个仓库里的文件</text>
<rect class="dd-zone" x="400" y="16" width="248" height="400" rx="8"/>
<text class="t-zone" x="416" y="44">agent harness</text>
<text class="t-note" x="416" y="68">Claude Code · Codex · Cursor</text>
<rect class="dd-zone" x="664" y="16" width="120" height="400" rx="8"/>
<text class="t-zone" x="680" y="44">模型</text>
<path class="dd-edge" d="M368 106 H414" marker-end="url(#bl-stack-arrow)"/>
<path class="dd-edge" d="M368 206 H414" marker-end="url(#bl-stack-arrow)"/>
<path class="dd-edge-acc" d="M368 306 H414" marker-end="url(#bl-stack-arrow-acc)"/>
<path class="dd-edge" d="M632 382 H674" marker-end="url(#bl-stack-arrow)"/>
<rect class="dd-card" x="32" y="64" width="336" height="84" rx="6"/>
<path class="dd-card-head" d="M32.6 96 V70 Q32.6 64.6 38 64.6 H362 Q367.4 64.6 367.4 70 V96 Z"/>
<line class="dd-edge" x1="32" y1="96" x2="368" y2="96"/>
<text class="t-mono" x="48" y="85">tools/README.md</text>
<text class="t-note" x="352" y="85" text-anchor="end">读</text>
<line class="dd-hair" x1="48" y1="122" x2="352" y2="122"/>
<text class="t-mono" x="48" y="113">索引</text>
<text class="t-val" x="112" y="113">50 个包，一包一行</text>
<text class="t-mono" x="48" y="139">GUIDE</text>
<text class="t-val" x="112" y="139">USE-WHEN · GATES · INVOKE</text>
<rect class="dd-card" x="32" y="164" width="336" height="84" rx="6"/>
<path class="dd-card-head" d="M32.6 196 V170 Q32.6 164.6 38 164.6 H362 Q367.4 164.6 367.4 170 V196 Z"/>
<line class="dd-edge" x1="32" y1="196" x2="368" y2="196"/>
<text class="t-mono" x="48" y="185">run_selftests.py</text>
<text class="t-note" x="352" y="185" text-anchor="end">跑</text>
<line class="dd-hair" x1="48" y1="222" x2="352" y2="222"/>
<text class="t-mono" x="48" y="213">命令</text>
<text class="t-val" x="112" y="213">python3 run_selftests.py</text>
<text class="t-mono" x="48" y="239">退出码</text>
<text class="t-val" x="112" y="239">0 全过 · 1 有 FAIL · 4 拒绝</text>
<rect class="dd-card" x="32" y="264" width="336" height="84" rx="6"/>
<path class="dd-card-head" d="M32.6 296 V270 Q32.6 264.6 38 264.6 H362 Q367.4 264.6 367.4 270 V296 Z"/>
<line class="dd-edge" x1="32" y1="296" x2="368" y2="296"/>
<text class="t-mono" x="48" y="285">skills/referee-sim/SKILL.md</text>
<text class="t-note" x="352" y="285" text-anchor="end">加载</text>
<line class="dd-hair" x1="48" y1="322" x2="352" y2="322"/>
<text class="t-mono" x="48" y="313">要求</text>
<text class="t-val" x="112" y="313"><tspan class="t-acc">fresh agent context</tspan></text>
<text class="t-mono" x="48" y="339">格式</text>
<text class="t-val" x="112" y="339">Agent Skills · 两个插件</text>
<rect class="dd-derived" x="32" y="364" width="336" height="36" rx="6"/>
<text class="t-note" x="48" y="387">agent 循环、提示词编排、模型 API 调用</text>
<rect class="dd-card" x="416" y="88" width="216" height="36" rx="6"/>
<text class="t-name" x="432" y="111">Read · 读文件</text>
<rect class="dd-card" x="416" y="188" width="216" height="36" rx="6"/>
<text class="t-name" x="432" y="211">Bash · 跑命令</text>
<rect class="dd-card" x="416" y="288" width="216" height="36" rx="6"/>
<text class="t-name" x="432" y="311"><tspan class="t-acc">子 agent</tspan> · 加载技能</text>
<rect class="dd-card" x="416" y="364" width="216" height="36" rx="6"/>
<text class="t-name" x="432" y="387">循环 · 上下文压缩 · 权限</text>
<rect class="dd-card" x="676" y="364" width="96" height="36" rx="6"/>
<text class="t-name" x="692" y="387">可替换</text>
<text class="t-say" x="32" y="444">虚线框：两个仓库里都找不到这些代码，它们只在 harness 里。</text>
<text class="t-say" x="32" y="464">BootLoops 只写下“要新开一个上下文”，真正开出子 agent 的是 harness。</text>
</svg>
</div>
<figcaption>图 3 · BootLoops 不含 agent 循环，只通过 agent harness 已有的读文件、跑命令、加载技能三种能力接入</figcaption>
</figure>

图里那条强调色的线值得多看一眼。几份重型技能对 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 让它动手的结果能被检查。** 一个跑在另一个上面，谁也替代不了谁。

---

## 八、回到开头那句话

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

> 这个积分等于 $\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))
```

## 参考资料
