用 AI 调试代码时,给模型的东西最好和你给坐在旁边的同事的一样:完整的错误、引发它的代码、你期望的结果、实际发生的情况,以及你已经排除了什么。模型无法运行你的程序,也看不到你的数据,所以它对这个 bug 的全部了解都来自你粘贴的内容。最有用的一个习惯,是先问原因,再要修复。
粘贴全部信息,而不只是错误
下面两个标签页描述的是同一个 bug。一个脚本读取从 Excel 导出的 CSV 文件,结果因 KeyError: 'email' 而崩溃,尽管文件里明明有 email 这一列。
KeyError 表示字典中没有名为 'email' 的键。你可以用 .get() 来避免这个错误,它会返回 None 而不是抛出异常:
email = row.get("email")
或者先检查一下:
if "email" in row:
email = row["email"]
第一条回复对 KeyError 是什么的解释是正确的,但在这里它的建议是有害的。用了 row.get("email") 之后,崩溃消失了,而每个 email 都变成了 None。模型回答的是这个错误最常见的情况,因为提示词描述的就只有这些。
完整的报告之所以有效,靠的是两句话。“只有第一列出错”和“从 Excel 另存为 CSV UTF-8”是指向字节顺序标记的线索,而这两点都不在错误信息里。你很少能知道哪个细节才是线索,所以要把它们全部粘贴进去。
要包含的五项内容
- 完整的错误信息。 完整的 traceback 或控制台输出,而不是转述。行号和调用链能显示故障从哪里开始,而那里往往和错误暴露的地方隔着好几层调用。
- 引发错误的代码。 出错的函数以及调用它的代码。如果错误信息提到了某一行,确保你粘贴的内容里包含这一行。
- 期望与实际。 各写一句。对于根本没有报错的 bug(输出错误、空白页面、查询很慢),这就是全部的 bug 报告。
- 你的环境。 在可能有影响时,写明语言版本、操作系统、框架和库的版本。编码、路径和日期相关的 bug 常常取决于平台。
- 你已经试过的方法。 这能防止模型再建议一遍,而且每次尝试的结果都是证据。“去掉 email 那部分就能运行”把 CSV 的 bug 缩小到了一列。
先问原因,再要修复
要求修复,等于请模型不停地改代码,直到错误消失。要求找原因,则是让它推理错误为什么会发生,而修复方法会随之而来。要求它给出一个简短的、排好序的原因列表,并附上检查每个原因的方法,然后由你自己来运行这些检查。下面这个模板就是这样做的:
TypeError: Cannot read properties of undefined (reading 'map')function UserList() {
const [users, setUsers] = useState();
useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then((data) => setUsers(data));
}, []);
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}- 组件在还没有任何数据时就渲染了。
useState()的初始值是undefined,而 React 会在 effect 运行之前先渲染一次组件,所以users.map是在undefined上调用的。仅这一点就足以在每次加载时产生和你完全一样的错误。检查:在return之前加上console.log(users)。如果它打印出undefined,紧接着就出现错误,那就是这个原因。 - API 返回的不是数组。 如果它返回的是一个像
{ users: [...] }这样的对象,错误信息会不一样(users.map is not a function),所以这不是你粘贴的那个错误。但在修复原因 1 之后,它仍可能成为下一个问题。检查:在第二个.then中加上console.log(data)。 - 请求失败了。 目前还牵涉不到它:因为第一次渲染就抛出了异常,React 根本不会运行 effect,所以请求甚至都没有发出。检查:Network 标签页中没有
/api/users请求,这同时也证实了原因 1。
这条回复做了一件“先修复”式回答会跳过的事:它排除了一些原因,因为它们会产生的错误和你粘贴的不同,或者因为代码根本走不到它们会发生的那一步。这也是错误信息的确切文字之所以重要的原因。让模型在回答之前先列出原因,是一种轻量的思维链提示:推理在前,结论建立在推理之上。
制作一个最小复现
最小复现是仍能表现出这个 bug 的最小程序:用写死的数据代替数据库调用,用一个函数代替整个模块。制作它的过程常常在你问任何人之前就找到了 bug,因为你去掉的每一部分,要么让 bug 依然存在(说明它与此无关),要么让 bug 消失(说明问题就在这里)。如果没找到,这个最小复现就是理想的提示词:短到模型能读完每一行,也没有可能把它引向错误方向的无关代码。
如果 bug 依赖于数据,就附上几行能触发它的数据。模型能对 [{"id": 1, "name": null}] 进行推理,却没法对“生产环境中的某些行”进行推理。
当修复不再奏效时
如果模型的第三次修复以同样的方式失败了,第四次要求修复也不太可能更好。有两件事更有帮助:
- 给它新的证据。 加一行 print 或日志,显示出错位置的实际值,运行它,然后把输出粘贴进去。与模型的推测相矛盾的证据,是让它得出更好推测的最快途径。
- 开一个新对话。 长长的调试对话里充满了被放弃的推测和旧版本的代码,模型可能还会在这些基础上继续推理。在新对话中附上当前的代码、错误、证据,以及一句“已排除:X 和 Y”,常常一次回复就能走得更远。
对关于库的行为的自信回答要保持警惕。模型可能会描述一个并不存在的选项或函数;如何核实,见 AI 幻觉。当代码并没有坏,但你不理解它为什么会这样运行时,用于解释代码的提示词是更合适的工具。
常见问题
怎么让 ChatGPT 或 Claude 修复我的代码?
粘贴完整的错误信息和引发它的代码,再加三句简短的话:你期望什么、实际发生了什么、你已经试过什么。在要求修复之前,先问最可能的原因以及如何证实它。只说一句“修复这个”,得到的是针对这个错误最常见情况的修复,而那未必是你遇到的情况。
应该把整个项目都粘贴给 AI 吗?
不应该。粘贴出错的函数、调用它的代码,以及它收到的数据样本。更好的做法是把问题缩减成仍能复现它的最小程序。无关的文件会让回答变慢,还会让模型有更多地方去寻找一个并不存在的问题。
为什么 AI 的修复让错误消失了,程序却还是不对?
因为这个修复只治标。比如,把 row["email"] 换成 row.get("email") 能阻止 KeyError,但如果这个键是因为上游的 bug 才缺失的,那么现在每个 email 都会悄无声息地变成 None。先问原因,并要求给出证实它的方法,就能避免那些只会掩盖问题的修复。
如果 AI 一直给出不管用的修复怎么办?
别再要修复了,改为给它提供证据。说明每次尝试改了什么,加一行能显示实际值的 print 或日志,然后把输出粘贴进去。如果对话已经很长,就开一个新对话,附上一份清楚的总结:代码、错误、证据,以及已经排除的修复方法。