同一个模型,同一份 review 清单。这次它精准指出"这个函数嫉妒了另一个对象的数据,该搬过去",下次面对更简单的代码,它却只回一句"整体看起来不错"。
它不是忘了规则。Feature Envy、Duplicated Code、命名该用领域词汇,这些知识它一直都在。变差的不是知识,是它当时看到的东西(上下文外无物)。参数里的规则是固化的、静态的,真正决定这一轮怎么做的,是它这一轮读进去的东西。
规则本身不用教
代码坏味道是二十年前就写进书里的公共知识。Fowler 的 12 个 smell、耦合与内聚、“接口即测试面”,任何一个训练过代码的模型都反复见过这些文本。所以一份 code-review skill 真正该做的不是把知识再讲一遍。
规则只需要点到:过哪几个视角、每个视角看什么形状的问题。六个视角各自对应一个可回答的问题——不看注释能否说出这段代码在做哪个用户故事(语言);改一个概念要动几个文件(耦合);新人从上往下读要跳转几次(阅读);删掉所有 mock 测试还跑不跑得起来(边界);能否向用户演示现在能做什么(完整);删掉这段代码有没有测试变红(无用)。每个问题都有个数字或是非答案,模型没法用"还行"糊过去。
视角只是方向,方向上具体找的是那些有名字的坏味道,Feature Envy、Data Clumps、Shotgun Surgery。给坏味道起名字,你才看得见它。没有"Feature Envy"这个词,你只会觉得这段读着别扭,说不清别扭在哪。
为什么会失手
review 是个判断任务,判断依赖它当时看到的输入,而输入经常是脏的。三种脏法各有各的失效方式。
最直接的一种是它正在看的代码本身就烂。你让它 review 一段祖传泥球,周围全是data、temp、五层嵌套。模型是概率机器,被这些劣质样本一带,判定基线就往下滑,满屏的坏味道看多了,它开始觉得这好像就是这个项目的正常水平,于是该报的不报了。坏代码不只是被审对象,它还在悄悄重设审查者的及格线。
需求和命名里的歧义导致的是另一种结果。前期需求澄清时本该锁定的领域词汇如果没锁死,模型接手的就是一堆模糊词,一个东西在这里叫 order、那里叫 request、注释里又叫 form,它没法判断命名是否一致,因为它自己都分不清这三个到底是不是一个东西。歧义不产生错误结论,它产生的是放弃判断——模型绕开这条视角,含糊带过。
最隐蔽的是长会话里自相矛盾的上下文。一个多次对话的 session,前面说"我们用 Result 类型不抛异常",后面又贴了一段到处 throw 的代码,中间还夹着三次反悔的决定。模型读到的是一份互相打架的上下文,它要 review 的那段代码到底该符合哪条约定?它不知道,会话越长,这种矛盾越多,判断越像抛硬币。
三种脏法都不是"模型不懂 review",而是它看到的东西不足以支撑一次干净的判断。
解法:一把不随上下文漂移的尺子
问题出在输入被污染,那么解法是递一把稳定的尺子,让判断有个不跟着会话漂的锚。
这把尺子是那份独立的review-lenses.md,6 个视角、12 个 smell 写死在文件里,不在会话里。它是 review、refactor、TDD 三个环节共用的同一份判定标准,改这份文件等于同时改三个 skill 的行为。模型每次 review 都回到这份文件,而不是回到那个已经吵了两小时、自相矛盾的对话。
尺子上还刻了三条硬规矩,每条对着一个脏源。只读、不改代码,审查者不下场,判断和修复分开,脏代码诱不动它顺手就改。发现必须可定位到文件和行号,这条逼它给证据,堵死"整体还行"这种被烂代码带出来的含糊结论。还有对照 spec 查实现,而不只看代码好不好——会话上下文自相矛盾时,spec 是那个不吵架的第三方,代码该符合的是它,不是最近那条反悔的消息。
写得好和做对了是两件事
质量轴问代码写得好不好,spec 轴问代码做没做对的事,查三类偏差:spec 要求了但没做的,spec 没要求却做了的,看着实现了其实做错的。
两轴必须分开报,因为代码可能六个视角全绿却实现错了需求,也可能需求全中但写得一团糟。合成一份打分,这两种情况会互相抹平——质量分拉高总分,读的人以为没大事。
没有 spec 的代码怎么办
这里有个明显的反对意见:review 的对象常常是遗留代码、别人的 PR、外部 diff,它们没有 spec,也没有锁定过的领域词汇表。这把尺子靠两样东西校准——spec 和领域词汇表,两样都缺,那还量什么?
答案是降级,不是硬套。没有 spec,spec 轴整个跳过,并在汇总里注明"无 spec 可对照"——空着比编一个假 spec 去对照要好。没有领域词汇表,视角 1 从"命名是否使用锁定的领域词汇"退化为"命名是否有意义、是否一致"。退化后的判断更弱,但标准仍然是外部的、稳定的。
这也划出了实际能力边界:它在上游做过需求与领域词汇锁定的代码上最锋利,想让它更锋利,得往上游补 spec。
开源链接
code-review
Coding Style
用在最小实现步骤(红→绿的绿),不是重构。下笔时就照它写,让代码一开始就整洁。
- 重构(
refactoring-techniques.md)是补救:处理绿之后才浮现的结构问题。 - 本文件是预防:写第一行时就避免产生泥球。
- review 时(
review-lenses.md)从"看"的方向复查这些原则是否被遵守。
不要因为现有代码劣质就依样画瓢,也不要被歧义错词带偏。参考的是这份风格,不是身边最脏的那段代码。
Unix 哲学(Eric Raymond 归纳的 17 条)
写实现时的正向约束。挑与当前 slice 相关的用。
- 模块:用简洁的接口拼合简单的部件。
- 清晰:清晰胜于机巧。
- 组合:设计时就考虑能被拼接组合。
- 分离:策略同机制分离,接口同引擎分离。
- 简洁:设计要简洁,复杂度能低则低。
- 吝啬:除非确无它法,不写庞大的程序。
- 透明:设计要可见,以便审查和调试。
- 健壮:健壮源于透明与简洁。
- 表示:把知识叠入数据,以求逻辑质朴而健壮。
- 通俗:接口设计避免标新立异(最小惊讶)。
- 缄默:程序没什么好说的,就沉默。
- 补救:出现异常时马上退出,并给出足够错误信息。
- 经济:宁花机器一分,不花程序员一秒。
- 生成:避免手工 hack,尽量写程序去生成程序。
- 优化:雕琢前先有原型,跑之前先学会走。
- 多样:不信"不二法门"的断言。
- 扩展:设计着眼未来,但别为想象中的需求预留(配合 YAGNI)。
落到当前 slice 的硬约束
写每条 slice 时至少守住这几条:
1. 命名先于实现(清晰 + 表示)
- 变量、函数、测试名一律用 grill 锁定的领域词汇,不用
data/result/temp/handler/utils。 - 命名描述"做什么"而非"怎么做"。起不出诚实的名字,说明这段设计本身模糊——停下来想,别硬写。
- “傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。”
2. 只写这条 slice 需要的(吝啬 + 简洁 + YAGNI)
- "最少代码"度量的是认知,不是行数。
- 不提前抽象、不加 spec 没要求的参数/钩子/未来扩展点。真实需求出现前,内联比抽象好。
- “YAGNI——你不会需要它。”
3. 让人从上往下读得懂(透明 + 阅读顺序)
- 前提条件(卫述句)在顶部处理完,主逻辑不深嵌。
- 声明和初始化放在一起;复杂子表达式提取成有意图的解释型变量。
4. 出错就地退出,给足信息(补救 + 健壮)
- 异常在发生处就地退出,不静默吞掉、不返回一个"看起来正常"的空值让错误往下游漂。
- 错误信息要能定位:说清哪个操作、什么输入、期望什么。
throw new Error("失败")等于没说;要throw new Error(\拉取 ${url} 失败:HTTP ${status}`)`。 - Unix 补救原则:出现异常时马上退出并给出足够错误信息——排查的人靠这条信息,而不是靠猜。
5. 默认不可变(表示 + 健壮)
- 更新数据用拷贝,不原地改:
{...user, name}、[...items, x],而非user.name = x/items.push(x)。 - 不可变默认让"谁改了这个值"这个问题消失——状态只在一处产生,读代码不用追踪它被谁改过。
- 需要原地修改时(性能、大数组),显式注释说明为什么。
深模块:小接口,大实现
写实现时倾向深模块——把复杂逻辑藏在一个小接口后面,而不是摊成一堆浅模块让调用方自己拼。接口小,LLM 和人每次要读懂的上下文都少。
设计语言、判据(删除测试)和可测性原则见module-design.md——写、review、重构三态共用同一份。