Blog Entry

从工程实践看 Andrej Karpathy Skills:让 AI 编程助手少犯“聪明但危险”的错误

2026-05-30 · AI 编程 · Skills · 工程实践 · 一月 · 15 min read

作者:一月

项目地址:https://github.com/multica-ai/andrej-karpathy-skills
视角:全栈开发工程实践
核心观点:这个项目的价值不在于提供新工具,而在于把“高级工程师如何约束 AI 写代码”的经验,压缩成一份可复制的 CLAUDE.md 行为协议。


1. 这个项目到底是什么?

andrej-karpathy-skills 这个项目很小,核心几乎就是一份 CLAUDE.md。它不是框架,不是 SDK,不是自动化脚本,而是一组写给 Claude Code / AI 编程助手的行为准则。

它基于 Andrej Karpathy 对 LLM 编程行为的观察:

  • 模型经常替用户做错误假设,并且不主动暴露困惑;
  • 模型喜欢把简单需求做复杂,生成过度抽象的代码;
  • 模型会顺手改动无关代码、注释、格式,造成 diff 污染;
  • 模型如果没有明确成功标准,就会“看起来很努力”,但未必真的解决问题。

项目把这些问题总结成四条准则:

  1. Think Before Coding:不要默默假设,不要隐藏困惑,要展示权衡;
  2. Simplicity First:只写解决问题所需的最少代码,不做投机式扩展;
  3. Surgical Changes:只改必须改的地方,只清理自己制造的垃圾;
  4. Goal-Driven Execution:先定义成功标准,再循环验证直到达标。

从工程角度看,这四条不是“提示词技巧”,而是把软件工程里的几个基本纪律——需求澄清、最小实现、变更隔离、测试验证——重新包装成 AI 可执行的约束。


2. 为什么 LLM 写代码会出现这些问题?

在分析四条准则之前,先要理解一个关键背景:LLM 不是在“理解项目后写代码”,它是在基于上下文预测最可能的下一段代码。

这会带来几个天然倾向。

2.1 它倾向于补全,而不是追问

人类工程师看到“加一个导出用户数据功能”,通常会问:

  • 导出哪些用户?
  • 导出哪些字段?
  • 是否包含手机号、邮箱等敏感信息?
  • 是后台下载、异步任务,还是 API 返回?
  • 数据量多大?是否要分页?

但 LLM 常常会直接生成:

def export_users():
    users = User.query.all()
    with open('users.json', 'w') as f:
        json.dump([u.to_dict() for u in users], f)

这不是因为它“故意冒险”,而是因为训练数据里充满了完整代码片段。用户提出需求后,模型最自然的行为是继续补全一个“看起来完整”的实现。

问题是:真实工程中最危险的 bug,往往不是语法错误,而是错误假设。

2.2 它倾向于展示能力,而不是控制复杂度

LLM 见过大量设计模式、框架抽象、通用组件、错误处理、配置系统。于是一个简单的“计算折扣”需求,它可能生成策略模式、工厂模式、配置类、接口、枚举、校验器。

这些代码不一定错,但很可能“不合时宜”。

工程复杂度有一个重要特点:

每一层抽象都会产生维护成本,只有当它抵消了更大的重复成本时,才是值得的。

AI 很容易把“我知道这种抽象”误用成“这里应该使用这种抽象”。

2.3 它对代码所有权边界不敏感

在真实团队里,改动边界非常重要。你修一个登录 bug,却顺手格式化整个文件、改变量名、删注释、重构工具函数,会让 Code Review 变得痛苦。

LLM 没有天然的“代码所有权”感。它看到一段不优雅的代码,很容易顺手优化。结果是:

  • diff 变大;
  • review 成本变高;
  • 回滚变困难;
  • 引入无关回归;
  • 团队无法判断哪些改动是必要的。

2.4 它擅长循环,但需要明确目标

Karpathy 提到的一个关键洞察是:LLM 很擅长围绕明确目标循环,例如:

  • 运行测试;
  • 看错误;
  • 修改代码;
  • 再运行测试;
  • 直到通过。

但如果目标只是“把认证系统修好”“优化一下搜索”“改善代码质量”,模型就很容易进入一种模糊执行状态:做了很多事,却没有明确证据证明问题已解决。

