第 23 讲 · 代码生成与自主智能体 · Code Generation + LLMs / VLMs as Autonomous Agents

Generative AI · Lecture 23。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:CMU 10-423 / 10-623 / 10-723 Generative AI(Matt Gormley),Lecture 23 · Wed 8-Apr · Code Generation / Autonomous Agents(slides:lecture23-code)。 阅读材料: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);Jimenez et al. (2024), SWE-bench;Feng et al. (2020), CodeBERT;Wang et al. (2021), CodeT5;Fried et al. (2022), InCoder;Bavarian et al. (2022), Efficient Training of Language Models to Fill in the Middle;Li et al. (2023), StarCoder;Guo et al. (2023), LongCoder;Jiang et al. (2024), A Survey on Large Language Models for Code Generation;Kimi 团队 (2025), Kimi K2;Wang et al. (2024), Executable Code Actions Elicit Better LLM Agents(CodeAct);Deng et al. (2023), Mind2Web;Yang et al. (2023), Set-of-Mark Prompting;Cheng et al. (2024), SeeClick。 依据说明:本讲按原课第 23 讲公开的 slides 展开:怎样用 LLM 提高编程效率、代码数据(GitHub、Dolma、The Stack)、代码模型的应用;评估(BLEU、CodeBLEU、功能正确性;HumanEval、MBPP、SWE-Bench Verified、Terminal-Bench 2.0);代表性的代码模型(CodeBERT、Codex、CodeT5、InCoder / FIM、StarCoder、LongCoder);代码特有的技巧(迭代自我修正);工具调用(Kimi K2);编程智能体(CodeAct、智能体循环、OpenHands 和 Codex CLI 的系统提示、多智能体架构、关键挑战);自主智能体(Mind2Web、视觉语言模型做 GUI 智能体、Set-of-Mark、SeeClick、用强化学习训练智能体)。所有实验是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:code/lecture23/(4 个脚本 + 1 个模块;03 第一次运行约 2.3 分钟,02 约 1 分钟,其余几秒) 预计阅读时间:2 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

学完这一讲,你应该能够:

  1. 说清楚代码和自然语言最大的不同——可以执行——以及它怎样改变了评估、推理和训练。
  2. 推导 pass@k 的无偏估计,解释为什么”直觉”的估计偏低、为什么每题要采比 k 更多的样本。
  3. 解释为什么 BLEU 不适合评估代码;温度怎样分别影响 pass@1 和 pass@k。
  4. 说出几个代表性代码模型的要点,以及填中间(FIM)的数据变换和”FIM 免费”性质。
  5. 描述工具调用和编程智能体的循环,解释为什么”有测试反馈”的自我修正能起作用,以及它的局限(测试覆盖率、沙盒)。
  6. 说出网页 / GUI 智能体的两种做法(读 HTML、看截图)和 Set-of-Mark 的思路。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,不展开 Previewed only
第 3 讲:解码(温度、贪心、采样)各个基准的具体排行榜数字(变化很快)
第 4 讲:BERT / T5 / GPT 三种结构;第 11 讲:指令微调、RLHF智能体框架的工程细节(权限系统、上下文管理)
第 19 讲:长上下文与稀疏注意力;第 20 讲:用规则奖励做 RLGUI 智能体的视觉定位模型的训练细节

0.3 本讲地图 Map

§1  为什么是代码:编程效率、代码数据、应用
§2  怎样评估代码:功能正确性、pass@k、基准                ← 代码 01、02
§3  代码模型:CodeBERT、Codex、CodeT5、FIM、StarCoder、LongCoder ← 代码 03
§4  代码特有的技巧:用测试反馈迭代修正                      ← 代码 04
§5  工具调用
§6  编程智能体:循环、系统提示、多智能体、关键挑战            ← 代码 04
§7  自主智能体:网页与 GUI
§8–12  小结、误区、练习、延伸阅读、下一讲

所有实验都在 code_lab.py 的两门小语言上做:

  • 语言一(示例编程):输入是 a–h 组成的 3–5 个字母的字符串,程序是 1–2 个操作(rev 反转、rot 每个字母 +1、dup 每个字母写两遍、drop 去掉第一个字母),一共 19 个程序。“规格”是 1–3 个”输入 → 输出”的例子,例如 d a c > c a d ; b e g > g e b,模型要写出 rev。
  • 语言二(直线程序):每行”变量 = 操作 变量”,最后 out 变量,例如 v3 = rot inp ; v7 = rev v3 ; out v7 ;,变量名随机。

两门语言都由我们自己写的解释器执行——从不把模型的输出交给 Python 去运行。这既是为了安全(真实的编程智能体同样要在沙盒里运行生成的代码),也让”执行”变得完全可控。


1. 为什么是代码 · Why code

