ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

AI写代码总过度设计?用KISS和YAGNI原则让它输出简洁代码

AI写代码总过度设计?用KISS和YAGNI原则让它输出简洁代码 帮我在终端里让AI写个解析环境变量的函数规则是KEYvalue分行解析忽略空行和#注释。大概三秒后agent交回的答案是一个EnvParser接口、一个DefaultEnvParser实现、一个ParserFactory工厂类工厂内部还加了缓存逻辑签名里带了个“以后没准能用上”的ignore_unknown_keys参数。功能当然能跑测试也全绿。可我只想要一个函数。这个画面2025年还在用AI编程的人应该都不陌生。问题根本不在于agent能不能写代码而在于我们想表达的“编程风格朴实简洁、防止复杂化、强化易读性”在软件工程的专业语境里到底叫什么、用什么词去描述、怎么把它变成一条AI agent能稳定执行的指令。这篇文章不聊空泛的“提示词技巧”就把这件事从术语对齐、生成机理、Prompt模板、工程配置到代码评审一条线讲透。1. 先确定你在提的到底是哪种需求把“好读”翻译成工程术语和AI协作最忌讳的一件事就是直接用“代码写得朴实一点”“别搞太复杂”这种自然语言去下达工程指令。因为“朴实”“复杂”是感受词不同人有完全不同的尺度。更麻烦的是大模型不会追着你问清楚它会按照它理解的平均值去执行而它的平均值往往是偏重的。在软件工程体系里“编程风格简洁、防止复杂化、强化易读性”不是一个单一需求它是一组非功能性需求Non-Functional Requirements。要传达清楚至少需要拆成四个可被独立判断的维度。1.1 简洁性Simplicity不等于简陋软件工程谈简洁通常落到KISS原则Keep It Simple, Stupid上在所有能满足当前验收标准的方案里选结构和逻辑最简单的那一个。很多人担心“写简单了是不是显得水平不够”这是被带偏了。系统设计领域里一个公认事实是新增一个概念、一个抽象层、一个间接调用都会给后来的维护者增加理解成本。真正的KISS不是让你省略边界处理、省略错误捕获而是不在没有收益的方向上增加结构。举个典型例子两个方案都能读取配置方案A是直接读文件、拆行、返回字典。 方案B先建ConfigSource接口再写FileConfigSource和EnvConfigSource两个实现然后通过ConfigLoaderFactory根据运行时参数决定装配哪个来源。方案A一眼望穿方案B看起来“优雅”但如果当前根本不存在“多个配置来源”这个真实需求B就是纯粹的过度设计。KISS要求的是A。1.2 YAGNI不要为想象中的未来买单YAGNI全称是You Ain‘t Gonna Need It出自极限编程XP领域。它的原始含义非常直接永远不要为“你觉得以后可能会有”的需求写代码。这一条恰恰是AI agent最常违反的。你在Prompt里说“实现一个用户登录接口”模型内部会自动补全出一套“未来可能需要支持多种登录方式”的判断然后顺手给你引入策略模式。你根本没说需要它先替你设计了。所以在软件工程评审中对这类问题的标准术语叫“投机性泛化”Speculative Generality也叫“为未来预留抽象”。这是代码坏味道里非常经典的一种Martin Fowler的Refactoring目录里就有专门条目。如果你的目标是“防止复杂化”那YAGNI就是必须写进约束里的第一原则。1.3 可读性核心指标是认知负荷可读性Readability是很虚的词吗在工程实践里其实不虚。它有一个非常实用的解释一个此前没看过这段代码的工程师从零开始读懂它需要付出多少认知努力。这个努力程度在认知科学里叫认知负荷Cognitive Load。降低认知负荷的操作包括命名直接表达意图、函数短到能放进工作记忆、消灭多层嵌套、不依赖调用顺序做隐式状态传递。行数本身不是唯一标准但一个超过40行的函数对多数人来说已经很难一次在脑子里模拟执行完这时候拆解往往是为了可读性而不是为了制造更多类型。还有一个特别重要的点可读性差的代码往往靠注释找补。正宗的做法正好反过来——如果一段代码需要靠注释才能看懂优先怀疑代码结构本身有问题也就是代码整洁之道里反复强调的“命名和结构优先于注释”。1.4 最小惊讶原则让读者永远猜得中最小惊讶原则Principle of Least Astonishment在UI设计里提得多软件工程其实一样适用。它的意思是一段代码的行为和读代码的人心里预设的行为尽量保持一致不要整出“惊喜”。比如一个函数叫load_config()读者预期它可能返回配置对象结果它内部还顺带改了全局状态、建立了网络连接。这会让所有调用方都活在恐惧中。放到AI生成代码的语境下我们同样要约束agent不搞“花活”不使用没必要的装饰器、不引入控制反转、不在常规数据流里埋隐式上下文。1.5 一张表把“人话”翻译成agent能执行的规则和同行沟通时可以直接说术语但要让agent理解最好每条都有一个“可观察”的行为准则。你想要的中文表达软件工程术语落到agent身上的约束规则示例“代码朴实点”Simplicity / KISS在满足全部验收标准的前提下优先选择代码行数最少、调用链最短的实现方式“别整那些用不上的抽象”YAGNI / No Speculative Generality禁止为“未来可能会出现”的第二个调用方预先设计接口、基类或工厂“好读、一眼能懂”Readability / Low Cognitive Load单个函数默认不超过30-40行命名让读者不需要注释就能说出意图“别让读代码的人意外”Principle of Least Astonishment函数名只承诺它字面上的行为不添加副作用与隐藏的全局状态变更“不要提前做架构”Evolutionary Design等第二个真实需求出现时再做抽象现在先用最直接的结构落地2. AI 为什么会把代码越写越重一套必然的“加戏”机制很多人骂AI写代码啰嗦其实根源不复杂。先理解大模型的生成逻辑它不是在执行最优代码搜索而是在预测一个“看起来最合理、最像优秀工程师会写出的token序列”。这个机制决定了两个倾向。2.1 复杂化是模型的“概率默认值”训练语料里GitHub上那些大型开源项目、带有完整分层架构的代码库、各类设计模式教程占的比例远高于“三行写完一个功能”的极简片段。模型在预测下一个词的时候对“接口实现工厂”这种组合模式的概率估计天然偏高因为它见过太多次了。可以类比一个刚看完大量“企业级架构课”的实习生。你让他写一个“加载配置”的任务他脑子里最想展示的是自己刚学会的抽象能力而不是交付一个刚好够用的函数。AI没有“展示欲”但它的概率分布本身就是“重口味”的。更关键的是在主流代码能力评测里功能正确性是硬指标“多余抽象”几乎不受惩罚。RLHF阶段的人类标注员看到一份结构完整、包含接口设计的代码时也确实更容易觉得它“专业”。于是在模型看来多写抽象不仅无风险甚至可能加分。这个先验只有靠用户的显式约束去压制。2.2 Agent比普通对话模型更容易膨胀普通ChatGPT生成代码时只是单轮响应Agent则多了一层“任务规划”。我自己观察过不少次agent的思考过程它会把一个大任务拆成子任务列表然后逐个执行。问题就出在子任务列表上——模型拆任务拆到一半经常自己补一个“考虑后续可扩展性”或者“预留配置接口”的子任务进去。它为什么会这么干因为“设计一个可扩展的架构”在训练数据里是被高度赞扬的行为。既然没有惩罚机制说“这个任务根本不需要扩展性”它就会把这当成增值项。还有一个隐蔽的因素是Prompt里的模糊带。当你说“实现一个配置管理”时AI会自动把需求放大成“实现一个可以管理多种格式、支持动态重载、未来对接配置中心的配置管理子系统”。它不是坏是过度补全。用户写的范围描述里没写明“不需要什么”模型就会按“平均需求”来补全。2.3 结论不显式约束轻松写就会被默认走高复杂度路线所以我们必须接受一个现实如果你想只用一个函数解决却不在指令里明确“这是一次性代码不为未来预留接口”agent大概率会给你整出一个包着三层结构的模块。而反过来只要你把“禁止引入接口层、禁止增加未使用配置参数”写成硬性规则模型是完全有能力输出简洁代码的。它的能力不差差的是你定义验收标准的完整度。3. 用结构化约束写 Prompt让“简朴”不再是模糊感受既然模型默认走复杂路线我们的应对就是让Prompt带一套风格约束块。这套约束不是一句“请写简单点”而是把上一章的术语翻译成可执行的规则。我实践下来真正好用的约束条数是6到10条太多了模型会稀释注意力太少了又拦不住它加戏。3.1 应该往Prompt里塞哪些约束以下几类约束项是我最常用的每一条都尽量让agent可以依据代码本身判断对错约束范围只实现当前列出的功能不要为后续可能的需求做预留设计。禁止投机抽象不引入当前代码中只有一种具体实现的接口、基类或工厂。选择偏好在满足功能正确性的前提下优先选择最短且直接的写法标准库能力优先于自定义封装。结构限制不要在单次实现中新增超过一个文件新增类或函数必须在回复中说明它解决了当前哪个具体问题答不上来就不允许引入。命名优先变量、函数、文件命名以“未读过代码的人能猜出意图”为准。去掉死配置不添加任何当前功能没有用到的参数、配置项、依赖不要为一段简单逻辑单独开配置文件。交付前自查代码完成后按上述约束逐条检查并在最后列出你删减了哪些冗余结构。3.2 一个可直接复用的Prompt模板我用得最多的模板长这样可以直接复制改写# 角色 你是一名务实的中级工程师不是架构师。写出让团队新人能直接接手维护的代码。 # 本次要做的功能 在这里写清功能范围、输入输出、验收标准 # 硬性风格约束 1. 只实现上面列出的功能。不为将来可能出现的需求增加抽象。 2. 在同样满足验收标准的前提下选行数最少、调用链最短的实现。 3. 除非当前功能确实需要多态/继承否则禁止新增接口、抽象基类、工厂类。 4. 不新增没有被调用的参数、配置项、公共方法。 5. 新函数或新类必须先回答它解决了当前哪个具体问题答不上来就不要引入。 6. 变量、函数名必须让读者不需要注释就能看懂意图。 7. 不要在命名已经清晰的地方堆解释性注释。 8. 实现后自查圈出所有可能被评审认为过度设计的位置并说明为什么保留或直接删除。这个模板的核心是给Agent立一个“务实中级工程师”的人设。效果不是玄学它把生成风格从“架构展示模式”切换到了“交付维护模式”两种模式下模型产出的结构差异非常明显。3.3 一次对照实验约束前后差多少拿最常见的一个任务来试从一个多行文本中解析出路径和对应的文件大小。不写风格约束直接问时模型大概率会给一个PathEntry数据类再配一个PathParser工具类里面加一个parse静态方法方法里再做try/except双分支处理不同格式最后在控制器里调用。三层结构四五十行。把上面3.2的模板套上去后模型给的是一个普通函数输入文本返回list[tuple[str, int]]十几行收工。该处理的空行、异常情况一样没少只是没有包壳的层次感。同一套功能需求差一倍的代码量和三层的理解跳转区别只在Prompt里有没有那几条硬约束。这说明约束是真实起作用的不是心理安慰。3.4 写Prompt时最容易失效的三句话有几种表达方式我建议直接淘汰。第一句是“代码质量要高”质量的定义太宽模型只会加大它默认那套“高”的分量。第二句是“不要过度设计”这句话有效但太弱它没有给出具体的判断维度模型不知道该砍什么。第三句是“尽量简单”副作用是模型可能连必要的错误处理都省了因为它把“简单”理解成了“少写边界代码”。更有效的做法是在Prompt里写明“不做什么”的具体清单。如果Agent输出的代码还是复杂了你就直接指出具体位置。比如“这个EnvLoaderFactory在当前需求下只有一个实现删掉Factory层让调用方直接创建EnvLoaderignore_unknown_keys参数目前没有任何调用方删掉之后跑一遍测试。”这种反馈每次都作用在具体的类、参数和行上而不是停留在“太多太复杂”的抽象层面。迭代三四次之后agent会越来越熟悉你的标准因为它实际上是在通过你的纠正做少样本学习。4. 把规则沉淀成项目级配置一次声明让 agent 每次自动带上单独一轮写Prompt的约束再强也敌不过时间。过两天新开一个会话、换一个agent工具风格又回到默认值。所以要把规则写进项目级别的配置文件里让它成为agent每次进入项目都能看到的“工作手册”。4.1 不同工具读的是哪些规则文件目前主流AI编程工具的配置方式差异较大而且迭代很快以下信息以写这篇文章时的主流版本为准新版本建议查官方文档工具规则文件说明Cursor.cursorrules或.cursor/rules/*.mdc项目根目录下创建后每次会话自动加载Claude CodeCLAUDE.md支持项目级和子目录级指令agent会主动读取Continue.continue/config.json里的rules字段也可以让规则指向外部文档Devin / 类似云端Agent项目根目录的AGENTS.md或平台级偏好设置按各平台的说明为准GitHub Copilot agent模式.github/copilot-instructions.md会让代码补全与聊天中的agent都参考该文件你不需要每个文件全建选自己主力工具对应的即可。这套文件的意义是项目风格约束从“每次临时口述”变成了“版本管理的一部分”随代码一起进Git仓库团队所有人都能复用。4.2 一份可以在仓库里落地的最小规则示例我习惯把它命名为AGENTS.md或者在Cursor中叫.cursorrules内容控制在十行左右让模型每次轻松吞下而不是被长文冲淡注意力# 工程风格准则AI协作版 这个仓库的所有AI生成代码必须遵守以下规则 1. 每次改动遵循最小变更原则完成Issue要求即可不做无关重构。 2. 不为“未来需求”增加接口、抽象基类或Factory。只有出现第二个真实调用方时才允许抽象。 3. 新增任何新的依赖、配置文件、目录结构必须显式说明用途并等待确认。 4. 函数目标控制在40行以内优先用清晰的命名而不是注释表达意图。 5. 如果一个类只有一个真实使用点优先考虑合并到调用方。 6. 所有新公共API必须附带一个真实场景可以执行的示例而不是空泛的文档。 生成代码后请检查输出是否违反了上述任意一条。如果违反请主动修改后输出。这些条款写得越像“工作时审核标准”模型执行越稳定。因为它读到的不是情绪化的“要简洁”而是一组可以被代码本身验证的客观条件。值得提醒的是规则文件不是越多越好。我见过有人把一份长达200行的“规范大全”塞给AI实际效果反而变差——大模型在上下文中对长文件的注意力有限规则太长后面十几条就变成了摆设。宁可保留最频繁违反的十到二十条定期往里补一条“最近的教训”也不要一次堆满。另外一个细节是持续会话过程中最有效的规则其实是“你上一轮接收到的纠正”。如果agent在单次对话里被你纠正过“不要Factory”它后续遵守得很好但下次新会话又忘了。项目级规则文件的作用就是把这类高频纠正固化成长期记忆。5. 代码评审闭环让 agent 自己先瘦身再交给你的自审清单Prompt和项目规则解决了“生成前”的问题但AI生成的代码依然可能偶尔不受控。这时候不能只靠人去做代码评审更划算的做法是在生成阶段就要求Agent带着一份“瘦身检查表”自查。这一段提供一套能直接用的自审清单和对应Prompt。5.1 让Agent在交付前先做一轮“复杂度审计”交付前自审和“直接生成”有着肉眼可见的差异。经我要求后不少Agent在处理同一需求时会主动报告“本实现未添加接口层我原本想用工厂模式处理配置来源但当前只有单一来源所以改为直接调用额外参数已移除。”把这个流程固化下来的Prompt如下在你输出最终代码前先执行一轮复杂度审计并且把审计结论用几句话写在最前面。 审计清单 1. 这个实现里每个interface/abstract class/factory是否都有超过一个真实调用方 2. 有没有参数或配置项只是为“将来某个功能”留的 3. 如果一个类只有一个使用点能否直接合并到调用方 4. 有没有引入了标准库或项目内已有工具已能实现的重复轮子 5. 能否让一个只看了函数名的同事正确猜出它的行为 只有当你对上述每一条都给出明确答案后再输出最终版本的代码。如果审计中发现冗余结构直接在输出版本里删除并用一行说明删了什么、为什么能删。这类Prompt之所以高价值是因为它把“让模型自己批判自己”变成显式动作。模型本身完全有能力识别出“这个Factory不必要”只是缺乏一个触发它去做批判的指令。你不让它查它就默认一切设计合理你让它查它的诊断能力其实比多数人想象中好。5.2 让Agent“证明”每个抽象的必要性还有一个技巧是我最近才总结出来的当模型的代码中出现一个新抽象时要求它给出保留的依据。比如它给一段普通数据处理逻辑增加了一个Processor接口而目前只有一个CsvProcessor实现。评审时你可以直接问“这个接口目前只有一个实现请你说明接口层在当前代码里带来了什么可测试性或扩展性收益。如果拿不到净收益把接口删掉把CsvProcessor的方法变成模块级函数。”大多数情况下模型沉默一秒后会列出文件名然后动手改。因为它真的算不出“单实现接口”在当前场景里有什么净收益。这个方式的本质是把代码设计的证明义务交还给生成方让它为每一层抽象负责。一旦形成这个规矩它在生成阶段就会提前预判你后续会不会这样问从而主动减少无必要的抽象。5.3 用量化信号辅助发现复杂化最终的人工评审环节我建议可以盯几个相对客观的信号。抽象层数量、单文件行数、函数参数个数、循环嵌套层数、未被调用的公共方法数这些数据不一定代表坏代码但一旦异常飙升就值得停下来问一句“这个复杂是不是被提前发明出来的”。结合我日常review的经验下面几条可以作为触发雷达的阈值一个功能需求提交的代码量超过手写量的两倍以上概率上是加了没用的结构。新增文件之间互相依赖超过两层大概率可以合并或删层。一个配置项查遍整个代码库没有任何读取点说明agent在做“无忧配置”。AI辅助编程时代人的核心角色正在从“写字的人”变成“定标准和做裁决的人”。代码是agent写的但每一条“这里不需要抽象”的判断都来自你。写在最后的一点体会做了半年AI辅助开发我自己最大的感触是想让agent写出“朴实”的代码光靠你告诉它“要朴实”是不够的。你需要把“朴实”翻译成它可以执行、可以自查、可以被你的review验证的一组规则并且让这些规则出现在项目配置、Prompt约束和代码评审这三个环节里。我现在的习惯是任何一个和我AI协作者共享的仓库根目录第一份文档一定是工程约束而不是README因为每一次新会话的agent都会先读到它。这个措施比我说一百遍“别写复杂了”都管用。而当你明确告诉我自己只是要一个能用的模块、一个跑通原型的工具、一段将来会丢掉的数据处理脚本时绝大多数agent其实都能给出超出预期的收敛输出。真正难的不是让模型“会写简单代码”而是让它在每个节点都记得“你不需要那么复杂”。这件事的权重现在基本全在写需求的人手里。
返回列表