AI 编码代理生成的代码为什么总有隐藏 bug?我的 Code Review 实战清单

上周五晚上,我信心满满地让 Claude Code 帮我重构一个数据处理模块。它干得又快又漂亮------代码结构清晰,命名规范,注释齐全。我直接 merge 了。

然后周一早上,线上告警炸了。

一查,是重构后的代码在边界条件下会把空列表当成有效输入,跳过了校验逻辑,导致下游服务拿到脏数据。AI 写的代码,逻辑"看起来"完全没问题,但它默默吃掉了一个本该抛异常的场景。

这件事让我彻底改变了对 AI 编码代理的使用方式。不是不用------而是必须建立一套针对 AI 生成代码的 review 方法。

AI 代码的"好看陷阱"

用过 Claude Code、Cursor、OpenCode 这类 AI 编码工具的人大概都有过类似感受:AI 生成的代码格式永远完美

变量命名比你自己写的规范,注释比你写的详细,函数拆分比你做的合理。你一看,心想"这比我写得好多了",然后就放松了警惕。

但问题恰恰就在这里。AI 代码的危险不是"写得差",而是"看起来太好了"。

人类写的烂代码有个优点------它长得就很可疑,你会自然地多看两眼。但 AI 写的 bug 藏在漂亮的代码结构下面,像一个穿着西装的骗子,你很难靠直觉发现它。

HN 上最近在讨论 OpenCode(一个开源 AI 编码代理,12 万 GitHub Stars),评论区有个观点特别犀利:

"用 AI 生成所有代码,只在你把'尽快发布功能'的优先级放在代码质量之上时才有意义,因为只有这种情况下写代码本身才是瓶颈。"

我觉得这话只说对了一半。AI 完全可以帮你写出高质量代码,前提是你知道怎么 review 它。

AI 代码常见的 5 类隐藏 bug

我回顾了过去三个月用 AI 编码代理踩过的坑,总结了 5 类最常见的问题:

1. 边界条件遗漏

这是出现频率最高的。AI 非常擅长处理"正常路径",但经常忽略边界:

python 复制代码
# AI 生成的代码
def get_average_score(scores: list[float]) -> float:
    return sum(scores) / len(scores)

# 看起来没问题?空列表呢?
# 正确的写法
def get_average_score(scores: list[float]) -> float:
    if not scores:
        raise ValueError("scores cannot be empty")
    return sum(scores) / len(scores)

更隐蔽的情况:

python 复制代码
# AI 生成的分页查询
def get_page(items: list, page: int, size: int = 20) -> list:
    start = (page - 1) * size
    return items[start:start + size]

# page=0 呢?page=-1 呢?size=0 呢?
# AI 不会主动考虑这些
def get_page(items: list, page: int, size: int = 20) -> list:
    if page < 1:
        raise ValueError(f"page must be >= 1, got {page}")
    if size < 1:
        raise ValueError(f"size must be >= 1, got {size}")
    start = (page - 1) * size
    return items[start:start + size]

2. 错误处理被"吞掉"

AI 特别喜欢用 try-except 包裹一切,但经常把异常吞掉或者用一个笼统的 except Exception 了事:

python 复制代码
# AI 经典操作
def fetch_user_data(user_id: str) -> dict:
    try:
        response = requests.get(f"{API_URL}/users/{user_id}", timeout=10)
        return response.json()
    except Exception:
        return {}  # 网络错误、超时、JSON 解析失败、404... 全都静默返回空字典

# 下游代码拿到空字典,不知道是"用户不存在"还是"服务挂了"
# 这两种情况的处理方式完全不同

正确的做法:

