搜索/让 AI 认真编写系统级代码,细到类型和错误处理

让 AI 认真编写系统级代码,细到类型和错误处理

在让 AI 协助开发要求严格的系统级代码库时使用。AI 审查代码的时间至少是编写时间的三倍,处理所有错误情况,把规则写进类型,让新旧数据格式互相兼容,并让每个提交只做一件事且都能构建通过。

要使用这个AI规则
## 审查 AI 写的代码,时间至少是编写时间的三倍 把助手当作质量的倍增器,而不是速度的倍增器。把它节省下来的时间投入到超出无辅助工作所能达到的严谨程度上。 - 追求卓越:覆盖更多边界情况,为可理解性而重构,清理遗留的 TODO,做那些繁琐的事情,并以零缺陷为目标。 - 审查的时间要远长于生成的时间。审查生成代码所花的时间,至少是生成它所用时间的三倍,要逐行、逐个设计决策地权衡,并主动寻找破坏它的方法。 - 作者对结果负责。除非你明确只是在就某个方向征求反馈,否则不要把初稿当作已完成的工作交出去。 - 显露出未经用心打磨迹象的工作——例如遗漏了认真的人工审查本会发现的情况——可能会被直接拒收,而不是被退回修改。 - 在任何涉及代码变更的对话开始时声明这一标准,并在开启拉取请求之前再声明一次,使作者与审查者共享同一条标准线。 ## 把正确性交给类型系统,并处理所有边界情况 宁可选择正确的实现,也不选择便利的实现;并尽可能把正确性推进到类型系统中,让错误在编译时失败,而不是在生产环境中失败。 - 完整建模错误空间。不走捷径,不做简化的错误处理。 - 处理所有边界情况,包括竞态条件、信号时序,以及不同操作系统之间的行为差异。 - 用类型编码正确性约束:为领域取值使用包装类型,为复杂构造使用构建者模式,在状态转换重要时于泛型中使用类型状态,以及使用借用而不是克隆。 - 只要存在选择余地,就优先采用编译期保证,而不是运行期检查。 - 可见性保持尽可能窄。只有当模块之外确实需要某一项时,才扩大它的可见性。 - 在稳定的、供外部使用的包中,把公开类型标记为可扩展的,以保证向前兼容。不作稳定性承诺的内部包不需要这样做,不过错误类型仍可能需要。 - 使用消息传递或 actor 模型,而不是共享可变状态,这样数据竞争就无从发生。 - 把细节做对,比尽快完成更重要。 ## 给用户有用的错误提示,并在各操作系统上保持一致 面向用户的行为是设计的输入,而不是收尾时的润色。 - 产出结构化、有帮助的诊断信息,而不是干巴巴的错误字符串;凡是消息会呈现给人看的地方,都使用功能丰富的诊断库。 - 让进度报告及时响应且信息充分。 - 即使底层能力存在差异,也要让行为在各平台之间保持一致;并且每个平台都用它自己的原生机制来实现,而不是在一个平台上模拟另一个平台。 - 用清晰的现在时撰写面向用户的消息:"the tool now supports",而不是 "the tool now supported"。 - 把复数形式的处理集中在一个共享的辅助模块中,其函数根据数量返回单数或复数形式。新增辅助函数要加到那里,而不是在调用处内联复数逻辑。 ## 逐步完善设计,并把选择理由写进设计文档 让设计保持具体且可组合,而不是抽象且过度通用。 - 渐进地演进设计,而不是一开始就试图做出完美的架构。 - 把设计决策及其取舍记录在随项目一起保存的设计文档中,让后来的阅读者能看到为什么选择了某种形态。 - 不确定时,去探索和迭代,而不是把一个猜测固化成抽象。 ## 区分用户造成的错误和内部缺陷,并附上详细上下文 错误是用户所见界面的一部分,值得与成功路径同等的设计关注。 - 使用基于 derive 的错误类型,而不是字符串化的错误;一旦错误集合变大,就按种类枚举把相关错误归组。 - 在错误上携带丰富的结构化上下文,而不是把细节压平成单独一条消息。 - 采用双层模型。由用户造成的错误和外部错误构成一个独立的类型,映射到语义化的退出码;内部编程错误则是另一类,可以 panic。 - 错误的显示文本写成小写的句子片段,使其嵌入到诸如 "failed to ..." 这类包装短语中时读起来通顺。 ## 对配置中的未知项给出警告,并让配置错误清楚易懂 反序列化方面的决定,决定了用户今后见到的每一条配置错误的质量。 - 跟踪并报告配置中被忽略或未知的路径,让拼写错误以警告的形式浮现,而不是悄无声息地什么都不做。 - 绝不要在反序列化过程中把嵌套结构展平。它所需要的内部缓冲会削弱未知字段警告;应改为把这些字段显式复制进结构体。 - 绝不要依赖无标签变体的反序列化,它产生的错误消息很差。应改为编写自定义 visitor,并给出精确的期望描述字符串。 ## 修改保存或传输格式前,确认新旧版本能互相读写 任何对写入磁盘或通过网络传输的结构所做的变更,在落地之前都必须在完整的兼容性矩阵上走查一遍。 - 旧的读取方遇到新数据:它还能反序列化吗?会不会丢失信息? - 新的读取方遇到旧数据:所声明的默认值产生的是正确的值,而不仅仅是有效的值吗? - 旧的写入方遇到新数据:它能往返而不丢弃字段吗?这是最常被遗漏的情况。 - 主动提升格式版本号。如果一个新字段在语义上将会很重要,就在添加该字段时提升版本,而不是等到首次写入非默认值时才提升;否则较旧的构建版本在把数据写回时会静默地损坏数据。 - 把默认值视为必要但不充分的手段。它们让旧的读取方能够解析新数据,但旧的写入方仍会丢弃它不认识的字段。 ## 只在需要处使用异步,不无限制地派发任务 异步是用于输入输出和并发的工具,而不是默认的风格。 - 使用多线程异步运行时,但把异步限制在确实需要它的代码中,其余部分一律保持同步。 - 使用结构化并发,这样并发的工作就不必被赋予人为拉长的生命周期。 - 调度排队的工作时要考虑背压,而不是无限制地派生任务。 - 在作业控制可以挂起和恢复进程的场景中,提供可暂停的计时原语,使挂起的时间不计入超时。 ## 聚合模块不放逻辑,导入写在文件开头 模块边界由结构和可见性来强制,而不是仅靠约定。 - 聚合模块文件只用来重新导出公开项。不琐碎的逻辑应放在实现子模块或更具体的模块中。 - 把平台相关代码放在按平台划分的独立文件中,用条件编译隔开,并在它们之间提供一个共享接口。 - 把测试辅助代码放在专门的模块或文件中。 - 在模块的最顶部导入类型和函数。绝不要在函数体内部导入;唯一的例外是需要受条件门控导入的条件编译函数。 - 优先导入类型本身,只有在那样才是惯用写法的地方,才改为导入模块。避免在签名中使用完全限定路径:导入父模块,并只用父模块名来限定。 - 为模式匹配而导入枚举的变体是可以的。 ## 用借用或共享代替复制数据 选择能避免额外工作的表示方式,而不是事后再做优化。 - 通过引用计数或借用来共享不可变数据,而不是复制它。 - 在代码需要处理大量短字符串的地方,使用针对小字符串优化的类型。 - 留意克隆与引用。当数据本身具有借用已经能够表达的天然树形结构或所有权结构时,避免克隆。 - 尽可能通过迭代器以流的方式处理数据,而不是把它全部缓冲在内存中。 ## 每次提交前运行格式化,并在一处管理依赖版本 机械性的一致性,强制执行起来成本很低,事后补做则代价很高。 - 每个源文件都以项目的版权声明行和许可证标识符开头。 - 固定语言版次,并在每次提交前运行项目的格式化命令。格式化在持续集成中会被强制执行,因此未经格式化的提交会在那里失败。 - 抑制某条 lint 时使用"预期"形式而不是"允许"形式,这样当该 lint 不再触发时,抑制本身就会发出警告,失效的抑制也不会累积。 - 在某条 lint 禁止把格式化文本追加到字符串上的地方,改用项目的写入宏,而不是推入一个格式化后的字符串。 - 在工作区根目录集中管理所有依赖的版本,并把内部包固定到精确版本。 - 对不显而易见的依赖选择和特性选择加以注释,尤其是出于安全或体积原因禁用某个特性的地方。 ## 注释写明为什么这样做,而不只是做了什么 - 注释解释为什么,而不只是解释是什么。 - 不要在函数体内部做流水账式的叙述。只在某处不显而易见或特殊,或者意图需要更深入解释的地方才添加注释。 - 模块级文档说明该模块的用途和职责。 - 代码注释一律以句号结尾。 - 标题和题名绝不使用词首大写;一律使用句首大写。 - 始终使用牛津逗号。 - 不要省略冠词。写 "the file has a newer version",而不是 "file has newer version"。 ## 用项目指定的测试运行器跑测试,并复用现有夹具 按项目运行测试的方式来测试,并复用已经存在的测试模型。 - 单元测试和集成测试都使用项目指定的测试运行器。当测试套件依赖于指定运行器的执行模型时,不要退回到语言默认的运行器。 - 只有在验证工具自身行为的改动(例如新增一个标志或输出格式)时,才使用本地构建出来的工具副本。测试套件本身要用标准的运行器来跑,因为集成测试会从构建产物中派生出它们自己的内部进程。 - 当主运行器不支持文档测试时,用支持文档测试的命令来运行它们。 - 单元测试与被测代码放在一起,集成测试放在专门的包中,共享的夹具放在夹具包中,测试工具放在支持包中。 - 优先使用夹具包对预期结果的建模,而不是手写的抽查。 - 先去用已经存在的工具设施:参数化用例、基于属性的测试、快照断言、自定义测试框架,以及改进后的断言输出。在引入新机制之前,先查清现有可用的东西。 ## 提交标题加上范围前缀,并让每个提交都能构建 历史记录是一件可供审查的产物,因此要把每个提交当作一个审查单元。 - 每条提交主题都以方括号标出它所触及的范围作为前缀,后面跟一段简短描述。跨领域的工作(例如工具链最低版本、发布和持续集成的变更)使用一个元范围。 - 版本号提升和发布准备的提交遵循它们各自固定的主题,并且通常由发布工具生成。 - 用一般过去时和一般现在时撰写正文:过去发生的是什么、现在发生的是什么,以及新增了哪些测试。 - 提交信息采用 Markdown。主题行中不要使用反引号,但正文中要使用。 - 让提交保持原子性。每个提交都是一个逻辑上的变更单元。 - 保持历史可二分定位。每个提交都必须能够构建并通过所有检查。 - 分离关注点。格式化修正和重构应放在它们自己的提交中,与行为变更分开。 ## 把并发运行器分成调度器和执行器,通过消息联系 当一个组件掌管外部世界、另一个组件掌管执行时,并发的运行器最容易被理解。 - 调度器与外部世界交互,把来自各个来源的事件线性化,用 select 结构在这些来源上多路复用,并持有关于当前正在运行什么的权威状态。同步点使报告过程不受竞态影响。 - 执行器把工作单元作为异步状态机来调度和运行,负责重试、超时、进程派生和输出捕获。每个单元都是其自身状态的真实来源,并且拥有一条通向调度器的专用通道,而不是广播。 - 不直接共享任何状态;一切都以消息的形式在组件之间传递。 - 完整建模失败空间,并使用可暂停的计时器,使挂起和恢复信号不会扭曲计时。 - 使用各平台原生的进程隔离与信号机制,用条件编译和位于共享接口之后的按平台划分的模块加以保护,并在注释中记录平台差异与取舍。