Skip to content

规范驱动——用 Rule 对抗 AI 的编码天性

AI 的四种编码坏习惯

AI 生成代码有两种问题:一种是"写不对",一种是"写不好"。

Rule 主要解决的是"写不好"——AI 天然有四种编码坏习惯,不分模型、不分工具,是所有 Code Agent 的通病。只有在 Rule 层面明确约束,才能让 AI 写出符合项目要求的代码。

问题一:深层嵌套——AI 喜欢 if else 地狱

这是最普遍的问题。AI 生成的代码中,条件嵌套的深度经常达到四五层甚至更多:

csharp
// AI 的天然写法:深层嵌套
public async Task<Result> ProcessAsync(Order order)
{
    if (order != null)
    {
        if (order.IsActive)
        {
            if (order.Items.Any())
            {
                // 真正的逻辑在这里,被三层 if 包裹
            }
        }
    }
}

这种代码逻辑没错,但可读性极差。人在阅读时需要记住每一层的条件状态,心智负担很重。所以在项目中创建了 early-return 约束:遇到前置条件不满足时,提前返回,不要继续嵌套。

csharp
// early-return 约束后的写法
public async Task<Result> ProcessAsync(Order order)
{
    if (order == null) return Result.Failure("订单不存在");
    if (!order.IsActive) return Result.Failure("订单已失效");
    if (!order.Items.Any()) return Result.Failure("订单无商品");

    // 真正的逻辑在这里,没有嵌套
}

这条 Rule 投入产出比极高——几乎每个工程师都会遇到嵌套代码的问题,一条 Rule 就能根治。

问题二:异常吞咽——AI 喜欢 try catch 吞异常

对于 Java、C# 这类有异常处理机制的语言,AI 有一个非常顽固的习惯:到处写 try catch,然后吞掉异常

csharp
// AI 的天然写法:try 地狱 + 异常吞咽
public async Task<Result> ExecuteAsync()
{
    try
    {
        // 业务逻辑
        try
        {
            // 内部操作
            try
            {
                // 更深层的操作
            }
            catch (Exception ex)
            {
                // 什么都不做
            }
        }
        catch (Exception ex)
        {
            return Result.Failure("操作失败");
        }
    }
    catch (Exception ex)
    {
        // 吞掉
    }
}

这是 AI 在训练数据中学到的"安全策略"——捕获所有异常就不会崩溃。但在工程实践中,这种写法导致三个严重后果:

  1. 异常被吞掉后,问题无法被追踪——线上出了问题,连错误日志都没有
  2. try 嵌套和 if 嵌套一样,代码可读性急剧下降
  3. 调用方不知道操作失败了——应该抛出的异常被静默处理

我们的约束很简单:异常处理统一在最顶层进行,内部代码不捕获异常,遵循异常透明模式。

csharp
// 约束后的写法:异常透明
public async Task<Result> ExecuteAsync()
{
    // 内部方法不 try catch,异常向上抛出
    var data = await LoadDataAsync();
    var result = await ProcessDataAsync(data);
    return Result.Success(result);
}

// 最顶层统一处理
public async Task<IActionResult> ApiEndpointAsync()
{
    try
    {
        var result = await _service.ExecuteAsync();
        return Ok(result);
    }
    catch (DomainException ex)
    {
        return BadRequest(ex.Message);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "未预期的错误");
        return StatusCode(500);
    }
}

问题三:过度兼容——AI 喜欢写 fallback

这是最隐蔽的问题。当需求变更导致重构时,AI 为了不犯错,会保留旧逻辑并加上新逻辑,用条件分支兼容两者:

csharp
// AI 的天然写法:新旧逻辑并存
public async Task<Result> ProcessAsync(Data data)
{
    // 旧逻辑(兼容)
    if (Config.UseOldMethod)
    {
        return await OldProcessAsync(data);
    }
    // 新逻辑
    return await NewProcessAsync(data);
}

每一次重构都留下一层"兼容壳",而不是干净地替换。后果就是:代码持续膨胀,调用链越来越难以追踪,旧逻辑永远没人敢删。

我们的规则:不做兼容性设计。 重构就是替换,旧逻辑直接删除,不保留 fallback 路径。如果需要灰度切换,应该在基础设施层面做(如功能开关、流量路由),而不是在代码里写 if else。

问题四:过度设计——AI 喜欢把简单问题复杂化

AI 在生成代码时,有时会设计出远超当前需求的抽象——工厂模式套策略模式、多层接口继承、泛型约束层层叠加。代码"看起来很优雅",但实际只解决了一个简单的 CRUD 问题。

过度设计的危害不在当下——一个过度设计的类本身不会出 Bug。问题在于未来的维护者看不懂为什么要这么设计,不敢动,最后代码变成了"没人敢改的遗迹"。

解决方案就是第一章讲的"文档先行"。 在编码之前先让 AI 输出设计方案,你审查一遍。如果发现设计过于复杂,在方案阶段就纠正,而不是等代码写完再重构。文档先行的价值在这里体现得最直接——过度设计在文字阶段就能被发现,不需要等到代码阶段。

四个问题的共同根源

这四个问题的根源其实是一个:AI 在训练数据中学到的"安全模式"与工程实践的"最优模式"之间存在偏差。

AI 的安全模式工程实践的最优模式
嵌套 if 保证每个条件都覆盖early-return 提前退出,减少嵌套
try catch 吞异常保证不崩溃异常透明,让异常被上层统一处理
保留旧逻辑做 fallback 保证不出错直接替换,不做兼容
设计复杂抽象保证可扩展够用就好,不过度设计

Rule 的作用就是纠正这个偏差——在 AI 生成代码时,用项目规范覆盖它的训练数据记忆。

Growth Client 的 14 条 Rule

在 Client 项目中,我们沉淀了 14 条 Rule,覆盖了从命名规范到架构约束的全方位。其中前四条直接针对上述四个问题:

核心规范(针对 AI 天性)
├── early-return:必须用卫语句,避免深层嵌套
├── exception-handling:异常透明,只在最顶层 try catch
├── no-fallback:不做兼容性设计,旧逻辑直接删除
├── design-review:复杂设计必须先输出方案供审查

命名规范类
├── logger-convention:必须用 LogFormatHelper
├── naming-convention:C# PascalCase,前端 camelCase

架构约束类
├── dependency-injection:只能通过构造函数注入依赖
├── layer-boundary:严格遵守分层架构,禁止跨层引用

项目专属类
├── api-response-format:统一 API 响应格式
├── repository-pattern:数据库访问必须通过 Repository 层
...

检查表:Rule 编写质量

  • [ ] 每条 Rule 是否针对一个具体的、反复出现的问题?
  • [ ] 是否包含"AI 会怎么写(错误)"和"应该怎么写(正确)"的对比?
  • [ ] 是否有明确的约束条件(什么场景下触发)?
  • [ ] 是否已在至少一个 Code Review 中验证过效果?
  • [ ] 有没有因为 Rule 过严导致 AI 拒绝执行的情况?