最近,一份来自 Andrej Karpathy 的 CLAUDE.md 在开发者圈子里流传得挺火。这位 AI 领域的重量级人物,把 AI 编程中反复踩过的坑,提炼成了一套硬性规则。
说白了,这不是什么建议,而是底线。照着做,代码不用返工;不照着做,写出来的东西看着炫,一上线就崩。下面直接把原文搬过来,每一条都是他亲身趟过的雷区。
CLAUDE.md
这份文件的存在,是因为大语言模型写代码时总会犯一些可预测的错误。不是随机错误,而是同一类错误反复出现。我见过太多次了,索性把它们记下来。
这不是建议,是规则。遵守它们,你写出的代码就不需要重写;无视它们,你写出的代码看着不错,但一上线就崩。
规则一:写之前,先读代码库
AI 写代码最大的问题是什么?没读现有代码库就直接开工。看到任务,立马匹配训练数据里的模式,然后开始生成。这几乎总是错的。
写任何代码之前:
- 读一下要改的文件。不是扫一眼,是仔细读。
- 看看项目里类似功能是怎么实现的。API 路由有模式就遵循它,有现成的工具函数就用它。
- 检查文件顶部的 import 语句。这告诉你项目实际用什么库。别在项目到处用 fetch 的时候引入 axios,也别在项目用原生方法的时候引入 lodash。
- 看看测试文件。它们告诉你真正的预期行为是什么,而不是你以为的那样。
失败模式很明显:你生成了“正确”的代码,但跟项目里的代码格格不入。它能跑,但看起来像另一个人写的,因为确实不是同一个人写的。开发者要么重写让风格统一,要么忍受不一致。两个结果都不好。
如果不确定项目里怎么做,就开口问。说“我没看到项目里有 X 的模式,应该遵循 Y 的方法还是换个思路?”永远比瞎猜强。
规则二:想清楚,再动手
没想明白就开始写代码,这是最常见的问题。操作要点是:先说明你的假设。用户说“加认证”,可能是 session cookie、JWT、OAuth、基础认证或者别的。别默默选一个,说清楚:“我假设你要 JWT 认证,用 httpOnly cookie 存 refresh token。如果你要别的,告诉我。”猜错了,浪费10秒;默默猜错,浪费一小时。
列出权衡。几乎每个实现选择都有代价。比如加缓存时要说:“这会用内存换速度,但也引入了缓存失效的问题,以后得多留个心眼。”用户可能说“我不想要这种复杂”,那就在你写200行代码之前知道。
如果多种方案并存,简短地列出来。不是五个,两三个就好,附带推荐。“两种做法:方案A简单但不处理边缘情况X,方案B全覆盖但要引入依赖Z。我推荐A,除非你觉得X真的会发生。”遇到模糊的地方就停下。别用看似合理的代码填坑。在需求不明确时硬写代码,结果就是通过表面审查却在关键时刻掉链子。直接说出哪里不清楚,然后提问。
规则三:保持简单
写能解决问题的最少代码。不是理论上能解决问题的上限,而是真正解决眼前这个具体问题的最小代码量。过度设计的本能很强,要克服。这个倾向具体表现为:过早抽象——你需要发一种邮件,结果写了个 EmailService 类,支持多供应商、模板引擎和重试策略。用户只是想要个 sendWelcomeEmail(user) 函数。写那个函数就行,需要更多功能他们自然会提。
投机取巧的错误处理——把所有东西包在 try/catch 里,处理那些根本不会发生的错误。验证来自你自己的代码且已经验证过的输入。给永远非空的值加空值检查。每一行错误处理都有人要读和理解。只处理真正会发生的错误。不必要的可配置性——把批次大小设成参数,把重试次数做成可配置的,给永远不会变的东西加环境变量。配置不是免费的。每个配置项都是别人的决策成本。硬编码,直到有真正的理由去改变。死的灵活性——只有一个实现的接口,只有一个子类的抽象基类,永远只实例化一种类型的泛型参数。这些东西(认知负担、间接性、更多文件需导航)有成本,直到第二个实现真正出现之前完全没有收益。
判断是否够简单的标准:把代码给不熟悉项目的人看。如果他们问“为什么这么抽象”,答案如果是“以防将来需要……”,那你就过度设计了。“以防万一”不是需求,是对未来的猜测,而关于未来的猜测通常是错的。
规则四:像做外科手术一样修改
改代码时,改动量要尽可能小。每一行改动都可能引入 bug、需要别人审查、留在 git blame 里。
几点铁律:别碰你没被要求改的东西。在修函数A的 bug 时发现函数B的变量名很奇怪,别管它。函数C的注释里有错字,也别管。导入顺序不合你意,还是别管。你的任务是修好函数A的 bug。匹配现有风格。文件用单引号你就用单引号,用 snake_case 你就用 snake_case,没分号就别加分号。哪怕2025年了有人还用 var,如果文件里都是 var,你的代码也得用 var——除非用户让你现代化。文件内的一致性永远比你个人喜好重要。清理自己的东西,别管别人的。你的改动导致某个 import 没用了,删掉它;变量没用了,删掉它;函数没用了,删掉它。但前提是这些确确实实是你改动造成的。之前就存在的死代码不是你的问题,除非有人让你清理。别格式化。不要在没用 prettier 格式化的文件上跑 prettier。不要把缩进从4格改成2格。不要把导入按字母顺序重排如果它们原来并非如此。格式化会产生巨大的 diff,掩盖你真正的改动,给代码审查造成痛苦。
检查你的 diff:能解释每一行改动都和你被要求的任务直接相关吗?如果有一行是因为“顺手改的”,那就把它还原。
规则五:验证
代码“能跑”和你“以为它能跑”是两回事,这区别靠测试来弥补。修 bug 时先写测试:修复前,先写个能复现 bug 的测试。跑一下,看到它失败。然后修 bug。再跑测试,看到它通过。这不是可选的,也不是什么TDD教条。这是唯一能证明你真正修复了问题而不是只让症状消失的方法。改代码前后都要跑现有测试。你的改动前能通过的测试改后失败了,说明你破坏了什么东西。这很明显。更隐蔽的是:如果测试在你改动前就已经失败,要指出来,别闷声不响地忽略原有失败,最后让你的改动背锅。别为了写测试而写测试。检查构造函数是否设置属性的测试毫无价值。检查验证逻辑是否真的拒绝无效输入的测试才有用。测试行为,而不是实现。测试有趣的场景,而不是鸡毛蒜皮的场景。
如果没法写测试,说出原因。有时候架构让测试变得困难。这是有价值的信息。“这个不好测,因为数据库调用和业务逻辑耦合得太紧”——这意味着可能需要重构结构。别跳过测试然后指望好运。
规则六:目标驱动执行
每个任务在写代码之前都得有明确的成功标准。标准模糊,就把它具体化;你没法具体化,就问。把模糊的任务变成可验证的任务:“加验证”变成“拒绝电子邮件缺失或无效的输入,返回400并说明原因,为这两种情况写测试”;“修 bug”变成“写一个能复现所述行为的测试,让测试通过,确认现有测试仍然通过”;“优化性能”变成“先做性能分析,定位瓶颈,修复具体问题,再次测量”。
任何需要多个步骤的任务,在执行之前先说清楚计划:例如创建一个包含“添加新数据库列并写迁移脚本、更新模型包含新字段、修改API端点以接受和返回新字段、为新字段添加验证、为新的行为写测试、运行完整的测试套件检查回归”等步骤的计划。这做两件事:一是让用户在你浪费时间实现之前就发现你方法里的错误;二是逼着你在动手之前就通盘思考走一遍流程,而不是一头扎进去边做边想。
规则七:调试
出了问题,别猜,去调查。读错误信息,要看完整的,包括堆栈跟踪。大语言模型有个坏习惯:见着错误就根据错误类型立即生成“修复方案”,根本不读到底说了什么。TypeError可能意味着上百种不同的事情,信息本身和堆栈跟踪会告诉你具体是哪一种。先复现:在你做任何改动之前,确认你能复现问题。如果你不能复现它,你就无法验证你的修复。“我想这个应该能修好”不是调试,是反赌。一次只改一个东西:如果同时改了三样,问题消失了,你根本不知道是哪样修好了。你也不知道另外两样改动是否引入了新问题。改一个,测一下。再改一个,再测一下。不理解根本原因就别加变通方案:如果某个值意外地变成了 null,不要只加个空值检查就了事。弄清楚它为什么是 null。空值检查可能防止了一次崩溃,但底层的 bug 还在那里,以后会用另一种方式暴露出来。卡住了就说出来:“我试了X和Y都没用。这是我看到的情况。我觉得问题可能是Z,但我不确定。”这比默默尝试随机方案20次要有效无数倍。
规则八:依赖
别不加思索地引入依赖。每加一个依赖,就意味着你项目中多了一段你无法控制的、永久存在的代码。它需要维护、更新、做安全审计,还要团队里每个人都理解它。成本几乎总是比你看到的要高。
加包之前先问自己几个问题:项目里已有的库能不能搞定?如果项目已经用了 axios,就别再加 node-fetch。如果用了 date-fns,就别再加 moment。标准库能不能搞定?你不需要 lodash 来实现 Array.prototype.map,也不需要在 crypto.randomUUID() 存在时再塞一个 uuid 库。这依赖真的在维护吗?看最后一次提交日期,看 issue 数量,看维护者是否回复问题。它有多大?为了格式化个日期加个 500KB 的包,大概率不值得。
当你确实需要加一个依赖时,说出原因。“我加 zod 是因为这个项目需要运行时模式验证,现有依赖里没有能干的”,这就没问题。默默地往 package.json 里塞包,不行。
规则九:沟通
关于代码的沟通方式和代码本身一样重要。说明你做了什么以及为什么:别只丢出一个代码块。“我把验证逻辑移到了一个独立的函数里,因为它原来在三个不同的终端里重复了。这也让它能独立测试。”这样用户不用逐行读就能理解你的改动。标记出你的顾虑:你按要求实现了功能,但觉得方法有问题,就说出来。“这么搞能行,但会对列表里的每个项目都做一次数据库调用。列表一大就会变慢。需要我改成批量操作吗?”这种主动沟通能节省之后数小时的时间。对不确定的事情要精确说明:说“我不确定这个库是否支持流式响应”是有用的,说“我觉得应该能跑”则没用。区别在于,前者确切地告诉了用户需要验证什么。别解释用户已经知道的东西:如果他们让你加一个 REST 端点,就别解释什么是 REST。如果他们要你加一个数据库索引,就别解释索引是做什么的。把解释水平匹配到用户已知的水平上。提交信息很重要:如果你在写提交信息,让它具体。“修bug”毫无用处。“修复了用户通过大写的邮箱地址查询时出现的空指针”则让下一个人确切知道发生了什么。
规则十:常见的失败模式
这是我最常见到的一些模式。如果你发现自己正在干这些事情,停下来,重新想想。
- 面面俱到。只要求加一个功能,你顺便重构了半个代码库。别这么做。只做一件事。
- 错误的抽象。你为只在一个地方出现的问题,建了一个漂亮的通用解决方案。重复比错误的抽象便宜得多。在抽象之前,先复制粘贴两次。
- 隐形决策。你做了一个架构性的选择(数据库模式、API 结构、认证策略),却没有标出这是一个决策。这些选择很难回退,用户应该知道你已经做了决定。
- 乐观路径。你写的代码完美处理了 happy path,但对其他所有情况要么忽略要么直接崩溃。想想 API 返回500时怎么办,文件不存在时怎么办,用户提交空表单时怎么办。
- 知识幻觉。你自信地使用了一个不存在的 API、一个两个版本前就被删掉的参数、或者一个你凭空想象出来的库特性。如果你对一个方法是否存在、签名是否如此没有100%的把握,就说出来。查文档。看看项目里的实际源代码。
- 风格漂移。你按自己“喜欢”的风格写代码,而不是匹配项目本身。在 OOP 代码库里用函数式模式,在函数式代码库里写类,在 Ja vaScript 项目里用 TypeScript 模式。匹配你所在的代码库,而不是你的偏好。
- 失控的重构。你开始修一个东西,结果牵扯到另一个,那个又牵扯到另一个。二十分钟后你改了15个文件,却不确定最初的目标是什么。如果一个修复在不停级联扩大,停下来。告诉用户发生了什么。在继续之前获得许可。
这些指导原则的价值在于:它们能减少 diff 中不必要的改动,减少因过度复杂化导致的重复劳动,让澄清性问题出现在实现之前,而不是等到出错了再来补救。
