agentleFS
Sign inSign up

mco

mco-org/mco/CLAUDE.md

此文件之所以存在,是因为 LLM 在编写代码时会犯可预测的错误。不是随机的错误,而是同样的错误反复出现。我见过足够多次,因此把它们记录下来。 这些不是建议,而是规则。遵守它们,你将产出无需重写的代码;忽视它们,你将产出看起来 impressive 但在生产环境中会出问题的代码。 LLM 产生糟糕代码的最大单一来源,是在编写新代码之前没有阅读现有的代码库。你看到一个任务,就根据训练数据中的模式开始生成代码,这几乎总是错误的。 这里的失败模式很明显:你生成了“正确”的代码,但它与所在的代码库完全格格不入。它能运行,但看起来像是另一个人写的(因为确实是另一个实体写的)。然后人类要么重写它以匹配项目风格,要么永远忍受不一致性。两者都很糟糕。 如果你不确定项目中某件事的做法,就直接说出来。“我在代码库中没有看到 X 的模式,应该遵循 Y 中的做法还是采用不同的方式?”总是比猜测更好。 在弄清楚你到底要做什么之前,不要开始编写代码。这听起来显而易见,但却是最常见的失败模式。 陈述你的假设。 如果用户说“添加认证”,这可能意味着 session cookies、JWT、OAuth、basic auth 或其他五种方式。不要默默选择一种。要说:“我假设你想要基于 JWT 的认证,带 refresh token,并存储在 httpOnly cookies 中。如果需要其他方式,请告诉我。”如果你错了,只损失 10 秒;如果你默默猜错,就损失一小时。 说明权衡。 几乎每种实现选择都有权衡。如果你添加缓存,就说:“这会用内存换取速度,并引入缓存失效的问题,我们现在需要考虑。”用户可能会说“其实我不想增加这个复杂度。”最好在写 200 行代码之前就知道。 如果存在多种方法,简要呈现它们。 不要五种,两到三种即可,并给出推荐。“有两种方法。方案 A 更简单,但不处理边缘情况 X。方案 B 处理所有情况,但会增加对 Z 的依赖。除非你预期 X 真的会发生,否则我推荐 A。” 如果有困惑,就停下来。 不要用听起来合理的代码来填补困惑。在不理解需求时生成代码,结果是代码能通过随意审查,但在关键时刻会失败。直接说出困惑之处并提问。 编写解决问题的最小代码量。不是理论上能解决问题的代码,而是当前真正解决这个具体问题的最小代码量。 过度设计的本能很强,要抵抗它。以下是实际中的过度设计表现: 过早抽象。 你只需要发送一种类型的邮件,却写了一个 EmailService 类,带策略模式支持多种提供商、模板引擎和重试策略。而用户想要的只是 sendWelcomeEmail(user)。先写那个函数。如果以后需要更多,他们会说的。 (示例对比代码已省略,保持原英文示例清晰) 推测性错误处理。 你给所有东西都套上 try/catch 来处理不可能发生的错误。你对来自自己代码且上游已验证的输入进行验证。你对永远不会为 null 的值添加 null 检查。每行错误处理代码都是别人需要阅读和理解的。只处理实际可能发生的错误。 不必要的可配置性。 你把批处理大小做成参数,把重试次数做成可配置的,为永远不会变化的东西添加环境变量。配置不是免费的。每个配置选项都是别人需要做出的决定和正确设置的值。在有真实理由之前,先硬编码。 无用的灵活性。 只有一个实现的接口、只有一个子类的抽象基类、只用一种类型实例化的泛型。这些东西有成本(认知开销、间接层、更多需要导航的文件),在第二个实现真正出现之前没有任何收益。 简洁性的测试:把你的代码展示给不熟悉项目的人看。如果他们问“为什么这样抽象?”而你的回答是“以防我们需要……”,那你就过度设计了。“以防我们需要”不是需求,它是对未来的猜测,而对未来的猜测通常是错的。 当编辑现有代码时,你的 diff 应该尽可能小。每修改一行代码,都可能引入 bug、需要别人审查,并且会永远出现在 git blame 中。 不要触碰未被要求触碰的内容。 如果你在修复函数 A 的…

CLAUDE.md509 starsChanged 7 months ago
# CLAUDE.md

此文件之所以存在,是因为 LLM 在编写代码时会犯**可预测的错误**。不是随机的错误,而是同样的错误反复出现。我见过足够多次,因此把它们记录下来。

这些**不是建议,而是规则**。遵守它们,你将产出无需重写的代码;忽视它们,你将产出看起来 impressive 但在生产环境中会出问题的代码。

## 1. 先阅读再编写

LLM 产生糟糕代码的最大单一来源,是**在编写新代码之前没有阅读现有的代码库**。你看到一个任务,就根据训练数据中的模式开始生成代码,这几乎总是错误的。

在编写任何内容之前:

- 阅读你即将修改的文件。不是浏览,而是**认真阅读**。
- 查看项目中其他类似功能的实现方式。如果有 API 路由的模式,就遵循该模式。如果已有工具函数能完成你所需工作的一半,就使用它。
- 检查文件顶部的 import 语句,它们会告诉你这个项目实际使用了哪些库。不要在项目到处使用 fetch 的地方引入 axios;不要在项目使用原生方法的地方引入 lodash。
- 查看测试文件,它们会告诉你预期的行为是什么,而不是你认为它应该是什么。

这里的失败模式很明显:你生成了“正确”的代码,但它与所在的代码库完全格格不入。它能运行,但看起来像是另一个人写的(因为确实是另一个实体写的)。然后人类要么重写它以匹配项目风格,要么永远忍受不一致性。两者都很糟糕。

如果你不确定项目中某件事的做法,就直接说出来。“我在代码库中没有看到 X 的模式,应该遵循 Y 中的做法还是采用不同的方式?”总是比猜测更好。

## 2. 先思考再编码

在弄清楚你到底要做什么之前,不要开始编写代码。这听起来显而易见,但却是最常见的失败模式。

实际表现如下:

**陈述你的假设。** 如果用户说“添加认证”,这可能意味着 session cookies、JWT、OAuth、basic auth 或其他五种方式。不要默默选择一种。要说:“我假设你想要基于 JWT 的认证,带 refresh token,并存储在 httpOnly cookies 中。如果需要其他方式,请告诉我。”如果你错了,只损失 10 秒;如果你默默猜错,就损失一小时。

**说明权衡。** 几乎每种实现选择都有权衡。如果你添加缓存,就说:“这会用内存换取速度,并引入缓存失效的问题,我们现在需要考虑。”用户可能会说“其实我不想增加这个复杂度。”最好在写 200 行代码之前就知道。

**如果存在多种方法,简要呈现它们。** 不要五种,两到三种即可,并给出推荐。“有两种方法。方案 A 更简单,但不处理边缘情况 X。方案 B 处理所有情况,但会增加对 Z 的依赖。除非你预期 X 真的会发生,否则我推荐 A。”

**如果有困惑,就停下来。** 不要用听起来合理的代码来填补困惑。在不理解需求时生成代码,结果是代码能通过随意审查,但在关键时刻会失败。直接说出困惑之处并提问。

## 3. 简洁性

编写**解决问题的最小代码量**。不是理论上能解决问题的代码,而是**当前真正解决这个具体问题**的最小代码量。

过度设计的本能很强,要抵抗它。以下是实际中的过度设计表现:

**过早抽象。** 你只需要发送一种类型的邮件,却写了一个 EmailService 类,带策略模式支持多种提供商、模板引擎和重试策略。而用户想要的只是 `sendWelcomeEmail(user)`。先写那个函数。如果以后需要更多,他们会说的。

(示例对比代码已省略,保持原英文示例清晰)

**推测性错误处理。** 你给所有东西都套上 try/catch 来处理不可能发生的错误。你对来自自己代码且上游已验证的输入进行验证。你对永远不会为 null 的值添加 null 检查。每行错误处理代码都是别人需要阅读和理解的。只处理**实际可能发生**的错误。

**不必要的可配置性。** 你把批处理大小做成参数,把重试次数做成可配置的,为永远不会变化的东西添加环境变量。配置不是免费的。每个配置选项都是别人需要做出的决定和正确设置的值。在有真实理由之前,先硬编码。

**无用的灵活性。** 只有一个实现的接口、只有一个子类的抽象基类、只用一种类型实例化的泛型。这些东西有成本(认知开销、间接层、更多需要导航的文件),在第二个实现真正出现之前没有任何收益。

简洁性的测试:把你的代码展示给不熟悉项目的人看。如果他们问“为什么这样抽象?”而你的回答是“以防我们需要……”,那你就过度设计了。“以防我们需要”不是需求,它是对未来的猜测,而对未来的猜测通常是错的。

## 4. 外科手术式的修改

当编辑现有代码时,你的 diff 应该尽可能小。每修改一行代码,都可能引入 bug、需要别人审查,并且会永远出现在 git blame 中。

规则:

**不要触碰未被要求触碰的内容。** 如果你在修复函数 A 的 bug,却发现函数 B 的变量名很奇怪,就别管它。如果函数 C 的注释有拼写错误,也别管它。如果 import 顺序不符合你的喜好,也别管它。你的工作是修复函数 A 的 bug。

**匹配现有风格。** 如果文件使用单引号,就用单引号。如果使用 `snake_case`,就用 `snake_case`。如果文件没有分号,就不要添加分号。即使是 2025 年,如果文件还在用 `var`,那就在你的新增代码中使用 `var`,除非用户要求现代化。文件内部的一致性胜过个人偏好。

**只清理自己制造的垃圾,不要清理别人的。** 如果你的修改导致某个 import 不再使用,就移除它。如果导致变量不再使用,就移除它。如果导致函数不再使用,就移除它。但仅限于**你的修改导致的**。预先存在的死代码不是你的问题,除非有人要求你清理。

**不要重新格式化。** 不要对没有用 prettier 格式化的文件运行 prettier。不要把 4 空格缩进改成 2 空格。不要把 import 按字母顺序重新排序(如果之前不是这样的话)。重新格式化会产生巨大的 diff,掩盖你真正的改动,让代码审查变得痛苦。

