数据STUDIO

这份 Agentic Loop Engineering 工程手册火了!附完整代码!

Image

这是一份完全可复现的 Agentic Loop Engineering 工程手册,全手册共 18 个 py 文件(加一个共享工具,完整源码免费获取方式在文末)在一张 A100 80GB GPU 上,用同一个模型(Qwen2.5-Coder-32B-Instruct-AWQ)、同一套 shared library、同一个可信评分器,逐项测量了 17 种 loop 工程化技术。每个组件更换了数据集以确保提升来自组件本身而非 benchmark 过拟合。

核心结论贯穿始终:一个 loop 的质量完全取决于它接入了什么可验证信号——接入真实测试/schema/检索事实/评估 harness 的 loop,每次都产生可测量的提升;接入模型自我意见的 loop,几乎不动。


01第一部分:什么是 Loop Engineering

四次变迁

Agent 系统的工程热点高在过去两年里经历了四次变迁:

Image
  1. Prompt Engineering:写一个好 prompt,读一个回复。你在 prompt 框里调整措辞。
  2. Context Engineering:为一个 turn 组装正确的上下文——文件、文档、示例。你开始给模型"看到什么"。
  3. Harness Engineering:设计单个 agent 的运行环境——工具、权限、规则。你开始设计 agent 的"活动空间"。
  4. Loop Engineering:设计一个递归系统:发现工作 → 交给 agent → 验证结果 → 持久化学习 → 决定下一步 → 循环,直到目标达成或升级给人类。你不再坐在 prompt 前面,你设计那个替你坐在 prompt 前面的系统。

Loop engineering 是用系统来取代你自己作为提示代理的角色,由你来设计这个执行任务的系统。

Loop 结构

一个典型的 loop 包含这些组件:

Schedule(调度)→ Triage skill(优先级判断)→ State / Memory(状态与记忆)
→ Worktree(隔离工作区)→ Implementer(实现者)→ Verifier(验证者)
→ Connectors(外部连接器)→ Human gate(人工门)→ (act 自动执行 or escalate 升级)

本手册的 18 个 PY 逐一构建这些组件,然后最后将它们组合回去,在真实评估 harness 上验证结果。

三条测量规则

本手册所有数字的可信度建立在三条规则之上:

  1. 验证信号,再接入 loop。 PY 00 的大部分精力花在评分器而不是模型上。如果评分器静默地给错误代码打 pass,每个 downstream 增益都是虚构的。
  2. 基线必须先真正失败。 每个 PY 先展示朴素尝试在真实数据上的低分或不安全行为,再展示组件如何弥合差距。
  3. 报告 null 结果和饱和。 Best-of-N 无效、loop 在算法难题上饱和、closed-book 地板是 7%、技术债检测器 F1 反降——全部保留并解释,不隐藏。

还有一个刻意的设计选择:每个组件使用不同的数据集。单一 benchmark 会让某个组件因错误原因看起来很好。Run-until-done 测在浅层可修复的代码上(MBPP+),context 测在缺失 schema 就致命的 text-to-SQL 上,retrieval 测在模型权重之外的事实上,isolation 测在真实并发 git 写入上,triage 和 dedup 测在真实 bug tracker 上。


02第二部分:基础设施——引擎与可信评分器

2.1 环境固定

整个系列固定在一组实验常数上:

项目
值
GPU
NVIDIA A100 80GB PCIe
驱动
580.167.08
模型
Qwen2.5-Coder-32B-Instruct-AWQ (4-bit)
推理服务
vLLM
接口
OpenAI-compatible, http://localhost:8000/v1
Embedding 模型
BAAI/bge-large-en-v1.5

Image

2.2 统一的 LLM 客户端

所有 PY 通过同一个 LLM 类访问模型,核心功能是每次调用记录 token 消耗和延迟:

class LLM:
"""OpenAI-compatible wrapper around the local server with usage tracking."""
def chat(self, messages, temperature=0.0, max_tokens=1024, n=1,
             stop=None, seed=0, label="chat", retries=3
):
for attempt in range(retries):
try:
                t0 = time.time()
                resp = self.client.chat.completions.create(
                    model=self.model, messages=messages, temperature=temperature,
                    max_tokens=max_tokens, n=n, stop=stop, seed=seed,
                )
                dt = time.time() - t0
self.usage.add(label, resp.usage.prompt_tokens,
                               resp.usage.completion_tokens, len(texts), dt)
return Generation(texts=texts, seconds=dt, label=label)
except Exception:
                time.sleep(1.5 * (attempt + 1))  # 网络抖动有限重试

这个共享的 token 记账是后面所有成本比较公平的唯一原因。当 PY 08 说 run-until-done loop 的成本是 single-shot 的 2.1 倍,那个数字是从真实运行的 recorded usage 重建的。

2.3 评分器自检——整个系列最重要的部分

在测任何东西之前,评分器必须被证明是正确的。common/eval.py 的 self_test() 喂入已知正确和已知错误的解,断言每个 checker 的判定与预期一致:

def self_test() -> dict:
# 1. 正确的 stdin/stdout 程序和故意写错的
    good = "import sys\na,b=map(int,sys.stdin.read().split())\nprint(a+b)"
    bad  = "import sys\na,b=map(int,sys.stdin.read().split())\nprint(a-b)"
    io_tests = [("2 3", "5"), ("10 4", "14")]
assert check_io(good, io_tests)["all_pass"] is True# good 必须 pass
assert check_io(bad,  io_tests)["all_pass"] is False# bad 必须 fail
# 2. 死循环必须被 timeout 杀死,不能挂住整个 run
assert check_io("while True:\n    pass", [("", "")], timeout=2.0)["all_pass"] is False
# 3. pass@k 的无偏估计返回教科书值
# n=5, c=1, k=1 → 1/5 = 0.2

自检输出——每个 checker 都正确区分好坏,死循环被捕获,all_passed: true:

{
"check_io":{ "good_all_pass": true,"bad_all_pass": false},
"check_asserts": { "good": true,"bad": false},
"timeout_caught": true,
"pass_at_k":{ "n5_c1_k1": 0.2 },
"sql_exec_match":{ "same": true, "diff": false},
"all_passed":true
}

评分器被证为可信之后,跑一个 5 道简单题的 single-shot baseline:pass@1 = 0.80。这道线是整本手册的参照。

Image


03第三部分:六大核心

3.1 Run Until Done——关键是 feedback,不是 retry

核心问题: 让模型"再试一次"和给它真实的失败反馈,区别有多大?

Image

实验设置: MBPP+ 60 道题,最多 3 次尝试。三个条件:baseline(单次 greedy)、loop + 真实执行 feedback、loop + "你之前的解错了,再试一次"(无 feedback)。

核心机制: feedback 是真实的 Python traceback 或断言失败信息,不是模型的自我意见。

def self_refine(llm, problem, max_attempts=3, temperature=0.0, use_feedback=True):
    code, _ = agents.single_shot(llm, problem, temperature=temperature)
    res = agents.grade(problem, code)
while not res["all_pass"] and attempt < max_attempts:
        attempt += 1
if use_feedback:
            fb = _assert_feedback(problem, code)   # 真实的 traceback
else:
            fb = "Your previous solution was incorrect. Produce a different, correct solution."
        gen = llm.complete(_refine_prompt(problem, code, fb), ...)
        code = agents.extract_code(gen.text)
        res = agents.grade(problem, code)

结果:

条件
第 1 次
第 2 次
第 3 次
token 消耗
baseline(single-shot)
0.767
—
—
10,504
loop + 执行 feedback
0.767
0.817
0.850
21,680
loop + "再试一次"
0.767
0.767
0.783
20,675

Image

核心结果:

  • 有真实 feedback 的 loop 提升了 8.3 个百分点。同样的 loop 只喊"再试一次"仅提升 1.7 个百分点,token 消耗几乎一样。
  • 增益集中在第 2 次尝试,之后迅速平坦——loop 修的是浅层 bug(off-by-one、边界条件、参数签名),修不了算法根本错误。
  • 案例 mbppplus/306(最大递增子序列变种)三次尝试全部失败——算法本身就错了,feedback 说"答案不对"并不能告诉模型正确的算法是什么。
  • 这也是为什么 attempt cap 不是可选的——没有 cap 的 loop 在修不好的问题上无限烧 token,6 次尝试耗 3,992 tokens 仍失败,24 次尝试将烧约 16,000 tokens。

Image

这条曲线复现了 Huang et al. (2023) 在真实代码上的结果——LLM 在没有外部 feedback 时无法可靠自我纠错。Loop 的价值来自信号,不是来自多问几次。


3.2 Skill 注入与 Context Engineering

核心问题: 当模型缺少不可猜测的局部知识(数据库 schema、项目约定)时会发生什么?

Image

实验设置:sql-create-context benchmark,60 道自然语言→SQL 题。三个条件:cold(只给问题)、skill(注入 CREATE TABLE schema)、skill + few-shot(再加两个示例)。

两个评分维度:

Image
  • schema_validity(便宜):查询中的每个标识符是否真实出现在 schema 中?检测"发明列名"。
  • exec_equiv(严格):构建真实 SQLite 数据库,预测 SQL 和 gold SQL 返回相同的行吗?

结果:

条件
schema_validity
exec_equiv
cold(无 schema)
0.017
0.000
skill(注入 schema)
1.0000.950
skill + few-shot
1.000
0.950

Image

Image

这一条结果是整本手册最戏剧性的。 Cold 状态下,60 道题没有一道返回正确行——因为模型在凭空发明表名和列名。注入 schema 后,同一个模型直接跳到 95% 可执行。

单题 walkthrough 最直观——Cold 查询:SELECT MIN(lap) FROM your_table_name WHERE qual = 145.926(发明了表名 your_table_name,把列名 laps 单数化为 lap,把 VARCHAR 当成裸数字)。Skill 查询:SELECT MIN(laps) FROM table_name_6 WHERE qual = '145.926'——正确。

Null result 的价值: 在 schema 之上叠加 few-shot 示例,结果一模一样——95%。一旦真正的瓶颈(schema)被移除,更多上下文不产生任何增量。找到那一个承重的知识点,只注入它,不要用更多上下文填充 prompt。