slides 的开场问题:“怎样提高程序员的效率?“左边是结对编程(pair programming),右边是和 LLM 一起编程。代码是生成式 AI 最早、最成功的应用之一,原因有三:

  1. 数据多。slides 展示了 GitHub 上各种编程语言的代码量;Dolma 是一个 3 万亿 token 的语料,来自多个现有数据集(其中就有代码);The Stack 是 3 TB 的宽松许可(permissively licensed)的代码,专门用来训练 LLM——第 22 讲的版权问题在这里通过只用宽松许可的代码来缓解。大多数 LLM 的训练数据里都有大量的各种编程语言。
  2. 应用多。slides 列出:IDE 里的自动补全(GitHub Copilot)、根据文字描述写代码、根据文档字符串写函数或类、读一个大代码库并加一个新功能、找 bug 和修 bug、写单元测试、把代码从一种语言翻译成另一种、给已有的代码写注释。
  3. 可以执行。这是本讲反复出现的主题:程序对不对,跑一遍测试就知道。这一点让代码的评估(§2)、推理时的修正(§4)和训练(用测试结果当奖励,第 20 讲的规则奖励)都有了可靠的信号——自然语言的大多数任务没有这种运气。

2. 怎样评估代码 · Evaluating code generation

2.1 指标和基准

slides 把指标分成三类:

  • BLEU:机器翻译的指标,和一个已知的参考答案比 n-gram 重合;
  • CodeBLEU:BLEU 加上语法(抽象语法树的匹配)和语义(数据流的匹配)的混合指标;
  • 功能正确性:跑单元测试,看通过多少。功能正确性已经成为主流的评估方式。

基准(slides 列出了一部分,“还有很多”):

基准内容怎样判分
HumanEval(2021,随 Codex 发布)164 道手写的题:函数签名 + 文档字符串单元测试,pass@k
MBPP(Mostly Basic Python Programs)974 道众包的题,面向编程新手;每题有题目描述、3 个测试、一个参考解单元测试
DS-1000数据科学库(NumPy、Pandas 等)的题单元测试
SWE-bench / SWE-bench Verified真实的 GitHub issue:在代码库里生成一个补丁来解决它运行仓库里的测试
Terminal-Bench 2.0需要操作命令行环境的真实任务检查容器的最终状态

SWE-bench 的构造:从 12 个流行的 Python 仓库里收集约 9 万个 pull request,要求 PR 解决了一个 issue 并且修改了测试、至少有一个测试从”失败”变成”通过”,得到 2,294 个任务。原论文(2023 年)里当时最好的模型只解决了 1.96%。SWE-bench Verified 是其中经过人工验证的 500 个子集(去掉描述不清、测试不合理的题)。slides 展示了 Epoch AI 整理的 SWE-bench Verified 成绩随时间的变化:一年多的时间里从三成左右涨到了八成左右。

Terminal-Bench 2.0 的每个任务有:一个装好相关文件的 Docker 容器、给智能体的英文指令、检查容器最终状态的测试、一个参考解;一共 89 个人工验证过的任务。slides 举了两个例子:从源码编译安装一个旧版本的渲染软件并用测试场景验证;分析一张棋局图片、把最佳着法写进文件。这类任务已经不是”写一个函数”,而是”在一个真实的环境里完成一件事”——这就是 §6 的编程智能体要做的。

2.2 pass@k 和它的无偏估计(代码 01)

slides:“pass@k = k 个代码样本里有一个通过全部单元测试的题目比例(实际实现用更多的样本来减小方差)。”

直接按定义估计:每题生成 k 个样本,看有没有一个通过——方差很大。Codex 论文的做法:每题生成 n≥kn \ge k 个样本,数出通过的 cc 个,然后用

pass@k^=1−(n−ck)(nk)\widehat{\text{pass@}k} = 1 - \frac{\binom{n-c}{k}}{\binom{n}{k}}

它的含义是:从这 n 个样本里不放回地随机抽 k 个,至少抽到一个正确的概率。对所有题目取平均就是数据集上的 pass@k。

为什么不用”直觉”的估计 1−(1−c/n)k1 - (1 - c/n)^k? 它先估单次成功率 p^=c/n\hat p = c/n,再代入真实的公式 1−(1−p)k1 - (1-p)^k。01 第 2 部分对二项分布精确求期望:无偏估计的期望在每一行都等于真值;直觉估计总是偏低,p 小、k 大时最严重(p = 0.05、n = k = 20 时偏低 0.16)。原因是 Jensen 不等式:f(p)=1−(1−p)kf(p) = 1 - (1-p)^k 是凹函数,E[f(p^)]≤f(E[p^])=f(p)E[f(\hat p)] \le f(E[\hat p]) = f(p)。

为什么要 n > k? n = k 时一道题的估计只能是 0 或 1。01 第 3 部分:估计 pass@10、真实 p = 0.1 时,n = 10 / 20 / 50 / 200 的估计都是无偏的,但一道题的标准差是 0.48 / 0.29 / 0.17 / 0.08。Codex 论文每题采 200 个样本。

数值稳定。 n = 2000、k = 1000 时 (nk)\binom{n}{k} 有 601 位数,转成浮点就溢出。Codex 论文给的写法是连乘:

(n−ck)(nk)=∏i=n−c+1n(1−ki),\frac{\binom{n-c}{k}}{\binom{n}{k}} = \prod_{i=n-c+1}^{n}\left(1 - \frac{k}{i}\right),