所以,AI 编程的核心不是“让它多写代码”,而是“给它可验证的终点”。


3. 第一条:不要假设,不要隐藏困惑,要展示权衡

原文:

Don't assume. Don't hide confusion. Surface tradeoffs.

3.1 深层含义

这条准则解决的是需求理解阶段的问题。

传统软件开发里,需求不清时最专业的行为不是“先做一个版本”,而是把不确定性显性化。AI 也应该如此。

一个高级工程师不会在以下问题不明确时直接动手:

  • 数据范围不明确;
  • 安全边界不明确;
  • 用户体验不明确;
  • 业务规则不明确;
  • 多种实现路径成本差异很大。

LLM 最大的问题是:它经常在这些地方“替用户决定”。

但在工程实践中,沉默的假设非常危险。因为代码一旦写出来,用户很容易误以为“这就是正确方案”。

3.2 坏的做法:默默选择一种解释

用户需求:

给订单加一个取消功能。

AI 可能直接写:

async function cancelOrder(orderId: string) {
  const order = await db.order.findUnique({ where: { id: orderId } });

  if (!order) {
    throw new Error('Order not found');
  }

  await db.order.update({
    where: { id: orderId },
    data: { status: 'cancelled' },
  });

  return { success: true };
}

这段代码看起来合理,但它隐藏了大量业务假设:

  • 已支付订单能不能取消?
  • 已发货订单能不能取消?
  • 取消后是否退款?
  • 是否释放库存?
  • 谁有权限取消?用户本人、客服、管理员?
  • 是否需要记录取消原因?
  • 是否发送通知?

这类问题不是代码细节,而是业务规则。AI 如果直接实现,就是在用代码伪装业务决策。

3.3 好的做法:先暴露不确定性

更好的响应应该是:

“取消订单”涉及几个业务规则,我需要确认:

1. 哪些状态允许取消?
   - 仅 pending?
   - paid 但未发货也允许?
   - shipped 是否禁止?

2. 取消后是否需要副作用?
   - 恢复库存
   - 触发退款
   - 记录取消原因
   - 通知用户

3. 谁可以取消?
   - 下单用户本人
   - 管理员
   - 客服

如果你想先做最小版本,我建议:
只允许取消 pending 状态订单,并记录 cancelled 状态,不处理退款和库存。
确认这个范围后我再实现。

如果用户确认“只做 pending 订单取消”,实现就可以很简单:

async function cancelPendingOrder(orderId: string, userId: string) {
  const order = await db.order.findFirst({
    where: {
      id: orderId,
      userId,
      status: 'pending',
    },
  });

  if (!order) {
    throw new Error('Order cannot be cancelled');
  }

  return db.order.update({
    where: { id: orderId },
    data: { status: 'cancelled' },
  });
}

注意这里的关键不是“代码多高级”,而是:代码背后的假设已经被确认。

3.4 工程价值

这条准则能显著减少三类问题:

  • 需求偏差:做出来的不是用户真正想要的;
  • 安全事故:比如导出敏感数据、越权操作;
  • 返工成本:错误假设越早暴露,修复成本越低。

对非技术读者来说,可以把它理解成:

AI 不应该像一个“自作主张的实习生”,而应该像一个“会先确认边界的工程师”。


4. 第二条:只写解决问题所需的最少代码

原文:

Minimum code that solves the problem. Nothing speculative.

4.1 深层含义

这条准则针对的是 AI 的过度工程化倾向。

很多人误以为“复杂代码 = 高级代码”。实际上,高级工程师更重视:

  • 代码是否直接解决问题;
  • 是否容易阅读;
  • 是否容易删除;
  • 是否容易测试;
  • 是否在需求变化时仍然可演进。

“最少代码”不是偷懒,而是控制系统复杂度。

软件系统的复杂度不是线性增长的。一个多余的抽象,可能带来:

  • 更多文件;
  • 更多命名;
  • 更多跳转;
  • 更多测试;
  • 更多隐含约定;
  • 更多新人理解成本。

AI 生成代码成本很低,所以它更容易忽略维护成本。

4.2 坏的做法:为一个简单需求设计一套框架

用户需求:

根据订单金额计算 10% 优惠。

坏实现:

interface DiscountStrategy {
  calculate(amount: number): number;
}

