对应 Maps to:CMU 17-630 Prompt Engineering(Prof. Travis Breaux),第 7 周 · Code Generation(原课作业 3”Prompt Augmentation”在这一周截止)。原课 Schedule 对这一讲的说明(意译):软件开发辅助工具的研究现状——代码模型的演变、评估生成代码的指标、自我调试,以及软件开发智能体的最新进展。 依据说明:原课的 slides 和阅读清单不公开。Schedule 点名的四项——代码模型的演变(§1)、评估指标(§2、§4)、自我调试(§5)、软件开发智能体(§6)——各占一节;§3(沙箱:怎样安全地运行模型写的代码)是为没有系统背景的读者补充的,§5.4(CodeT)是讲义补充的。材料来自 Chen et al.(2021,Codex / HumanEval)、Austin et al.(2021,MBPP)、Li et al.(2022,AlphaCode)、Liu et al.(2023,EvalPlus)、Ren et al.(2020,CodeBLEU)、Chen et al.(2023,CodeT)、Chen et al.(2024,Self-Debugging)、Jimenez et al.(2024,SWE-bench)、Yang et al.(2024,SWE-agent)、Xia et al.(2024,Agentless)等;引用的数字都核对过原文。全部实验是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:
code/lecture11/(只用 Python 标准库;3 个模块 + 7 个脚本;不需要 API key;第一次运行要在沙箱里执行所有可能的程序,每个脚本 15 秒以内,之后读缓存) 预计阅读时间:3.5 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说出代码为什么是提示工程里一个特殊的领域:代码可以被执行,执行结果是一个客观、便宜、外部的反馈信号。
- 解释 pass@k 的含义,写出它的无偏估计,并说明为什么朴素的 会偏低;知道温度怎样影响 pass@1 和 pass@100。
- 说明为什么 BLEU、精确匹配不适合评估代码,以及”测试不够”怎样让 pass@k 虚高。
- 描述一个最小的代码沙箱(子进程、超时、资源上限、临时目录),并知道它能防住什么、防不住什么。
- 设计自我调试的反馈(失败的测试、报错信息、解释),知道它的上限由”模型看得见的测试”决定;在没有测试时用 CodeT / 多数投票挑程序。
- 解释 SWE-bench 在测什么,以及”智能体–计算机接口”(ACI)为什么会大幅影响智能体的表现。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| 第 4 讲:self-consistency(多数投票) | 提示注入通过工具和代码造成的危害(第 12 讲) |
| 第 8 讲:工具使用、错误信息就是提示、CRITIC | 智能体的规划与记忆(第 13–14 讲) |
| 第 10 讲:外部反馈 vs 内在的自我纠正;开发集虚高 | 评估设计与 benchmark 污染(第 17 讲) |
| 前置讲 P2:进程、HTTP、重试、缓存 |
0.3 本讲地图 Map
§1 代码模型的演变:为什么代码特殊
§2 HumanEval 与 pass@k:用执行来评估 ← 代码 02
§3 沙箱:怎样安全地运行模型写的代码 ← 代码 01
§4 指标:为什么不用 BLEU;测试够不够;数据污染 ← 代码 02、03
§5 自我调试与执行反馈:Self-Debugging、CodeT ← 代码 04、05
§6 软件工程智能体:SWE-bench、SWE-agent 与 ACI ← 代码 06
§7 在真实模型上 ← 代码 07(可选)
§8 小结 §9 常见误区 §10 练习 §11 延伸阅读 §12 下一讲预告
0.4 记号与术语 Notation & terms
| 术语 | 本讲义里的意思 |
|---|---|
| 功能正确性(functional correctness) | 程序通过测试,而不是”长得像参考答案” |
| pass@k | 每道题让模型写 k 次,至少一次通过全部测试的题目比例 |
| 基础测试 / 加强测试 | 像 HumanEval 那样每题几个测试 / 像 EvalPlus 那样补上边界情况 |
| 给定的测试(visible tests) | 题目里给模型看的测试;评估用的测试可以更多,模型看不见 |
| 沙箱(sandbox) | 一个隔离的执行环境:被测代码出任何问题都不影响评测程序 |
| 自我调试(self-debugging) | 把执行结果(失败的测试、报错)交给模型,让它修改自己的程序 |
| ACI(agent-computer interface) | 给智能体用的命令和它们的输出格式,类似给人用的 IDE |
0.5 本讲的实验装置 · The setup
一个小型的 “HumanEval”(code_lab.py):20 道题,每道题是一个函数签名加文档字符串:
def max_subarray(nums):
"""Return the largest sum of a non-empty contiguous subarray of nums."""
每道题有 1–3 个基础测试(像 HumanEval:只覆盖常见情况)和 3–5 个加强测试(像 EvalPlus:空输入、负数、重复值、大小写……)。
模拟的代码模型 SimCoder 不是一个神经网络,而是这样写代码的:每道题有 1–2 种写法(模板),模板里有几个”容易写错的地方”(槽位),每个槽位有参考写法、等价的正确写法和常见的错误写法:
def max_subarray(nums):
best = cur = <<INIT>> # nums[0](对)/ 0(常见错误:全是负数时错)
for x in nums<<REST>>: # [1:](对)/ 空(和 nums[0] 搭配时会把第一个数算两次)
cur = <<UPD>> # max(x, cur + x)(对)/ cur + x(错)
best = max(best, cur)
return best
模型按每个选项的 logit 和温度采样;“能力”越强,参考写法的 logit 越高;题目越难,越容易写错。small / medium / large 三种规模只差一个能力参数。程序是真的 Python 代码,执行和测试是真的——只有”模型会写出哪个程序”是模拟的。所以它能演示评估方法本身的性质(估计量的偏差、测试不足、BLEU 的问题、反馈的作用),但不能说明真实模型的数字;每次用它,讲义都给出论文里真实模型的数字作对照。
所有程序都在 sandbox_runner.py 的子进程里运行(§3),结果缓存在 cache/ 里。§6 另有一个迷你仓库 swe_lab.py。
1. 代码模型的演变:为什么代码特殊 · Why code is special
1.1 代码是”可以被执行的文本”
对语言模型来说,代码只是另一种文本:GitHub 上有海量的代码,模型在上面预训练,就学会了续写代码。但和普通文本相比,代码有一个根本的不同:它可以被执行。
这一点改变了提示工程的很多东西:
- 评估:一篇摘要好不好需要人或判官(第 10 讲)来判断;一个函数对不对,跑一下测试就知道。
- 反馈:第 10 讲反复看到,自我纠正要有可靠的外部反馈。代码天然有:报错信息、失败的测试、运行结果。
- 挑选:模型写 100 个版本,跑测试就能挑出对的那个——生成很多个、用执行筛选,这是代码生成里最有效的技巧之一。
但执行也带来了新的问题:模型写的代码是不可信的,必须在隔离的环境里运行(§3);测试本身可能不够(§4.2);智能体可能学会”让测试通过”而不是”把问题修好”(§5.3)。
1.2 一段很短的历史
- Codex(Chen et al. 2021):在 GitHub 的 Python 代码上微调 GPT-3。论文同时发布了 HumanEval(164 道手写的题,平均每题 7.7 个单元测试)和 pass@k 指标。Codex-12B 的 pass@1 是 28.8%,GPT-3 是 0%,GPT-J 是 11.4%;每题采样 100 次时 Codex 解决 70.2%。它是 GitHub Copilot 最初的模型。
- MBPP(Austin et al. 2021):974 道入门级的 Python 题,每题 3 个测试——另一个常用的 benchmark。
- AlphaCode(Li et al. 2022):竞赛编程。核心是大规模采样 + 按程序行为过滤:每道题生成大量程序,用题目给的样例过滤,再按”在新输入上的行为”聚类挑出少量提交。在模拟的 Codeforces 比赛中平均排在前 54.3%(参赛者超过 5000 人的比赛)。
- 开源代码模型:InCoder、CodeGen、StarCoder(BigCode 社区,许可证宽松的代码数据集 The Stack)、Code Llama、DeepSeek-Coder、Qwen-Coder 等。几个反复出现的技术主题:
- 中间填充(fill-in-the-middle,Bavarian et al. 2022):训练时把文档切成”前缀 / 中间 / 后缀”,让模型学会根据前后文填中间——IDE 里补全光标处的代码正需要这个;
- 更长的上下文:理解一个仓库需要看很多文件;
- 代码的指令微调(第 3 讲)和用执行结果做强化学习(测试通过就是奖励)。
- 今天:最强的通用模型也是最强的代码模型;推理模型(第 13 讲)在竞赛题上大幅进步;评估的重心从”写一个函数”转向”在真实仓库里修一个 issue”(§6)。
这一讲不追逐最新的排行榜,而是讲这条演变中不变的方法:用执行评估、用执行反馈、用执行挑选,以及给智能体设计好的接口。
2. HumanEval 与 pass@k · Evaluating by execution
2.1 pass@k 是什么
HumanEval 的每道题给模型一个函数签名和文档字符串,模型续写函数体,然后跑隐藏的单元测试。pass@k:每道题让模型写 k 次,只要有一次通过全部测试就算解决;pass@k 是解决的题目比例。
为什么要 k?因为”第一次就写对”和”能写对”是两回事。用户在 IDE 里会看好几个补全建议;AlphaCode 式的系统会生成成百上千个再筛选。pass@1 衡量前者,pass@100 衡量模型”有没有能力”写出正确程序。
02_pass_at_k.py 演示 1:20 道题,T = 0.8,每题 200 个样本(括号里是模拟器精确算出的真实值):
| 模型 | 贪心 pass@1 | pass@1 | pass@10 | pass@100 |
|---|---|---|---|---|
| small | 30% | 27.3%(27.5%) | 78.0%(78.8%) | 97.4%(96.7%) |
| medium | 45% | 45.3%(45.0%) | 92.4%(93.2%) | 99.9%(99.9%) |
| large | 80% | 62.1%(62.9%) | 99.1%(98.9%) | 100.0%(100.0%) |
small 模型贪心只解决 30% 的题,采样 100 次时 97%。
2.2 怎样估计:一个容易犯的错误
最直接的做法:每题采样 k 次,看有没有对的。但这样方差很大,而且要为每个 k 重新采样。Chen et al.(2021)的做法是:每题采样 次(他们用 n = 200),数出正确的个数 c,然后估计
意思是:从这 n 个样本里不放回地抽 k 个,至少有一个正确的概率。 是”抽到的 k 个全是错的”的概率。
一个看起来等价、其实有偏的做法是:先估单次成功率 ,再算 。演示 2:每题只采样 n = 20 次,重复 200 次取平均(medium,T = 0.8):
| k | 真实值 | 朴素估计 | 偏差 | 无偏估计 | 偏差 |
|---|---|---|---|---|---|
| 1 | 44.99% | 45.08% | +0.09 | 45.08% | +0.09 |
| 5 | 84.43% | 82.32% | −2.11 | 84.42% | −0.02 |
| 10 | 93.17% | 90.80% | −2.37 | 93.11% | −0.05 |
| 20 | 97.15% | 94.82% | −2.32 | 97.00% | −0.15 |
为什么朴素估计偏低? 是 的凹函数。 是 的无偏估计,但凹函数的期望小于期望的函数值(詹森不等式):。每道题都偏低,平均之后也偏低。无偏估计没有这个问题,因为它直接回答”不放回地抽 k 个”的问题,不需要先把 估出来再代进非线性函数。
数值上的小技巧:,不用算巨大的组合数(code_lab.pass_at_k,和原论文的代码一样)。 时直接返回 1。
2.3 温度:pass@1 和 pass@100 要的东西不一样
演示 3(真实值):
| 模型 | 温度 | pass@1 | pass@10 | pass@100 |
|---|---|---|---|---|
| large | 贪心 | 80.0% | 80.0% | 80.0% |
| large | 0.2 | 73.0% | 93.5% | 99.9% |
| large | 1.0 | 59.4% | 98.9% | 100.0% |
| medium | 贪心 | 45.0% | 45.0% | 45.0% |
| medium | 0.2 | 44.4% | 65.3% | 81.1% |
| medium | 0.4 | 46.1% | 82.4% | 97.6% |
| medium | 1.0 | 43.9% | 94.5% | 100.0% |
- pass@1 要”最有把握的那个”:large 模型贪心最好,温度越高越差。
- pass@100 要多样性:贪心时模型每次都写同一个程序,pass@100 = pass@1;温度越高,100 次里越可能有一次写对。medium 在 T = 0.2 时 pass@100 只有 81%,T = 1.0 时 100%。
Chen et al.(2021)在一个 6.79 亿参数的模型上报告:pass@1 最好的温度约 0.2,pass@100 约 0.8。按 k 选温度,并在报告里写明用的是哪个温度。
medium 那几行有一个值得多想的现象:它的 pass@1 几乎不随温度变化,贪心并不比采样好。原因是在好几道题上,正确的写法有好几种(.lower() 和 .casefold()、abs(n) 和 -n if n < 0 else n),概率被它们分掉了,而某一个错误写法单独的概率反而最大。贪心解码挑的是”最可能的那一种写法”,不是”最可能正确的行为”。§5.4 的 CodeT 正是利用这一点。
3. 沙箱:怎样安全地运行模型写的代码 · Sandboxing
3.1 为什么不能直接 exec
评测程序要运行模型写的代码。最省事的写法是在评测程序自己的进程里 exec(code)。问题是,模型写的代码是不可信的——不是因为模型”有恶意”,而是因为它会写错:
- 死循环:评测程序永远卡住;
- 递归太深、吃光内存:评测程序崩溃;
- 加载时就调用
sys.exit():评测程序直接退出,后面的题都不评了,你甚至可能以为”正常结束了”; - 写文件、删文件、改全局变量、改掉内置函数:评测程序的状态被破坏;
- 读环境变量:你的 API key 可能被它读到、甚至打印出来。
01_sandbox.py 演示 2 在本进程里 exec 一个”一加载就退出”的程序:它抛出 SystemExit,我们用 try 接住了;没有接住的话,评测程序就在那一行结束。try 也接不住死循环(不会抛异常)和 os._exit()(不走异常机制,直接结束进程)。
3.2 一个最小的沙箱:子进程
给没有系统背景的读者补两个概念:
- 进程(process):操作系统里一个正在运行的程序,有自己独立的内存。一个进程崩溃、卡死,不影响别的进程。
- 子进程:一个程序启动的另一个程序。父进程可以给它输入、读它的输出、给它设时限,必要时把它杀掉。
所以最基本的隔离办法是:每段被测代码都在一个新的子进程里运行。code_lab.run_in_sandbox 这样做:
subprocess.run([sys.executable, "-I", "sandbox_runner.py"], # 新的 Python 进程;-I:隔离模式
input=任务的JSON, capture_output=True, text=True,
cwd=临时目录, # 它写的文件落在这里,运行完整个删掉
env={"PATH": ..., "PYTHONHASHSEED": "0"}, # 只给最少的环境变量
timeout=总时限) # 超时就把整个子进程杀掉
子进程里(sandbox_runner.py)再加几层:
- 每个测试一个计时器(
signal.setitimer),超时就打断死循环; - CPU 时间和内存的上限(
resource.setrlimit); - 把被测代码的
print重定向到内存里,免得混进结果; - 结果只通过标准输出的一行 JSON 交回父进程。
演示 1 的结果:
| 代码 | 加载 | 状态 | 沙箱报告的信息 |
|---|---|---|---|
| 正确的程序 | ok | pass | 返回了 6 |
| 答案错 | ok | fail | 返回了 5 |
| 抛异常 | ok | error | IndexError: list index out of range |
| 死循环 | ok | timeout | 超过了每个测试的时间上限(约 1 秒) |
| 递归太深 | ok | error | RecursionError |
| 一加载就退出 | 失败 | error | SystemExit: 0 |
| 狂打印 | ok | pass | (1 万行输出被截住) |
| 写文件 | ok | pass | (文件写进了临时目录,项目里没有) |
| 读环境变量 | ok | fail | 返回了 ‘not visible’(读不到 API key) |
| 语法错误 | 失败 | error | SyntaxError |
每一种问题都变成了一个结论,评测程序自己一直好好的。
代价:启动一个 Python 子进程要几十毫秒,比运行这些小函数本身慢得多。评测几千个样本时这是主要开销,所以要缓存:同一段代码 + 同一组测试只运行一次(模型的采样里重复的程序非常多)。缓存的前提是运行结果是确定的——我们固定了 PYTHONHASHSEED,让字符串的哈希和 set 的遍历顺序每次一致。
3.3 这不是一个真正的安全沙箱
子进程 + 超时 + 资源上限 + 临时目录,挡得住写错了的代码,挡不住故意的恶意代码:它仍然能读你能读的文件、能联网、能启动别的进程。真正的隔离要靠操作系统层面的机制:
- 容器(Docker 等):独立的文件系统视图、网络和资源限制;
- 更强的隔离:gVisor(在用户态拦截系统调用)、Firecracker 这样的轻量虚拟机;
- 网络策略:默认不能联网,只开放需要的地址。
Codex 论文发布 HumanEval 的评测代码时,默认把执行模型代码的那一行注释掉了,并提醒使用者在沙箱里运行。到了第 12 讲你会看到,当模型能执行代码、调用工具时,“不可信”不只是”会写错”,还包括”被提示注入操纵”——那时最小权限和隔离就是主要的防线。
4. 指标:为什么不用 BLEU;测试够不够 · Metrics
4.1 字面相似 ≠ 功能正确
机器翻译用 BLEU:数候选和参考答案之间重合的 n 元组。早期的代码生成工作也用 BLEU 或精确匹配。03_bleu_vs_execution.py 把 20 道题所有可能的程序(114 个)和参考解比 BLEU:
- 正确的程序平均 BLEU 0.80,错误的 0.73,AUROC 0.68——看起来 BLEU 有一点区分能力。
- 但这主要来自 20 个和参考解一字不差的程序(BLEU = 1)。去掉它们,剩下 25 个写法不同但正确的程序平均 BLEU 只有 0.65,比错误程序还低,AUROC 降到 0.42:BLEU 更偏爱长得像参考解的错误程序。
两个极端:
# BLEU = 0.97,但是错的(fizzbuzz)——和参考解只差一个词:
for i in range(1, n + 0): # 参考解是 range(1, n + 1)
# BLEU = 0.14,但完全正确(second_largest)——参考解是 sorted(set(nums))[-2]:
first = second = float("-inf")
for x in nums:
if x > first:
first, second = x, first
elif first > x > second:
second = x
return second
更严重的是排名会反过来(演示 3):
| 模型 | 平均 BLEU | pass@1(执行测试) |
|---|---|---|
| A:small,偏爱参考解的写法 | 0.81 | 30% |
| B:large,偏爱另一种写法 | 0.68 | 75% |
按 BLEU,A 比 B 好;按执行,B 远好于 A。参考解只是很多种正确写法中的一种;用 BLEU 选模型、调提示,会把你推向”模仿参考解的风格”,而不是”写出正确的程序”。这就是 Codex 论文改用功能正确性的理由。CodeBLEU(Ren et al. 2020)在 BLEU 之外加入了语法树和数据流的匹配,比 BLEU 好,但它衡量的仍然是”像不像”,不是”对不对”。
(这和第 10 讲 LLM 判官的长度偏差是同一类问题:指标奖励了一个和目标相关、但不是目标本身的特征。)
4.2 测试够不够:pass@k 也会虚高
执行测试比 BLEU 好,但它只能发现测试覆盖到的错误。HumanEval 平均每题 7.7 个测试,很多边界情况没有覆盖。02_pass_at_k.py 演示 4(贪心解码):
| 模型 | 只用基础测试 | 基础 + 加强测试 | 差 |
|---|---|---|---|
| small | 75.0% | 30.0% | −45 |
| medium | 90.0% | 45.0% | −45 |
| large | 95.0% | 80.0% | −15 |
medium 模型”通过了基础测试、没通过加强测试”的程序,错在什么地方?
count_vowels('HELLO') 期望 2 实际 0 ← 忘了 .lower()
max_subarray([-3, -1, -2]) 期望 -1 实际 0 ← 初始值写成 0
second_largest([4, 4, 3]) 期望 3 实际 4 ← 没去重
binary_search([1, 3], 1) 期望 0 实际 -1 ← while lo < hi
unique_in_order([1, 2, 1]) 期望 [1,2,1] 实际 [1, 2] ← 理解成了"全局去重"
title_case("don't stop") 期望 "Don't Stop" 实际 "Don'T Stop" ← 用了 str.title()
digit_sum(-45) 期望 9 实际 ValueError ← 没处理负号
全是典型的边界情况。Liu et al.(2023)的 EvalPlus 把 HumanEval 的测试扩充了 80 倍(HumanEval+),各个模型的 pass@k 最多下降 19.3%–28.9%——原来的测试漏掉了大量错误的代码。
带走的经验:测试不够,pass@k 就会虚高——和第 10 讲”开发集太小会虚高”是同一个道理。给自己的项目做代码生成评估时,测试要专门覆盖边界情况,最好用属性测试(property-based testing)或参考实现在随机输入上对拍。
4.3 数据污染
HumanEval 是 2021 年公开的,之后训练的模型很可能在预训练数据里见过这些题和答案(污染,contamination)。一个模型在 HumanEval 上 95%,可能是会写代码,也可能是背过答案。对策:用模型训练截止日期之后才出现的新题(例如 LiveCodeBench 持续收集新的竞赛题),或者用私有的、从不公开的测试集。第 17 讲会系统地讲 benchmark 污染。
5. 自我调试与执行反馈 · Self-debugging
5.1 Self-Debugging:小黄鸭调试法
程序员有一种调试方法叫”小黄鸭调试法”(rubber duck debugging):对着一只玩具鸭子逐行解释自己的代码,解释着解释着就发现了错误。Chen et al.(2024)的 Self-Debugging 用少样本示范教模型做同样的事:写出程序 → 执行 → 把结果作为反馈 → 修改。他们比较了几种反馈:
- 简单反馈:只说”上面的代码是错的,请修改”;
- 单元测试反馈:失败的测试、期望输出、实际输出或报错信息;
- 代码解释:让模型逐行解释自己的代码,再判断哪里错了(不需要测试);
- 执行轨迹:逐行的变量值。
他们报告:在有单元测试的 TransCoder(C++ 到 Python 的翻译)和 MBPP 上,Self-Debugging 最多提升 12 个百分点;在没有单元测试的 Spider(文本到 SQL)上,只靠代码解释也能提升 2–3 个百分点,最难的题上提升 9 个百分点;而且利用失败的尝试和反馈,它能追上甚至超过采样 10 倍以上候选程序的基线。
一条单元测试反馈长这样(来自 04 号脚本):
The code failed the test: run_length('aaabcc') should return 'a3b1c2', but it returned 'a3b1'.
它几乎直接指出了错误:最后一组字母没有输出——循环结束后忘了把最后一组写进结果。
5.2 在模拟器上:反馈越具体越好,但有天花板
04_self_debug.py 演示 1:T = 0.8 写初稿,跑题目给定的 2 个测试,没通过就按反馈修改,最多 3 轮(20 道题 × 10 个随机种子)。模拟器里模型怎样”修”是一个旋钮:有具体的失败测试时,它以 0.7 的概率定位到出错的地方并改对;定位不到、或者只被告诉”错了”时,它只能整个重写——而它对这道题的误解还在,重写出来的常常还是同样的错。
| 模型 | 反馈 | 通过给定的测试 | 真正正确(全部测试) | 调用次数 |
|---|---|---|---|---|
| medium | 不改(初稿) | 80.0% | 45.0% | 1.0 |
| medium | 只说”错了” | 97.0% | 58.0% | 1.4 |
| medium | 失败的测试 | 99.0% | 60.0% | 1.3 |
| medium | 失败的测试 + 逐行解释 | 99.5% | 61.0% | 1.3 |
| small | 不改(初稿) | 70.0% | 25.0% | 1.0 |
| small | 只说”错了” | 94.0% | 40.0% | 1.5 |
| small | 失败的测试 | 96.0% | 44.0% | 1.4 |
两件事:
- 反馈越具体,模型越容易定位错误:失败的测试 > 只说”错了”。(“逐行解释”在模拟里只是把定位概率从 0.7 提到 0.85,和”只给测试”的差别在噪声以内。)
- “通过给定的测试”和”真正正确”之间始终有一大段差距:medium 用单元测试反馈后,99% 通过了给定的测试,只有 60% 真正正确。
第二点是自我调试的天花板。演示 3 改变”给模型看多少测试”(medium,贪心初稿):
| 模型看得见的测试 | 真正正确 |
|---|---|
| 给定 2 个测试 | 55.0% |
| 全部基础测试 | 55.0% |
| 基础 + 加强测试 | 95.0% |
自我调试的上限由它看得见的测试决定:测试没有覆盖的边界情况,它既发现不了,也不会去修。(“给它看全部测试”在评估时是作弊——你会用同样的测试判它对不对。正确的做法是给模型一部分测试,用另一部分或一份隐藏的测试集评估,和第 10 讲的开发 / 测试划分一样。)
演示 2 再做一个公平的对照:同样最多 4 次调用,“采样 4 个、挑第一个通过给定测试的”得到 63.0%,和带单元测试反馈的自我调试(60.0%)差不多。原因很清楚:两者用的是同一个信号(给定的 2 个测试),最后都卡在同一个天花板上。没有具体反馈的自我调试,本来就等于”重新采样、再用测试挑”。这个模拟里每道题的程序很短、可能的写法很少,重新采样很容易碰对,“定位”的优势显不出来;程序越长、写法越多,从头再来越难碰对,把”在哪种输入上错了”变成”改哪一行”的价值就越大——这是论文里真实模型上的结论。
5.3 两个风险:针对测试写代码,以及改测试
模型(尤其是被要求”让测试通过”的智能体)有时会走捷径:
- 针对测试写代码:
if nums == [-3, -1, -2]: return -1——通过了看得见的测试,在别的输入上仍然是错的。 - 改测试:智能体能修改仓库里的文件时,它可能把失败的测试删掉、改掉,或者让测试永远通过。
这是第 10 讲古德哈特定律在代码上的样子:“测试通过”是目标的代理,一旦被直接优化,就可能和目标脱节。对策:评估用模型看不见的测试;智能体没有权限修改测试文件;检查补丁有没有动测试;人工审查一部分补丁。
5.4 没有测试时:CodeT 与多数投票
很多真实场景没有现成的测试。Chen et al.(2023)的 CodeT 让模型自己写测试:采样很多个程序和很多个测试,然后用”双重执行一致性”挑程序——把通过同一组生成测试的程序归为一组,组的得分 = 组里的程序数 × 它们通过的测试数,选得分最高的那一组。一个更简单的变体是按输出多数投票(MBR-exec):在同样的输入上输出完全相同的程序归为一组,选最大的一组。两者的直觉相同:正确的程序彼此一致,错误的程序各错各的——和第 4 讲的 self-consistency 是同一个想法,只是”投票”的单位从答案变成了程序的行为。
05_codet.py:每题 20 个样本(T = 0.8)、20 个生成的测试(输入随机,期望输出是模型”心里认为”的答案——用它另外采样的一个程序的输出来代表)。
| 模型 | 随便挑一个 | 贪心 | 输出多数投票 | CodeT | 上限(20 个里有对的) |
|---|---|---|---|---|---|
| small | 26.7% | 30% | 26% | 31% | 89% |
| medium | 46.3% | 45% | 63% | 66% | 99% |
| large | 62.8% | 80% | 83% | 82% | 100% |
- medium:CodeT 比随便挑和贪心都高约 20 个百分点,完全没有用到人写的测试。Chen et al.(2023)报告 CodeT 把 code-davinci-002 在 HumanEval 上的 pass@1 提到 65.8%,绝对提升 18.8 个百分点。
- 贪心不一定比随便挑好(medium 45% vs 46.3%):贪心挑”最可能的写法”,CodeT 挑”最常见的行为”——正确写法有很多种时,后者才对得上(§2.3)。
- 离上限还很远,而且 small 上几种方法都差不多:在难题上,模型最常见的行为本身就是错的,一致性救不了它。
模型自己写的测试有多少是对的?small 70.5%、medium 81.8%、large 87.8%。模型写测试时对答案的”信念”和它写代码时同源,会把自己的误解原样写进期望输出里。CodeT 不要求每个测试都对:一个错的测试只会被”犯同样错误的程序”通过;但如果模型在一道题上大多数时候都犯同一个错,错误的程序和错误的测试会互相支持,CodeT 会自信地挑错——和第 4 讲 self-consistency 修不了系统性误解是同一件事。
6. 软件工程智能体 · Software engineering agents
6.1 SWE-bench:从”写一个函数”到”修一个 issue”
HumanEval 的题是独立的小函数,真实的软件工程是另一回事:在一个几十万行的仓库里,读懂一个 issue,找到相关的代码,改对,不弄坏别的东西。Jimenez et al.(2024)的 SWE-bench 从 12 个流行的 Python 仓库里收集了 2294 个真实的 GitHub issue 和对应的修复:给模型仓库和 issue 描述,它生成一个补丁;补丁要让原来失败的测试通过,并且不让原来通过的测试失败。论文发布时,最好的模型(Claude 2)只解决了 1.96%。之后出现了更小的子集 SWE-bench Lite,以及 500 道经过人工确认的 SWE-bench Verified。
这个任务暴露了很多 HumanEval 测不到的东西:在大仓库里定位(应该改哪个文件的哪个函数),读长代码,做精确的编辑,运行测试并根据结果调整——这些都是智能体的能力(第 13–14 讲)。
6.2 SWE-agent:接口设计决定表现
Yang et al.(2024)的 SWE-agent 的核心观察是:语言模型是一种新的”终端用户”,就像人需要 IDE,智能体也需要为它设计的接口——他们叫”智能体–计算机接口”(ACI)。他们给智能体设计的命令包括:
search_file/search_dir/find_file:在仓库里搜索,返回简洁的结果;open/scroll_up/scroll_down:文件查看器,一次只显示 100 行,带行号;edit 起始行:结束行:只替换指定的几行;编辑之后自动做语法检查,有错误就拒绝这次编辑、告诉智能体哪里错了;- 简洁、格式固定的反馈(命令成功但没有输出时,也明确说一声)。
用这套接口,SWE-agent 在 SWE-bench 上得到 12.5%(pass@1),在 HumanEvalFix 上 87.7%。更有启发的是他们的消融实验(SWE-bench Lite,GPT-4 Turbo):
| 接口的变化 | 解决率 |
|---|---|
| 完整的 ACI | 18.0% |
| 编辑后不做语法检查 | 15.0% |
| 没有 edit 命令 | 10.3% |
| 文件查看器显示 30 行 | 14.3% |
| 文件查看器显示整个文件 | 12.7% |
6.3 在迷你仓库上
06_swe_agent.py 做了一个能在几秒内跑完的版本:两个文件、14 个函数的小仓库,每次注入一个 bug(26 个 issue),issue 只描述现象(“Calling count_vowels(‘Python’) returns 2, but 1 is expected.”)。模拟智能体的旋钮:看的代码越多越难认出出错的那一行;窗口比函数短时只看到一部分;按行编辑以 15% 的概率写错缩进;重写整个文件时每 10 行有 3% 的概率出错、10% 的概率顺手改坏别的函数。文件编辑、语法检查(Python 的 compile)、测试运行都是真的。最多 8 步,26 个 issue × 5 个种子:
| 接口 | 解决 | 平均步数 | 平均 token |
|---|---|---|---|
| 完整的 ACI(search + 12 行窗口 + edit + 语法检查) | 73.1% | 6.3 | 166 |
| 去掉语法检查 | 68.5% | 6.8 | 194 |
| 窗口太小(4 行) | 56.9% | 6.8 | 107 |
| open 显示整个文件 | 66.2% | 6.6 | 739 |
| 没有 edit(只能重写整个文件) | 54.6% | 6.7 | 819 |
| 没有 search | 63.1% | 6.8 | 265 |
| 只有 shell(cat、重写整个文件、没有 search 和语法检查) | 52.3% | 7.0 | 1955 |
数字由旋钮决定,但每一项的方向都和 SWE-agent 的消融一致,而且每一项背后的机制都很直观:
- 语法检查把错误挡在发生的那一刻:没有它,写错的缩进进了文件,要等跑测试才发现,智能体还可能没意识到是语法问题、接着去”修逻辑”。这是第 8 讲”错误信息就是提示”的又一个例子:好的接口让错误早、清楚地暴露。
- 窗口大小有一个最佳点:看得太多,难以在一大段代码里认出那一行(第 7 讲的”迷失在中间”);看得太少,连整个函数都看不全。
- 按行编辑比重写整个文件安全得多:改的越少,出错的机会越少,也不会顺手改坏别的东西。
- token:只有 shell 命令时,智能体读进上下文的文字是完整 ACI 的 11.8 倍——更贵、更慢,也更容易迷失。
6.4 一定要用智能体吗?
Xia et al.(2024)的 Agentless 提出了一个反问:不让模型自己决定下一步做什么,只用一个固定的三阶段流水线——定位(逐级缩小到文件、函数、行)、修复(生成多个候选补丁)、验证(用测试挑补丁)——在 SWE-bench Lite 上得到 32.00%(96 个),成本约 0.70 美元,在当时的开源方案里既是最好的、也是最便宜的。这正是第 9 讲”任务分解”和第 8 讲”先规划 vs 边做边看”的取舍:流程是固定的、可预测的时候,固定的流水线往往比让模型自由探索更便宜、更稳;任务需要根据中间结果改变计划时,智能体才更有优势。第 13 讲会接着讨论这个问题。
7. 在真实模型上 · On a real model
07_real_llm.py(可选,需要 API key,约 20–40 次调用,预算上限 1.00 美元)让真实的 Claude 写这 20 个函数:
- 只给函数签名和文档字符串,看基础测试和全部测试各通过多少——它在哪些边界情况上出错?
- 给定的 2 个测试没通过的题,把失败的测试作为反馈交给它,最多改 2 轮;修好的程序在加强测试上也对吗?
- 它写的代码和参考解的 BLEU,和是否正确有关系吗?
模型写的代码都在沙箱的子进程里运行。强的模型可能一题都不差——那说明这 20 道题对它太简单了,可以换成你自己项目里的函数和测试。--toy 模式用 SimCoderLLM 跑同一段代码(编写讲义的环境里没有运行真实模型)。
8. 小结 · Summary
- 代码特殊在可以被执行:执行给出客观、便宜、外部的信号,可以用来评估、反馈和挑选。
- pass@k:k 次里至少一次通过全部测试。用 n ≥ k 个样本和无偏估计 ;朴素的 因为凹函数而偏低(n = 20、k = 10 时偏低 2.4 个百分点)。pass@1 要低温,pass@100 要高温。
- 沙箱:每段代码在子进程里运行,加超时、资源上限、临时目录、最少的环境变量;它挡得住写错的代码,挡不住故意的恶意代码——生产环境要用容器或虚拟机级别的隔离。
- BLEU 不适合代码:去掉和参考解一字不差的程序后,BLEU 偏爱长得像参考解的错误程序(AUROC 0.42),甚至会把两个模型的排名颠倒。测试不够时 pass@k 也会虚高:加上边界情况的测试,medium 从 90% 掉到 45%。
- 自我调试:具体的反馈(失败的测试)比”错了”好;但它的上限由模型看得见的测试决定。和同样调用次数的”采样 + 用测试挑”比较,才知道”改”本身的价值。小心针对测试写代码和改测试。
- CodeT / 多数投票:没有测试时,用程序之间行为的一致性挑——正确的程序彼此一致,错误的各错各的;它修不了模型一贯的误解。
- SWE-bench 与 ACI:真实的软件工程需要定位、精确编辑、根据测试调整;同一个模型,接口设计(搜索、窗口、按行编辑、编辑后检查)能让解决率差一大截。固定的流水线(Agentless)有时比自由的智能体更便宜、更稳。
9. 常见误区 · Common pitfalls
| 误区 | 实际情况 |
|---|---|
| “pass@10 就是每题采样 10 次看有没有对的” | 方差大、浪费;采样 n ≥ k 次,用无偏估计 |
| “先算 p̂ = c/n,再算 1−(1−p̂)^k” | 有偏(偏低),因为 1−(1−p)^k 是凹函数 |
| “温度设成 0 最好” | 对 pass@1 通常是;对 pass@100 要高温,需要多样性 |
| “在评测脚本里 exec 一下模型的代码就行” | 死循环、sys.exit、写文件、读环境变量都会作用在评测程序身上;用子进程,生产环境用容器 |
| “子进程加超时就是安全的沙箱” | 它只挡得住写错的代码,挡不住故意的恶意代码 |
| “BLEU 高说明代码写得好” | BLEU 奖励的是像参考解,差一个字符的错误程序 BLEU 可以接近 1 |
| “通过了测试就是对的” | 只说明测试覆盖到的情况对;测试不够时 pass@k 会大幅虚高 |
| “自我调试能修好所有错误” | 它只能修看得见的测试暴露的错误;还可能针对测试写特判 |
| “模型自己写的测试没用,因为它会写错” | CodeT 不要求每个测试都对,只要正确的行为比任何一种错误行为更常见 |
| “智能体越自由越好” | 接口设计决定错误多容易发生、多容易被发现;流程固定时,固定的流水线常常更便宜、更稳 |
10. 练习 · Exercises
练习 1(计算) 某道题采样 n = 10 次,c = 3 次正确。(a)用无偏估计算 pass@1、pass@3、pass@8。(b)用朴素估计算 pass@3,和(a)比较。(c)n − c < k 时为什么直接返回 1?
参考答案:(a)pass@1 = 1 − C(7,1)/C(10,1) = 1 − 7/10 = 0.3;pass@3 = 1 − C(7,3)/C(10,3) = 1 − 35/120 ≈ 0.708;pass@8 = 1 − C(7,8)/C(10,8),C(7,8) = 0(7 个错的里抽不出 8 个),所以 pass@8 = 1。(b)朴素:1 − 0.7³ = 0.657,比无偏估计低约 5 个百分点。(c)错的样本只有 n − c 个,少于 k 时,不放回地抽 k 个必然抽到至少一个对的。
练习 2(设计) 你要为公司内部的一个 Python 工具库评估”让模型写新函数”的能力。设计评估方案:题目从哪里来、测试怎样写、用什么指标、怎样防止污染。
参考答案(一种可能):题目:从库的 issue 和近期的功能需求里挑 50 个,写成”签名 + 文档字符串 + 用法示例”;从不公开。测试:每题由两个人分别写——常见情况、边界情况(空、负数、Unicode、超大输入)、错误输入的行为;再用参考实现在随机输入上对拍(属性测试)。给模型看的测试只用其中 2–3 个,其余作为隐藏测试。指标:贪心 pass@1(日常使用)、T = 0.8 时 n = 20 的 pass@5(“给几个建议”的场景)、加上运行时间和是否使用了禁止的依赖。防污染:题目是新写的、不公开;定期轮换。所有代码在容器里运行,不能联网。
练习 3(解释) §2.3 里,large 模型贪心的 pass@1(80%)明显高于 T = 0.2 采样(73%),而 medium 模型两者差不多。用”正确的写法有好几种”解释这个差别,并说明为什么 CodeT 在 medium 上收益最大。
参考答案:large 模型在大多数题上,参考写法本身的概率就最高,贪心直接选中它;采样只会引入随机错误。medium 模型在一些题上,正确的行为分散在两三种写法上(每种的概率都比某一个错误写法低),贪心选中了那个错误写法;但把所有正确写法加起来,正确的行为仍然是最常见的行为。采样之后按行为分组(CodeT、多数投票),就能把分散的正确写法合在一起、胜过那个单独概率最大的错误写法——所以 medium 上收益最大(45% → 66%)。large 上贪心已经选对,收益小;small 上正确的行为在难题上连”最常见”都不是,一致性也救不了。
练习 4(动手) 在 code_lab.py 里给 count_vowels 的基础测试加上 ("HELLO",),重新运行 02 号脚本的演示 4 和 04 号脚本。哪些数字变了?为什么?
参考答案:
count_vowels的”忘了 .lower()“这个错误会被基础测试抓住:演示 4 里”只用基础测试”的通过率下降(medium 少一道),和”基础 + 加强”的差距变小;04 号脚本里,如果("HELLO",)在给定的前两个测试里(要把它放在前面),自我调试就能看到这个失败、修好它,“真正正确”上升。这说明测试的选择直接决定了 pass@k 的高低和自我调试的上限。(注意:改了测试,缓存的键也会变,第一次运行会重新执行相关的程序。)
练习 5(批判) 一篇博客说:“我们的编程智能体在内部 benchmark 上的通过率从 40% 提升到了 85%,方法是允许它在提交前运行测试并反复修改。“列出至少三个你想问的问题。
参考答案:(1)它能运行的测试和评估用的测试是同一套吗?如果是,85% 衡量的是”让这些测试通过”,不是”代码正确”——要用它看不见的测试评估。(2)它有没有修改测试文件的权限?补丁里有没有动测试、加特判?(3)调用次数和成本增加了多少?和”同样预算下多采样几次、用测试挑”比过吗?(4)测试本身覆盖得怎样?有没有像 EvalPlus 那样补充边界情况再测一次?(5)benchmark 的题目是否可能出现在模型的训练数据里?
练习 6(设计接口) 你要给一个”修改 SQL 查询”的智能体设计 ACI。参照 §6.2 的原则,列出你会给它的 4–5 个命令,以及每个命令的输出格式。
参考答案(一种可能):(1)
list_tables→ 表名和行数,一行一个;(2)describe 表名→ 列名、类型、是否可为空、外键,最多 50 行;(3)run_query SQL→ 只读事务里执行,返回前 20 行和总行数,超时 5 秒,报错时给出数据库的原始错误信息和出错位置;(4)edit_query 起始行:结束行 新内容→ 修改草稿查询,修改后自动做语法检查(例如用数据库的 EXPLAIN),有错就拒绝并给出错误;(5)submit→ 提交草稿查询。原则:只读权限(最小权限,第 12 讲)、输出简短固定、错误早暴露、限制每一步读进上下文的量。
11. 延伸阅读 · Further reading
代码模型与 benchmark
- Chen et al. (2021). Evaluating Large Language Models Trained on Code(Codex、HumanEval、pass@k)。
- Austin et al. (2021). Program Synthesis with Large Language Models(MBPP)。
- Li et al. (2022). Competition-Level Code Generation with AlphaCode. Science.
- Bavarian et al. (2022). Efficient Training of Language Models to Fill in the Middle.
- Li et al. (2023). StarCoder: May the Source Be with You!;Rozière et al. (2023). Code Llama: Open Foundation Models for Code.
- Jain et al. (2024). LiveCodeBench: Holistic and Contamination Free Evaluation of Large Language Models for Code.
指标
- Ren et al. (2020). CodeBLEU: A Method for Automatic Evaluation of Code Synthesis.
- Liu et al. (2023). Is Your Code Generated by ChatGPT Really Correct? Rigorous Evaluation of Large Language Models for Code Generation(EvalPlus). NeurIPS.
执行反馈与挑选
- Chen et al. (2024). Teaching Large Language Models to Self-Debug. ICLR.
- Chen et al. (2023). CodeT: Code Generation with Generated Tests. ICLR.
- Shi et al. (2022). Natural Language to Code Translation with Execution(MBR-exec)。
软件工程智能体
- Jimenez et al. (2024). SWE-bench: Can Language Models Resolve Real-World GitHub Issues? ICLR.
- Yang et al. (2024). SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering. NeurIPS.
- Xia et al. (2024). Agentless: Demystifying LLM-based Software Engineering Agents.
12. 下一讲预告 · Next lecture
这一讲的沙箱防的是”写错了的代码”。当模型能执行代码、调用工具、读网页和邮件时,还有一类更危险的输入:有人故意写给模型看的指令。第 12 讲进入原课第 8 周:对抗性提示与护栏(以防御为主线)。
- 威胁模型:谁是攻击者,能控制什么,想得到什么;
- 提示注入:直接注入,以及藏在网页、文档、工具结果里的间接注入(Greshake et al. 2023)——为什么”数据”和”指令”在一个提示里很难分开;
- 越狱为什么能成功的两种解释(目标冲突与泛化不匹配,Wei et al. 2023)——只讲机理与分类;
- 数据投毒与后门的风险面;
- 护栏:输入 / 输出分类器、结构化分隔(spotlighting)、指令层级、最小权限和人工确认;
- 红队评估与”自适应的攻击者”:为什么一个在固定攻击集上 0% 成功率的防御,不等于安全。