每一项都在 0 和 1 之间,不会溢出。

pass@1 和 pass@100 为什么差那么多? 因为难度不均:很多题单次成功率很低但不为零,采 100 次就有相当大的机会对一次(p = 0.01 时 1−0.99100≈0.631 - 0.99^{100} \approx 0.63)。Codex 论文的摘要:Codex 解出 HumanEval 28.8% 的题(GPT-3 是 0%,GPT-J 是 11.4%);每题采样 100 次时能解出 70.2%。但 pass@k 衡量的是”能不能写出来”,前提是有办法从 k 个候选里挑出对的那个——§4 会看到,测试就是这个办法。

2.3 一个小代码模型(代码 02)

02 训练一个 20 万参数的 GPT(第 2 讲的结构,4 层、1 个 64 维的头),只训练 2000 步(45 秒):看 1–3 个例子,写出语言一的程序。它的表现很有代表性:

  • 贪心解码在 200 道题上只有约 0.37 的程序通过全部 8 个隐藏测试;
  • 但每题采 20 个样本(温度 0.8)时,pass@20 = 0.995——几乎每道题,正确的程序都在模型的候选里。

为什么?语言只有 19 个程序,可以全部枚举、精确算出每个程序的概率(program_logprobs)。模型学会了看长度(dup 让输出变长一倍、drop 让输出少一个字母),却分不清 rev 和 rot(两者都不改变长度),于是把概率大致平均地分给几个长度相符的程序(各约 0.17–0.20)。更多训练、更大的模型、把例子补齐成固定宽度,在我们的时间预算里都没能让它学会这个区分(code README 有记录)。这个”大方向知道、细节分不清”的模型,正好用来研究”多采样 + 测试”能带来什么。

2.4 温度:pass@1 和 pass@k 要的不一样(代码 02 第 3 部分)

温度pass@1pass@5pass@2020 个样本里不同的程序
0.20.3770.7360.9553.9
0.50.3880.7900.9804.5
0.80.4000.8350.9954.7
1.00.3910.8190.9954.8
1.50.3620.7830.9855.9

高温度让 20 个样本更多样、覆盖更多可能的程序,所以 pass@20 在 0.8–1.0 最高——这和 Codex 论文一致(他们发现 pass@100 用温度 0.8 最好)。但 Codex 论文里 pass@1 是低温度(0.2)最好,我们这里低温度反而让 pass@1 变差。原因:低温度把概率集中到模型最喜欢的程序上,只有当这个首选通常是对的时候,集中才有好处;我们的模型最喜欢的常常是错的(在同样长度的几个程序里它总是略偏向 “rev rev”)。温度要和”模型的首选有多可靠""能用多少个样本、怎样挑”一起选。

2.5 BLEU 为什么不行(代码 02 第 4 部分)

slides 展示了 Codex 论文的图:HumanEval 上正确和错误的解的 BLEU 分布大幅重叠,“结果清楚地表明 BLEU 不是一个好的替代指标”。

在我们这门小语言里,BLEU 整体上其实分得不错(区分对错的 AUC 0.993):程序只有 1–2 个词,写对的程序几乎总是和参考答案共用同样的词。但它在一个地方完全失效:同样的词、换了顺序。“rot drop”和”drop rot”功能相同,“drop dup”和”dup drop”功能不同(前者先去掉一个字母再加倍,后者先加倍再去掉一个),两对的 BLEU 都是 0.707。BLEU 在 0.7–1 之间的样本里,69% 是对的、31% 是错的——要知道是哪一种,只能执行它。真实代码里,正确的写法千差万别(变量名、循环还是递归、库函数),而一个字符的差别就能让程序出错;BLEU 还需要参考答案,测试不需要。


3. 代码模型 · Code models

slides 展示了 2020–2023 年代码模型的”家谱”(越来越多、越来越大),然后挑了几个代表。

3.1 CodeBERT、Codex、CodeT5

  • CodeBERT(2020):早期的成功例子,1.25 亿参数,结构和 RoBERTa 一样(第 4 讲的仅编码器)。两个预训练目标:掩码语言模型(MLM)和替换 token 检测(RTD,判断每个 token 是不是被替换过)。用在自然语言 → 代码的检索上。
  • Codex(2021):最初的 GitHub Copilot 背后的模型。GPT-3 结构的 120 亿参数模型,在 159 GB 的 Python 代码上微调。slides 特别指出:从预训练好的 GPT-3 开始并没有提高最终的性能,但让训练收敛得更快。 论文的图:模型越大,pass@1(温度 0.2)和 pass@100(温度 0.8)都按对数线性地提高。
  • CodeT5(2021):基于 T5 的编码器-解码器(第 4 讲),像 T5 一样把多种任务统一成”文本到文本”(总结代码、生成代码、修 bug、改进代码、把 Python 翻译成 C),并加了感知标识符(变量名、函数名)的预训练任务。