python 复制代码
def fetch_user_data(user_id: str) -> dict:
    try:
        response = requests.get(f"{API_URL}/users/{user_id}", timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.Timeout:
        logger.warning(f"Timeout fetching user {user_id}")
        raise
    except requests.HTTPError as e:
        if e.response.status_code == 404:
            return {}  # 用户确实不存在
        logger.error(f"HTTP error fetching user {user_id}: {e}")
        raise
    except requests.RequestException as e:
        logger.error(f"Request failed for user {user_id}: {e}")
        raise

3. 并发安全问题

AI 生成的代码绝大多数是单线程思维。如果你的应用有并发场景,它几乎不会主动考虑线程安全:

python 复制代码
# AI 生成的缓存实现
class SimpleCache:
    def __init__(self):
        self._cache = {}
    
    def get_or_set(self, key: str, factory):
        if key not in self._cache:
            self._cache[key] = factory()  # 并发下可能重复计算
        return self._cache[key]

# 多线程环境下这段代码会导致:
# 1. 重复执行 factory()(如果 factory 有副作用就完蛋)
# 2. 字典可能在遍历时被修改(Python 3.x 较安全但不保证)

线程安全版本:

python 复制代码
import threading

class SimpleCache:
    def __init__(self):
        self._cache = {}
        self._lock = threading.Lock()
    
    def get_or_set(self, key: str, factory):
        if key in self._cache:
            return self._cache[key]
        with self._lock:
            if key not in self._cache:  # 双重检查
                self._cache[key] = factory()
            return self._cache[key]

4. 类型假设不安全

AI 经常假设数据的结构跟你描述的完全一致,不做防御性检查:

python 复制代码
# AI 生成的 API 响应解析
def parse_order(data: dict) -> Order:
    return Order(
        id=data["id"],
        amount=data["items"][0]["price"] * data["items"][0]["quantity"],
        user_email=data["user"]["email"]
    )

# data["items"] 为空列表?data["user"] 为 None?
# 线上接口返回的数据永远不要无条件信任

防御性写法:

python 复制代码
def parse_order(data: dict) -> Order:
    items = data.get("items") or []
    if not items:
        raise ValueError(f"Order {data.get('id')} has no items")
    
    first_item = items[0]
    price = first_item.get("price", 0)
    quantity = first_item.get("quantity", 0)
    
    user = data.get("user") or {}
    email = user.get("email", "")
    
    return Order(
        id=data["id"],
        amount=price * quantity,
        user_email=email,
    )

5. 资源泄漏

AI 有时候会忘记关闭文件句柄、数据库连接或 HTTP session:

python 复制代码
# AI 生成的批量文件处理
def process_files(paths: list[str]) -> list[dict]:
    results = []
    for path in paths:
        f = open(path)
        data = json.load(f)  # 如果这里抛异常,f 永远不会关闭
        results.append(process(data))
        f.close()
    return results

# 用 context manager
def process_files(paths: list[str]) -> list[dict]:
    results = []
    for path in paths:
        with open(path) as f:
            data = json.load(f)
        results.append(process(data))
    return results

我的 AI 代码 Review 清单

踩够了坑之后,我给自己整了一个 checklist,每次 AI 生成代码后对着过一遍:

✅ 快速扫描(30 秒)

  • 空值/空列表:函数入参有没有处理 None、空字符串、空列表?
  • 错误处理 :有没有 bare exceptexcept Exception?异常被吞了还是传播了?
  • 资源管理 :文件、连接、session 有没有用 withtry-finally

✅ 逻辑审查(2 分钟)

  • 边界值:page=0、size=0、负数、超大数字?
  • 并发安全:这段代码会在多线程/多进程下运行吗?共享状态有没有保护?
  • 类型假设:外部数据(API 响应、用户输入、数据库查询)有没有做防御性检查?
  • 幂等性:这个操作重复执行会怎样?会不会重复扣费、重复发通知?

✅ 杀手级问题(1 分钟)

  • 删掉这段代码的注释,纯看逻辑,它还说得通吗?(AI 的注释经常美化了逻辑漏洞)
  • 给这个函数传入最刁钻的参数,它会返回什么?
  • 这段代码凌晨 3 点报错了,错误信息够不够定位问题?

用 AI 写测试来验 AI 的代码

一个反直觉的操作:让 AI 自己给自己的代码写测试。

关键在于 prompt。不要说"给这个函数写单元测试",而是说:

复制代码
给这个函数写边界条件测试和异常路径测试。
重点覆盖:空输入、超大输入、类型错误、并发调用、网络超时。
不需要测正常路径,专注异常场景。

AI 在"攻击自己的代码"这件事上意外地好用。因为它没有人类程序员的"我写的代码不会有 bug"的心理偏见。

python 复制代码
# 让 AI 写的攻击性测试示例
import pytest

def test_get_average_score_empty():
    with pytest.raises(ValueError, match="cannot be empty"):
        get_average_score([])

def test_get_average_score_single():
    assert get_average_score([42.0]) == 42.0

def test_get_average_score_nan():
    result = get_average_score([float('nan'), 1.0])
    assert result != result  # NaN 检测... 但这是期望行为吗?

def test_get_page_zero():
    with pytest.raises(ValueError):
        get_page([1, 2, 3], page=0)

def test_get_page_negative():
    with pytest.raises(ValueError):
        get_page([1, 2, 3], page=-1)

def test_get_page_beyond_range():
    result = get_page([1, 2, 3], page=100, size=20)
    assert result == []  # 超出范围返回空,而不是报错

我的 AI 编码工作流

现在我的日常流程是这样的:

  1. 描述需求:用自然语言告诉 AI 我要做什么,包括边界条件和错误处理要求
  2. AI 生成代码:让它写初版
  3. 对照 checklist review:过一遍上面的清单,标出问题
  4. 让 AI 写测试:专注异常路径
  5. 跑测试修 bug:通常第一轮会发现 2-3 个问题
  6. 手动确认关键逻辑:特别是涉及金额、权限、数据删除的部分

整个过程比纯手写快 3-4 倍,同时代码质量不会下降。关键就在于第 3 步和第 4 步------这两步省不得。

一个实际的例子

最近我在做一个定时任务,每天从多个数据源拉取数据,合并后存入数据库。让 AI 写的初版:

python 复制代码
async def sync_all_sources():
    sources = await get_active_sources()
    tasks = [fetch_source(s) for s in sources]
    results = await asyncio.gather(*tasks)
    
    merged = merge_results(results)
    await save_to_db(merged)
    
    logger.info(f"Synced {len(merged)} records from {len(sources)} sources")

看起来简洁优雅。但用 checklist 一过,问题全出来了:

  1. asyncio.gather 默认某个 task 失败会立即抛异常,其他 task 的结果丢失
  2. 没有超时控制,某个数据源响应慢会拖住整个任务
  3. 没有重试机制
  4. save_to_db 失败了数据就丢了,没有持久化中间结果
  5. 日志不够,哪个源成功了哪个失败了看不出来

修改后:

python 复制代码
async def sync_all_sources():
    sources = await get_active_sources()
    if not sources:
        logger.warning("No active sources found, skipping sync")
        return
    
    results = await asyncio.gather(
        *[fetch_with_timeout(s) for s in sources],
        return_exceptions=True
    )
    
    successful = []
    for source, result in zip(sources, results):
        if isinstance(result, Exception):
            logger.error(f"Failed to fetch {source.name}: {result}")
            await record_failure(source, result)
        else:
            successful.append(result)
            logger.info(f"Fetched {len(result)} records from {source.name}")
    
    if not successful:
        logger.error("All sources failed, aborting sync")
        return
    
    merged = merge_results(successful)
    
    try:
        await save_to_db(merged)
        logger.info(f"Synced {len(merged)} records from {len(successful)}/{len(sources)} sources")
    except Exception as e:
        # 保存到本地文件作为兜底
        await dump_to_local(merged)
        logger.error(f"DB save failed, dumped to local: {e}")
        raise


async def fetch_with_timeout(source, timeout: float = 30.0):
    try:
        return await asyncio.wait_for(
            fetch_source(source),
            timeout=timeout
        )
    except asyncio.TimeoutError:
        raise TimeoutError(f"{source.name} timed out after {timeout}s")

代码量多了不少,但每一行都有存在的理由。线上跑了两周,零事故。

总结

AI 编码代理是真的好用,我现在几乎每天都在用。但它不是魔法------它是一个写代码特别快、格式特别好、但不会主动替你想边界条件的"初级工程师"。

你不会让一个刚入职的实习生写完代码直接上线,对吧?同样的道理。

三个核心原则:

  1. AI 写初版,人 review 逻辑------别因为代码好看就跳过 review
  2. 用 AI 攻击 AI------让它给自己写异常路径测试
  3. 关键路径手动确认------涉及钱、权限、数据安全的逻辑,自己过一遍

掌握了这套方法,AI 编码代理就不是定时炸弹,而是真正的效率倍增器。

相关推荐
ZzT5 小时前
cc 写了一周代码,烧了 $2203 Token — 于是我让它给自己做了个账本
ai编程·claude·vibecoding
huohuopro5 小时前
提升你的claude能力--Everything Claude Code 本地化配置手册
claude·everything
xyzso1z5 小时前
基于 Claude Code Hooks 的 IP 地理位置检测达到账号防封方案记录
claude·anthropic·账号安全·claude code·封号防护
deephub7 小时前
Claude Code 命令体系解析:三种类型、七大分类、50+ 命令
人工智能·大语言模型·claude·claude code
不可能的是9 小时前
我是怎么搞清楚 Claude Code 每天用了多少 token
aigc·ai编程·claude
demo007x1 天前
如何提高 AI 做小程序的效率?
微信小程序·ai编程·claude
门豪杰1 天前
Ubuntu下安装Claude Code
linux·运维·ubuntu·claude·claude code
JimmtButler1 天前
我用 Claude Code 给 Claude Code 做了一个 DevTools
后端·claude
AntonCook1 天前
我打通了飞书→OpenClaw→Claude Code 的完整链路
ai编程·claude