Andrej Karpathy 在 2025 年中的一次持续数周的 Claude Code 使用后,在社交媒体上分享了他对当前大语言模型编码行为的深度反思。这篇观察迅速在开发者社区引发共鸣,并被社区开发者整理为可直接加载的 CLAUDE.md 插件 forrestchang/andrej-karpathy-skills。该项目的核心价值不在于提供新的技术能力,而在于揭示 LLM 编码过程中的系统性行为模式 —— 这些模式往往在单次任务中不易察觉,但在长期使用时会导致代码质量的隐性衰退。本文将深入解析这四大行为陷阱的内在机制,并给出可落地的工程约束参数。
假设盲区:模型替你做决定,却不告诉你
Karpathy 最核心的观察指出,LLM 倾向于在任务开始前 silently pick an interpretation( silently pick an interpretation,即 silently 选择一个解释)并直接执行,而不会主动暴露这种选择背后的假设。这与人类程序员在面对模糊需求时的行为形成鲜明对比 —— 后者通常会主动提问、确认边界,而前者则像是一位过度自信的工程师,自以为理解了需求,实际上可能基于错误的假设构建了一套完整的错误方案。
这种假设盲区的危害在于它的隐蔽性。当模型输出看似合理的代码时,用户往往不会怀疑底层的假设是否正确。只有当功能出现偏差或逻辑出现漏洞时,问题才会浮出水面。更为关键的是,模型不会主动标记不确定性 —— 当遇到模糊的函数名、不明确的需求描述或存在多种实现路径的场景时,它会选择一个看起来 “最合理” 的方案,然后继续推进,完全忽略了这个选择本应是需要人类确认的决策点。
工程化应对的首要原则是强制显式假设声明。在任务规划阶段,模型应该被要求列出所有关键假设,包括对需求文本的理解、对现有代码结构的推断、对边界条件的默认处理。每个假设都应该有明确的标记,表明其确定性等级 —— 是高置信度的推断,还是仅基于有限信息的猜测。对于不确定性较高的假设,应该自动触发澄清请求,而非自行决定后直接执行。
从工程参数角度看,建议在 CLAUDE.md 中预设以下约束:任何涉及数据格式、业务规则、接口契约的假设,必须在代码实现前以注释形式明确列出;在模型输出的开头增加 “假设清单” 段落,说明本次实现所依赖的所有关键前提;对于存在多种合理解法的任务,必须列出至少两种方案及其 trade-off,供用户选择而非自行决定。
过度工程:代码膨胀与抽象滥用
Karpathy 观察到的第二个典型行为是模型对复杂性的天然偏好。他指出,模型 “非常喜欢把代码和 API 过度复杂化,膨胀抽象层级,不清理死代码 —— 用 1000 行实现一个 100 行就能解决的问题”。这种倾向的根源在于训练数据中充满了各种 “最佳实践”“可扩展架构”“灵活设计模式”,模型在生成代码时会不自觉地引用这些模式,即使当前场景根本不需要它们。
这种过度工程的表现形式多种多样:创建一个完整的抽象基类来处理只有两个实现类的场景;为单个函数添加复杂的配置参数和钩子机制,而该函数在整个生命周期内只会用到一个配置值;引入第三方库来处理本可以用原生 API 实现的简单功能;设计一套 “扩展性极强” 的数据层架构,却忽略了当前业务场景的实际情况。当这些过度设计累积到一定程度,代码库的维护成本会显著上升,而新加入的开发者面对这些看似专业实则不必要的抽象层时,往往会陷入困惑。
工程化应对的核心原则是最小可行实现。Simplicity First 原则要求模型在生成代码时始终追问一个核心问题:这段代码的最小完整版本是什么?在没有看到明确的功能扩展需求之前,任何 “预留” 的灵活性都是过早优化。具体而言,代码生成时应遵循以下参数:单个文件或模块的代码行数应有硬性上限,超过阈值时必须拆分而非继续膨胀;禁止为单一使用场景创建抽象接口或基类;所有引入的第三方依赖必须有明确的、已存在的使用场景,而非 “以后可能会用到”。
一个实用的工程技巧是将 Senior Engineer Test(资深工程师测试)作为代码生成的必检环节:生成代码后,模型应自我评估 —— 如果一位资深工程师看到这段代码,是否会觉得它过度复杂?如果答案是肯定的,则必须简化。简化的标准很简单:能否用更少的代码行数实现完全相同的功能?如果能,立即重写。
边界腐蚀:改动范围的无声扩大
第三个行为陷阱涉及 LLM 在修改现有代码时的边界控制问题。Karpathy 指出,模型 “有时会修改或删除它们理解不够充分的注释和代码,作为副作用,即使这些代码与当前任务正交(orthogonal)”。这种边界腐蚀现象在实践中极为常见:一个简单的 bug 修复,最终变成了一次大规模的代码重构;一个函数的小改动,导致相邻模块的格式、命名、注释都发生了微妙的变化。
边界腐蚀的深层原因在于模型对 “代码质量” 的过度主动性。当模型看到一段可以 “改进” 的代码时,它很难抑制住优化的冲动 —— 即使这种改进完全不在用户的计划之内。这种行为的后果是:PR(Pull Request)的审查难度大幅增加,因为 diff 中混杂了大量与原始任务无关的改动;代码的版本历史变得混乱,难以追踪真正的变更意图;团队的代码规范共识被逐步侵蚀,因为模型会用自己的风格偏好替换原有的约定。
Surgical Changes(外科手术式改动)原则正是针对这一问题的解药。它的核心要求是:只改你必须改的。在修改现有代码时,模型应该被严格限制在用户明确指定的任务范围内,任何超出范围的 “改进” 都是不被允许的。这包括:不修改相邻的代码风格或注释、不对未请求的模块进行重构、不删除用户未提及的死代码、不引入新的命名约定或格式化规则。
从工程实践角度看,建议在任务执行前增加 Scope Verification(范围验证)环节:在开始编码前,明确列出本次改动将涉及的文件和函数,并确认这个范围是否与用户意图一致。如果在执行过程中发现需要超出原定范围的改动,必须先暂停,向用户说明原因并获得批准后,再扩大修改范围。此外,对于修改过程中发现的正交问题(如无关的死代码、可以优化的相邻模块),正确的处理方式是在任务结束后以建议的形式向用户报告,而非直接在本次改动中一并处理。
目标迷失:从指令到验证的范式转换
Karpathy 观察到的第四个行为模式涉及任务执行的方法论。他指出,LLM “非常擅长循环直到达成特定目标”,但问题在于人类往往倾向于告诉模型 “做什么”(imperative,命令式),而非给出 “达成什么”(declarative,声明式)。这种指令形式的差异导致了两种截然不同的执行效果:当用户说 “添加验证” 时,模型可能实现一套复杂的验证逻辑,却无法判断验证是否真正完整;当用户说 “修复这个 bug” 时,模型可能尝试了多种方案,却无法确认是否真正解决了根本问题。
Goal-Driven Execution 原则正是为了解决这一范式错配。其核心思想是将所有的任务指令从 “做什么” 转换为 “达成什么”,并配套可验证的成功标准。这种转换的关键在于:不是告诉模型 “写测试”,而是告诉模型 “写出能暴露该 bug 的测试,并使其通过”;不是告诉模型 “添加输入验证”,而是告诉模型 “针对无效输入编写测试用例,然后让这些测试通过”;不是告诉模型 “重构 X 模块”,而是告诉模型 “确保重构前后的测试全部通过”。
在工程实现层面,建议为每个任务建立清晰的 Verify Loop(验证循环):任务开始前,明确列出成功标准 —— 什么指标表明任务已经完成?这个标准必须是可观测、可测试的,而非模糊的 “让它工作”。对于复杂任务,应将整个过程分解为多个步骤,每个步骤都有对应的验证点。例如,一个功能实现任务可以分解为:步骤一,编写功能测试覆盖核心场景 → 验证:测试失败(因为功能尚未实现);步骤二,实现最小功能代码 → 验证:测试通过;步骤三,编写边界条件测试 → 验证:所有边界测试通过。这种 Verify Loop 的好处在于,它让模型能够在没有人类持续干预的情况下自主判断任务进度,并在遇到问题时及时停下来,而非盲目推进直到产生大量无效代码。
工程落地的核心参数清单
将上述四大原则转化为工程实践,需要在 Claude Code 的配置文件或项目级 CLAUDE.md 中预设以下参数。这些参数不是建议,而是强制约束:
在假设管理维度,要求模型在每个任务的初始阶段输出 “假设声明” 段落,列出所有关键假设及其确定性等级;不确定性超过阈值的假设必须触发澄清流程。在复杂度控制维度,单个文件的最大代码行数建议设为 500 行(具体阈值可根据项目规范调整),超过必须拆分;禁止为单一使用场景创建抽象接口;所有新引入的依赖必须有当前代码的直接引用。在边界控制维度,所有改动在执行前必须通过 Scope Verification,明确列出将修改的文件和函数范围;任何超出原定范围的改动必须先暂停并获取用户批准。在目标验证维度,每个任务必须转化为 “成功标准 + 验证点” 的形式;多步骤任务必须包含中间验证环节,而非一次性完成所有改动。
这些参数的目的是在保持 LLM 强大编码能力的同时,引入足够的约束机制来抑制其系统性行为偏差。Karpathy 本人也强调,这些指南偏向 “谨慎优于速度”—— 对于简单的任务(如修改一个拼写错误),不需要如此严格的流程;但对于复杂的非平凡工作,这些约束能够显著降低错误成本。
资料来源
- forrestchang/andrej-karpathy-skills: 基于 Karpathy 观察构建的 Claude Code 行为指南插件
- Karpathy 在社交媒体上关于 LLM 编码陷阱的原始观察帖文