3.2 StarCoder 与 LongCoder

  • StarCoder(2023):一度是最好的开源代码模型之一。155 亿参数,在 1 万亿 token 上预训练,用了 FIM(§3.3)。slides 的表:上下文长度 8192、多查询注意力(第 4 讲的 MQA);HumanEval 33.6、MBPP 52.7,超过了同期更大的开源通用模型(例如 LLaMA-65B 的 23.7 / 37.7)。
  • LongCoder(2023):针对”处理大代码库”的问题——一个函数的正确写法常常取决于很远的地方定义的类和函数。它用稀疏注意力(第 19 讲):滑动窗口看局部,加上”桥接 token”汇总窗口外的信息、“记忆 token”保留 import 语句和类定义这类全局重要的内容。

3.3 填中间:InCoder 和 FIM(代码 03)

slides 的问题:怎样用一个因果掩码的语言模型去填补代码文件中间的代码? 这是 IDE 里最常见的情形:光标前后都有代码,补全必须和后面的代码一致。从左往右的语言模型只看得见前缀。

  • InCoder(2022 年 4 月):在要填的地方放一个掩码记号,把被挖掉的内容移到文档末尾去预测。

  • FIM(Bavarian et al.,2022 年 7 月):把代码切成(前缀、中间、后缀),训练时写成

    <PRE> 前缀 <SUF> 后缀 <MID> 中间

    预测时给 <PRE> 前缀 <SUF> 后缀 <MID>,让模型接着写中间。模型仍然是从左往右生成,只是生成”中间”时已经读过了后缀。这只是一个数据变换,不改模型结构——所以可以和普通的预训练混在一起做。

03 在语言二上训练三个模型(同样 3000 步):只从左往右;50% 的样本做 FIM、在任意 token 处切;50% 做 FIM、按行切。测试:挖掉中间一行,而后面的代码用到了这一行定义的变量。

模型看到什么补完的程序能运行从左往右的损失(比真实分布的熵高)
只从左往右只有前缀0.06+0.006
FIM 50%,在任意 token 处切前缀 + 后缀0.03+0.115
FIM 50%,按行切前缀 + 后缀0.99+0.016

例如前缀是 v0 = drop inp ;、后缀是 v3 = rev inp ; v5 = drop v8 ; v6 = dup v0 ; out v6 ;:只看前缀的模型写出 v9 = rev inp ;(v8 仍然没有定义);按行切的 FIM 模型写出 v8 = dup v0 ;——它知道后面需要 v8。变量名是随机的,只看前缀根本无从知道。

FIM 免费(FIM-for-free)。 Bavarian et al. 的主要发现:用 FIM 训练不损害从左往右的能力——他们在大模型上发现 FIM 比例到 90% 都没有可见的损失,并建议今后的自回归模型默认就用 FIM 训练。我们的语言二的真实分布可以精确计算(变量名、操作、参数都是随机挑的,熵是 0.764 纳特 / token),所以能看”离最优还差多少”:按行切的 FIM 模型只比纯从左往右的模型高 0.010,基本”免费”;但按任意 token 切的 FIM 模型高了 0.115——它要学的格式五花八门,同样 3000 步里从左往右的部分学得慢了。“免费”是在充分训练以后才成立的性质,小模型、短训练不一定满足。

怎样切”中间”。 在任意位置挖掉一段(可能从半行开始、到半行结束)时,两个 FIM 模型的表现正好相反:按 token 随机切的模型 0.54 能补成可运行的程序,按行切的只有 0.04;而补整行时是 0.03 vs 0.99——各自只擅长和训练一致的切法(按 token 随机切的模型 66% 的时候直接输出一个空的中间:训练时切点很少恰好落在行边界上)。论文的表 2 有同样方向的取舍:按行切训练,补单行 0.586、随机切点 0.015;字符级随机切,0.557、0.321。他们的大模型在字符级随机切时补单行只差一点,我们的小模型没有复现这一点。论文最终推荐在字符级随机切(真实的分词器下,用户的光标常常落在一个 token 的中间),并同时用 PSM 和 SPM(后缀在前)两种格式训练。


4. 代码特有的技巧:用测试反馈迭代修正 · Iterative self-refinement(代码 04)

slides 引用 Jiang et al. (2024) 的综述:“一般来说,用 LLM 做自我修正(例如在推理题上)不起作用;但在代码模型上做自我修正时,我们还可以拿到单元测试的输出——这个输出能让测试时的迭代自我修正非常成功。“综述里的图:代码任务 → 代码 LLM 生成一个程序 → 执行器运行单元测试 → 测试结果(以及可选的自我反思,例如”它没有保持元素的顺序,因为用了 set()“)作为反馈 → 再生成。

为什么没有测试就不起作用?自我修正需要知道哪里错了。如果判断”对不对”的也是同一个模型,它犯错的地方往往也就是它判断错的地方。测试是一个外部的、可靠的信号。

04 第 2 部分用 02 的模型比较几种策略(最多 B 次生成,温度 0.8,200 道题,隐藏测试判分):