工程实践: "Intent debt"——每次会话 agent 从冷启动开始,缺失的信息被自信的猜测填满。Skill 是偿还这笔债的方式:约定和领域事实写一次,每次 run 都读。


3.3 Maker-Checker 分离

核心问题: 写代码的 agent 能可靠地判断自己的代码是否正确吗?

Image

实验设置: MBPP+ 60 道题,其中 13 个解是真正错误的。对比四种 checker 在误接受率(错误代码被放行的比例)上的表现:

  1. Trust-All:永远接受(纯摆设)
  2. Self-Assess:实现者自己给自己的代码打分
  3. LLM-Judge:另一个独立的 LLM 审查代码,但不执行
  4. Test-Running:checker 从 spec 自己写测试并实际运行,全部通过才 accept(默认 REJECT)

Image

关键机制: Checker 从 spec 生成 4-6 个断言,绝不看实现者的代码,也不看隐藏测试:

def selftest_verify(llm, problem, code, label="selftest"):
    st = gen_self_tests(llm, problem, label=label)
    n_assert = sum(1 for ln in st.splitlines() if ln.strip().startswith("assert"))
if n_assert == 0:
return True, st
return _count_passing(code, st) == n_assert, st

单例 mbppplus/644:实现者自信地认为自己的错误代码是对的(self_assess=True),只有 test-running checker 说 REJECT。

结果:

checker
误接受率
误拒绝率
accept 池精度
trust_all
1.000
0.00
0.783
self_assess
0.769
0.00
0.825
llm_judge
0.538
0.17
0.848
selftest_run0.3080.340.886

Image

Image

Image

从上往下读第一列:

  • 全信接受 100% 的错误代码。
  • 实现者给自己打分仍然接受 77% 的错误解——盲点是共享的。
  • 另一个 LLM 审查降到 54%——但它仍然是"意见"。
  • 只有写测试并运行的 checker 把误接受率砍到 31%,同时 accept 池精度最高(88.6%)。

Null result:Best-of-N 对这个模型完全无效。 在 temperature 0.8 下,这个 32B 模型在每个问题上实际上是确定性的——所有样本要么一起 pass 要么一起 fail,selector 没有 oracle gap 可以利用。与其硬造一个 win,PY 转向测量 verifier 的辨别力。

工程实践——loop 的黄金法则:验证必须针对模型无法用话术绕过的真实信号。 误拒绝 34% 是代价:checker 自身的 LLM 生成的测试有时是错的或过严的。但对一个可以重新生成的 loop,误拒绝只浪费一次额外尝试;误接受则让 bug 进入下游。


3.4 记忆与检索

核心问题: 模型无法知道发生在训练截止日期之后或运行在本机上的事实。Loop 跨多次会话运行时,记忆从哪里来?

Image

实验设置: 22 段短文包含 loop engineering 原则和本项目的实测事实(模型名、+8.3 点提升、31% 误接受率、0%→95% text-to-SQL 结果)。14 道只有从这些段落才能回答的问题。对比 closed-book(只靠模型权重)和 RAG(检索 top-3 相似段落注入 prompt)。

Image

机制: BGE-large embeddings 归一化后,点积就是 cosine similarity——embed 一次,每次查询做一次矩阵乘法即得 top-k。

class EmbeddingMemory:
def index(self, batch_size=64):
self.emb = self.model.encode(self.docs, normalize_embeddings=True, batch_size=batch_size)

def retrieve(self, query, k=3, exclude_idx=None):
        sims = self.emb @ self._encode_query(query)   # cosine,因为向量已归一化
        order = np.argsort(-sims)
# 返回 top-k {index, score, doc, meta}

结果:

条件
正确率
closed-book
7.1%
 (1/14)
retrieval (RAG)
100%
 (14/14)

Image

7% 到 100%。唯一答对的那道是通用安全原则("举出常规黑名单里的一个文件路径。"),模型可以从常识中猜到。每一个项目特定的实测事实都是 closed-book 的盲区。 这个 7% 刻意保留为非零——它证明模型知道通用领域事实,但无法知道具体这次运行产出的数字。

工程实践——检索永远返回 top-k,即使库里没有相关内容。 需要一个相似度阈值:低于阈值的段落丢弃,回退到 closed-book 回答,而不是用不相关的段落"接地"幻觉。读路径只有在能说"我没找到"时才是安全的。


3.5 Worktree 并行隔离

核心问题: 多个 agent 并发编辑同一个文件时会发生什么?

Image

实验设置: 6 个 agent 各写一个 Python 特性函数并注册到共享的 registry.py。对比共享 working tree 和每人一个独立 git worktree + merge。

结果:

条件
存活率
共享 working tree
16.7%
 (1/6)
git worktree + merge
100%
 (6/6, 5 个冲突 surfaced 并 resolved)

Image

共享 tree 上的丢失是静默的。 不报任何错,产生的 registry.py 格式有效、结构完整,但 5 个 agent 的贡献凭空消失。幸存者只是最后一个写入者。如果 agent 运行顺序不同,换一个条目幸存,存活率不变。碰撞不是崩溃,是静默擦除。

