karpathy-coding-guidelines.md 16 KB

Karpathy编码规范

基于Andrej Karpathy对LLM编码陷阱的观察,提供5条核心编码原则。

输入要求

  1. 代码片段(可选):需要评估的代码
  2. 具体问题(可选):如"这段代码是否过度设计"
  3. 场景描述(可选):编码场景说明

执行流程

第一步:明确目标与假设

  • 识别用户真实目标和成功标准
  • 标注不确定信息,必要时向用户澄清
  • 列出关键假设,不把猜测隐藏在代码里

第二步:应用五大原则

  • 用“先证后写”覆盖正确性敏感算法(原则0优先于其余四条)
  • 用“编码前思考”检查需求歧义
  • 用“简洁优先”排除过度设计
  • 用“精准修改”限制变更范围
  • 用“目标驱动执行”定义验证闭环

第三步:检查代码或方案

  • 对照五大原则逐项检查
  • 识别具体代码位置、改动点或设计风险
  • 区分必须修复和可选优化

第四步:输出改进建议

  • 给出最小修改方案
  • 提供修改前后对比或明确执行步骤
  • 标注保留现状的理由和风险

第五步:验证与沉淀

  • 验证建议是否满足原始目标
  • 将可复用的编码规范、陷阱和决策记录到项目记忆

五大原则

原则0:先证后写(Prove Before Code)

关键算法先写不变量,再写码。正确性先于简洁。

编写递归、图遍历、状态机、排序等正确性敏感算法前必须:

  • 先书面陈述不变量(invariant)、前置条件、后置条件与终止条件
  • 再据此写出实现,使不变量在每次递归入口/出口成立
  • 用结构化断言验证不变量,而不是依赖"流程跑通"

反模式:

  • 直接写环检测递归,边写边改,多次迭代都没写对(缺不变量,测试结果错也看不出来)
  • 给关键算法只配"正常值"测试,结构边界(自环/互环/平行边)全空

正确做法:

环检测不变量:
- 前置:图节点集合 G,遍历方向 dir ∈ {down, up, both}
- 不变量:递归栈 stack 中任意节点 v,其下游环已被正确计数且 v 不重复计入
- 后置:返回环数与环成员,遍历方向与边有向性解耦
→ 先有这张表,再写 DFS
符号/边抽取不变量(静态分析类):
- 前置:① 源码在抽取前已剥离注释/死代码,或抽取层显式排除注释/字符串字面量;② 抽取层按语言隔离——某语言的专属语法正则(如 PHP 的 namespace/use/class/function/new/::)只对该语言文件运行,非目标语言文件(.py/.js/.ts/.java 等)不得套用,否则 .py 的 `class X:`、.js 的 ES6 `class X {}` 会被误抽成目标语言符号污染符号表
- 不变量:抽取出的每个符号 s ∈ 真实语句 token 集;∀ 边 e=(a,b),a,b 均在真实代码中定义或被引用;且 s 所属语法恰好属于当前文件的目标语言
- 后置:symbols/edges 中无来自注释/死代码/字符串字面量的伪符号或假边,也无跨语言误抽(非目标语言文件不含目标语言专属符号)
→ 先有这张表,再写抽取正则(漏剥离任一层、或漏语言隔离,断言都会立刻暴露 phantom/跨语言误抽,而非上线后静默成假边)

原则0 优先于其余四条:简洁、精准、目标驱动都不能以牺牲正确性为代价。

不变量推导三步法(原则0 的执行方法)

原则0 要求"先写不变量再写码",但没讲不变量怎么来。任何图遍历 / 递归 / 状态机 / 排序 / 依赖解析算法,先按下面三步把不变量显式推出来,再动码——边界用例表(见 test-generation.md)是"授人以鱼",这三步是"授人以渔",P8 面对任意新算法能自己推出不变量,靠的是这个。

第一步:找循环保持量(Loop Invariant)

定义:每次循环 / 递归迭代前后都恒真的命题。

