AI 代码评审:自查清单与幻觉 API 识别

让 AI 评审代码,最大的价值不是「帮你发现 bug」,而是把一份稳定的清单复核一遍——空指针、错误处理、边界、并发、资源释放、注入、脱敏。它偶尔会漏,但很少全漏。前提是你把它放在审查者位置,而不是作者位置。

角色分工:审查者不是作者

同一个模型,刚写完函数就让它「检查一下」,它倾向于复述自己的思路;换一个会话、只喂 diff,它才像审查者。有效的提示词长这样:

角色:资深 Go 后端评审者,只评审,不改写。
范围:只评审下面这段 diff,不评价文件其他部分。
输出:按 [阻塞/建议/可选] 三档分级,每条含 文件:行号、问题、触发条件、最小修复方向
禁止:泛泛的风格问题(命名、注释)、未被 diff 改动到的历史问题。

三条经验:只给 diff 加必要上下文,别让它在几千行文件里自由发挥;要求给出触发条件,「这里可能有问题」不是结论,「当 user 为 nil 且缓存命中时 panic」才是;要求分级,否则你会收到 30 条同权重建议,无法判断先做哪个。

评审清单

类别具体关注点Go 常见形态
空指针nil map/slice 写入、nil 接口、可选字段解引用var m map[string]int; m[k]=1 直接 panic
错误处理是否吞错、是否包装、是否区分可重试_ = f()、用 %v 而非 %w、未特判 sql.ErrNoRows
边界空输入、零值、超长、正好等于阈值s[0] 未判空、for i := 0; i <= len(s); i++
并发共享变量、锁粒度、goroutine 泄漏、闭包捕获循环里 go func(){ use(i) }()、errgroup 无退出条件
资源释放行集、文件、锁、事务是否必然释放rows.Close() 在 err 分支被跳过
SQL 注入拼接参数、动态表名与排序字段fmt.Sprintf("... WHERE name='%s'", name)
日志脱敏手机号、身份证、token、密码入日志log.Printf("req=%+v", req) 打印整个请求结构体

下面这段代码能同时命中四类问题:

// 有问题的写法
func (r *OrderRepo) ByUser(ctx context.Context, uid int64, orderBy string) ([]Order, error) {
    q := "SELECT id, order_no, amount FROM orders WHERE user_id = " + strconv.FormatInt(uid, 10) +
        " ORDER BY " + orderBy // 1) 排序字段拼接,注入点
    rows, err := r.db.QueryContext(ctx, q)
    if err != nil {
        return nil, nil // 2) 吞错,调用方拿到 nil 错误却无数据
    }
    defer rows.Close()

    var out []Order
    for rows.Next() {
        var o Order
        rows.Scan(&o.ID, &o.OrderNo, &o.Amount) // 3) 忽略 Scan 错误
        out = append(out, o)
    }
    return out, nil // 4) 未检查 rows.Err()
}

正确方向是:占位符传值、动态标识符走白名单映射、错误用 %w 包装、Scan 与 Err() 都要检查。

// 修好后的写法
var allowedOrderBy = map[string]string{
    "created": "created_at DESC",
    "amount":  "amount DESC",
}

func (r *OrderRepo) ByUser(ctx context.Context, uid int64, key string) ([]Order, error) {
    orderBy, ok := allowedOrderBy[key] // 只允许白名单里的排序字段
    if !ok {
        return nil, fmt.Errorf("invalid order key: %q", key)
    }
    q := "SELECT id, order_no, amount FROM orders WHERE user_id = ? ORDER BY " + orderBy
    rows, err := r.db.QueryContext(ctx, q, uid)
    if err != nil {
        return nil, fmt.Errorf("query orders: %w", err)
    }
    defer rows.Close()

    var out []Order
    for rows.Next() {
        var o Order
        if err := rows.Scan(&o.ID, &o.OrderNo, &o.Amount); err != nil {
            return nil, fmt.Errorf("scan order: %w", err)
        }
        out = append(out, o)
    }
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("iterate orders: %w", err)
    }
    return out, nil
}

幻觉 API:识别与验证

AI 最危险的输出不是明显的逻辑错误,而是不存在的函数、参数、配置项——名字很像真的,语法也对,只是编译不过或静默不生效。

形态例子识别信号
版本错位建议用 slices.Contains、errors.Join,项目还在 Go 1.19拼写正确但 undefined,先查该 API 的最低版本
参数错位记混 http.Server 字段名或 sql.DB 方法签名参数顺序、个数与文档不符
拼接函数名db.QueryRowContextAll、strings.TrimPrefixAll名字符合惯例但标准库里不存在
配置项幻觉skip-grant-table(正确写法是 skip-grant-tables)官方文档搜不到,只在某篇博客里出现过
假出处引用不存在的 issue 编号、含混的「某版本之后支持」在官方 changelog 里定位不到

验证成本很低,养成本能即可:

go doc strings.CutPrefix        # 存在吗?签名对吗?
go doc database/sql.DB.QueryContext
go build ./... && go vet ./...  # 编译与静态检查是第一道闸
go list -m -versions github.com/gin-gonic/gin

规则很简单:没有编译通过、没有 go doc 确认、没有官方文档出处的 API,不进代码。配置项同理,先在官方文档里搜到再改配置文件。

检查清单

检查项通过标准
评审范围只评审本次 diff,历史问题另开单
空值路径每个解引用点有 nil 判断或上游保证
错误路径没有 _ = 吞错,包装保留错误链,可重试错误可区分
并发安全有 -race 跑过的证据,goroutine 有退出条件
资源释放行集、文件、锁、事务在 err 分支也释放
注入面外部输入走占位符,动态标识符走白名单
日志内容无 token、密码、完整手机号等敏感字段
API 真实性新增函数与配置项均有官方文档或 go doc 依据

常见坑

  • 同一会话既写又审:模型会维护「我刚写的没错」的立场,换会话、只喂 diff 效果明显更好。
  • 把建议当阻塞:AI 一次给很多条,全盘接受会拖慢迭代;先修阻塞项,建议项评估后再做。
  • 只看结论不看触发条件:没有触发条件的问题无法验证,也写不出覆盖它的测试。
  • 忽略它漏报的类别:模型对业务语义不敏感(额度、状态机、幂等性),这部分必须人工过一遍。
  • 为让幻觉 API 编译而乱改调用方:把调用方改成迁就一个不存在的接口,损失远大于直接换掉它。

小结:AI 评审的价值在于稳定复核一份清单——空指针、错误处理、边界、并发、资源释放、注入、脱敏;让它当审查者、只给 diff、要求分档并给出触发条件,比让它当作者更有效。同时把幻觉 API 当常态对待:每个新增函数与配置项,都要经过 go doc、编译和官方文档三重确认才落进仓库。

笔记加载中…