策略B = 1B = 2B = 4B = 8
只生成一次(不执行)0.3450.3650.3800.375
生成 B 个,交模型最自信的(不执行)0.3450.3700.3300.310
生成 → 跑可见测试 → 不过就重试0.3700.5650.7100.875
提示从 1 个例子开始,失败的测试加进提示0.3600.4950.7250.885
上限:B 个里有一个对(pass@B)0.3450.5500.7400.900

(同一列里的差别在 ±0.03 左右是采样噪声。)三个结论:

  1. 不执行代码,多生成几个没有用——“交最自信的”在 B = 8 时甚至比只生成一次还差(0.310 vs 0.375):模型最自信的就是它最喜欢的,而它最喜欢的常常是错的。没有外部信号,模型没法知道自己哪里错了——这就是”一般来说自我修正不起作用”。
  2. 能执行代码,同样 8 次生成就到了 0.875,接近上限 0.900:可见测试替模型做了它自己做不到的判断。这就是 pass@k 和”真正交出一个答案”之间的桥梁。
  3. “把失败的测试写进提示”并没有比单纯重试更好(0.885 vs 0.875)。这个小模型几乎不利用例子的内容(它只看长度),把反馈写进上下文对它没有用。反馈要起作用,模型得会读反馈——真实的代码模型被专门训练过读报错和测试输出(§6.4 的训练数据)。

通过测试不等于写对了。 04 第 3 部分:只有 1 个可见测试时,200 道题里有 9 道的测试本身有歧义(例如例子恰好是回文,rev 就和”什么都不做”分不开),智能体照样”通过测试”、提交,其中 4 个在隐藏测试上是错的;可见测试有 3 个时,歧义消失。测试越少、越不全面,“通过测试”离”写对了”越远。真实世界里更严重:SWE-bench 用仓库里已有的测试判分,受测试覆盖率限制;智能体如果能修改测试本身,“让测试通过”可以退化成”把测试改掉”——这是第 26 讲”奖励投机”的一个具体例子。


5. 工具调用 · Tool calling

slides 的定义:

  • 工具调用让 LLM 选择并调用外部函数,而不只是生成文本。
  • 一个工具通常有名字、描述、参数的模式(schema,例如 JSON Schema:参数叫什么、什么类型、哪些必填)。
  • 模型决定什么时候需要工具,输出一个结构化的调用,拿回工具的结果,再接着推理。
  • 适合需要行动或需要依据真实数据的事:查天气、查数据库、发邮件、做计算。
  • 模型并没有自己”做”这个外部动作,它只是请求外围的系统去运行工具。

slides 的例子是一个天气工具:工具定义里写着 get_weather、描述、参数 city 和 date;模型输出 {"tool_name": "get_weather", "arguments": {"city": "Pittsburgh", "date": "2026-04-09"}};系统运行工具,把结果交回模型。04 第 1 部分用同样的格式打印了一个回合:

[第 1 次] 模型 → {"tool": "run_tests", "arguments": {"program": "rev rev"}}
          系统 → {"passed": 0, "total": 3, "failures": [{"input": "cfhf", "expected": "gagd", "got": "cfhf"}, ...]}
[第 2 次] 模型 → {"tool": "run_tests", "arguments": {"program": "rot rev"}}
          系统 → {"passed": 3, "total": 3, "failures": []}
          模型 → {"tool": "submit", "arguments": {"program": "rot rev"}}

Kimi K2:怎样让模型学会用工具。 slides 介绍了 Kimi K2 的三步:

  1. 大规模合成工具使用的监督数据:建一个大的工具定义库(真实的 MCP 工具 + 合成的工具),再生成”智能体”(带不同的系统提示和工具组合)、“任务”(带评分标准)和成功的工具调用轨迹(由一个模拟的用户和工具环境交互产生,再由一个评判模型按标准过滤)。
  2. 在真实和合成的环境里联合做强化学习:模型从交互的结果(而不只是模仿)里学会选择和编排工具。
  3. 推理时:每次请求都把可用的工具列表交给模型,由它自主决定什么时候、怎样调用。

slides 展示的结果图里,K2 在工具使用(Tau2-bench、ACEBench)、编程(SWE-bench Verified、多语言版、LiveCodeBench、OJBench)和数学 / 科学推理的基准上都和当时的强模型相当。


6. 编程智能体 · Coding agents

6.1 什么是编程智能体:CodeAct

slides 引用 OpenHands 的一篇博客,把”运行软件智能体(写代码、测试、交付代码的智能体)“的技术栈分成三根支柱:外壳(harness)——建在一个或多个 LLM 之上的智能体循环(例如 OpenHands、Claude Code、Codex、OpenCode、Cursor 的智能体模式);编排层(orchestrator)——智能体运行的环境(例如 OpenHands Agent Server、Claude Code Web、E2B 沙盒、Cursor 的后台智能体);控制平面(control plane)——大规模地观察和编排许多智能体(博客说这还是一个正在形成的类别)。模型本身只是其中的一部分。

CodeAct(Wang et al., 2024)把智能体的”动作”统一成可执行的代码:与其让模型输出一个 JSON 格式的工具调用(每次只能调一个工具),不如让它写一段 Python 代码——代码里可以有循环、条件、变量,可以把多个工具的结果组合起来,一次完成更多的事;执行的输出(包括报错)作为下一步的观察。论文报告这种方式在多个基准上比 JSON 或文本形式的动作成功率更高。

