AI编程助手在修复代码方面已经变得非常出色。
将错误粘贴到 AI 工具中,几秒钟内你就会得到一个修正后的实现。当你只想让某件事能够运行时,这非常有用。
但当你在学习编程时,还有一个值得思考的问题:AI 是帮助你理解了问题,还是仅仅为你消除了问题?
这种差别很重要。
调试不仅仅是得到能运行的代码。它还包括理解失败的原因,找出错误的假设,做出修改,并验证该修改确实解决了问题。
我在使用 Coddy.tech 时进行了探索,这是一个交互式编程学习平台,结合了编程练习、测试反馈、调试工具、提示以及名为 Bugsy 的 AI 导师。
我不只是看 AI 是否能解决编程问题,而是尝试考察另一个方面:AI 编程导师在直接给出答案之前,应该提供多少帮助?
在这篇文章中,我将探讨这个问题,提出一个简单的 AI 辅助调试框架,并结合我在 Coddy 上的一些实践实验,看看这些想法在实际中是如何运作的。
目录
调试不仅仅是产生正确的代码
让我们从一个简单的 Python 函数开始:
def calculate_average(numbers):
total = 0
for number in numbers:
total += number
return total / (len(numbers) - 1)
scores = [80, 90, 70, 100]
print(calculate_average(scores))
上面的程序运行起来没有任何语法错误或异常,但结果却是错的。
四个分数的总和是 340,所以期望的平均值应该是:
340 / 4 = 85
但函数实际计算的是:
340 / 3
原因出在这一行:
return total / (len(numbers) - 1)
AI 助手可能会直接给出修复方案:
return total / len(numbers)
问题解决了。但对正在学习编程的人来说,AI 已经替他们完成了大部分关键推理。
另一种回应方式可能是:
你计算总和的逻辑没有问题。但再仔细看看除数:numbers 里实际有多少个值?
这样,开发者仍然需要自己去排查逻辑。这个细微的差别,代表了两种截然不同的 AI 辅助思路。
开发者实际是如何调试的
手动调试时,我们通常会经历类似下面这样的流程:
调试 → 隔离问题 → 分析原因 → 修复 → 验证
假设下面这个测试失败了:
assert calculate_average([80, 90, 70, 100]) == 85
我们通常先检查实际结果,然后看 total 是否等于期望值;如果 total 正确,再验证除法。
最终,我们注意到有四个值在参与除法,仿佛只有三个存在。
这一整个过程有助于形成理解。
如果 AI 助手立即重写函数,代码会变得有效,但其中的大部分推理过程会消失。这表明,专为学习设计的编码助手不仅需要代码生成能力,还需要一种决定提供多少帮助的策略。
AI 辅助调试框架
我通常会把这个过程分为五个阶段:
上下文 → 诊断 → 提示 → 验证 → 解释
每个阶段有不同的目的。
1. 上下文
在提出解决方案之前,助手需要理解你想要完成什么。
这些上下文可能包括:
需求或问题陈述
当前代码
期望输出
实际输出
编译器或运行时错误
失败的测试
之前的尝试
没有这些信息,即使在技术上正确的建议也可能不符合实际需求。
考虑:
def is_adult(age):
return age > 18
这个实现是否正确?我们不知道。
如果需求规定一个人必须超过18岁,那么它就是正确的。
但如果需求规定在18岁或以上即视为成年人,那么我们就存在一个边界条件错误。
代码本身不包含足够的信息来做出这一判断。需求提供了缺失的上下文。
2. 诊断
一旦有足够的上下文,助手就能识别出问题的可能来源。
诊断应回答看起来哪里出错了。它不一定需要回答应该用什么确切的代码来替换。
就我们的平均值例子而言,AI助手可以说:
总计算是正确的,但用于除法的元素数量与列表中的值的数量不匹配。
仅此就能缩小问题范围,而无需完全解决它。
3. 提示
如果诊断不够,助手可以提供更具体的提示,例如:
检查len(numbers)对示例输入的返回值,并将其与返回语句中的除数进行比较。
现在你有一个具体的调试步骤,但仍然需要自行进行修正。
这形成了类似提示阶梯的东西:
观察 → 方向 → 更强的提示 → 解释 → 解决方案
AI 辅助不必是二元的。在不提供任何帮助和完全揭示逻辑/实现之间,存在有用的层次。
4. 验证
仅仅修复失败是不够的。
在更正你的实现后,你可能会进行如下测试:
assert calculate_average([80, 90, 70, 100]) == 85
assert calculate_average([10, 20]) == 15
assert calculate_average([5]) == 5
一切看起来都很正常。
但接下来再试试:
calculate_average([])
现在你又遇到了一个问题:除以零。
原始错误已经修复,但验证暴露出你之前未曾考虑的另一种情况。
任何有用的 AI 助手不应仅仅帮助你让一个失败的示例通过;相反,它还应鼓励你思考还有哪些可能会失败。
5. 解释
当你得到解决方案后,AI 可以加强这一概念:
平均值是通过将总和除以元素或值的数量来计算的。由于列表长度为四个元素,在其长度上减一导致总数被除以三而不是四。
此时,解释强化了推理过程,而不是取代它。
渐进式援助很重要
想象有人正在实现此需求:年满 18 岁或以上的人被视为成年人。
他们写了:
def is_adult(age):
return age > 18
与其立即把 > 替换为 >=,AI 导师可以先提升帮助程度。第一个提示可能是:
检查你的边界条件。
如果学习者仍然遇到困难:
当 age 正好等于 18 时,应该发生什么?
然后:
你的比较目前排除了边界值本身。
只有在必要时,助手才会最终显示:return age >= 18。
与其是 问题 → AI → 答案,我们得到 问题 → 观察 → 提示 → 推理 → 尝试 → 验证 → 解释。
这是一种截然不同的学习体验。
我在 Coddy 尝试了这个学习循环
我想看看这些想法如何在实际的编程学习环境中体现,于是我在 Coddy.tech 进行了实验。
我从一个关于行注释的初级 Python 挑战开始。
有一个直接的要求:在不删除的情况下注释掉 print("Goodbye!") 这一行,使得下面的行被打印:
Hello, Python!
从编程角度来看,这个练习并不特别有趣。吸引我注意力的是围绕代码的一切。
在同一工作区,我可以访问挑战需求、基于浏览器的 Python 编辑器、运行代码、测试结果、预期输出、多个提示、解决方案访问、解释挑战的选项,以及 Coddy 的 AI 导师 Bugsy。
这样就提供了多种应对失败的方式,而不仅仅是立即向 AI 请求解决方案。
测试反馈先于 AI
我故意输入了一个错误的解决方案并执行了代码。
Coddy 的测试区域将失败与需求关联起来,告诉我需要在 Goodbye 行的开头添加注释符号,而不能删除它。
预期输出也被显示出来:
Hello, Python!
从测试角度看,虽然看起来简单,但它很有用。学习者不仅在问:代码能否执行? 他们还在问:实现是否产生了练习所需的行为?
这两个问题并不相同。程序可以成功执行,但仍然在功能上不正确。
提前展示期望行为能够引入这一区别。
渐进式提示
同样的练习也提供了多个提示层级。
第一个提示引导我在适当行的开头添加 #,而其他提示仍然可用。
这创建了另一条路径:尝试 → 测试反馈 → 提示 1 → 提示 2 → 提示 3 → 解决方案。
学习者不一定需要直接从失败跳到完整答案。这支持我们之前讨论的渐进式辅助模型。
用我的错误代码测试 Bugsy
接下来,我在编辑器中仍然有错误代码时打开了 Bugsy。
这产生了更有趣的结果。
Bugsy 理解了练习的目标,并引导我注释掉 Goodbye 行。
但它还注意到我当前实现中的另一个问题:Hello 语句的结束引号/括号格式不正确。
第二个问题最为重要,因为它不仅仅是练习所教授的概念。
这个问题源于我的当前代码。Bugsy 似乎同时响应了挑战的上下文以及我实际上在编辑器中编写的内容。
这说明了为什么上下文是框架的第一个元素:上下文 → 诊断 → 提示 → 验证 → 解释
考虑一下练习外的这段代码:
print("Goodbye!")
print("Hello, Python!")
这本身没有什么问题。
只有面对较复杂的需求时,你才会明白 Goodbye! 不应出现,而且应该注释掉那行而不是删除它。
在学习环境中引入 AI 就变得有趣起来。
将帮助与解答分离
我发现另一个有趣的细节:Bugsy 在提供指导的同时,将“显示解答”锁定为一个独立的操作。
这在帮助我前进和显示答案。之间创造了一个有用的区别。
这种区别可能并不完美(稍后我们会再讨论),但我喜欢其背后的设计思想。
AI 导师不必把每一次求助都当作是要求给出完整实现的请求。
迎接更具挑战性的任务
初学者的注释练习只能告诉我们一些基本情况。于是我尝试了一个需要更多推理的中等难度 Python 挑战。
任务是实现:
find_book_descriptions(catalog, query)
函数需要搜索二维图书馆目录。
每本书包含一个 ID 和描述。
实现需要:
遍历书籍
执行不区分大小写的匹配
同时搜索 ID 和描述
收集匹配的描述
使用换行符连接多个结果
当没有匹配项时返回
"No books found."
这为测试提供了更好的环境。
我故意创建了一个包含 Python 和伪代码混合的破损实现。
当我运行它时,多个测试用例失败。
那些测试用例也展示了行为,而不仅仅是失败
测试面板显示了多个测试用例,以及参数、程序输出和预期输出。这一点很重要。
除了仅看到 失败,你可以研究 输入 → 实际行为 → 预期行为 之间的关系。
这基本上是一种测试工作流。
单个成功的例子并不一定意味着实现满足完整需求。不同的输入可能会暴露不同的缺陷。
不立即求助 AI 的调试
同样的挑战还有一个单独的 Debug 选项,我在破损实现上使用了它。
与其纠正整个程序或解释整个实现,不如 Debug 面板直接暴露了即时的 Python 错误:
SyntaxError: invalid syntax (main.py, line 5)
我喜欢这种分离。不是每个编程问题都需要生成式 AI。
如果 Python 已经知道解析失败的位置,暴露该信息会给你提供独立调查的机会。
此时,我有三种不同的反馈机制:
| 机制 | 它有助于回答的问题 |
|---|---|
| 测试用例 | 我的实现是否按预期行为? |
| 调试 | 执行目前在哪里失败? |
| Bugsy | 我的方法可能有什么问题,我该如何前进? |
然后我询问了 Bugsy
我将同样的损坏实现交给了 Bugsy。
这次的回复不仅仅是识别语法错误。Bugsy 认识到该实现将 Python 与伪代码混合在一起。
它引导我进行了几项更改,包括创建一个用于匹配描述的列表,遍历每本书,分离书籍 ID 和描述,使用小写比较进行不区分大小写的搜索,以及追加描述而不是查询。
它还识别出一个更有趣的控制流问题。
“未找到书籍”的决定不应在仍在搜索单本书籍时发生。为什么?
假设第一本书不匹配,但第二本匹配。
如果程序在仍在搜索循环内部得出“未找到书籍”的结论,那么它可能在遍历目录剩余部分之前就做出了该决定。
这不仅仅是语法纠正。它还需要理解需求与控制流之间的关系。
这就是上下文 AI 辅助比通用错误解释更有趣的地方。
但多少帮助才算太多?
中等难度的挑战也暴露出一种限制,或者说至少是一个重要的权衡。
Bugsy 在识别出问题区域后并未停止。它提供了一个相当详细的结构,展示了函数如何实现。
从生产力角度来看,这非常有用。如果我是一名经验丰富的开发者,试图快速完成某项任务,我非常欣赏这一点。
但如果我试图学习概念,我就不那么相信更多的信息总是更好。
请考虑下面两种响应。
方法 A
Here is the corrected implementation...
方法 B
Your "No books found" condition is being evaluated while you're still searching the catalog.
如果第一本书不匹配而第二本书匹配,会发生什么?两种方法最终都可能导致正确的代码。
但方法 B 需要你思考控制流。
这为 AI 导师暴露了一个棘手的问题。
他们可能有两个目标:
帮助学习者成功
和
保持足够的难度以促使学习者思考
这些目标可能会冲突。
即使能够生成完整解决方案的 AI 助手,也仍需决定生成它是否真的是最有用的做法。
不同的学习者可能需要不同程度的帮助
适当的帮助程度也取决于提问者是谁。
初次学习循环的初学者可能从渐进式提示中受益。有经验的开发者在调试不熟悉的库行为时可能只想得到答案。
因此,理想的互动或许不应总是:
Here's how to fix it.
它可以从理解意图开始:
Do you want a hint, an explanation, or the corrected implementation?
这是一个相对较小的 UX 决策,但它改变了 AI 的角色。
AI 并非整个学习系统
在花更多时间探索 Coddy 之后,另一件事变得更加清晰:Bugsy 并不是唯一的学习体验。
该平台还将活动划分为旅程、练习、项目和任务等区域。
在我探索的 Python 旅程中,课程通过大纲和进度路径进行组织。
界面还包含 XP、等级、连续天数、每日任务和排行榜。
这些听起来更像是游戏化功能而非 AI 功能。但正是因此,它们值得讨论。
学习编程需要重复和鼓励,以帮助使学习既有趣又富有乐趣。
AI 能解释循环失败的原因。但仅仅理解这一次的解释并不意味着你明天就能正确实现另一个循环。
你仍然需要练习。这为我们提供了两套互补的系统。
学习进度:旅程 → 练习 → 项目 → 重复
出现问题时的帮助:运行代码 → 测试反馈 → 调试/提示 → Bugsy → 解决方案
我认为在评估 AI 学习产品时,这一区别很重要。
问题不应仅仅是 AI 有多强大?
我们还应该问,学习者在提问 AI 之前和之后在做什么?他们是通过使用它来打下更扎实的基础,还是变得更依赖 AI?
编码助手应采用不同的测试方式
对编码助手的大多数评估自然地集中在它们是否能生成正确的代码上,这一点很重要。
但对于旨在支持学习的 AI 系统,我认为我们需要额外的测试用例。
例如:
| 场景 | 我将评估什么 |
|---|---|
| 语法错误 | 它是否能正确定位问题? |
| 运行时错误 | 它是否能解释执行失败的原因? |
| 逻辑错误 | 它能否在不不必要地重写全部代码的情况下诊断问题? |
| 边界条件 | 它是否能理解诸如 0、空输入或等值边界之类的值? |
| 错误算法 | 它能否引导学习者朝向正确的概念? |
| 重复错误尝试 | 援助是否能够自适应? |
| 正确实现 | 它是否能识别出无需修复? |
| 另一种有效实现 | 它是否能接受与参考答案不同的解决方案? |
最后两个尤其有趣。
正确的代码也是一种测试用例
考虑:
def square(number):
return number * number
假设这完全满足需求。
如果我仍然请求 AI 帮助会怎样?
一个不称职的助手可能会因为觉得有义务产出东西而建议不必要的更改。
一个更好的助手应该能够说:
您的实现已经满足了所述需求。
这与我们在测试生成式 AI 系统时遇到的情况密切相关:误报。
乐于助人并不总是意味着要找出错误。有时,乐于助人意味着认识到不需要修复任何东西。
替代方案也很重要
编程问题也很少只有一种有效的实现方式。
考虑:
def is\_even(number):
return number % 2 == 0
Someone else might write:
def is\_even(number):
if number % 2 == 0:
return True
return False
第一个更简洁,但两者都满足需求。
AI学习助手不应把与参考解不同的解答误认为是错误的。
这是任何编码学习系统的重要测试用例。
重复失败是另一个测试
假设学习者收到一个提示并提交了另一个错误的解决方案。
应该怎样?重复完全相同的提示可能没有帮助。立即透露整个解决方案可能过于激进。
相反,帮助可以逐步变得更具体。
例如:
尝试 1
仔细观察你用来判断数字是否为偶数的运算。
尝试 2
除法得到商。思考哪种运算能告诉你余数。
尝试 3
在 Python 中,% 返回除法后的余数。尝试用它和 2 进行运算。
这是 AI 导师的一个有趣评估维度,因为评估不仅仅关乎正确性,还关乎适应性。
在这个例子中,第二次请求给出了对同一底层问题的另一种解释,同时也指出了我当前 Hello 语句中的问题。
这提出了另一个有用的评估问题:重复请求是应该仅仅产生另一种解释,还是应该根据学习者之前的互动来调整帮助的程度?
AI 编码助手也有边界条件
传统软件测试在边界附近花费大量时间。
零时会发生什么?
在最大值时会发生什么?
输入为空时会发生什么?
在阈值处恰好会发生什么?
AI 编码助手也有边界,但其中许多是行为上的。
在助手开始猜测之前,我们能提供多少上下文?
在它实际上把练习答案说透之前,它能提供多少帮助?
提示何时应该变成解释?
解释何时应该变成代码?
反复失败后会发生什么?
如果学习者写出了一个不同但同样正确的实现,又会怎样?
AI 又该在什么时候坦言:目前我掌握的信息还不够。
这些不仅是教育问题,更是质量工程问题。
评估 AI 编程辅助的实用框架
经过这些实验,我回到前面介绍过的五个阶段:
情境理解:助手是否明白开发者真正想实现什么?
问题诊断:它能否找出当前实现失败的原因?
提示引导:它能否在不过度泄露完整答案的前提下,给出足够的方向?
验证测试:环境能否帮助开发者在更多场景下验证修正是否有效?
解释说明:整个交互结束后,开发者是否理解最终实现为何能行得通?
合在一起就是:
情境理解 → 问题诊断 → 提示引导 → 验证测试 → 解释说明
一个在这几个维度上都表现出色的编程助手,作用远不止生成代码——它实质上参与了整个调试过程。
Coddy 的定位
正因如此,我才觉得 Coddy 值得一探。它最吸引我的地方,并不在于它内置了一个 AI 导师这么简单。
如今,AI 几乎可以接到任何编程界面上。实际上,不只是编程界面,几乎任何东西都能挂上 AI。
更有意思的组合是:结构化学习 + 编程练习 + 可执行代码 + 测试反馈 + 调试 + 情境化 AI 辅助。
每个组件各司其职。
结构化学习指明方向,练习促使动手实践,运行代码带来即时反馈,测试用例把实现结果与预期行为进行比对,调试暴露技术层面的问题,而提示则提供循序渐进的帮助。
Bugsy 可以提供额外的情境化引导,而完整答案则是更进一步的帮助手段。
在我试过的练习中,工作流更接近于:
学习 → 编码 → 运行 → 失败 → 检查 → 调试 → 求助 → 重试
而不是:
遇到问题 → 问 AI → 复制答案
这个区别至关重要。
与此同时,我在中等难度实验中的发现也表明,情境化 AI 依然能在极短时间内给出大量实现层面的指导。
AI 揭示多少内容、在什么时机揭示,依然是一个关键的设计决策。
较少的 AI 并不总是目标。这并不意味着开发者应该避免使用 AI 生成的代码。实际上,在许多情况下,立即生成实现正是我们想要的。
有经验的工程师可能会使用 AI 来:
生成样板代码
创建单元测试
重构重复代码
理解不熟悉的库
原型化实现
解释遗留代码
创建文档
在这些情况下,速度可能是主要目标。
但请比较这两个请求:
帮我完成这个实现。
和:
帮我理解为什么我的实现失败。
它们可能涉及完全相同的代码。但它们代表完全不同的目标。一个有用的 AI 编码助手理想情况下应该能够识别出这种差异。
总结
最令人印象深刻的 AI 编码助手并不一定是生成代码最多的那个。有时,它可能是知道何时不应生成代码的那个。
良好的调试帮助应该帮助开发者从:
“我的代码无法运行。”
到:
“我明白我的代码为什么没能工作。”
这不仅仅需要代码生成。
这需要上下文、诊断、逐步帮助、验证和解释。
我对 Coddy 的实验表明,将 AI 与编码环境集成可能很有用:Bugsy 能够对练习和我正在使用的代码作出响应,而测试用例、调试、提示和解决方案访问则提供了不同层次的帮助。
这也暴露出一个更难的问题:当 AI 知道如何解决问题时,它应该透露多少解决方案?
随着 AI 在编程教育中更深入地融合,我认为评估助手是否生成正确代码将继续重要。
但我们也应该衡量一些更难的东西:开发者在互动结束时是否比进入时更好地理解了问题?
对于 AI 导师来说,这最终可能是更有意义的测试。
如果您想尝试本文讨论的功能,可以在 Coddy.tech 上探索。