而 worktree 方案:5 个冲突被 git 拒绝猜测,每一个都被 surfaced 出来并通过 union 解决。冲突一直都在;隔离 + 显式 merge 是把静默丢失变成可见可解决事件的东西。

工程实践: 这是并发安全的便宜保险。同一个思路可以缩放:给每个并发工作单元自己的隔离环境(这里是 worktree,生产环境是 per-attempt container),让 loop 可以 fan-out 而不被 fan-out 自身摧毁。


3.6 连接器与工具

核心问题: 模型面对需要精确计算的数据时不是计算,是猜。给 loop 一个真正的执行器能有多大提升?

Image

实验设置: 30 道数据分析题(对随机生成的列表计算 sum of divisible、prime count、max-minus-min 等)。Ground truth 由 Python 计算。对比无工具(模型从权重直接猜答案)和有 Python 执行工具的 ReAct agent。

机制: ReAct 循环——模型写 Python 代码 → python_tool 在 subprocess 中实际执行 → 输出喂回模型 → 模型读取真实结果后回答 FINAL:。算术离开了模型的"脑袋"进入了一台不可能算错 72 % 3 == 0 的机器。

结果:

条件
正确率
无工具(从权重猜)
10.0%
 (3/30)
ReAct + Python 工具
83.3%
 (25/30)

Image

10% 到 83%。无工具答案展现两种系统性的失败特征:大数求和偏小(882 猜成 660,910 猜成 615——加法中途停了的特征)和计数坍缩为 round guess(值 10 被输出了 6 次,对应真实答案 17/7/20/9/7/18——一个从未运行的 tally 被预测替代了)。

工具侧的 5 个 miss 全部是 specification error,不是 computation error。 Agent 写了代码但数了错误的东西——adjacency 的 off-by-one、divisibility 标准读错。工具彻底消除了算术错误类,只留下规格错误类。这就是连接器的能力边界。

工程实践——这泛化到远超计算器。 任何时候 loop 需要一个它权重产不出的关于真实世界的事实,同样的结构适用:emit 一个 action → 让外部 actuator 执行 → 读取结果 → 在结果上推理。这就是让 agent 能够跑测试、查数据库、开 PR、移动 ticket 的前提。


六核心小结

核心
解决的问题
基线
结果
Run until done
修复浅层 bug
pass@1 0.767
0.850
(需真实 feedback)
Skills / context
缺失局部知识
0% executable
95%
Maker / checker
自我评估盲点
100% false-accept
31%
Memory / retrieval
跨会话知识
7% closed-book
100%
Worktrees
并发写丢失
17% survives
100%
Connectors / tools
精确计算
10% no-tool
83%

04第四部分:协调与运营

4.1多 Loop 协调

核心问题: 当多个 loop 同时在一个仓库上运行时,它们会不知不觉做重复工作。

Image

这不是 worktree 问题(NB05 解决的)。Worktree 隔离的是写入位置;协调仲裁的是谁有权对目标采取行动。浪费不可见于任何输出级别的检查——每个修复单独看都是正确的——你只能在 token 账单上看到。

实验设置: 三个 loop(ci-sweeper 优先级 1、pr-babysitter 优先级 2、dependency-sweeper 优先级 3),各自有 4 项工作队列,其中 api.py 出现在全部三个队列中。每次修复行动定价 200,000 tokens。

机制: 共享 acting_on registry——行动前先查,如果更高优先级的 loop 已认领,当前 loop 让路。

结果:

指标
无协调
有协调
碰撞次数
5
0
重复(浪费的)行动
5
0
浪费的 tokens
~1,000,000
0

Image

Image

五个碰撞全部由 ci-sweeper(最高优先级)赢得仲裁——仲裁是策略决定的,不是碰运气。没有任何目标被遗漏——每个目标仍然被最高优先级的认领者执行一次。

工程实践: 多 loop 协调 + worktree 隔离是并发安全的两半:worktree 保护 loop 内部的写入;协调仲裁 loop 之间的决策。


4.2 预算、成本与可观测性

核心问题: Token 不是免费的。一个无人值守的 loop 必须知道它即将花多少,并在达到上限时拒绝继续。这是研究 demo 和能长期运行的东西之间的区别。

ImageImage

成本估算器: 从三个参数出发——运行频率、每次运行的 mix(no-op/report/action 的比例)、每次行动的 token 成本。关键发现:频率,而非每次运行的优化,是成本的主导杠杆。 每次运行的成本几乎不变(~19K-23K tokens),但每日总量跨越两个数量级(daily-triage 每天一次 23K vs ci-sweeper 每 15 分钟触发 184.8 万)。

实际运行日志: 本系列 27 次运行共消耗 406,240 tokens,全部可追溯。maker-checker 单独占了一半(204,967 tokens / 3 次运行)。

成本-质量:

策略
tokens
质量 (pass-rate)
每 1k tokens 质量
single shot
10,504
0.767
0.073
loop + feedback
21,680
0.850
0.039
loop - feedback
20,675
0.783
0.038

Image