class PercentageDiscountStrategy implements DiscountStrategy {
  constructor(private percentage: number) {}

  calculate(amount: number): number {
    return amount * this.percentage;
  }
}

class FixedDiscountStrategy implements DiscountStrategy {
  constructor(private value: number) {}

  calculate(amount: number): number {
    return Math.min(amount, this.value);
  }
}

class DiscountContext {
  constructor(private strategy: DiscountStrategy) {}

  apply(amount: number): number {
    if (amount < 0) {
      throw new Error('Amount cannot be negative');
    }

    return this.strategy.calculate(amount);
  }
}

const discount = new DiscountContext(
  new PercentageDiscountStrategy(0.1)
).apply(order.amount);

这段代码有设计模式,有接口,有类,看起来很“工程化”。但如果当前业务只有 10% 优惠,它就是明显过度设计。

4.3 好的做法:先用最直接的表达

function calculateTenPercentDiscount(amount: number): number {
  return amount * 0.1;
}

const discount = calculateTenPercentDiscount(order.amount);

如果需求稍微泛化为“传入百分比”,也仍然不需要策略模式:

function calculatePercentageDiscount(amount: number, rate: number): number {
  return amount * rate;
}

什么时候才值得引入策略模式?当业务真的出现多个可替换规则,例如:

  • 满减;
  • 百分比折扣;
  • 会员等级折扣;
  • 优惠券叠加;
  • 不同渠道不同规则;
  • 规则需要动态配置或灰度实验。

也就是说,抽象应该由真实重复驱动,而不是由想象中的未来驱动。

4.4 坏的做法:加入未被要求的配置能力

用户需求:

给接口加一个简单的请求频率限制,每个 IP 每分钟最多 60 次。

坏实现可能会做出一整套“通用限流平台”:

type RateLimitAlgorithm = 'fixed-window' | 'sliding-window' | 'token-bucket';

interface RateLimitRule {
  path: string;
  algorithm: RateLimitAlgorithm;
  windowMs: number;
  maxRequests: number;
  burst?: number;
}

class RateLimitConfigLoader {
  loadFromFile(path: string): RateLimitRule[] {
    // 读取配置文件
    return [];
  }
}

class RateLimitMetricsReporter {
  report(rule: RateLimitRule, blocked: boolean) {
    // 上报监控
  }
}

class RateLimitService {
  constructor(
    private rules: RateLimitRule[],
    private reporter: RateLimitMetricsReporter,
  ) {}

  async check(ip: string, path: string): Promise<boolean> {
    // 复杂规则匹配和多算法实现
    return true;
  }
}

这不是不能做,而是当前需求不需要。

好的最小版本可以是:

const requests = new Map<string, { count: number; resetAt: number }>();

function isRateLimited(ip: string): boolean {
  const now = Date.now();
  const current = requests.get(ip);

  if (!current || current.resetAt <= now) {
    requests.set(ip, { count: 1, resetAt: now + 60_000 });
    return false;
  }

  current.count += 1;
  return current.count > 60;
}

如果部署是单实例,这就能满足需求。只有在多实例部署、重启保持状态、跨节点共享计数时,才需要 Redis 或更复杂算法。

4.5 工程价值

“Simplicity First”带来的好处非常直接:

  • 更少 bug,因为状态和分支更少;
  • 更快 review,因为意图更清晰;
  • 更容易测试,因为行为更集中;
  • 更容易删除,因为没有无谓依赖;
  • 更容易演进,因为没有被错误抽象绑死。

真正成熟的工程判断,不是知道多少模式,而是知道什么时候不用模式。


5. 第三条:只改必须改的地方,只清理自己制造的问题

原文:

Touch only what you must. Clean up only your own mess.

5.1 深层含义

这条准则对应的是变更管理。

在大型项目里,代码不是孤立文本,而是团队协作资产。每一次修改都会产生审查、测试、合并、回滚成本。

AI 最大的问题之一是“顺手”。它可能在修一个 bug 时:

  • 改格式;
  • 改命名;
  • 改注释;
  • 改 import 顺序;
  • 重构附近函数;
  • 删除它认为没用的代码。

这些行为在人类开发中也常见,但 AI 因为速度快,破坏范围更大。

5.2 坏的做法:修 bug 顺手重构

