Karpathy 视角下的 LLM 编码缺陷:四条行为准则的深度解析
LLM 编码的自作主张
长期用 Claude Code、Cursor、Copilot 写代码的人大概都熟悉一种感觉:模型并不是不会写,而是写得太多、太聪明、太自作主张。让它修一个 bug,它顺手把无关代码风格也改了;让它加一个简单功能,它给你抽象出三层接口和一套配置系统;让它重构 X,它没问清楚 X 的边界就开始动手。
Andrej Karpathy 在 X 上发过一段对这类问题的精炼归纳,forrestchang 把它整理成了一个 GitHub 仓库 andrej-karpathy-skills,提供一份 60 多行的 CLAUDE.md,外加 Claude Code Plugin 的安装方式。这个仓库的内容可以同时用于 Claude Code、Cursor 以及任何支持 CLAUDE.md 形态的 LLM 编码工具。
这篇文章想讨论三件事:Karpathy 到底诊断出了哪四个偏差、对应的四条行为准则怎么用、以及一个更实际的问题——这套准则到底应该装成 skill 还是直接写进 CLAUDE.md。
Karpathy 观察到的四个系统性偏差
仓库 README 引用了 Karpathy 原帖的三段话,对应四种独立的失效模式:
“The models make wrong assumptions on your behalf and just run along with them without checking. They don’t manage their confusion, don’t seek clarifications, don’t surface inconsistencies, don’t present tradeoffs, don’t push back when they should.”
“They really like to overcomplicate code and APIs, bloat abstractions, don’t clean up dead code… implement a bloated construction over 1000 lines when 100 would do.”
“They still sometimes change/remove comments and code they don’t sufficiently understand as side effects, even if orthogonal to the task.”
替用户做隐藏假设
模型遇到歧义时不发问,而是悄悄选一个解释往下走。这不是它不愿问,而是训练目标里"给出一个完整答案"的权重远高于"承认我不确定"。
一个常见例子:用户说"加一个导出用户数据的功能"。歧义至少有三处:导出全部还是分页?导出哪些字段,里面是否涉及隐私敏感信息?是浏览器直接下载文件,还是后台任务加邮件通知,还是 API 返回数据?典型的 LLM 反应是挑一个最像 demo 的解释直接写出来。这里的根本问题是模型把"看起来完整的代码"当成最终交付物,而不是把"和用户对齐过的需求"当成最终交付物。
偏爱过度抽象
让 LLM 写一个折扣计算函数,它经常会先抽出 DiscountStrategy 抽象基类、PercentageDiscount / FixedDiscount 子类、DiscountConfig 配置 dataclass、DiscountCalculator 上下文,然后在输出里附上一句"这样以后扩展新的折扣类型很方便"。
这不是错的,但它解决的是未来可能存在的问题,代价是现在的代码理解成本翻了五倍。仓库里的 EXAMPLES.md 把这种过度抽象和精简版本做了直接对比,简单版几乎在所有维度都更优:更易读、更易测、更快写完,将来真有第二种折扣类型时再重构也来得及。模型混淆了"经过验证的设计模式是好的"与"在所有场景下都该使用设计模式",把模式当成了默认交付物,而不是应对真实复杂度时的可选工具。
改动外溢
被要求修一个 bug,顺手把附近的代码风格、引号种类、变量命名、缺失的类型注解、不规范的 docstring 一并"改进"了。
仓库里给了一个真实案例:用户只是说"修复空 email 导致校验器崩溃的 bug",模型同时把 username 长度校验、字母数字校验也加上了,还把变量名、注释、缩进全部"统一"了一遍。review 时这种 PR 最难处理,审阅者无法区分哪些改动是为了修 bug、哪些是顺带美化的。根源在于模型从训练数据里习得了"良好实践"的偏好,但没有内化一条工程纪律:维护既有代码时,每一行变更都需要被解释。
目标不可验证
让 LLM “fix the auth system”,它会回答:“好的,我会回顾代码、识别问题、做出改进、测试改动”,然后改一堆东西,交差时丢一句"应该好了"。
"应该好了"不是一个可验证的状态。Karpathy 的关键洞察是:
“LLMs are exceptionally good at looping until they meet specific goals… Don’t tell it what to do, give it success criteria and watch it go.”
LLM 在有明确成功标准的循环里非常擅长收敛,在成功标准模糊的任务里,它要么过早声称完成,要么不停打扰用户要澄清。问题的症结是把祈使句任务(修 bug、加校验、重构 X)当成动词执行,而没有先把它翻译成可验证的目标(写一个能复现 bug 的失败测试,让它通过)。
四条准则逐条解析
仓库的核心交付物是一份精简的 CLAUDE.md,把上面四种偏差对应到四条可被 LLM 引用的行为准则。
Think Before Coding
一句话概括是 “Don’t assume. Don’t hide confusion. Surface tradeoffs.”。落到实操,要求模型在动手前完成四件事:不确定就直接问;歧义存在时不要默默选一个解释,多种可能并列展示;存在更简单方案就说出来,必要时反驳用户的方案;遇到不清楚的地方就停下,明确说出哪里不清楚。
这一条最容易被误读为"凡事都要问用户",准则的要求其实更精确:只在存在真实歧义时问。用户说"把这个变量名从 data 改成 payload“,没有歧义,直接做;用户说"加个导出功能”,不问就是失职。一个可操作的判定标准是:能不能用一句话把当前行动描述出来,并且这句话只引用用户原话和代码里已有的事实?做不到,就说明正在用假设填补空白,必须先把假设说出来。
Simplicity First
一句话概括是 “Minimum code that solves the problem. Nothing speculative.”。具体边界:不实现没要求的功能;不为单次使用的代码做抽象;不引入没要求的灵活性或可配置性;不为不可能的场景写错误处理;200 行能压成 50 行,就重写。
这条准则最有杀伤力的是最后那个反问句:“Would a senior engineer say this is overcomplicated? If yes, simplify.”。它把审美权交给一个虚拟的资深工程师,而不是模型自己的"代码完整性焦虑"。准则不反对设计模式,反对的是在没有真实需求驱动时就引入设计模式。当一种新的折扣类型真的出现,再把简单函数重构成策略模式完全合理。复杂度的价值只在当下问题已经容纳不下时才成立,提前抽象等于提前付出维护成本,而且常常付错,因为没人知道未来真正的扩展方向是什么。
Surgical Changes
一句话概括是 “Touch only what you must. Clean up only your own mess.”。具体行为:编辑既有代码时不去"改进"相邻代码、注释、格式;不重构没有坏的东西;匹配既有风格,即便自己觉得有更好的写法;注意到无关的死代码就告诉用户,但不要删;自己修改产生的孤儿(unused imports、变量、函数)要清理掉;既有的死代码不要主动删,除非用户明确要求。
判定标准很清晰:每一行改动都应该能直接追溯到用户的请求。如果某一行改动解释不出"这是为了满足用户哪句话",就该回滚。这条准则反的是模型的训练偏好,训练数据里大量"良好实践"鼓励顺手优化、补类型、加 docstring,这些在写新代码时是优点,维护代码时是噪声。准则强制模型区分这两种场景。维护既有代码的纪律是小改动加高可解释性,每一处看起来无关的改动都会让 review 时间和回归风险翻倍。
Goal-Driven Execution
一句话概括是 “Define success criteria. Loop until verified.”。把祈使句任务翻译成可验证目标的对照表是这样:
- “Add validation” → “Write tests for invalid inputs, then make them pass”
- “Fix the bug” → “Write a test that reproduces it, then make it pass”
- “Refactor X” → “Ensure tests pass before and after”
对多步任务,要求陈述一个简短计划,每一步都带验证:
1 | |
这条直接呼应 Karpathy 的核心洞察:模型在明确成功标准下非常擅长收敛。强成功标准让 LLM 可以独立循环,弱成功标准(“make it work”)会导致它不停打扰用户。这对 Claude Code、Cursor 这类 agent 形态尤其重要。它们的工作模式本来就是工具调用循环,退出条件越清晰,agent 的自主性就越高。只说"修好 auth",agent 不知道何时该停;说"把这个失败测试变绿,且其他测试不能挂",agent 知道何时该停。
给 LLM 派任务时可以问自己一个问题:完成是一个可以被脚本检查出来的状态,还是一个需要肉眼判断的状态?前者 LLM 能独立完成,后者几乎必然带来反复返工。
四条准则之间的关系
这四条不是平行的清单,中间有时序和依赖。入口处(接到任务还没动手)由第一条起作用,失效就会跑偏整个任务方向;决定写多少代码时由第二条起作用,失效会写出五倍于必要量的代码;编辑既有代码时由第三条起作用,失效会让 PR 包含大量噪声、review 不动;判断"完成"与否由第四条起作用,失效会过早宣称完成或者不停打扰用户。
第一条和第四条其实互为镜像:前者在入口处澄清歧义、声明假设,后者在出口处用可验证标准判定完成。中间两条规范执行过程:Simplicity First 控制写多少代码,Surgical Changes 控制改多少范围。
装成 skill 还是直接写进 CLAUDE.md
skill 的触发机制
这个 skill 不是一个命令,没有 slash command 入口,用户不会主动敲 /karpathy-guidelines。它装上之后是否生效,取决于 agent 在每一步对话里扫描所有可用 skill 的 description,判断当前任务是否匹配。karpathy-guidelines 的描述是 “Use when writing, reviewing, or refactoring code”,只要 agent 判定当前对话属于写代码、审代码、重构代码,它应该加载这个 skill 的 SKILL.md 内容进上下文。
这个模式有两个脆弱点。
一是漏触发。description 的措辞能不能匹配上用户当前消息,完全依赖 agent 的元认知。用户如果只说"帮我把这个脚本跑通"或"改一下这里的逻辑",agent 很可能不觉得这算 “writing / refactoring code”。尤其在长对话里,agent 注意力已经被上下文分散,skill 列表是个需要主动扫描的集合,漏扫是常事。这种时候 skill 就像没装一样。
二是装载时机。即便触发了,SKILL.md 也是在某个时刻被注入上下文的,注入之前的几轮对话里准则不起作用。如果早期几轮就已经决定了任务方向,比如模型已经默认做了隐藏假设、已经开始写过度抽象的代码,后面再注入准则就来不及纠偏了。
CLAUDE.md 是系统级常驻
CLAUDE.md 的工作方式完全不同。Claude Code、Cursor 以及大多数支持这个约定的工具都会把项目根目录的 CLAUDE.md 或全局 ~/.claude/CLAUDE.md 拼进 system prompt。它从会话第一轮起就在,每一轮都在,不需要 agent 自己判断现在要不要加载它,用户说什么都不影响它生效。这是零触发成本的,不需要任何 description 匹配,不需要任何元认知决策,是 agent 从第一个 token 开始就看到的背景知识。
两种形态都会被稀释,但方式不同
严格来说 CLAUDE.md 也会被稀释。注意力稀释是 transformer 的结构性特征,任何 token 都会随着上下文变长而被稀释。但两者被稀释的方式不一样。
CLAUDE.md 的稀释是均匀可预测的。它永远位于 system prompt 这个固定位置(通常是上下文的开头或专门的 system 区),每一轮对话里的相对位置和内容都一样。模型对这种稳定背景的处理是训练里反复见过的,注意力权重相对稳定。同时由于它一直在,不存在"某几轮没它"的窗口。
skill 的稀释是非均匀且不确定的。首先存在加载与未加载这个二值状态,加载失败的那几轮相当于稀释到零。其次即便加载了,位置也不固定,可能作为工具返回值注入、可能作为新的 system message 注入、也可能作为 user 消息的一部分注入,不同位置在模型注意力里的权重不一样。第三是长对话里的堆积效应:当上下文已经塞满了几十轮工具调用结果、代码片段、错误输出之后,即便 skill 被重新注入,它也会淹没在一堆无关内容里,比 CLAUDE.md 更难被模型真正"看到"。
所以对于常驻型的行为准则,CLAUDE.md 的注意力稳定性明显优于 skill。
skill 和 CLAUDE.md 适合的场景
skill 并不是全面不如 CLAUDE.md,两者解决的是不同问题。
skill 的优势是按场景精准加载、不污染无关任务的上下文。pdf 处理 skill、slack-gif-creator、docx、xlsx 这类工具性 skill 只在用户真的在处理对应文件类型时才需要被看到,平时不加载反而是好事,能省下宝贵的上下文预算。systematic-debugging、pua-debugging 这种阶段性 skill 也一样,内容只在特定状态下才相关,常驻反而是浪费。
行为准则类型的内容不吃这个优势。karpathy-guidelines 的四条准则应该在任何一次代码工作里都被遵守,不存在"这个任务需要 Simplicity First,那个任务不需要"这种区分。给这种内容套上 skill 的按需加载机制,相当于给永远该生效的规则加了一个会概率性失效的开关。
仓库作者也意识到了这点。README 里主推的安装方式是 CLAUDE.md,把一份 60 行的 markdown 直接拷到项目根或追加到已有 CLAUDE.md 末尾,比 skill 路径简单,效果也更可靠。skill 形态更像是给已经用上 skill 基建的人提供的一个兼容形态。
实操结论
个人全局使用时,在 ~/.claude/CLAUDE.md 里追加这四条准则,常驻、跨项目、零触发成本。团队项目使用时,在项目根 CLAUDE.md 里追加这四条准则作为团队基线的一部分。不得已必须走 skill 路径时(比如工具链强制要求 skill 形态),装成 skill 也行,只是要知道它的生效率比 CLAUDE.md 低,漏触发时不要奇怪。
一份参考:把准则装成 skill 的流程
上游仓库同时提供了 skill 版本,虽然不是最优形态,落地过程还是值得完整记录一下。
skill 可以装在项目级(<项目根>/skills/<skill-name>/)或用户级(~/.aone_copilot/skills/<skill-name>/、~/.claude/skills/<skill-name>/),我选择把它放在一个专门管理 skill / agent / prompt 的本地 git 仓库里,便于版本化和同步。
最朴素的手工安装三条命令:
1 | |
SKILL.md 头部的 frontmatter 长这样:
1 | |
这个 description 就是前面说的"触发条件",agent 靠它来决定要不要加载。
如果本地已经有一套 skill 治理工具链(比如 skill-installer + generate-skill-doc),流程会规范一些:先检测安装目录,下载新版到临时目录并校验 frontmatter,备份旧版 DESIGN.md,完整清除旧目录再复制新版,恢复 DESIGN.md 历史,触发 generate-skill-doc 生成版本记录,最后清理临时文件。不用覆盖式更新而是清除加复制的原因是 skill 目录结构会演化(比如新增 references/ 子目录、删除某些文件),覆盖可能留下幽灵文件,行为变得不可预测。
SKILL.md 和 DESIGN.md 的分工在这套流程里也重要。SKILL.md 是给 LLM 看的操作指令,会被加载到上下文,token 必须紧凑;DESIGN.md 是给人看的设计说明,不被 LLM 加载,记录"为什么这么设计"和版本历史。版本号只放在 DESIGN.md 里,不写进 SKILL.md frontmatter,否则每次升级都让 LLM 上下文里多一行无意义的 token。这种双文件结构对准则型 skill 尤其合适:操作语义给机器读,设计意图给人读,否则 SKILL.md 容易被设计讨论灌水变长,DESIGN.md 也容易被压缩成清单失去解释力。
这套准则不能解决的问题
这套准则不是代码审查工具,不会指出"这段代码有 N+1 查询"或"这里缺了空指针检查",那属于专门的 reviewer skill 的职责。它不是 lint 或格式化器,不约束代码风格本身,只约束模型在改动时不要主动改风格。它不是测试框架的替代品,Goal-Driven Execution 强调写测试先于实现,但具体怎么写、用哪个框架,还是 TDD 类 skill 的事。它也不对琐碎任务做强制约束,仓库 README 明确写了 “These guidelines bias toward caution over speed. For trivial tasks, use judgment.”,改个 typo、写一行明显的修复时硬套四条准则就是自缚手脚。
准则的价值在非平凡任务上。代价是每次多花一些时间澄清和规划,平凡任务不值得付这个代价。
和已有 skill 生态的关系
如果本地 skill 库里已经有一些"编码态度类"skill,比如 test-driven-development、systematic-debugging、verification-before-completion、brainstorming、receiving-code-review、pua-debugging,很自然会问:为什么还要再装一份 karpathy-guidelines?
区别在于触发面。前面这些都是特定阶段触发的:brainstorming 在创意工作之前,TDD 在写实现之前,systematic-debugging 在遇到 bug 时,verification-before-completion 在声称完成之前,pua-debugging 在反复失败两次以上时。它们的定位是"某个状态下需要切入的流程"。karpathy-guidelines 的覆盖面是整段编码期间常驻的态度准则,触发面更宽。
具体看,Think Before Coding 和 brainstorming 有重叠,但后者是需求阶段,前者是动手前那一刻。Goal-Driven Execution 和 TDD 有重叠,但 TDD 给的是具体流程,Goal-Driven 给的是态度理由。而 Simplicity First 和 Surgical Changes 在现有 skill 里基本没有直接等价物,这两条正好是 LLM 高频翻车、但又没被其他 skill 专门覆盖的盲区,这也是这套准则值得独立存在的理由。