无 feedback 的 loop 是被支配的——它付了 loop 的价格(20,675 tokens),得到的是接近基线的质量(0.783)。有 feedback 的 loop 用 2.1 倍的 token 成本买了 8.3 个百分点的真实提升。Single shot 的 quality-per-token 最高;loop 在正确性优先时 earn its tokens。

工程实践: Attempt cap 回答"一次运行何时放弃",budget guard 回答"整个系统何时停止花钱"。两者都没有 = S1 事故(有记录的无 cap unattended loop 在 48 小时内烧了约 800 万 tokens)。


4.3 安全线

核心问题: 无人值守 loop 面对一个二进制选择——auto-act 还是 escalate to human。朴素策略"verifier 通过了就 apply"会把危险变更放行。因为通过测试只覆盖你写了测试的路径,不覆盖不可逆操作或高风险变更。

Image

实验设置: 57 个候选动作,其中 38 个真正有风险(21 个直接触碰 denylist 路径如 auth/、payments/、migrations/、secrets/)。三分之二是高风险的——刻意设置的压力测试。

Image

护栏策略使用了三道独立检查:

  1. 路径 denylist(auth/, payments/, billing/, migrations/, secrets/, k8s/production/, .env)
  2. 文件数阈值(≤10 个文件)
  3. 置信度下限(≥0.8)

三道中任何一道不通过就升级。 这是纵深防御:三个独立、保守的检查。

结果:

策略
误自动执行率
升级召回率
升级精确率
朴素(只看 verifier)
60.5% (23/38)
39.5%
—
护栏(多轴决策)0.0%
 (0/38)
100.0%
77.6%

Image

安全线零误自动执行、100% 升级召回——代价是约 11 次误升级(安全却被升级的变更),精确率 77.6%。考虑到一次进入 payments/ 的坏合并是不可逆的而一次多余的 human glance 只浪费几秒,这是正确的犯错方向。

Image

工程实践: Verifier(NB03)判断正确性;护栏判断一个正确的变更是否安全到可以无人值守地 apply。它们是两个不同的轴。默认升级;没有 allowlist 永远不要 auto-merge。


05第五部分:七大生产 Pattern

原语测完后,PY 10–16 将它们指向真实仓库中产生的日常重复工作。

5.1 日度 Triage

问题: 从 5,395 条 HBase bug 报告中捞出约 5% 的 Blocker/Critical。

Image

三类策略:

Image
策略
召回
F1
majority(永远不 flag)
0.000
0.000
关键词规则(14 个 urgent 词)
0.346
0.156
embedding 分类器0.654
0.206

Image

Image

关键词规则抓到 28/81 个紧急报告;embedding 抓到 53/81——多捞 25 个关键词列表里没有对应词汇的紧急报告。 精度方面两者都低(5% 基础率下任何可用召回的策略都会产生大量误报),但 triage 场景下召回是目标——漏掉一条 Blocker 可能意味着生产事故,一个误报只需秒级 dismiss。

工程实践: 这不是自主 gate,是排序和 surfaced 辅助。Loop 在 L1 以 report-only 运行:它产出优先级排序的短列表,人类做决定。


5.2 重复检测

问题: 识别"crash on startup"和"app dies when I open it"是同一个 bug。

Image

实验: 同一个 HBase 语料库(4000 条报告,107 对已知重复)。对比 TF-IDF(词汇匹配)和 BGE embeddings(语义匹配)。

Image
方法
Recall@1
Recall@10
MRR
TF-IDF(词汇)
0.346
0.561
0.414
BGE embeddings0.4950.6540.546

Image

Embedding 在每个 k 都赢,且 Recall@1 的优势最大(+0.149)——embedding 不只是把边缘匹配拖进宽网,它更频繁地把真正的重复排在了第一位。这是语义匹配在改写过的报告上的特征。

诚实发现: 相似度分数在 0.7 以下的阈值携带零可用信号(precision = coin flip)。即使 threshold 拉到 0.85,仍有近 4/10 的 flag 是错的。这个 loop 提议,不自动关闭。


5.3 CI Sweeper——先分类再修

问题: 红的 build 一半是真 bug,一半是 flaky test(间歇性失败,不需代码变更就会自己绿)。

Image

关键内容: 真回归和 flaky test 是不同的东西。真回归有从 intended behavior 到 actual behavior 的确定性差距——存在一个 diff 可以弥合它。Flake 没有这样的差距——失败来自非确定性,不是代码错误。不存在"修复"flake 的 diff。如果你把 implementer 指向一个 flake,它仍然会产出一个 diff(因为那是它做的事),那个 diff 会修改正常工作的应用代码来抑制一个间歇性症状——掩盖 infra 问题 + 添加随机改动。

分类器机制: 把每个失败测试重跑 ~5 次。真的每次都失败;flake 给出混合结果。在这个构造集上分类器准确率 100%(生产中的 flake 如果 50 次只失败 1 次则需要更多重跑——机制是持久的,满分是这个受控集的性质)。

结果:

指标
朴素(修一切)
先分类再修
修复尝试
20
10
浪费在 flake 上的尝试
10
0
浪费在 flake 上的 tokens
~2,000,000
0

