用执行反馈驱动代码修正,而不是随机重试
调试循环
程序员写代码的流程通常是:
写代码 → 运行 → 看报错 → 理解报错 → 定位问题 → 修改 → 再运行
这个循环要走几轮,取决于任务复杂度。对于 LLM 来说,一个自然的问题是:它能不能做同样的事情?
Self-Debugging 这条研究线回答的正是这个问题。Chen et al. 2023 年的论文《Teaching Large Language Models to Self-Debug》是这个方向较早的系统性工作。
三种调试策略
论文定义了三种调试方式:
策略一:基于执行结果调试(Execution-based)
代码运行了,但输出结果不对:
# 原始代码(逻辑错误)
def count_vowels(s):
return sum(1 for c in s if c in 'aeiou')
# 测试用例
assert count_vowels("Hello World") == 3 # 实际返回 2,漏掉大写
Agent 看到 AssertionError,加上预期值与实际值,可以推断出问题在于没有处理大写字母,然后生成修正版本:
def count_vowels(s):
return sum(1 for c in s.lower() if c in 'aeiou')
测试用例在这里充当调试信号。
策略二:基于错误信息调试(Error-based)
代码直接报错,有 traceback:
Traceback (most recent call last):
File "solution.py", line 8, in merge_sorted
result.append(arr1[i])
IndexError: list index out of range
LLM 需要:
- 定位到第 8 行,
arr1[i]越界 - 联系上下文理解为什么越界:
i没有检查是否超过arr1的长度 - 生成边界检查逻辑
这要求模型对 Python 异常类型有理解,并能将错误信息映射回代码逻辑。
策略三:基于代码解释的调试(Explanation-based)
Agent 先用自然语言解释自己的代码在做什么,在解释过程中发现逻辑漏洞,再修改代码:
[Agent 的自我解释]
"这段代码的逻辑是:维护两个指针 i 和 j,分别指向两个数组,
每次把较小的元素放入结果……当 i 到达 arr1 末尾时,
循环条件 `while i < len(arr1) and j < len(arr2)` 会终止,
但此时 arr2 还有剩余元素没有被追加——这是 bug 所在。"
这种策略类似程序员的橡皮鸭调试(rubber duck debugging):通过向自己解释代码来发现问题,不依赖执行报错。
实验结果
论文在 HumanEval 和 MBPP 两个 benchmark 上进行了对比:
| 方法 | HumanEval pass@1 | MBPP pass@1 |
|---|---|---|
| 直接生成(no debug) | 65.8% | 54.2% |
| + 执行反馈 | 71.3% | 58.9% |
| + 错误信息 | 73.5% | 61.2% |
| + 代码解释 | 76.2% | 63.8% |
几个值得关注的点:
- 反馈越精确,提升越明显
- 代码解释策略在没有执行环境时也有效,Agent 可以在沙箱受限的场景下通过先想清楚再改来提升质量
- 多轮调试的收益递减,第一轮修正收益最大,第三轮之后收益明显下降
工程实践:调试循环的实现
在实际的 Code Agent 系统里,自我调试通常实现为一个有上限的重试循环:
MAX_ATTEMPTS = 3
def run_with_self_debug(agent, task, executor):
code = agent.generate_code(task)
for attempt in range(MAX_ATTEMPTS):
result = executor.run(code)
if result.success:
return code, result.output
debug_prompt = f"""
你生成的代码执行失败了:
代码:
{code}
错误信息:
{result.error}
请分析错误原因并生成修正后的代码。
"""
code = agent.generate_code(debug_prompt)
return None, "超过最大重试次数"
几个工程细节:
错误信息的截断:完整的 traceback 有时很长,通常只截取最后几行放入 prompt,避免上下文过长。
防止来回横跳:有时 Agent 会在两个错误状态之间交替。可以记录每次生成的代码,如果发现重复就终止循环。
部分成功的处理:如果有多个测试用例,有些通过、有些失败,错误信息应该包含"哪些通过了、哪些没通过",帮助 Agent 做定向修复。
边界在哪里?
几类情况自我调试表现较差:
逻辑错误但没有明确错误信息:代码运行成功但答案错了,且没有测试用例——Agent 无法知道哪里不对。这是代码验证问题,不是调试问题。
需要领域知识的错误:ValueError: Input contains NaN 背后可能需要理解数据清洗的业务逻辑,仅靠错误信息无法推导。
整体方案就是错的:如果整个思路就是错的,逐行调试只会让 Agent 在错误方向上越走越深,这时候需要的是重新规划,而不是调试。
这也解释了为什么在 CodeRL(第 03 篇)和 Reflexion(第 08 篇)中,研究者会在调试循环之外加入更高层次的反思机制。
在生产系统中的位置
在当前的 Code Agent 系统(SWE-agent、OpenHands、GitHub Copilot Workspace 等)里,自我调试是标准组件,通常位于 Agent 流程的最内层循环:
任务规划(外层)
└── 代码生成(中层)
└── 执行 + 自我调试(内层)
└── 单次代码执行
没有自我调试能力的 Code Agent,代码质量完全取决于第一次生成的结果。加上这个能力,Agent 才能在执行失败时有针对性地修正,而不是盲目重试。
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:06. Self-Debugging:让模型自己读报错、自己改代码
本文链接:https://www.sshipanoo.com/blog/ai/code-agent-harness/06-Self-Debug/
本文最后一次更新为 天前,文章中的某些内容可能已过时!