即便 AI 能快速写代码,Joel Spolsky 的软件真理仍然适用。了解如何通过测试、招聘和简化设计来确保正确性。

AI 可以在几分钟内产出看起来可用的代码。这改变了项目的节奏,但并没有改变什么能让软件成功。Joel Spolsky 的“软件真理”从来不是关于打字速度,而是关于判断力、反馈循环,以及避免自我施加的复杂性。
变化的是写出代码的成本。你可以要求三种方案、五个变体,或一次完整重写,然后立刻得到结果。未变化的是选择正确方案、校验它并在几个月中与之共处的成本。写作上节省的时间往往转到决定你真正想要什么、验证边缘情况,以及确保今天的速胜不会成为明天的维护税上。
正确性、安全性和可维护性仍然需要时间,因为它们依赖于证据,而不是自信。一个登录流程并不是在编译通过时就完成了;它是在能可靠拒绝坏输入、处理怪异状态且不泄露数据时才算完成。AI 可能在遗漏一个关键细节(例如端点上的权限检查或支付更新中的竞态条件)时听起来很确定。
AI 最擅长的是把它当作快速草稿机:它在样板代码、重复模式、快速重构和并排比较选项上很亮眼。用得好,它能压缩“空白页”阶段。
AI 最有害的是当你给它模糊目标并接受表面结果。相同的失败模式会反复出现:隐藏的假设(未陈述的业务规则)、未经测试的路径(错误处理、重试、空状态)、自信的错误(看起来合理但微妙地错误的代码)以及难以后续解释的“巧妙”方案。
当代码变得廉价时,新的稀缺资源是信任。这些真理之所以重要,是因为它们保护这种信任:对用户、对队友,以及对未来的你自己。
当 AI 能在分钟级生成一个功能时,把测试当成需要消除的慢环节很诱人。Spolsky 的观点仍然成立:慢的部分藏着真相。代码容易产出,但正确行为难以保证。
一个有用的转变是把测试当作可运行的需求。如果你不能以可检查的方式描述期望行为,你就还没想清楚。在 AI 辅助的工作中,这点更重要而不是更不重要,因为模型可能自信地产出只有细微错误的结果。
从最会造成伤害的地方开始测试。对于大多数产品,那是核心流程(注册、结账、保存、导出)、权限(谁能查看、编辑、删除)和数据完整性(无重复、正确的汇总、安全迁移)。然后覆盖那些会导致深夜事故的边界:空输入、超长文本、时区、重试以及像支付、邮件和文件上传这类易波动的外部边界。
AI 很擅长提出测试用例,但它不知道你实际向用户承诺了什么。把它当作头脑风暴伙伴:询问遗漏的边缘情况、滥用场景和权限组合。然后做人的工作:把覆盖范围与真实规则匹配,移除那些只是“测试实现”而非行为的测试。
让失败具备可操作性。失败的测试应该告诉你哪里坏了,而不是把你带上侦探之旅。保持测试小巧,用句子式命名,并使错误信息具体。
假设你用 AI 帮忙构建了一个简单的“团队笔记”应用。CRUD 界面很快就出现了。正确性的风险不在 UI,而在访问控制和数据规则:用户不能看到别的团队的笔记,编辑不能覆盖比它更新更晚的更改,删除笔记不应留下孤立的附件。能锁定这些规则的测试会成为瓶颈,但它们也是你的安全网。
当测试成为瓶颈时,它会强迫你厘清逻辑。这种清晰让快速写出的代码不会迅速变成快速出错的代码。
经久不衰的真理之一是:简单的代码胜过聪明的代码。AI 会诱使你接受花哨的抽象,因为它们看起来抛光且生成迅速。但成本会在以后显现:更多藏匿 bug 的地方、更多要扫描的文件、更多“这到底在干什么?”的时刻。
当代码变得廉价时,复杂性就是你要付的代价。小而无聊的设计更容易测试、更容易修改,也更容易解释。当初稿来自能自信表达但可能微妙出错的模型时,这一点更重要。
一个实用规则是把函数、组件和模块保持得足够小,以便队友能在几分钟内审阅,而不是几小时。如果一个 React 组件需要多个自定义 hook、本地状态机和一个通用的“智能渲染”层,停下来问问你是不是在解决真实问题,还是因为 AI 提供了架构就默认接受了它。
几个“简单性检验”能帮助你抗衡复杂:
提示很重要。如果你问“最佳架构是什么”,通常会得到一个过度构建的方案。改问带约束的问题,推动更少的活动部件。例如:使用最少文件的最简单方法;除非在 3+ 个地方消除重复,否则避免新抽象;优先显式代码而非通用助手。
一个具体例子:你让 AI 给管理员页面添加基于角色的访问控制。聪明版本引入权限框架、装饰器和配置 DSL。简单版本在一个地方检查用户角色、在一个地方对路由做网关并记录拒绝访问。简单版本更容易审查、更容易测试且更不易被误解。
如果你在像 Koder.ai 这样的聊天式工具中构建,简单还会使快照和回滚更有价值。小而明显的改动更容易比较、保留或还原。
当代码易于产出时,稀缺技能是决定什么应该存在并确保它是正确的。老话“雇优秀的程序员”仍然适用,但岗位职责发生了转变。你不是雇人更快打字,而是雇人判断、润色并为产品辩护。
在 AI 辅助开发中最有价值的人通常具备四个特质:判断力(什么重要)、审美(好是什么样子)、调试能力(找出真实原因)和沟通(清晰说明权衡)。他们能把 AI 写出的“略微可用”的功能变成你可以信任的东西。
与其要求候选人从零给出完美方案,不如给他们一个 AI 生成的 pull request(或粘贴的 diff),其中包含一些现实的问题:命名不清、隐藏的边缘案例、缺少测试和一个小的安全错误。
让他们用普通语言解释代码试图做什么,找出最高风险的部分,提出修复,并补充(或列出)能捕获回归的测试。如果你想要更强的信号,还可以问他们如何改变指令,以便下一次 AI 尝试更好。
这会揭示他们在真实条件下的思考方式:不完美的代码、有限的时间,以及需要选择优先级。
AI 往往听起来很自信。优秀的员工会自如地提出反对。他们能对增加复杂性的功能说不、对削弱安全性的改动说不、对未经证据就发布说不。
一个具体信号是他们如何回答“你会合并这个吗?”强候选人不会只给感觉性的回答。他们会给出决定并列出简短的必需修改清单。
例如:当要求一个“快速”的访问控制更新且 AI 建议在各个处理器里散布检查时,强候选人会拒绝该方案并提出一个明确的授权层,同时为管理员和非管理员路径提供测试。
最后,建立共享标准,让团队以统一方式编辑 AI 输出。保持简单:一个完成定义、一致的评审期望和测试基线。
当 AI 能在几分钟生成大量代码时,跳过思考直接迭代很诱人。这对演示可行,但当你需要正确性、可预测行为和更少惊喜时,这种做法会失效。
一个好的提示通常是伪装成短规格的东西。在请求代码之前,把模糊目标转成几条验收标准和明确的非目标。这能防止 AI(和团队)悄悄扩大范围。
把规格保持小而具体。你不是写小说,而是在划定边界,包括:
在生成前定义“完成”,而不是事后。“完成”应不仅仅是“能编译”或“UI 看起来正确”。包含测试期望、向后兼容性以及发布后监控内容。
示例:你要“添加密码重置”。更清晰的规格可能写到:用户通过邮箱请求重置;链接 15 分钟后过期;无论邮箱是否存在,显示相同信息;按 IP 限流;记录重置尝试但不以明文存储令牌。非目标:不改登录页面设计。现在你的提示有了护栏,评审也更简单。
保留一个轻量的变更日志。每个决定一段话就够。记录为什么选择这种方案而放弃替代方案。当有人两周后问“为什么是这样?”你就有答案。
AI 最大的变化是产出代码变得容易。难的是决定代码应该做什么并证明它确实如此。
先用普通语言写下目标和约束。包括绝对不能发生的事、可以慢的部分以及本次变更不涉及的内容。一个可测试的约束是好的:"任何用户都不应看到别人的数据" 或 "汇总必须与财务导出完全一致"。
在请求代码前,先要一个简单设计和权衡说明。你希望 AI 以可以评判的形式展示其推理:它会存储什么、验证什么、记录什么。如果它提出巧妙方案,反驳并要求仍满足约束的最简单版本。
一个可重复的循环如下:
这里有个小场景:为订单页面添加“退款状态”。AI 可以很快生成 UI,但正确性体现在边缘情况。如果退款是部分的怎么办?支付提供方重试 webhook 怎么办?先写这些用例,再实现一个切片(数据库列加校验)并用测试验证,然后再继续。
如果你使用 Koder.ai,规划模式、快照和回滚等功能自然适配这个循环:先规划、分片生成,并为每个重要改动捕获一个安全还原点。
当代码生成很快时,很容易把“写代码”误认为是工作成果,但真正的成果是行为:应用在出错时也能做正确的事。
AI 常常听起来很确定,即便它在猜测。失败点在于跳过枯燥的部分:运行测试、检查边缘情况和验证真实输入。
一个简单习惯:在接受改动前问“我们怎么知道这是正确的?”如果答案是“看起来对”,那就是在赌博。
AI 喜欢添加额外功能:缓存、重试、更多设置、更多端点、更好看的 UI。部分想法是有用的,但它们也提高了风险。很多 bug 来自没人要求的“锦上添花”。
保持硬边界:先解决原本要解决的问题,然后停止。如果一个建议有价值,把它作为独立任务并为其写测试。
大型 AI 生成的提交可能隐藏十几项无关决策。审查就变成了走过场,因为没人能一口气掌握全部内容。
把聊天输出当作草稿。把它拆成小改动,你可以读、运行并回滚。在合理点做快照和回滚才有意义。
一些简单限制能防止大部分问题:每次变更只包含一个特性、一次迁移只做一件事、每次只修改一个高风险区域(认证、支付、数据删除)、在同一次变更中更新测试,并附上清晰的“如何验证”说明。
AI 可能复现训练数据中的模式,或建议你不了解的依赖。即便许可没问题,更大的风险是安全:硬编码的密钥、薄弱的令牌处理、不安全的文件或查询操作。
如果你无法解释一个代码片段在做什么,就别上生产。要求更简单的版本,或自行重写它。
很多“我电脑上能跑”类的 bug 实际上是数据和规模问题。AI 可能生成模式改变而不考虑已有行、大表或停机时间。
现实例子:模型给 PostgreSQL 表加了一个 NOT NULL 列并用慢循环回填。在生产环境里,这可能会锁表并把应用弄崩。始终考虑在一百万行、慢网络或部署半途失败时会发生什么。
想象一个小型内部请求跟踪器:人们提交请求,管理者批准或拒绝,财务标记为已支付。听起来简单,且在 AI 辅助下你能很快生成界面和端点。但真正让你慢下来的仍然是老问题:规则,而不是打字。
先写下必须正确的最小项。如果你不能用普通话解释它,你就无法测试它。
一个紧凑的首版本定义通常像这样:字段(标题、请求人、部门、金额、原因、状态、时间戳);角色(请求人、审批人、财务、管理员);状态(草稿、已提交、已批准、已拒绝、已支付)。然后写出重要的转换:只有审批人可以把已提交改为已批准或已拒绝;只有财务可以把已批准改为已支付。
按受控顺序使用 AI,这样可以尽早发现错误:
最高价值的测试不是“页面能否加载”。而是权限检查和状态转换。证明比如说:请求人不能批准自己的请求,审批人不能标记为已支付,被拒绝的请求不能被支付,以及(如果这是你的规则)提交后金额不能被修改。
花最长时间的是厘清边缘情况。审批人在拒绝后能否反悔?如果两个审批人同时点了批准怎么办?财务是否需要部分支付?AI 可以根据你选择的任何答案生成代码,但它不能替你做选择。正确性来自做出这些决定,然后强制代码服从它们。
AI 能快速生成大量代码,但最后一公里仍是人工工作:证明它做了你想要的事,并在失败时安全降级。
在开始打勾前,先选出最小的“完成”定义。对一个小特性,这可能是一条正常路径、两条失败路径和一次可读性检查。对支付或认证则提高门槛。
假设 AI 给管理界面添加了“批量邀请用户”。正常路径可用,但真正风险在于边缘情况:重复邮箱、部分失败和限流。一个稳妥的发布决策可能包括一个自动化测试检测重复、一个人工检查部分失败信息的步骤,以及一个回滚计划。
当代码变得廉价时,风险转向决策质量:你要求了什么、你接受了什么、你发布了什么。让这些真理在 AI 辅助工作中发挥价值的最快方式是建立护栏,防止“差不多对”的改动溜过审查。
从下一特性的单页规格开始。保持朴实:适配对象、要做的事、不做的事,以及用日常语言写出的几条验收测试。这些验收测试在 AI 提供诱人捷径时会成为你的锚点。
一套可扩展且不会增加过多流程负担的护栏:
提示现在是你流程的一部分。就居家风格达成一致:允许哪些库、如何处理错误、什么是“完成”的定义、哪些测试必须通过。如果一个提示不能被其他队员重复使用,那它可能太模糊。
如果你偏好以聊天为先的方式来构建 Web、后端和移动应用,Koder.ai (koder.ai) 是一种聊天式编码平台的示例,规划模式、快照和源码导出能支持这些护栏。该工具能加速草稿阶段,但把人保持在正确性掌控中的,是纪律而不是工具。
把 AI 输出当作快速草稿,而不是完成的特性。先写下 3–5 条通过/不通过的验收检查,然后生成一个小切片(一个端点、一个界面、一次迁移),并用测试与失败场景验证它,再继续下一步。
因为测试能揭示代码“实际上”做了什么。AI 可以生成看起来合理但漏掉关键规则(权限、重试、边缘状态)的逻辑。测试把你的期望变成可运行、可重复、可信任的东西。
从最会造成损失的地方开始:
在这些高危行为被锁定后再补充更多覆盖。
要求最简单的实现并设置明确约束,然后删除不“付租”的额外层。一个好规则:只有在消除 3 个或更多地方的重复,或能让正确性更容易证明时,才引入新抽象。
写一个简短的规格:输入、输出、错误、约束和非目标。包含具体示例(请求/响应样例、边缘情况)。然后在生成前把“完成”的定义写清:必须的测试、向后兼容期望,以及如何快速验证。
把它拆分。保持每个变更集在几分钟内可审阅:
这样评审才是真实的,而不是走过场。
别相信自信——相信证据。运行测试、尝试畸形输入并验证权限边界。同时留心常见的 AI 陷阱:缺失认证检查、不安全的查询构建、弱令牌处理、以及悄无声息吞掉错误的做法。
倾向显式的状态转换端点而不是“随意更新”接口。例如使用 submit、approve、reject、pay 这样的端点代替通用的 update 路由。然后编写测试来强制谁能进行哪种转换以及哪些转换被禁止。
给候选人一个 AI 生成的 diff,里面包含真实的问题:命名不清、缺少测试、边缘用例未处理和一个小的安全漏洞。让他们用通俗语言解释意图,找出最高风险点,提出修复,并概述会添加哪些测试。
把工具功能作为纪律化循环的一部分:先规划、以小切片生成、在高风险改动前快照、验证失败并在必要时回滚。在类似 Koder.ai 的聊天式平台上,规划模式、快照和回滚能很好配合这套做法,尤其是触及认证、支付或迁移时。