连问三句:

  1. 算法"已经正确处理完的部分"是什么?把它写成一句命题;
  2. 若算法在任意时刻被强制中断,什么仍然成立?
  3. 这个命题在 base case(初始态)为真、且在 induction step(每次迭代后)仍为真吗?

第二步:找终止度量(Termination Variant)

定义:每次迭代严格递减、且有下界的量。

问一句:"每次递归,什么量在严格变小?它的下界是什么?"

  • 环检测示例:未访问节点数严格递减,下界 0 → 必终止。
  • 若找不到严格递减且有下界的量,递归可能不终止,必须在动码前先修正设计。

第三步:找状态守恒(State Conservation)

定义:算法维护的多个状态量之间必须满足的守恒关系。

问一句:"哪些量必须守恒?违反守恒就等于出 bug?"

  • 环检测示例(三条守恒):① 环计数不重不漏;② 遍历方向与边有向性一致;③ 平行边不翻倍。

推导自检清单(交出草稿不变量后,先自证再写码):

  1. 可判定性:能写成可断言的布尔命题,而不是"大概对";
  2. 初始成立:base case 下命题为真;
  3. 归纳保持:每次迭代后仍为真;
  4. 充分性:不变量 ∧ 终止条件 ⇒ 结果正确。

落地硬规则:遇到关键算法,先交出「不变量命题 + 终止度量 + 守恒关系」三件套;交不出、或过不了自检清单,禁止开始写码,也禁止声称"修复完成"。

原则1:编码前思考(Think Before Coding)

不要假设。不要隐藏困惑。暴露权衡。

编码前必须:

  • 明确陈述假设——如果不确定,提问而不是猜测
  • 存在多种理解时,列出选项——不要默默选择
  • 存在更简单方案时,说出来——该推回时推回
  • 遇到不清楚的地方,停下来——说出困惑所在,提问

反模式:

  • 用户说"导出用户数据",直接写导出所有用户的代码(假设了范围、格式、字段)
  • 用户说"让搜索更快",直接加缓存和索引(假设了"更快"的含义)

正确做法:

导出用户数据前,我需要澄清:
1. 范围:导出全部用户还是筛选后的子集?
2. 格式:JSON/CSV/直接API返回?
3. 字段:哪些字段?(有些可能涉及隐私)

原则2:简洁优先(Simplicity First)

最少代码解决问题。不要 speculative。

  • 不要超出需求的功能
  • 不要为单次使用代码做抽象
  • 不要未被要求的"灵活性"或"可配置性"
  • 不要为不可能场景做错误处理
  • 写了200行但50行能搞定,重写

自检问题:"资深工程师会说这过度复杂吗?"如果是,简化。

反模式:

  • 用户说"计算折扣",生成Strategy模式+抽象类+配置对象(30行设置做简单计算)
  • 用户说"保存用户偏好",加缓存、验证、合并、通知(没人要求的功能)

正确做法:

# 简单直接
def calculate_discount(amount: float, percent: float) -> float:
    return amount * (percent / 100)

原则3:精准修改(Surgical Changes)

只碰必须碰的。只清理自己制造的。

编辑现有代码时:

  • 不要"改进"相邻代码、注释或格式
  • 不要重构没坏的东西
  • 匹配现有风格,即使你自己会不同做法
  • 发现无关死代码,提一下——但不要删

你的修改制造孤儿时:

  • 删除你的修改导致的未使用import/变量/函数
  • 不要删除预先存在的死代码,除非被要求

测试标准:每一行修改都能追溯到用户的请求。

反模式:

  • 修复空邮箱Bug时,顺便"改进"邮箱验证、加用户名验证、改注释、加docstring
  • 添加上传日志时,改引号风格、加类型提示、重排空白

正确做法:

-     if not user_data.get('email'):
+     email = user_data.get('email', '')
+     if not email or not email.strip():
          raise ValueError("Email required")

原则4:目标驱动执行(Goal-Driven Execution)

定义成功标准。循环直到验证。

把指令式任务转化为可验证目标:

  • "加验证" → "写无效输入的测试,然后让它们通过"
  • "修Bug" → "写复现Bug的测试,然后让它通过"
  • "重构X" → "确保重构前后测试都通过"

