AI 生成的代码怎么 review

5 个必查项,每个都有我用真金白银买来的教训

Review 心态

Review AI 代码和 review 人的代码不一样。人写的代码,你可以假设作者理解自己写的东西;AI 写的代码,你不能假设它理解任何东西。它只是在统计意义上生成了"看起来对"的代码。

所以 review AI 代码的核心原则:不要读代码"看起来对不对",要证明它"在各种情况下都对不对"

必查项一:边界条件

AI 最容易漏的就是边界条件。happy path 几乎不会错,但空输入、超长输入、并发冲突、网络中断——这些 AI 不会主动考虑。

我的漏检案例:FinBuddy 的 K 线数据查询,AI 写的代码在股票代码不存在时返回空列表,但上层调用方假设返回值一定非空,直接取了 result[0]——IndexError。这个 bug 在正常使用时不会触发,只有查询一只退市的股票才会崩。

# AI 写的版本
kline = await db.fetch_daily_kline(stock_code)
return kline[0].close  # stock_code 不存在时 IndexError

# 修正
kline = await db.fetch_daily_kline(stock_code)
if not kline:
    raise StockNotFoundError(stock_code)
return kline[0].close

Review 时问自己:如果输入为空 / 超出预期范围 / 并发访问,这段代码会怎样?

必查项二:错误处理

AI 生成的代码经常有"看起来有错误处理,实际上没处理"的情况。最典型的:catch 了异常但什么都没做,或者 catch 了太宽泛的异常类型。

漏检案例:FinBuddy_Web 的 LLM 代理服务,AI 写了 try/except Exception 包住整个请求处理,异常时返回空响应。看起来有错误处理,但实际上所有异常都被吞了——包括 InsufficientBalanceError(余额不足应该返回 402)和 RateLimitError(限流应该返回 429)。客户端收到空响应,完全不知道发生了什么。

# AI 写的版本
try:
    result = await process_request(req)
    return result
except Exception:
    return Response(status=204)  # 所有异常都吞了

# 修正:区分异常类型
try:
    result = await process_request(req)
    return result
except InsufficientBalanceError:
    return JSONResponse(status_code=402, content={"error": "余额不足"})
except RateLimitError:
    return JSONResponse(status_code=429, content={"error": "请求过于频繁"})
except LLMServiceError as e:
    logger.error(f"LLM 服务异常: {e}")
    return JSONResponse(status_code=503, content={"error": "服务暂时不可用"})

Review 时问自己:每个 except 分支是否处理了正确的异常类型?异常信息是否传递给了调用方?

必查项三:性能

AI 不关心性能。它生成的代码在功能上可能是对的,但在性能上经常有问题:N+1 查询、不必要的对象创建、同步操作阻塞异步循环。

漏检案例:FinBuddy 批量获取股票数据,AI 写的版本在循环里逐条查数据库:

# AI 写的版本:N+1 查询
for stock_code in watchlist:
    kline = await db.fetch_kline(stock_code)  # 每次一个查询
    results.append(kline)

# 修正:批量查询
results = await db.fetch_kline_batch(watchlist)  # 一次查询

50 只股票,AI 版本发 50 次数据库查询,耗时 2.3 秒;批量查询版本 1 次搞定,耗时 0.05 秒。差了 46 倍。

Review 时问自己:有没有循环内的数据库查询?有没有不必要的对象创建?同步操作是否阻塞了异步循环?

必查项四:安全

AI 生成的代码经常忽略安全问题:SQL 注入、敏感信息泄露、权限检查缺失。不是 AI 不知道这些概念,而是它不会主动在生成的代码中加入防护。

漏检案例:FinBuddy_Web 的用户信息查询接口,AI 写的版本直接用用户传入的 user_id 查数据库,没有验证当前登录用户是否有权查看该 user_id 的信息。任何登录用户可以查看任意用户的数据。

# AI 写的版本:缺少权限检查
@router.get("/users/{user_id}")
async def get_user(user_id: int):
    return await db.get_user(user_id)  # 任何登录用户都能查

# 修正:验证权限
@router.get("/users/{user_id}")
async def get_user(user_id: int, current_user: User = Depends(get_current_user)):
    if current_user.id != user_id and not current_user.is_admin:
        raise PermissionDeniedError()
    return await db.get_user(user_id)

Review 时问自己:有没有用户输入直接拼进查询?有没有权限检查?有没有敏感信息暴露在响应中?

必查项五:架构一致性

AI 不知道你的项目架构约束。它可能生成一段功能正确但违反项目架构的代码——绕过中间件、不按分层调用、引入不存在的依赖。

漏检案例:让 AI 给 FinBuddy 加个新功能——行业分析。AI 直接在路由层写数据库查询,绕过了 DatabaseMiddleware。功能上没问题,但违反了项目的分层架构(Routes → Services → Engines → Middleware → DB)。后来重构时这块代码成了孤岛,差点漏改。

这和 什么时候该让 AI 写 里说的"不提供上下文"是一个问题的两面——你不告诉 AI 架构约束,它就不会遵守。

Review 时问自己:这段代码符合项目的分层架构吗?有没有绕过中间件或服务层直接操作底层?

工具辅助

纯靠人眼 review 容易漏,尤其是大段代码。我配合三个工具:

  1. 类型检查(mypy / pyright)— 捕获类型不匹配、可选值未检查。AI 经常把 Optional[str]str 用,类型检查能抓出来。
  2. Lint(ruff / flake8)— 捕获未使用的 import、变量名冲突、过长函数。AI 生成的代码经常有多余的 import。
  3. 测试— 写测试覆盖边界条件。AI 生成的测试只覆盖 happy path,你补的测试要覆盖空输入、异常路径、并发场景。

工具不能替代人脑,但能帮你抓住人眼容易漏的低级错误。就像 认知偏差 里说的,人的注意力有限,工具是外挂的注意力。

Review 清单一句话版

5 个必查项

边界条件(空输入/超范围/并发)→ 错误处理(异常类型/信息传递)→ 性能(N+1/冗余对象/同步阻塞)→ 安全(注入/权限/信息泄露)→ 架构一致性(分层/中间件/依赖方向)。按这个顺序查,漏检率从 30% 降到 5% 以下。