6.2 编程智能体的循环

slides 的总结:编程智能体反复执行一个循环,在一个编程任务上取得进展——

  1. 理解目标:读任务、代码库、相关的文件;
  2. 计划下一步:选一个具体的动作,例如查看代码、编辑文件、运行测试、查文档;
  3. 行动:修改代码或使用工具;
  4. 观察反馈:读编译错误、测试结果、日志、工具输出;
  5. 修正:更新理解,决定下一步试什么。

循环一直进行到任务完成、卡住、或者达到停止条件。紧凑地写就是:读 → 计划 → 编辑 / 运行 → 检查结果 → 重复。 04 的智能体是这个循环最小的版本:生成(行动)→ run_tests(观察)→ 不过就重试(修正)→ 到预算就停。

6.3 系统提示:把”好的工程习惯”写下来

slides 展示了两个开源编程智能体的系统提示:OpenHands 和 OpenAI 的 Codex CLI(后者还规定了 AGENTS.md 的约定:代码仓库里可以放 AGENTS.md 文件,写给智能体看的项目说明和规则)。OpenHands 的系统提示按主题分成几段,大意是:

  • 角色:彻底、有条理、质量优先于速度;如果用户只是问”为什么会这样”,就回答问题,不要去修。
  • 效率:每个动作都有成本,尽量把多个命令合成一个动作;探索代码库时用 find、grep、git 这类高效的工具。
  • 文件系统:不要假设路径是相对于当前目录的,先找到文件;直接修改原文件,不要建一堆带后缀的新版本;临时文件用完就删。
  • 代码质量:做解决问题所需的最小修改;动手之前先理解代码库。
  • 版本控制:谨慎对待 git 操作;除非用户明确要求,不要做有潜在危险的修改(例如推送到 main、删除仓库);不要提交不该进版本控制的文件(例如 .env、依赖目录)。
  • 拉取请求:除非明确要求,不要推送到远程分支、不要开 PR。
  • 解决问题的流程:探索 → 分析多种方案 → 测试(修 bug 时先写一个能复现问题的测试)→ 实现 → 验证。

这些规则读起来就像给一个新同事的入职指南。它们说明了两件事:一是模型本身的能力之外,怎样用它(提示、工具、流程)对智能体的表现影响很大;二是智能体会在真实的环境里做事,所以需要明确的边界——哪些操作必须先问人。真实的智能体框架还会用权限系统和沙盒(容器、受限的网络和文件访问)在技术上强制这些边界,而不是只依赖提示。

6.4 多智能体架构与关键挑战

slides 引用 Anthropic 的 2026 年智能体编程趋势报告里的图”从单个智能体到协调的团队”:单智能体的工作流在一个上下文窗口里按顺序处理任务;多智能体架构用一个编排者协调多个专门的智能体并行工作,每个都有自己专用的上下文,最后把结果综合成一个整体的输出。

slides 总结了构建编程智能体的关键挑战:

  • 代码搜索:找不到 bug 就修不了;不知道一个新功能该放在哪里,就很难实现它。在一个几十万行的代码库里定位要改的地方,本身就是一个难题(这也是第 19 讲长上下文的一个应用场景)。
  • 编辑代码:这是基本的代码生成问题——找到了要改的位置以后,要生成一个正确的补丁。
  • 创造训练数据:两种来源——用强化学习让智能体完整地跑完解决问题的轨迹(奖励来自测试是否通过);或者合成轨迹(例如 SERA),如果算力不多,可以考虑后者。

7. 自主智能体:网页与 GUI · Autonomous agents

slides 的最后一部分把视野从代码扩展到”在计算机上做任何事”的智能体。

7.1 读 HTML:Mind2Web

Mind2Web(Deng et al., 2023)让智能体直接读网页的 HTML 来和网页交互。完整的 HTML 太长,所以系统分两步:先用一个小的排序模型从网页的全部元素里挑出最相关的几十个候选,再让一个 LLM 以多选题的形式选出要操作的元素和动作(点击、输入、选择)。这是”检索 + 生成”的思路用在了动作上。

7.2 看截图:视觉语言模型做 GUI 智能体

很多界面没有干净的 HTML(桌面软件、手机应用),或者 HTML 和用户看到的不一致。于是另一条路是像人一样看截图。slides:用视觉语言模型(第 13 讲)做 GUI 智能体,通常有两个关键部分——

  • 视觉定位模型(visual grounding):根据提示和当前截图,确定下一步在哪里点击、输入、交互;
  • GUI 智能体模型:引导整个交互过程,朝着最终目标前进。

Set-of-Mark 提示(Yang et al., 2023):把图像中有语义的区域都标上数字——数字直接画进图像的像素里,提示里就可以说”点击 7 号”。怎样找出要标的区域?用一个现成的分割模型(例如 Segment Anything)。它对所有视觉语言模型都有效吗?不一定:论文发现 GPT-4V 能理解这些标记并据此定位,但 LLaVA-1.5 和 MiniGPT-v2 不能。

