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 污染;
- 模型如果没有明确成功标准,就会“看起来很努力”,但未必真的解决问题。
项目把这些问题总结成四条准则:
- Think Before Coding:不要默默假设,不要隐藏困惑,要展示权衡;
- Simplicity First:只写解决问题所需的最少代码,不做投机式扩展;
- Surgical Changes:只改必须改的地方,只清理自己制造的垃圾;
- 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 有什么好处?
落地后最明显的变化通常是:
- AI 更愿意提问
不会在需求模糊时直接开写。
- diff 变小
少了格式化、重构、无关优化。
- 代码更朴素
少了过早抽象和“炫技式架构”。
- 验证更充分
从“我改好了”变成“我用这些标准证明改好了”。
- 人类 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”走向“可靠生产力”的关键一步。