这份 Agentic Loop Engineering 工程手册火了!附完整代码!
这是一份完全可复现的 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 系统的工程热点高在过去两年里经历了四次变迁:
Prompt Engineering:写一个好 prompt,读一个回复。你在 prompt 框里调整措辞。 Context Engineering:为一个 turn 组装正确的上下文——文件、文档、示例。你开始给模型"看到什么"。 Harness Engineering:设计单个 agent 的运行环境——工具、权限、规则。你开始设计 agent 的"活动空间"。 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 上验证结果。
三条测量规则
本手册所有数字的可信度建立在三条规则之上:
验证信号,再接入 loop。 PY 00 的大部分精力花在评分器而不是模型上。如果评分器静默地给错误代码打 pass,每个 downstream 增益都是虚构的。 基线必须先真正失败。 每个 PY 先展示朴素尝试在真实数据上的低分或不安全行为,再展示组件如何弥合差距。 报告 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 环境固定
整个系列固定在一组实验常数上:
http://localhost:8000/v1 | |
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。这道线是整本手册的参照。
03第三部分:六大核心
3.1 Run Until Done——关键是 feedback,不是 retry
核心问题: 让模型"再试一次"和给它真实的失败反馈,区别有多大?
实验设置: 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)
结果:
| 0.850 | ||||
核心结果:
有真实 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。
这条曲线复现了 Huang et al. (2023) 在真实代码上的结果——LLM 在没有外部 feedback 时无法可靠自我纠错。Loop 的价值来自信号,不是来自多问几次。
3.2 Skill 注入与 Context Engineering
核心问题: 当模型缺少不可猜测的局部知识(数据库 schema、项目约定)时会发生什么?
实验设置:sql-create-context benchmark,60 道自然语言→SQL 题。三个条件:cold(只给问题)、skill(注入 CREATE TABLE schema)、skill + few-shot(再加两个示例)。
两个评分维度:
schema_validity(便宜):查询中的每个标识符是否真实出现在 schema 中?检测"发明列名"。exec_equiv(严格):构建真实 SQLite 数据库,预测 SQL 和 gold SQL 返回相同的行吗?
结果:
| 0.000 | ||
| 1.000 | 0.950 | |
这一条结果是整本手册最戏剧性的。 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 能可靠地判断自己的代码是否正确吗?
实验设置: MBPP+ 60 道题,其中 13 个解是真正错误的。对比四种 checker 在误接受率(错误代码被放行的比例)上的表现:
Trust-All:永远接受(纯摆设) Self-Assess:实现者自己给自己的代码打分 LLM-Judge:另一个独立的 LLM 审查代码,但不执行 Test-Running:checker 从 spec 自己写测试并实际运行,全部通过才 accept(默认 REJECT)
关键机制: 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。
结果:
| selftest_run | 0.308 | 0.34 | 0.886 |
从上往下读第一列:
全信接受 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 跨多次会话运行时,记忆从哪里来?
实验设置: 22 段短文包含 loop engineering 原则和本项目的实测事实(模型名、+8.3 点提升、31% 误接受率、0%→95% text-to-SQL 结果)。14 道只有从这些段落才能回答的问题。对比 closed-book(只靠模型权重)和 RAG(检索 top-3 相似段落注入 prompt)。
机制: 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}
结果:
| 7.1% | |
| 100% |
7% 到 100%。唯一答对的那道是通用安全原则("举出常规黑名单里的一个文件路径。"),模型可以从常识中猜到。每一个项目特定的实测事实都是 closed-book 的盲区。 这个 7% 刻意保留为非零——它证明模型知道通用领域事实,但无法知道具体这次运行产出的数字。
工程实践——检索永远返回 top-k,即使库里没有相关内容。 需要一个相似度阈值:低于阈值的段落丢弃,回退到 closed-book 回答,而不是用不相关的段落"接地"幻觉。读路径只有在能说"我没找到"时才是安全的。
3.5 Worktree 并行隔离
核心问题: 多个 agent 并发编辑同一个文件时会发生什么?
实验设置: 6 个 agent 各写一个 Python 特性函数并注册到共享的 registry.py。对比共享 working tree 和每人一个独立 git worktree + merge。
结果:
| 16.7% | |
| 100% |
共享 tree 上的丢失是静默的。 不报任何错,产生的 registry.py 格式有效、结构完整,但 5 个 agent 的贡献凭空消失。幸存者只是最后一个写入者。如果 agent 运行顺序不同,换一个条目幸存,存活率不变。碰撞不是崩溃,是静默擦除。
而 worktree 方案:5 个冲突被 git 拒绝猜测,每一个都被 surfaced 出来并通过 union 解决。冲突一直都在;隔离 + 显式 merge 是把静默丢失变成可见可解决事件的东西。
工程实践: 这是并发安全的便宜保险。同一个思路可以缩放:给每个并发工作单元自己的隔离环境(这里是 worktree,生产环境是 per-attempt container),让 loop 可以 fan-out 而不被 fan-out 自身摧毁。
3.6 连接器与工具
核心问题: 模型面对需要精确计算的数据时不是计算,是猜。给 loop 一个真正的执行器能有多大提升?
实验设置: 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% | |
| 83.3% |
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 的前提。
六核心小结
| 0.850 | |||
| 95% | |||
| 31% | |||
| 100% | |||
| 100% | |||
| 83% |
04第四部分:协调与运营
4.1多 Loop 协调
核心问题: 当多个 loop 同时在一个仓库上运行时,它们会不知不觉做重复工作。
这不是 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 让路。
结果:
| 0 | ||
五个碰撞全部由 ci-sweeper(最高优先级)赢得仲裁——仲裁是策略决定的,不是碰运气。没有任何目标被遗漏——每个目标仍然被最高优先级的认领者执行一次。
工程实践: 多 loop 协调 + worktree 隔离是并发安全的两半:worktree 保护 loop 内部的写入;协调仲裁 loop 之间的决策。
4.2 预算、成本与可观测性
核心问题: Token 不是免费的。一个无人值守的 loop 必须知道它即将花多少,并在达到上限时拒绝继续。这是研究 demo 和能长期运行的东西之间的区别。
成本估算器: 从三个参数出发——运行频率、每次运行的 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 次运行)。
成本-质量:
无 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"会把危险变更放行。因为通过测试只覆盖你写了测试的路径,不覆盖不可逆操作或高风险变更。
实验设置: 57 个候选动作,其中 38 个真正有风险(21 个直接触碰 denylist 路径如 auth/、payments/、migrations/、secrets/)。三分之二是高风险的——刻意设置的压力测试。
护栏策略使用了三道独立检查:
路径 denylist( auth/,payments/,billing/,migrations/,secrets/,k8s/production/,.env)文件数阈值(≤10 个文件) 置信度下限(≥0.8)
三道中任何一道不通过就升级。 这是纵深防御:三个独立、保守的检查。
结果:
| 护栏(多轴决策) | 0.0% | 100.0% |
安全线零误自动执行、100% 升级召回——代价是约 11 次误升级(安全却被升级的变更),精确率 77.6%。考虑到一次进入 payments/ 的坏合并是不可逆的而一次多余的 human glance 只浪费几秒,这是正确的犯错方向。
工程实践: Verifier(NB03)判断正确性;护栏判断一个正确的变更是否安全到可以无人值守地 apply。它们是两个不同的轴。默认升级;没有 allowlist 永远不要 auto-merge。
05第五部分:七大生产 Pattern
原语测完后,PY 10–16 将它们指向真实仓库中产生的日常重复工作。
5.1 日度 Triage
问题: 从 5,395 条 HBase bug 报告中捞出约 5% 的 Blocker/Critical。
三类策略:
| embedding 分类器 | 0.654 |
关键词规则抓到 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。
实验: 同一个 HBase 语料库(4000 条报告,107 对已知重复)。对比 TF-IDF(词汇匹配)和 BGE embeddings(语义匹配)。
| BGE embeddings | 0.495 | 0.654 | 0.546 |
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(间歇性失败,不需代码变更就会自己绿)。
关键内容: 真回归和 flaky test 是不同的东西。真回归有从 intended behavior 到 actual behavior 的确定性差距——存在一个 diff 可以弥合它。Flake 没有这样的差距——失败来自非确定性,不是代码错误。不存在"修复"flake 的 diff。如果你把 implementer 指向一个 flake,它仍然会产出一个 diff(因为那是它做的事),那个 diff 会修改正常工作的应用代码来抑制一个间歇性症状——掩盖 infra 问题 + 添加随机改动。
分类器机制: 把每个失败测试重跑 ~5 次。真的每次都失败;flake 给出混合结果。在这个构造集上分类器准确率 100%(生产中的 flake 如果 50 次只失败 1 次则需要更多重跑——机制是持久的,满分是这个受控集的性质)。
结果:
| 10 | ||
| 0 | ||
| 0 |
两个 sweeper 修了同样的 10 个真 bug——在正确性上打平。分类不增加任何修复能力。全部价值在于它阻止的支出。一次修复尝试 200K tokens;分类成本约 110 tokens(进程重跑,不是模型调用)。当 gate 的成本比它 gate 的动作低 3-4 个数量级时,你一定跑 gate。
工程实践: 在昂贵的 fix loop 前放一个便宜的分类步骤,是整套系统里杠杆率最高的操作之一。
5.4 PR Babysitter
问题: 保姆式照料 PR 从红到绿,但决不能在"准备好了"这件事上相信实现者的自报。
实验: 30 个 PR(6 个红的)。对比两个策略:naive(问实现者"修好了吗?")和 verifier-gated(跑真实测试,读 all_pass)。
PR 是一台状态机:open → red CI → under repair → ready to merge → merged。大部分转换便宜且可逆。但 "ready to merge"是 loop 停止保护代码库的边界——这整本手册的核心问题是:什么可以合法地 gate 这一条边?
结果:
| 33.3% | |
| verifier-gated | 0.0% |
两个 case 的实现者都声称修好了但测试说没有。同时,fix loop 确实修好并验证了 3 个红 PR。
工程实践: 合并就绪状态只能通过真实测试运行来达成,永远不能靠自报。"看起来很对"和"实现者说对了"与正确性相关,但不是对正确性的测量。
5.5 Dependency Sweeper——按风险路由
问题: 无人值守的依赖 bot 如果"绿了就 merge"是危险的。通过测试不捕获 major version API break 或恶意包。
实验: 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。其余全部升级给人。
结果:
| 83.3% | |
| 风险路由 | 0.0% |
路由器同时保持了全部收益——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"。
实验: Maldonado SATD 数据集,4000 条标记 commit(平衡采样),其中 ~7% 真含债。对比关键词规则(15 个标记词)和 BGE embedding → LogisticRegression。
结果:
| 0.979 | 0.857 | ||
| 0.855 |
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——先分类,再生成。
实验: 108 条 Vue core commit(9 个类型各 12 条),去掉 conventional-commit 前缀,让模型恢复类型标签。
结果:
ci) | |
| 0.427 |
约 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:
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+ 题上:
| 0.800 | ||
| orchestra | 0.950 |
6 道题从 fail 翻成 pass,每一次翻盘都被真实测试验证过。2 道题贯穿所有阶段仍未解决——深层算法错误无法被浅层修复纠正。这是 orchestra 的边界。
SWE-bench 验证:用真实 SWE-bench Docker harness 跑 3 个 gold patch,3/3 resolved,零错误。这确认了整本书论点的 ground-truth verifier 在真实基础设施上是活且正确的。
Null results 的价值
这些是刻意保留的结果,它们是整个课程最有用的部分:
Run until done 取决于 feedback,不是 retry。 +8.3 vs +1.7 的差距。 Best-of-N 对这个模型加零增益。 所以测了 verifier 的辨别力而不是硬造一个 win。 Closed-book 是 7%,不是 0%。 几个事实是可猜的;项目特定的实测事实完全不可猜——所以 retrieval 仍然达到 100%。 Recall ↑ 而 F1 ↓。 技术债检测器召回提升 9.3 个百分点,代价是更多误报。如实报告。 更多 stage 不总是更好。 Orchestra 在这里赢了,但讨论保持了 MAST 框架(Cemri et al., 2503.13657):编排只在额外验证和选择捕捉到真实错误时才 earn its keep。
0717 项结果总览
| +8.3 pts(无 feedback 仅 +1.7) | |||||
| 0% → 95% | |||||
| 7% → 100% | |||||
| 零静默丢失 | |||||
| +73 pts | |||||
| 所有高风险变更被升级 | |||||
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) → RunResultcheck_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,每一次都可测量地向前移动。
如果你要从零搭建这样的系统,按本手册的顺序来:
把引擎和评分器立起来,先证明评分器是对的(PY00 的 self-test)。 一次加一个原语,测量它买了什么,再加下一个。 不要一开始就组装整个乐团然后祈祷。
所有源码均可以在公众号【智性未来】点击下面👇名片,关注公众号后回复【loop】即可自动免费获取!