原代码:

function validateEmail(input: { email?: string }) {
  // Check email exists
  if (!input.email) {
    throw new Error('Email required');
  }

  // Basic email check
  if (!input.email.includes('@')) {
    throw new Error('Invalid email');
  }

  return true;
}

用户报告:

email 是空格字符串时没有报错,比如 ' '

坏 diff:

-function validateEmail(input: { email?: string }) {
-  // Check email exists
-  if (!input.email) {
+export function validateEmail(input: { email?: string }): boolean {
+  const email = input.email?.trim().toLowerCase();
+
+  // Validate required email field
+  if (!email) {
     throw new Error('Email required');
   }
 
-  // Basic email check
-  if (!input.email.includes('@')) {
+  // Validate email format using regex
+  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
     throw new Error('Invalid email');
   }
 
   return true;
 }

问题在于:

  • export 是额外改动;
  • 返回类型注解可能不符合原项目风格;
  • toLowerCase() 改变了数据语义;
  • 正则校验扩大了行为变化;
  • 注释也被改了;
  • 原需求只是处理空格字符串。

5.3 好的做法:最小 diff

 function validateEmail(input: { email?: string }) {
   // Check email exists
-  if (!input.email) {
+  if (!input.email || !input.email.trim()) {
     throw new Error('Email required');
   }
 
   // Basic email check
   if (!input.email.includes('@')) {
     throw new Error('Invalid email');
   }
 
   return true;
 }

如果担心重复调用 trim(),也可以稍微调整,但仍保持变更收敛:

 function validateEmail(input: { email?: string }) {
+  const email = input.email;
+
   // Check email exists
-  if (!input.email) {
+  if (!email || !email.trim()) {
     throw new Error('Email required');
   }
 
   // Basic email check
-  if (!input.email.includes('@')) {
+  if (!email.includes('@')) {
     throw new Error('Invalid email');
   }
 
   return true;
 }

这里每一行变化都能解释为:“为了让空格字符串被视为缺失邮箱”。

5.4 “只清理自己的垃圾”是什么意思?

假设你修改代码后删除了一个函数调用,导致 import 变成未使用:

-import { sendEmail, renderTemplate } from './mail';
+import { sendEmail } from './mail';

这是应该清理的,因为未使用的 renderTemplate 是你这次改动造成的。

但如果你看到文件里本来就有一个未使用函数:

function oldLegacyHelper() {
  // 已经没人调用
}

除非用户要求清理,否则不要顺手删除。正确做法是提醒:

我注意到文件里还有一个疑似未使用的 oldLegacyHelper,但它和本次修复无关,所以没有删除。需要的话可以单独清理。

这体现的是工程纪律:一次提交只做一件事。

5.5 工程价值

“Surgical Changes”最重要的价值是降低协作成本:

  • PR 更小,更容易 review;
  • 回滚更安全;
  • 问题定位更容易;
  • 不会把无关风险混入当前任务;
  • 更尊重现有项目风格和团队约定。

对 AI 编程助手来说,这条尤其重要。因为 AI 生成大 diff 太容易了,而大 diff 往往是软件工程事故的温床。


6. 第四条:定义成功标准,循环直到验证通过

原文:

Define success criteria. Loop until verified.

6.1 深层含义

这条准则把 AI 从“执行命令”转向“达成目标”。

人类说“修一下登录问题”,其实信息不足。AI 如果直接改代码,很可能是在猜。更好的方式是把任务转成可验证目标:

  • 什么输入会失败?
  • 当前错误是什么?
  • 修复后应该出现什么行为?
  • 用什么测试或命令证明?

这和测试驱动开发(TDD)的思想非常接近:先定义失败,再让它变成功。

6.2 坏的做法:没有验证闭环

用户需求:

修复用户重复注册的问题。

坏做法:

async function register(email: string, password: string) {
  const user = await db.user.create({
    data: { email, passwordHash: hash(password) },
  });

  return user;
}

AI 可能直接改成:

async function register(email: string, password: string) {
  const existing = await db.user.findUnique({ where: { email } });

  if (existing) {
    throw new Error('Email already registered');
  }

  const user = await db.user.create({
    data: { email, passwordHash: hash(password) },
  });

  return user;
}

这看起来修了,但仍有问题:

  • 有没有测试证明重复注册会失败?
  • 并发请求下是否仍可能重复?
  • 数据库是否有唯一索引?
  • 错误码是否符合 API 规范?
  • 前端是否能正确显示错误?

没有验证闭环,就只是“看起来修了”。

6.3 好的做法:先定义成功标准

更好的执行方式:

成功标准:
1. 同一个 email 第一次注册成功;
2. 同一个 email 第二次注册返回 409;
3. 并发重复注册最多只能创建一个用户;
4. 现有注册测试仍然通过。

计划:
1. 补充重复注册测试 → 验证当前失败;
2. 添加应用层重复校验 → 验证普通重复注册返回 409;
3. 确认数据库 email 唯一约束 → 验证并发场景不会插入两条;
4. 运行注册相关测试 → 验证无回归。

测试示例:

it('returns 409 when email is already registered', async () => {
  await request(app)
    .post('/register')
    .send({ email: 'a@example.com', password: 'secret123' })
    .expect(201);

  await request(app)
    .post('/register')
    .send({ email: 'a@example.com', password: 'secret123' })
    .expect(409);
});

实现示例:

async function register(email: string, password: string) {
  const existing = await db.user.findUnique({ where: { email } });

  if (existing) {
    throw new ConflictError('Email already registered');
  }

  try {
    return await db.user.create({
      data: { email, passwordHash: hash(password) },
    });
  } catch (error) {
    if (isUniqueConstraintError(error)) {
      throw new ConflictError('Email already registered');
    }
    throw error;
  }
}

这里应用层检查负责提供友好错误,数据库唯一约束负责兜住并发。验证标准比代码本身更重要。

6.4 为什么这条特别适合 LLM?

LLM 的优势不是一次写出完美代码,而是在明确反馈下快速迭代。

如果你给它一个模糊目标:

优化性能。

它可能会乱改。

如果你给它一个可验证目标:

/api/search?q=abc 的 p95 响应时间从 800ms 降到 200ms 以下,不能改变返回结构,现有测试必须通过。

它就可以围绕目标进行:

  • 加日志;
  • 找慢查询;
  • 加索引;
  • 运行基准测试;
  • 对比指标;
  • 继续优化。

这才是 AI 编程助手最有价值的工作模式。


7. 这套准则如何落地?

7.1 放在哪里?

这个项目的典型用法是把准则放进项目级 CLAUDE.md,或者作为 Claude Code / Cursor 等工具的规则。

更通用地说,它适合放在所有 AI 编程助手能读取的“项目指令”里,例如:

  • CLAUDE.md
  • Cursor rules;
  • Windsurf rules;
  • GitHub Copilot instructions;
  • 团队内部 AI 使用规范;
  • PR 模板或开发手册。

7.2 用在哪些场景?

场景一:修 bug

推荐提示:

请修复这个 bug。先用测试或最小复现证明问题存在,再做最小修改修复。不要改无关代码。最后说明验证方式。

场景二:加功能

推荐提示:

请先列出你对需求的假设。如果有不明确的业务规则,先问我。确认后只实现最小可用版本,不要添加未要求的扩展能力。

场景三:重构

推荐提示:

请只重构指定模块,不改变外部行为。先说明成功标准和回归验证方式。每个改动都要能追溯到本次重构目标。

场景四:性能优化

推荐提示:

请先定义性能指标和测量方法,不要凭感觉优化。优化前后都要给出可复现的 benchmark 或日志证据。

7.3 团队级落地方式

如果是团队使用 AI 编程助手,可以把这四条转化为 PR 检查清单:

AI 生成代码检查清单:

- [ ] 是否存在未确认的业务假设?
- [ ] 是否添加了用户没要求的功能或配置?
- [ ] 是否引入了只使用一次的抽象?
- [ ] 是否修改了无关文件、注释、格式?
- [ ] 是否清理了本次改动造成的未使用代码?
- [ ] 是否有明确成功标准?
- [ ] 是否有测试、命令或手动步骤证明已验证?

这个清单比“代码看起来还行吗”更有效,因为它把 AI 常见错误变成了可检查事项。

7.4 有什么好处?

落地后最明显的变化通常是:

  1. AI 更愿意提问

不会在需求模糊时直接开写。

  1. diff 变小

少了格式化、重构、无关优化。

  1. 代码更朴素

少了过早抽象和“炫技式架构”。

  1. 验证更充分

从“我改好了”变成“我用这些标准证明改好了”。

  1. 人类 review 更轻松

审查重点回到业务和行为,而不是在大段 AI 生成代码里找风险。


8. 这套准则的边界和代价

它不是万能的,也不是每个任务都要完整执行。

项目 README 里也提到一个 tradeoff:这些规则偏向谨慎而不是速度。对于非常简单的任务,比如:

  • 修一个拼写错误;
  • 改一个按钮文案;
  • 删除一行明显无用日志;
  • 调整一个常量;

如果每次都要求完整计划、假设、验证,会显得笨重。

所以更合理的策略是:

  • 小任务:快速执行,但保持最小 diff;
  • 中等任务:列简短假设和验证方式;
  • 高风险任务:必须先澄清、写测试、分步骤验证。

高风险任务包括:

  • 认证授权;
  • 支付退款;
  • 数据迁移;
  • 批量删除;
  • 用户隐私数据;
  • 并发一致性;
  • 生产部署;
  • 安全相关修改。

这套准则的本质不是拖慢开发,而是把谨慎用在值得谨慎的地方。


9. 对开发者使用 AI 编程助手的启示

9.1 不要只给任务,要给边界

低质量提示:

帮我加个导出功能。

更好的提示:

帮我给用户列表加 CSV 导出功能。
范围:只导出当前筛选结果,不导出密码、token 等敏感字段。
字段:id、name、email、createdAt。
要求:后端新增接口,前端添加按钮。
如果你发现分页和导出范围有歧义,先问我。

AI 的输出质量很大程度取决于边界是否清楚。

9.2 把 AI 当成初级但高速的工程师,而不是自动驾驶

AI 可以很快,但它不会自动拥有你的业务上下文。你需要给它:

  • 项目规则;
  • 业务边界;
  • 不允许改的地方;
  • 验证标准;
  • 现有风格约束。

如果你不给边界,它就会用训练数据里的“通用合理方案”填空。

9.3 优先要求测试和验证,而不是直接要实现

与其说:

帮我修复搜索 bug。

不如说:

这个搜索 bug 的现象是:关键词包含空格时结果为空。
请先写一个失败测试复现,然后做最小修改让测试通过,最后运行搜索相关测试。

这会迫使 AI 从“生成代码”切换到“闭环解决问题”。

9.4 认真看 diff,而不是只看最终效果

AI 时代,开发者的核心能力会从“逐行手写代码”转向“审查、约束、验证代码”。

看 AI 生成代码时,尤其要问:

  • 这行改动和需求有什么关系?
  • 有没有隐藏业务假设?
  • 有没有过度抽象?
  • 有没有顺手改无关代码?
  • 有没有测试证明?
  • 失败路径是否符合项目规范?

这正是四条准则想训练出来的审查习惯。

9.5 最好的 AI 规则,其实是最好的工程规则

这套 CLAUDE.md 看似是写给 AI 的,其实也是写给人类工程师的。

它提醒我们:

  • 不清楚就问;
  • 不需要就别加;
  • 不相关就别动;
  • 没验证就不算完成。

这些原则并不新,但 AI 放大了它们的重要性。因为 AI 能在一分钟内生成过去一天才能写完的代码,也就能在一分钟内制造过去一天才能排查的复杂度。


10. 总结:这不是提示词,而是工程护栏

andrej-karpathy-skills 的价值在于,它没有试图让 AI “更聪明”,而是让 AI “更守纪律”。

这很关键。

在真实软件工程中,很多事故不是因为工程师不会写代码,而是因为:

  • 需求没确认;
  • 抽象做早了;
  • 改动范围失控;
  • 没有验证标准。

AI 编程助手同样如此,而且更严重。

四条准则可以浓缩成一句话:

先澄清,再简化;只做必要修改,并用可验证标准证明完成。

如果开发者能把这套规则固化到自己的 AI 工作流里,AI 就不再只是“代码生成器”,而会更接近一个受约束、可审查、能迭代的工程协作者。

这可能才是 AI 编程从“惊艳 demo”走向“可靠生产力”的关键一步。