Image

Image

两个 sweeper 修了同样的 10 个真 bug——在正确性上打平。分类不增加任何修复能力。全部价值在于它阻止的支出。一次修复尝试 200K tokens;分类成本约 110 tokens(进程重跑,不是模型调用)。当 gate 的成本比它 gate 的动作低 3-4 个数量级时,你一定跑 gate。

工程实践: 在昂贵的 fix loop 前放一个便宜的分类步骤,是整套系统里杠杆率最高的操作之一。


5.4 PR Babysitter

问题: 保姆式照料 PR 从红到绿,但决不能在"准备好了"这件事上相信实现者的自报。

Image

实验: 30 个 PR(6 个红的)。对比两个策略:naive(问实现者"修好了吗?")和 verifier-gated(跑真实测试,读 all_pass)。

PR 是一台状态机:open → red CI → under repair → ready to merge → merged。大部分转换便宜且可逆。但 "ready to merge"是 loop 停止保护代码库的边界——这整本手册的核心问题是:什么可以合法地 gate 这一条边?

结果:

策略
false-ready 率
naive(相信自报)
33.3%
 (2/6)
verifier-gated0.0%
 (0/6)

Image

Image

两个 case 的实现者都声称修好了但测试说没有。同时,fix loop 确实修好并验证了 3 个红 PR。

工程实践: 合并就绪状态只能通过真实测试运行来达成,永远不能靠自报。"看起来很对"和"实现者说对了"与正确性相关,但不是对正确性的测量。


5.5 Dependency Sweeper——按风险路由

问题: 无人值守的依赖 bot 如果"绿了就 merge"是危险的。通过测试不捕获 major version API break 或恶意包。

Image

实验: 60 个依赖 PR,48 个有风险(major bump、CVE、denylisted 包如 cryptography/django/requests)。

路由逻辑:sweeper_merge(u) = (tier == "patch") AND (severity != "high") AND (not denylisted) AND (green_ci)。只有 patch 级别 + 无高危 CVE + 不在 denylist + CI 绿 → 自动 merge。其余全部升级给人。

结果:

策略
误自动合并率
naive(绿了就 merge)
83.3%
 (40/48 risky)
风险路由0.0%
 (0/48 risky)

Image

Image

路由器同时保持了全部收益——12 个安全 patch 全部自动 apply。它丢了全部伤害而保留了全部收益。路由决策本身是策略查表而非模型调用,约 0 tokens。

工程实践: 这是 NB09 安全护栏在依赖场景的专门化。allowlist 可证明低风险的形状(patch + 无 CVE + 非关键包),不是 denylist 已知坏东西。成本和收益不对称时,策略应不对称。


5.6 合入后清理——找技术债

问题: 代码合并后,自承认技术债(SATD)潜伏在注释里。关键词 grep(TODO / FIXME / HACK)抓带标记的,但很多债是平实叙述的:"this is a temporary workaround until the API stabilizes"。

Image

实验: Maldonado SATD 数据集,4000 条标记 commit(平衡采样),其中 ~7% 真含债。对比关键词规则(15 个标记词)和 BGE embedding → LogisticRegression。

结果:

方法
Precision
Recall
F1
关键词规则
0.979
0.762
0.857
embedding 分类器
0.834
0.855
0.844

Image

Image

Embedding 多找到 56 条真正债(513 vs 457),代价是约 92 条额外误报。F1 降了 1.4 个点——但刻意保留了这个结果。

工程实践: 清理是一个检测问题→修复问题。对于 review 队列,漏一条债的代价远大于人类多看一个候选。路由策略:关键词命中(precision 0.979)直送小修复自动提议(误报浪费 tokens);embedding 命中的宽集送人类队列(recall 0.855 是关键,reviewer 便宜吸收误报)。


5.7 Changelog 起草

问题: 从已合并的 commit 自动起草 release notes——先分类,再生成。

Image

实验: 108 条 Vue core commit(9 个类型各 12 条),去掉 conventional-commit 前缀,让模型恢复类型标签。

Image

结果:

基准
macro-F1
majority baseline(永远猜 ci)
0.111
模型 zero-shot 分类器
0.427

Image

约 4 倍于基线,足够起草分组化的、人类可编辑的笔记。用 macro-F1 是因为 perf 这种稀有类型和 fix 一样重要——不能让模型靠一直猜 fix 作弊。

结果: 0.43 是真实且有限的结果,不是接近解决。超过一半的 commit 分类错误,因为去掉前缀后"update docs for the new API"(docs)和"add the new API"(feat)和"fix the new API regression"(fix)看起来几乎一样。这个 loop 起草,不发布;人类修改后发布。


06第六部分:终章——编排成团队

核心理念

Capstone 不是新技术,它是把前面所有原语串成一条 pipeline:

Image

Stage 1: Run-until-done refinement loop(NB01,含 execution feedback)
    ↓ 如果仍失败
Stage 2: 采样 4 个候选(temperature 0.8)
    ↓