多步骤任务时,陈述简要计划:

1. [步骤] → 验证:[检查]
2. [步骤] → 验证:[检查]
3. [步骤] → 验证:[检查]

强成功标准让你独立循环。弱标准("让它工作")需要持续澄清。

失败回退机制

| 失败场景 | 回退目标 | 最大重试 | | - | - | - | | 目标模糊("让它工作"类) | 拆分为可验证子项,附验证方法 | 2 | | 歧义无法澄清 | 列2-3种最可能理解,标注"假设" | 2 | | 需求边界模糊 | 保守保留核心功能,标注"待确认" | 1 | | 测试环境/依赖不可用 | 最小化方式验证核心逻辑(直接调用+断言) | 2 | | 验证失败(外部依赖问题) | 标注"外部依赖异常",记录复现条件 | 1 | | 连续3轮未通过验证 | 暂停循环,产出当前最佳版本+差异报告 | 0 |

不可恢复时输出阻塞清单和升级路径,由用户决策。

输出格式

## Karpathy编码规范评估

**评估代码**:[文件名/片段]

### 原则0:先证后写
- [ ] 是否涉及正确性敏感算法(递归/图遍历/状态机/排序)?
- [ ] 关键算法是否先写不变量、前置/后置/终止条件?
- [ ] 是否用结构化断言验证不变量,而非"流程跑通"?

**评估**:[通过/需改进/不适用]
**问题**:[具体问题]
**建议**:[改进建议]

### 原则1:编码前思考
- [ ] 假设是否明确陈述?
- [ ] 是否存在未澄清的歧义?
- [ ] 是否遗漏了更简单的方案?

**评估**:[通过/需改进]
**问题**:[具体问题]
**建议**:[改进建议]

### 原则2:简洁优先
- [ ] 是否包含超出需求的功能?
- [ ] 是否存在单次使用的抽象?
- [ ] 是否可以更短更简单?

**评估**:[通过/需改进]
**问题**:[具体问题]
**建议**:[简化方案]

### 原则3:精准修改
- [ ] 是否只修改了必要的代码?
- [ ] 是否保持了原有风格?
- [ ] 是否清理了自己的孤儿代码?

**评估**:[通过/需改进]
**问题**:[具体问题]
**建议**:[改进建议]

### 原则4:目标驱动执行
- [ ] 是否有明确的验证标准?
- [ ] 是否可以独立验证每一步?

**评估**:[通过/需改进]
**问题**:[具体问题]
**建议**:[改进建议]

## 总体评估

**符合度**:[X]/100
**主要问题**:[Top 1-2问题]
**优先修复**:[具体建议]

反 AI 糟粕代码门禁

生成或审查代码时,以下模式必须作为质量门禁逐项排查:

| 门禁项 | 典型表现 | 处理规则 | | - | - | - | | 过度抽象 | 为一次性逻辑创建策略模式、工厂、管理器、配置层 | 删除抽象,回到直接、可读、可测的实现 | | 废注释 | 注释重复代码含义、为显而易见的赋值写解释 | 删除废注释,仅保留解释业务约束、边界条件、危险操作的注释 | | 模板化错误处理 | 每个函数机械包 try/catch,catch 内吞异常或只返回空值 | 让异常在合适层级处理;catch 必须记录、转换或给用户可见反馈 | | 防御式臆想 | 为未出现的输入、未要求的插件点、未知未来需求堆分支 | 删除未被需求或测试证明的分支 | | 全文件重写 | 为小修复重排格式、重命名无关变量、搬动无关函数 | 回退到局部修改,只触碰完成目标所需代码 | | 不可验证产物 | 只给代码,不给 lint/test/build/curl/手动验收路径 | 补齐最小验证命令或说明无法验证的具体阻塞 |

交付前必须回答:

  1. 新增代码是否直接服务于用户目标?
  2. 是否存在可删除而不影响功能的抽象、注释或配置?
  3. 是否触碰了与任务无关的文件、函数或格式?
  4. 是否有明确验证证据支撑"已完成"?

