单元测试通过——这四个字,是代码 Agent 最好的奖励信号
前两篇讲了用代码作为推理接口(PoT/CoC),以及动态选择推理模式(CodeSteer)。但都有一个隐含假设:代码生成是一次性的——生成,执行,得到结果。
现实里,代码第一次往往是错的。
这不是悲观陈述,而是代码生成的基本特征。即使是熟练程序员写的代码,也需要调试迭代。那么问题变成:能不能让模型从代码的执行结果中学习,自动迭代修正?
这正是 CodeRL(2022)和随后一系列迭代调试工作的核心思路。这一篇从 CodeRL 的训练机制讲到 Agent 推理时的迭代修正,把"执行反馈"这个信号完整地展开。
代码生成为什么特别适合强化学习
先说为什么代码生成比其他 NLP 任务更适合用强化学习(RL)。
传统 NLP 的 RL 有一个大问题:奖励信号很难定义。"这个翻译好不好""这个摘要准不准",需要人工评分,成本高且不稳定。
代码不同。代码有一个近乎完美的自动评判者:单元测试。
# 模型生成的代码
def find_max_subarray(arr):
max_sum = arr[0]
current_sum = arr[0]
for i in range(1, len(arr)):
current_sum = max(arr[i], current_sum + arr[i])
max_sum = max(max_sum, current_sum)
return max_sum
# 单元测试(不需要人参与)
assert find_max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) == 6 # ✅
assert find_max_subarray([-1]) == -1 # ✅
assert find_max_subarray([5, 4, -1, 7, 8]) == 23 # ✅
测试通过 = 正奖励,测试失败 = 负奖励或零奖励。这个奖励信号:
- 自动(不需要人工标注)
- 精确(测试要么通过要么失败,不模糊)
- 可扩展(每个编程任务都可以有测试集)
- 可靠(测试执行结果是客观的)
这几个性质凑在一起,让代码生成成为 LLM 强化学习的绝佳训练场。
CodeRL:Actor-Critic 的代码版
CodeRL(Salesforce Research,2022)把代码生成建模成经典的 Actor-Critic 强化学习框架:
Actor:代码生成模型(基于 CodeT5),输入题目描述,输出代码。 Critic:代码质量预测器,输入(题目, 生成的代码),预测这段代码通过测试的概率。
训练流程:
1. Actor 生成一批候选代码
2. 执行单元测试,得到真实的通过/失败信号
3. 用测试结果训练 Critic(让它学会预测哪些代码会通过测试)
4. 用 Critic 的预测为 Actor 提供密集奖励信号(比稀疏的通过/失败信号更平滑)
5. 迭代:Actor 生成更好的代码 → Critic 学到更准确的预测 → Actor 得到更好的反馈
为什么需要 Critic 而不是直接用测试结果训练?
测试结果是稀疏的(通过或失败),信号很粗。Critic 提供的是一个 0~1 的连续预测值,能给出"这段代码有 0.7 概率通过测试"的密集信号,让 Actor 的梯度更稳定。
推理时的关键策略:Critical Sampling
CodeRL 的另一个贡献是推理时的采样策略。普通的做法是生成 N 个候选,选最高概率的。CodeRL 的做法:
def critical_sampling(problem: str, n_candidates: int = 10):
# 生成多个候选
candidates = actor.generate(problem, n=n_candidates)
# 对每个候选,用 Critic 评分
scores = [critic.score(problem, code) for code in candidates]
# 不是选最高分,而是根据分数分布做加权采样
# 这样能平衡探索(低分候选偶尔被选中)和利用(高分候选更常被选)
weights = softmax(scores / temperature)
selected = random.choices(candidates, weights=weights, k=1)[0]
return selected
这个方法在 APPS benchmark(竞赛级编程题)上超越了当时所有基于 GPT 的系统,即使 CodeRL 用的基础模型参数量小得多。
实验结果:在 APPS 上,CodeRL 相比纯监督微调提升约 3~4%;在 MBPP(基础 Python 编程任务)上的零样本迁移也达到当时最优。
RLTF:用测试失败信息细化奖励
RLTF(Reinforcement Learning from Test Feedback,2023)进一步细化了测试反馈信号。
普通的"通过/失败"信号太粗:即使代码失败了,也有"错了一点"和"完全错了"的区别。RLTF 的思路是:从测试失败中提取更细粒度的信号。
具体做法:
def compute_rltf_reward(code: str, test_cases: list) -> float:
results = []
for test in test_cases:
try:
output = execute(code, test.input)
if output == test.expected:
results.append(1.0) # 完全正确
else:
# 部分奖励:输出类型对但值错,比完全类型错更好
if type(output) == type(test.expected):
results.append(0.3) # 类型对但值错
else:
results.append(0.0) # 完全错
except Exception as e:
if isinstance(e, (TypeError, ValueError)):
results.append(0.1) # 至少运行了,有语法
else:
results.append(0.0) # 连运行都没运行
return sum(results) / len(results) # 平均部分奖励
这种细粒度奖励让模型能区分"几乎对了"和"完全错了",训练信号更丰富。
推理时的迭代自修正
上面讲的是训练阶段怎么用执行反馈。推理阶段可以更直接:看到执行失败后,让模型自己修正代码。
这个思路在实践中有几个变体:
Self-Debugging(自我调试):
async def self_debugging_agent(problem: str, max_retries: int = 3):
code = await generate_code(problem)
for attempt in range(max_retries):
result = execute_code(code)
if result.success:
return code
# 把执行错误反馈给模型,让它修正
error_context = f"""
代码:
```python
{code}
执行错误: {result.error_message}
{f'测试失败:期望 {result.expected},实际 {result.actual}' if result.test_failed else ''}
请修正代码:"""
code = await generate_code(error_context)
return code # 最后一次尝试的结果
**关键设计决策**:错误信息怎么格式化?实验发现,**把错误信息、失败的测试用例和原始代码一起放进上下文**,比只给错误信息效果好得多。模型需要同时看到"什么错了"(错误信息)、"在哪里错了"(失败测试)和"原来的代码是什么"(代码本身),才能做出有效修正。
**Rubber Duck Debugging(鸭子调试)**:
更进一步的变体——让模型先解释代码的意图和逻辑,再基于这个解释来修正:
```python
async def rubber_duck_debug(problem: str, buggy_code: str, error: str):
# 第一步:让模型解释代码意图
explanation = await llm.ask(f"""
解释以下代码的意图和每一步的逻辑(不要评判对错):
```python
{buggy_code}
```""")
# 第二步:基于解释来修正
fixed_code = await llm.ask(f"""
原始问题:{problem}
代码意图:{explanation}
代码执行报错:{error}
基于上面的意图描述,修正代码:""")
return fixed_code
这个两步法的道理是:先让模型"理解"代码在做什么,再让它"修改"代码,避免直接修改时模型把已经正确的部分也改掉。
测试驱动的 Agent:TDD-as-Harness
更系统化的思路是直接采用测试驱动开发(TDD)的 Agent 框架:
async def tdd_agent(specification: str):
"""
TDD 流程:先写测试,再写代码,让测试驱动代码的迭代
"""
# 1. 根据规范生成测试
tests = await generate_tests(specification)
# 2. 尝试生成通过测试的代码
code = await generate_code_from_tests(specification, tests)
# 3. 迭代直到所有测试通过
max_iterations = 5
for i in range(max_iterations):
results = run_tests(code, tests)
passing = [r for r in results if r.passed]
failing = [r for r in results if not r.passed]
if not failing:
return code # 全部通过
# 只把失败的测试反馈给模型
feedback = format_failing_tests(failing)
code = await fix_code(code, feedback, f"第 {i+1} 次修正")
return code
TDD 框架的优点是:测试是在实现代码前就写好的,所以测试本身是正确的(不会因为代码写错了就改测试来通过)。这给迭代修正提供了一个稳固的"地面真相"。
一个被忽视的问题:测试本身的质量
用执行反馈训练或引导 Agent,一个根本前提是:测试是正确的。
但测试可能覆盖不全:代码通过了所有测试,但在测试未覆盖的边界情况上还是错的。这叫"测试过拟合"——Agent 学会了让代码通过测试,而不是真正解决问题。
SWE-bench(第 21 篇)里提到了这个问题:有些 issue 的测试套件不完整,Agent 可以生成一个通过测试但破坏其他功能的 patch。
工程上的缓解:
- 多样化测试:除了给定的测试,让模型额外生成边界测试用例
- 反例测试:除了"这个应该通过"的测试,也要有"这个应该失败"的测试
- 行为覆盖率:不只看测试通过率,看代码覆盖率指标(branches、statements)
async def generate_comprehensive_tests(spec: str, code: str):
"""让模型生成能发现边界情况的测试"""
edge_tests = await llm.ask(f"""
给定以下函数规范和实现,生成 5 个边界测试用例:
- 空输入
- 极大/极小值
- 已通过的测试的反例(什么情况下应该返回不同结果)
- 最容易出错的边界情况
规范:{spec}
实现:{code}
返回 Python 测试列表(assert 语句):""")
return edge_tests
从 CodeRL 到现代 Coding Agent
CodeRL 是 2022 年的工作。现在的 Coding Agent(SWE-agent、Claude Code 等)用的方法更复杂,但核心思路是一脉相承的:
执行反馈是代码 Agent 最好的学习信号。不管是训练时的 RL(CodeRL),还是推理时的迭代调试(Self-Debugging),都在利用"代码执行结果"这个天然存在的、免费的、精确的反馈信号。
这个信号的价值在于:它不需要人在循环里。代码能不能跑,单元测试过没过,这些判断不需要人来做,是客观的、自动的。这让代码 Agent 能在极短时间内迭代大量候选,形成快速的反馈循环。
你在 Claude Code 里看到的"检查结果 → 发现错误 → 修正代码 → 再试"这个循环,本质上就是 CodeRL 和 Self-Debugging 的运行时版本,只是集成到了更复杂的 Agent 框架里。
小结
代码执行结果是一个近乎完美的 Agent 反馈信号:自动、精确、可扩展、无需标注。CodeRL 把这个信号用到了训练时,RLTF 细化了奖励结构,Self-Debugging 把它用到了推理时迭代。
三条路线的共同点:把"这段代码执行失败了"从一个障碍变成了一个老师。
第一模块(代码作为推理接口)到此完整。接下来进入第二模块——代码作为行动接口:当代码不只是推理工具,而是 Agent 在真实世界采取行动的方式时,会带来什么新的设计挑战。第一篇讲 Voyager:一个用代码技能库做终身学习的 Minecraft Agent,是"代码作为技能表示"最经典的案例。
相关阅读
- CodeRL (Le et al., NeurIPS 2022) — Actor-Critic 代码生成
- RLTF (Liu et al., 2023) — 细粒度测试反馈强化学习
- Self-Debugging (Chen et al., 2023) — 代码自修正的系统研究
- AlphaCode (Li et al., DeepMind) — 竞赛级代码生成,大规模采样+过滤
- Reflexion (Shinn et al., 2023) — 用语言反馈做 Agent 自我修正(更通用的框架)
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:03. 从执行反馈中学习:CodeRL 与迭代自修正
本文链接:https://www.sshipanoo.com/blog/ai/code-agent-harness/03-CodeRL/
本文最后一次更新为 天前,文章中的某些内容可能已过时!