上周五晚上,我信心满满地让 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
except或except Exception?异常被吞了还是传播了? - 资源管理 :文件、连接、session 有没有用
with或try-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 编码工作流
现在我的日常流程是这样的:
- 描述需求:用自然语言告诉 AI 我要做什么,包括边界条件和错误处理要求
- AI 生成代码:让它写初版
- 对照 checklist review:过一遍上面的清单,标出问题
- 让 AI 写测试:专注异常路径
- 跑测试修 bug:通常第一轮会发现 2-3 个问题
- 手动确认关键逻辑:特别是涉及金额、权限、数据删除的部分
整个过程比纯手写快 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 一过,问题全出来了:
asyncio.gather默认某个 task 失败会立即抛异常,其他 task 的结果丢失- 没有超时控制,某个数据源响应慢会拖住整个任务
- 没有重试机制
save_to_db失败了数据就丢了,没有持久化中间结果- 日志不够,哪个源成功了哪个失败了看不出来
修改后:
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 编码代理是真的好用,我现在几乎每天都在用。但它不是魔法------它是一个写代码特别快、格式特别好、但不会主动替你想边界条件的"初级工程师"。
你不会让一个刚入职的实习生写完代码直接上线,对吧?同样的道理。
三个核心原则:
- AI 写初版,人 review 逻辑------别因为代码好看就跳过 review
- 用 AI 攻击 AI------让它给自己写异常路径测试
- 关键路径手动确认------涉及钱、权限、数据安全的逻辑,自己过一遍
掌握了这套方法,AI 编码代理就不是定时炸弹,而是真正的效率倍增器。