静态分析硬门禁

交付关键算法或修复前,以下静态检查作为硬门禁,必须通过或显式说明豁免理由:

| 门禁项 | 工具/动作 | 处理规则 | | - | - | - | | Lint | 语言对应 linter(PHP php -l、ESLint、ruff、golangci-lint 等) | 语法/静态错误 0 容忍,必须本地跑通后再交付 | | 类型检查 | mypy / tsc --noEmit / phpstan 等 | 类型错误必须修复或标注 # type: ignore 并说明原因 | | 死代码检测 | vulture / pyflakes / IDE 未使用检测 | 删除未使用 import/变量/函数,或说明保留理由 | | 测试门禁 | 关键算法必须跑通算法边界用例(见 test-generation.md) | 结构边界用例不过,不得声称"修复完成" |

未跑静态分析门禁即交付,视为未验证产物,按「反 AI 糟粕代码门禁」的"不可验证产物"处置。

依赖管理锁定

新增、升级或锁定依赖时,必须遵守以下规则,避免构建漂移和环境差异。

强制规则

| 规则 | 说明 | 反例 | | - | - | - | | 版本锁定 | 所有依赖必须使用锁文件精确锁定版本 | composer.json 写 "laravel/framework": "^10.0" 但未提交 composer.lock | | 禁止浮动版本 | 业务代码禁止使用 latest / * / dev-main / @stable 等浮动约束 | "vue": "latest" | | 锁文件入库 | composer.lock / package-lock.json / pnpm-lock.yaml / yarn.lock 必须提交到代码库 | .gitignore 排除锁文件 | | 子依赖锁定 | 不依赖平台自动解析子依赖版本,以锁文件全量快照为准 | 仅锁顶层依赖,子依赖由 registry 解析 | | 新增依赖必审 | 新增依赖必须说明用途、替代方案和体积影响,禁止为单次任务引入重框架 | 为计算折扣引入整个 symfony/expression-language | | 升级必分批 | 升级依赖必须分 minor → major 升级,每级跑测试;禁止跨大版本一步升级 | 直接从 PHP 7.4 跳到 8.3 不验证 | | 漏洞依赖处置 | 发现 CVE 必须在 7 天内升级或替换,未升级前标注为已知风险 | 已知漏洞依赖长期留存 |

检查清单

交付前逐项确认:

  • 锁文件存在且与 composer.json / package.json 一致
  • 业务代码无 latest / * / dev-* 浮动约束
  • 新增依赖已说明用途和替代方案
  • 升级路径经过分阶段测试
  • 已知漏洞依赖已标注或已处置

失败回退

| 失败场景 | 回退动作 | | - | - | | 锁文件与配置不一致 | 用 composer install / npm ci 重新生成锁文件后提交 | | 升级后测试失败 | 回退到上一稳定锁文件,分文件分模块排查 | | 新增依赖体积过大 | 评估轻量替代方案,必要时拒绝引入 |

质量标准

  • 评估必须基于代码实际内容,不能泛泛而谈
  • 每个原则的评估必须有具体的代码位置引用
  • 改进建议必须提供具体的修改前后对比
  • 不得为符合规范的代码强行找问题
  • 评估应平衡:不是追求绝对完美,而是避免常见陷阱
  • 必须逐项检查「反 AI 糟粕代码门禁」,发现命中项时优先压缩或删除
  • 不能用新增抽象、泛化配置或模板化错误处理替代真正的业务理解
  • 关键算法交付前必须通过静态分析门禁(lint/类型/死代码/边界测试),未通过不得声称完成
  • 正确性敏感算法必须先写不变量(原则0)再实现,不得边写边猜

关联 reference

  • code-review(代码审查)— 用 code-review 进行全面的技术审查(Bug/安全/性能)
  • code-generation(代码生成)— 用 code-generation 生成功能代码,再用本Skill评估是否符合规范
  • refactoring(重构建议)— 用 refactoring 获取结构优化方案,再用本Skill检查是否过度设计
  • project-memory-management(项目记忆管理)— 记录可复用的编码规范、陷阱和决策清单