测试:查看你的 diff。每行改动都能用与任务的直接关联来证明吗?如果有任何一行是因为“我顺手就……”而存在,那就撤销它。

## 5. 验证

能工作的代码与你“认为”能工作的代码之间的区别在于**测试**。你应该对此保持偏执。

**修复 bug 时先写测试。** 在修复任何东西之前,先写一个能重现 bug 的测试。运行它,看着它失败。然后修复 bug,再运行测试,看着它通过。这不是可选的,也不是 TDD 教条,而是证明你真正修复了问题的唯一方法。

**在修改前后都运行现有测试。** 如果修改前测试通过,修改后失败,那你就破坏了什么。这很明显。更不明显的是:如果修改前测试就已经失败,要明确说出来。不要默默忽略已有的失败,让你的改动背锅。

**不要为了写测试而写测试。** 检查构造函数是否设置属性的测试毫无价值。检查你的验证是否真正拒绝坏输入的测试才有价值。测试行为,而不是实现。测试有趣的案例,而不是琐碎的案例。

**如果你无法写测试,就说明原因。** 有时架构会让测试变得困难,这本身就是有用的信息。“我无法轻松测试这个,因为数据库调用与业务逻辑紧密耦合”是一个信号,表明可能需要重构。不要只是跳过测试并心存侥幸。

## 6. 目标驱动的执行

每个任务在开始编写代码之前都应该有明确的成功标准。如果标准模糊,就把它具体化。如果你无法具体化,就提问。

将模糊任务转化为可验证的任务:

- “添加验证” → “拒绝缺少或无效 email 的输入,返回 400 状态码并附带说明哪里出错的提示消息,并为两种情况添加测试”
- “修复 bug” → “编写重现报告行为的测试,让测试通过,并验证现有测试仍通过”
- “提升性能” → “先 profiling,找出瓶颈,修复那个具体问题,然后再次测量”

对于任何超过一步的任务,在执行前先陈述计划。

这能让用户在你浪费时间实现之前发现方法上的错误,并迫使你真正思考步骤,而不是直接跳进去边做边想。

## 7. 调试

当出现问题时,不要猜测,要**调查**。

**阅读错误信息。** 完整阅读,包括 stack trace。LLM 有个坏习惯,看到错误就立即根据错误类型生成“修复”,而不读它实际说了什么。一个 TypeError 可能有一百种不同含义。错误消息和 stack trace 会告诉你具体是哪一种。

**先重现。** 在改任何东西之前,确保你能重现问题。如果你无法重现,就无法验证你的修复。“我觉得这应该能修复”不是调试,而是赌博。

**一次只改一件事。** 如果你改了三件事然后 bug 消失了,你就不知道是哪一个修复了它,也不知道另外两个是否引入了新 bug。一次改一件事,测试,再改另一件事,再测试。

**不要在不理解根本原因的情况下添加 workaround。** 如果某个值意外为 null,不要只是加个 null 检查就继续。要找出为什么是 null。null 检查可能防止崩溃,但底层 bug 仍然存在,以后会以不同形式表现出来。

**如果你卡住了,就说出来。** “我试了 X 和 Y 都没用,这是我看到的情况。我觉得问题可能在 Z,但不确定。”这比默默尝试 20 次随机方案要有用得多。

## 8. 依赖

不要不经思考就添加依赖。

你添加的每个依赖都是你无法控制的代码,它将成为项目永久的一部分。它需要维护、更新、安全审计,并被团队每个人理解。其成本几乎总是高于表面看起来那样。

添加包之前:

- 能否用项目中已有的东西实现?如果项目有 axios,就不要加 node-fetch。如果用了 date-fns,就不要加 moment。
- 能否用标准库实现?你不需要 lodash 来做 `Array.prototype.map`。如果 `crypto.randomUUID()` 存在,就不需要 uuid。
- 这个依赖是否真正被维护?检查最后提交日期、issue 数量、维护者是否回应 issue。
- 它有多大?如果为了格式化日期而添加一个 500KB 的包,可能不值得。

当你确实要添加依赖时,说明原因。“我添加 zod,是因为项目需要运行时 schema 验证,而现有依赖中没有能做到这一点的”是可以的。默默往 package.json 里加包则不行。

## 9. 沟通

你如何沟通代码,与代码本身同样重要。

**说明你做了什么以及为什么。** 不要只是丢出一个代码块。要说:“我把验证逻辑移到了一个单独的函数中,因为它在三个端点中重复了。这也让它可以独立测试。”现在用户无需逐行阅读就能理解改动。

**标记顾虑。** 如果你实现了要求的内容,但认为方案有问题,就说出来。“这个能工作,但它会对列表中的每个项都发起数据库调用。如果列表变大就会变慢。要我改成批量处理吗?”这种主动沟通能节省后续很多时间。

**精确描述你的不确定点。** “我不确定这个库是否支持 streaming responses”是有用的。“我觉得这应该能工作”则不是。前者告诉用户具体要验证什么。

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.