SeeClick(Cheng et al., 2024):直接训练一个现成的视觉语言模型(Qwen-VL),只看截图就执行单个动作(输出要点击的坐标);先在大量”界面元素描述 → 屏幕位置”的数据上做 GUI 定位的预训练,再在具体任务上微调。

7.3 用强化学习训练智能体

slides 的最后一页:问题——完成一个任务往往有很多种方式,怎样训练模型而不只是模仿某个人的某一次操作?办法——用强化学习,在任务成功完成时给正的奖励(奖励在最后才出现)。slides 的图来自 WebRL:智能体在网页环境里交互、产生轨迹;一个评判模型判断任务是否完成;失败的任务被用来生成新的、难度合适的任务(“自我演化的课程”);策略更新时加 KL 约束,并用经验回放复用过去的成功轨迹。这和第 20 讲的推理模型、§6.4 的编程智能体用的是同一套思路:只要有一个可以自动判断”成功了没有”的信号(测试通过、最终状态正确),就可以用 RL 让模型在交互中学习,而不受人类示范的限制。


8. 小结 · Summary

  1. 代码的特殊之处是可以执行:功能正确性取代了 BLEU,测试让推理时的筛选和修正成为可能,测试结果也是 RL 的奖励。
  2. pass@k 用 1−(n−ck)/(nk)1 - \binom{n-c}{k}/\binom{n}{k} 无偏估计(连乘写法避免溢出),每题采 n > k 个样本降低方差;直觉的 1−(1−c/n)k1 - (1-c/n)^k 系统性偏低。
  3. 温度对 pass@1 和 pass@k 的影响不同:多样性帮助 pass@k;集中只在模型首选可靠时帮助 pass@1。
  4. 代码模型:CodeBERT(编码器,检索)、Codex(GPT-3 + 代码)、CodeT5(编码器-解码器,多任务)、StarCoder(FIM、长上下文、MQA)、LongCoder(稀疏注意力)。
  5. FIM 只是一个数据变换:把中间移到后缀之后;按行切 / 随机切各有所长;充分训练时从左往右的能力几乎不受影响。
  6. 没有外部信号时自我修正不起作用;有了测试,“生成 → 测试 → 重试”接近 pass@k 的上限。但通过测试不等于写对,测试的覆盖率决定了这个差距。
  7. 工具调用 = 模型输出结构化的请求、系统执行;编程智能体 = 读 → 计划 → 编辑 / 运行 → 检查 → 重复,加上系统提示、权限和沙盒划定的边界。
  8. GUI 智能体可以读 HTML(Mind2Web)或看截图(Set-of-Mark、SeeClick),并可以用以”任务完成”为奖励的 RL 训练。

9. 常见误区 · Common pitfalls

  • “pass@k 就是 1 − (1 − pass@1)^k。” 只有当每道题的成功率都一样时才对;用 c/n 代入这个公式还会系统性偏低(§2.2)。
  • “温度越低越好。” pass@k(k > 1)要多样性;即使是 pass@1,低温度也只在模型首选可靠时才有帮助(§2.4)。
  • “BLEU 高说明代码差不多对。” 同样的词换了顺序,功能可能完全不同(§2.5)。
  • “FIM 需要一个新的模型结构。” 它只是把训练数据切开、换个顺序(§3.3)。
  • “让模型多想几遍、自己检查,就能改对。” 没有外部信号时,模型最自信的答案就是它最喜欢的答案(§4)。
  • “通过了测试就是写对了。” 测试少或有歧义时,通过测试的程序可能是错的(§4)。
  • “智能体自己执行了动作。” 模型只输出结构化的请求,真正执行的是外围的系统——所以权限和沙盒要由系统来保证(§5、§6.3)。
  • “把测试的报错交还给模型,它就会改。” 模型得会读反馈;我们的小模型就不会(§4)。

10. 练习 · Exercises

练习 1(pass@k 的估计)。 一道题采了 n = 5 个样本,c = 2 个正确。(a) 用无偏估计算 pass@1、pass@2、pass@3、pass@4。(b) 用直觉估计算 pass@2。(c) 为什么 pass@4 的无偏估计是 1?

参考答案

(a) pass@1 = 2/5 = 0.4;pass@2 = 1 − C(3,2)/C(5,2) = 1 − 3/10 = 0.7;pass@3 = 1 − C(3,3)/C(5,3) = 1 − 1/10 = 0.9;pass@4 = 1 − C(3,4)/C(5,4) = 1 − 0 = 1。(b) 1 − 0.6² = 0.64 < 0.7。(c) 只有 3 个错的,抽 4 个必然至少有一个对。

练习 2(连乘写法)。 证明 (n−ck)/(nk)=∏i=n−c+1n(1−k/i)\binom{n-c}{k}/\binom{n}{k} = \prod_{i=n-c+1}^{n}(1 - k/i)。

参考答案