Stage 3: Checker 从 spec 写测试并运行,选出最佳候选(NB03)
def orchestra(p):
# stage 1+2: run-until-done loop with execution feedback
    r = loops.self_refine(llm, p, max_attempts=3, use_feedback=True)
if r["solved"]:
return True
# stage 3: maker/checker fallback
    cands = agents.sample_n(llm, p, n=4, temperature=0.8)
    st = agents.gen_self_tests(llm, p)
    bi, _ = agents.select_by_tests(p, cands, st)
return agents.grade(p, cands[bi])["all_pass"]

结果

在 40 道 held-out MBPP+ 题上:

条件
解决率
tokens
single agent(greedy)
0.800
 (32/40)
—
orchestra0.950
 (38/40)
26,844

Image

6 道题从 fail 翻成 pass,每一次翻盘都被真实测试验证过。2 道题贯穿所有阶段仍未解决——深层算法错误无法被浅层修复纠正。这是 orchestra 的边界。

SWE-bench 验证:用真实 SWE-bench Docker harness 跑 3 个 gold patch,3/3 resolved,零错误。这确认了整本书论点的 ground-truth verifier 在真实基础设施上是活且正确的。

Null results 的价值

这些是刻意保留的结果,它们是整个课程最有用的部分:

  1. Run until done 取决于 feedback,不是 retry。 +8.3 vs +1.7 的差距。
  2. Best-of-N 对这个模型加零增益。 所以测了 verifier 的辨别力而不是硬造一个 win。
  3. Closed-book 是 7%,不是 0%。 几个事实是可猜的;项目特定的实测事实完全不可猜——所以 retrieval 仍然达到 100%。
  4. Recall ↑ 而 F1 ↓。 技术债检测器召回提升 9.3 个百分点,代价是更多误报。如实报告。
  5. 更多 stage 不总是更好。 Orchestra 在这里赢了,但讨论保持了 MAST 框架(Cemri et al., 2503.13657):编排只在额外验证和选择捕捉到真实错误时才 earn its keep。

0717 项结果总览

#
组件
数据集(真实)
基线
加组件后
关键提升
00
foundations + scorer
smoke set (5)
pass@1 0.80
引擎和评分器已验证
仪器在测量前已被证明是可信的
01
run until done
MBPP+ (60)
pass@1 0.767
0.850
+8.3 pts(无 feedback 仅 +1.7)
02
skills / context
sql-create-context (60)
0% executable
95%
0% → 95%
03
maker / checker
MBPP+ (60, 13 wrong)
100% false-accept
31%
捕获 69% 的错误代码
04
memory / retrieval
project KB (14 Qs)
7% closed-book
100%
7% → 100%
05
worktrees
real git, 6 agents
17% survives
100%
零静默丢失
06
connectors / tools
data-analysis (30)
10% no-tool
83%
+73 pts
07
multi-loop 协调
3 loops on 1 repo
5 collisions, ~1M tokens
0
碰撞消除
08
budget / observability
measured runs
n/a
cost-quality frontier
每次提升都有确定的 token 价格
09
safety / guardrails
57 actions (38 risky)
60.5% false auto
0%
所有高风险变更被升级
10
daily triage
GitBugs hbase (5395)
recall 0.35
0.65
~2x 急报召回
11
duplicate detection
GitBugs hbase (107 pairs)
recall@10 0.56
0.65
语义重复被捕获
12
CI sweeper
flaky vs real (20)
修全部 20
修 10 个真回归
~2M tokens 节省
13
PR babysitter
MBPP+ red PRs (30)
33% false ready
0%
零坏 PR 标 ready
14
dependency sweeper
semver + CVE (60)
83% false merge
0%
风险 bump 升级给人
15
post-merge cleanup
Maldonado SATD (4000)
recall 0.76
0.86
捕获 prose 中未标记的债
16
changelog drafter
Vue commits (108)
majority 0.11
macro-F1 0.43
~4x baseline
17
capstone orchestra
MBPP+ (40) + SWE-bench
single 0.80
orchestra 0.95
+15 pts; 3/3 gold resolved

08附录:共享库参考

整个课程的所有 PY 都从 common/ 导入同一套库。这使得跨 PY 比较有效——同一个模型调用、同一个评分器、同一个 Problem 类型。任何测量到的差异只能来自被测的 loop 组件。

common/llm.py — LLM 客户端

  • LLM — OpenAI-compatible wrapper,包装 vLLM endpoint。chat(messages, temperature, max_tokens, n, stop, seed, label, retries) 每次调用计时并记录 tokens。track(label) context manager 提供隔离的子账户,完成后折叠回主使用量。
  • Usage — 累积 token 计数、调用次数、样本数、秒数,按 label 分桶。
  • Generation — dataclass:texts, prompt_tokens, completion_tokens, seconds, label。

common/eval.py — 可信评分器

所有执行类评分的唯一真相源。代码在 temp dir + subprocess + timeout 中运行:

  • run_program(code, stdin, timeout) → RunResult
  • check_io(code, io_tests, timeout) — stdin/stdout 程序评测。
  • check_asserts(code, test_block, timeout) — 断言评测。
  • pass_at_k(n, c, k) — Chen et al. 无偏 pass@k 估计器。
  • exact_match, edit_similarity — 字符串相似度。
  • sql_exec_match(db_path, pred_sql, gold_sql) — SQL 结果集匹配(序不敏感)。
  • prf1(tp, fp, fn), recall_at_k, average_precision — IR/分类指标。
  • self_test() → dict — 运行已知好/坏样本,断言评分器正确。

common/data.py — 数据集加载器

所有加载器归一化到统一的 Problem dataclass:

@dataclass
class Problem:
id: str
    prompt: str
    entry_point: str = ""
    kind: str = "asserts"# "asserts" or "io"
    tests: Any = ""
    meta: dict = field(default_factory=dict)
  • SMOKE_PROBLEMS + load_smoke() — 5 道免网络的测试题。
  • load_mbpp_plus(limit, seed) — EvalPlus MBPP+。
  • load_sql_create_context(limit, seed) — text-to-SQL + schema。
  • load_gitbugs(project, base, max_corpus, seed) — 真实 bug 报告语料库。

common/agents.py — 生成与评审构建块

所有 PY 使用相同的 prompt 生成和代码提取逻辑:

  • single_shot(llm, problem, ...) → (code, generation) — 一次生成 + 提取代码块。
  • grade(problem, code) → dict — 根据 problem.kind 分发到 check_io 或 check_asserts。
  • sample_n(llm, problem, n, temperature) → list[str] — 采样 n 个候选(maker)。
  • gen_self_tests(llm, problem) → str — 从 spec 生成 CodeT 风格测试(checker)。
  • select_by_tests(problem, candidates, self_tests) → (best_index, scores) — 执行测试选出最佳候选。
  • self_assess(llm, problem, code) → bool — 模型评判自己的解(不信任)。
  • judge_correct(llm, problem, code) → bool — 独立 LLM 审查,不执行。
  • selftest_verify(llm, problem, code) → (passed, self_tests) — 写并运行测试,默认 REJECT。

common/loops.py — 迭代引擎

  • self_refine(llm, problem, max_attempts=3, temperature=0.0, use_feedback=True) → dict — 主 loop。返回 solved, attempts, solved_at, code, history。
  • _assert_feedback(problem, code) → str — 捕获真实 stderr/traceback。
  • _io_feedback(problem, code) → str — stdin/stdout 程序的失败描述。
  • _refine_prompt(problem, code, feedback) → str — 构建包含前次尝试和具体失败的修复 prompt。

common/memory.py — 向量记忆

  • EmbeddingMemory(model_name="BAAI/bge-large-en-v1.5") — add(docs) → index(batch_size) → retrieve(query, k) → list[{index, score, doc, meta}]。归一化向量使点积 = cosine similarity。

common/tools.py — Python 执行工具

  • python_tool(code, timeout) → str — subprocess 执行,返回 stdout 或截断的错误。
  • react_solve(llm, question, max_steps=4) → str — 最小 ReAct loop:emit code block → 执行 → 喂回输出。

common/sqltools.py — SQL 专用评分

  • schema_validity(pred_sql, create_sql) → bool — 每个标识符都在 schema 中存在?
  • exec_equiv(create_sql, pred_sql, gold_sql) → bool — 构建合成 SQLite 数据库,运行两个查询,比较结果集。

common/show.py — 输出格式化

  • head(title), sub(title), step(n, msg) — 分节标记。
  • kv(label, value), metric(name, value) — 键值和指标。
  • code(snippet, title), text(body, title) — 代码块和文本。
  • diagram(name, caption) — 从 images/ 嵌入手绘图。
  • compare(name, baseline, treatment) → dict — A/B 对比含 delta、百分比变化和箭头。

common/plotting.py — 图表

  • ab_bar(name, labels, values, ...) — 条形图(保存到 results/figures/ 并内联渲染)。
  • line(name, x, series: dict, ...) — 折线图。
  • scatter_pareto(name, points, ...) — 成本/质量 Pareto 散点图。
  • Agg 后端,无需显示服务器即可导入。

common/runlog.py — 结果持久化

  • save_metrics(name, data) → Path — 写入 results/<name>.json。
  • log_run(pattern, duration_s, items_found, actions_taken, escalations, tokens_estimate, outcome) → dict — 追加结构化条目到 results/loop-run-log.json。
  • load_metrics(name), load_run_log() — 读取。预算 PY(08)从这里读取重建成本模型。

09写在最后

贯穿这 17 个组件 + 1 套共享库的规律只有一条:

一个 loop 的好坏,取决于它所连接的可验证信号的质量。

接在模型自我意见上的 loop 基本不动。接在真实测试 / 真实 schema / 检索到的事实 / 真实评估 harness 上的 loop,每一次都可测量地向前移动。

如果你要从零搭建这样的系统,按本手册的顺序来:

  1. 把引擎和评分器立起来,先证明评分器是对的(PY00 的 self-test)。
  2. 一次加一个原语,测量它买了什么,再加下一个。
  3. 不要一开始就组装整个乐团然后祈祷。

所有源码均可以在公众号【智性未来】点击下面👇名片,关注公众号后回复【loop】即可自动免费获取!

图片