(n−ck)(nk)=(n−c)! (n−k)!(n−c−k)! n!=∏i=n−c+1ni−ki\frac{\binom{n-c}{k}}{\binom{n}{k}} = \frac{(n-c)!\,(n-k)!}{(n-c-k)!\,n!} = \prod_{i=n-c+1}^{n}\frac{i-k}{i}:分子分母各有 c 个因子——(n−k)!(n−c−k)!=∏i=n−c+1n(i−k)\frac{(n-k)!}{(n-c-k)!} = \prod_{i=n-c+1}^{n}(i-k),n!(n−c)!=∏i=n−c+1ni\frac{n!}{(n-c)!} = \prod_{i=n-c+1}^{n} i。每一项 i−ki=1−ki\frac{i-k}{i} = 1 - \frac{k}{i}。

练习 3(温度与 pass@k)。 一道题有两个候选程序 A(错)和 B(对),模型给 A 的概率 0.6、B 0.4。(a) 温度 1 时 pass@1 和 pass@5 是多少?(b) 温度 → 0 呢?(c) 温度 τ 时 B 的概率是多少?在什么条件下降低温度对 pass@1 有利?

参考答案

(a) pass@1 = 0.4,pass@5 = 1 − 0.6⁵ ≈ 0.922。(b) 只会生成 A:pass@1 = pass@5 = 0。(c) pB(τ)=0.41/τ0.41/τ+0.61/τp_B(\tau) = \frac{0.4^{1/\tau}}{0.4^{1/\tau} + 0.6^{1/\tau}},τ 越小越接近 0。降低温度对 pass@1 有利,当且仅当正确答案是模型的首选(概率最大)。这正是 02 里低温度让 pass@1 变差的原因。

练习 4(FIM 的数据变换)。 把 v3 = rot inp ; v7 = rev v3 ; out v7 ; 按行切,中间是第 2 行,写出 PSM 格式的训练样本和推理时的提示。为什么推理时模型知道要定义 v7?

参考答案

训练:<PRE> v3 = rot inp ; <SUF> out v7 ; <MID> v7 = rev v3 ; <EOT>。推理:<PRE> v3 = rot inp ; <SUF> out v7 ; <MID>,模型接着写。后缀在中间之前,模型生成中间时已经”读过”了 out v7,而 v7 在前缀里没有定义——训练时它反复见过”中间要定义后缀里用到、前缀里没有的变量”。

练习 5(测试的歧义)。 在语言一里,可见测试只有一个例子 aba > aba。列出所有能通过它的程序。如果隐藏测试是 abc > cba,提交其中哪个程序才算对?

参考答案

输出和输入相同:rev(回文反转不变)、rev rev、以及……检查其余:rot 会改变字母,dup、drop 改变长度,所以只有 rev 和 rev rev。隐藏测试要求反转:只有 rev 对。智能体如果提交了 rev rev,就是”通过测试但写错”。

练习 6(智能体的预算)。 假设每次生成独立地以概率 p 写对,可见测试能完美判断对错。“生成 → 测试 → 重试”在预算 B 下的成功率是多少?p = 0.37、B = 8 时呢?和 04 的结果比较,为什么实际值(0.875)低于这个数?

参考答案

1−(1−p)B1 - (1-p)^B。p = 0.37、B = 8:1−0.638≈0.9751 - 0.63^8 \approx 0.975。实际更低,因为”每次独立地以同样的 p 写对”不成立:难度因题而异,有的题模型几乎总是写错(它给正确程序的概率远小于 0.37),重试 8 次也很难碰到(这和 §2.2 的”难度不均”是同一件事)。上限 pass@8 = 0.900 也低于 0.975。

11. 延伸阅读 · Further reading

  • 评估:Chen et al. (2021)(Codex、HumanEval、pass@k);Austin et al. (2021)(MBPP);Ren et al. (2020), CodeBLEU;Lai et al. (2023), DS-1000;Jimenez et al. (2024), SWE-bench;OpenAI (2024), Introducing SWE-bench Verified;Terminal-Bench 2.0 论文 (2026)。
  • 代码模型:Feng et al. (2020), CodeBERT;Wang et al. (2021), CodeT5;Fried et al. (2022), InCoder;Bavarian et al. (2022), FIM;Li et al. (2023), StarCoder;Guo et al. (2023), LongCoder;Jiang et al. (2024) 综述。
  • 修正与智能体:Chen et al. (2023), Teaching Large Language Models to Self-Debug;Wang et al. (2024), CodeAct;Yang et al. (2024), SWE-agent;Wang et al. (2024), OpenHands;Kimi 团队 (2025), Kimi K2。
  • GUI 与网页智能体:Deng et al. (2023), Mind2Web;Yang et al. (2023), Set-of-Mark;Cheng et al. (2024), SeeClick;Kirillov et al. (2023), Segment Anything。

12. 下一讲预告 · Next lecture

第 24 讲:音频理解与合成。声音怎样变成模型能处理的东西?我们会从零实现短时傅里叶变换和梅尔频谱,训练一个小的”语音识别”模型(用 CTC 损失把一串音调转成文字),再用残差向量量化把音频压缩成离散的 token——这是 AudioLM、MusicGen 这类”音频语言模型”的基础。

← 返回课程目录