AI 写的代码通过了所有测试,然后搞崩了生产环境
上个月,我一个朋友在团队里推了 AI 辅助编程。Cursor、Copilot 全用上,开发效率翻了一倍——至少前两周是这样的。
直到第三周,线上出了个 bug:用户在高峰期偶尔会看到别人的订单数据。不是每次都发生,一天大概三四次,很难复现。
查了一下午,定位到一段 AI 生成的代码——一个看似很标准的并发请求处理逻辑。代码写得漂亮,注释齐全,测试覆盖率 95%,CI 每次都是绿的。但有一个并发竞争条件,测试没覆盖到,Code Review 也没看出来。
修完 bug 后,他在群里说了句话:
"AI 写的代码通过了所有测试,然后搞崩了生产环境。"
这句话我记到现在。因为它触及了一个很多人在回避的问题:当 AI 能写代码也能写测试的时候,"测试通过"这四个字意味着什么?
AI 生成的"完美代码"
先看一个典型的场景。
你让 AI 实现一个用户余额扣减的功能:
def deduct_balance(user_id: str, amount: float) -> bool:
"""扣减用户余额,余额不足时返回 False"""
user = db.get_user(user_id)
if user.balance < amount:
return False
user.balance -= amount
db.save_user(user)
return True
然后你让 AI 写测试:
def test_deduct_sufficient_balance():
user = create_user(balance=100.0)
assert deduct_balance(user.id, 50.0) == True
assert db.get_user(user.id).balance == 50.0
def test_deduct_insufficient_balance():
user = create_user(balance=30.0)
assert deduct_balance(user.id, 50.0) == False
assert db.get_user(user.id).balance == 30.0
def test_deduct_exact_balance():
user = create_user(balance=50.0)
assert deduct_balance(user.id, 50.0) == True
assert db.get_user(user.id).balance == 0.0
def test_deduct_negative_amount():
user = create_user(balance=100.0)
assert deduct_balance(user.id, -10.0) == False
测试写了四个,覆盖充足、不足、刚好、负数四种情况。运行一下,全绿。覆盖率 92%。看起来没问题。
然后呢?
然后两个请求同时到达,同时读到 balance=100,同时判断 100 >= 50,同时扣减,同时写入 balance=50。本来应该扣两次变成 0,结果只扣了一次。用户的钱凭空多了 50 块。
这叫并发竞争条件(Race Condition)。在生产环境里,它是低频的、难以复现的、但一旦触发后果严重的 bug。
测试没覆盖到,不是因为写测试的人能力不够,而是因为这个 bug 不在任何人的"显而易见"列表里。
AI 更不可能想到。AI 生成测试的逻辑是:理解代码 → 找出明显的分支 → 为每个分支写一个用例。它不会想到"如果有人在高并发下调用这个函数会怎样",因为代码本身没有显式的并发逻辑——并发是系统层面的问题,不是函数层面的问题。
测试覆盖率的三个幻觉
幻觉一:行覆盖 = 质量覆盖
很多团队用行覆盖率(Line Coverage)作为质量指标:80% 以上算合格,90% 以上算优秀。
AI 很擅长刷这个指标。因为它知道怎么写测试才能让每一行都走到。
但行覆盖率是最浅的质量指标。它只告诉你"这行代码被执行过",不告诉你"这行代码在所有有意义的输入下都表现正确"。
看一个例子:
def calculate_discount(price: float, user_tier: str) -> float:
discounts = {
'bronze': 0.05,
'silver': 0.10,
'gold': 0.15,
'platinum': 0.20,
}
rate = discounts.get(user_tier, 0.0)
return round(price * (1 - rate), 2)
AI 生成的测试可能长这样:
def test_bronze_discount():
assert calculate_discount(100.0, 'bronze') == 95.0
def test_silver_discount():
assert calculate_discount(100.0, 'silver') == 90.0
def test_gold_discount():
assert calculate_discount(100.0, 'gold') == 85.0
def test_unknown_tier():
assert calculate_discount(100.0, 'unknown') == 100.0
行覆盖率 100%。每个分支都走过了。
但有一个问题没被测到:round(price * (1 - rate), 2) 在处理某些浮点数时会有精度问题。比如:
>>> calculate_discount(19.99, 'silver')
17.99 # 看起来对
>>> calculate_discount(0.29, 'gold')
0.24 # 但 0.29 * 0.85 在浮点数里是 0.24649999999999999
# round 后是 0.25,而不是直觉上的 0.24
这不是 AI 的错,是浮点数运算的固有特性。但如果你在做金融相关的应用,这个精度问题可能导致账单对不上。行覆盖率告诉你"这行代码被执行过了",但它不会告诉你"这个计算在所有价格下都是准确的"。
更深入的覆盖指标——比如分支覆盖(每个条件分支的真假都测过)和路径覆盖(每条可能的执行路径都测过)——会更严格,但也更难达到。AI 生成的测试通常停留在行覆盖层面,因为这是最省力、产出最快的方式。
幻觉二:测试用例 = 边界条件
AI 生成测试用例的模式很固定:
- 正常输入(Happy Path)
- 明显的不合法输入(负数、null、空字符串)
- 边界值(0、最大值、最小值)
这个模式是对的,但不够。真正的 bug 通常藏在 AI 想不到的边界条件里:
| AI 通常会测 | AI 通常不会测 |
|---|---|
| 空字符串 | Unicode 表情符号、RTL 文字 |
| null | 部分字段为 null 的对象 |
| 负数 | 浮点数精度问题、NaN、Infinity |
| 单个元素 | 10 万元素的列表性能 |
| 正常日期 | 闰年、时区切换、夏令时 |
| 单线程调用 | 并发调用、重入调用 |
| 正常网络 | 超时、重试、部分失败 |
这不是说 AI 笨。是说这些边界条件需要领域知识。
AI 不知道你在做金融应用,所以它不会想到去测浮点精度。AI 不知道你的用户在中东,所以它不会想到去测 RTL 文字。AI 不知道你的服务部署在三个时区,所以它不会想到去测夏令时切换。
这些不是"测试知识",是"业务知识"。而业务知识,只有你(或你的团队)知道。
幻觉三:通过的测试 = 正确的行为
这是最危险的幻觉。
测试通过了,只说明"代码做了测试要求它做的事"。如果测试本身的要求是错的,代码就会"正确地做错事"。
AI 写测试时,它的需求来源是什么?是你的 prompt。如果你的 prompt 说"写一个函数,把用户输入存到数据库",AI 就会写一个函数,把用户输入存到数据库。然后它会写一个测试,验证"输入被存到了数据库"。
但如果你的 prompt 没说"要先做 SQL 注入过滤"呢?AI 不会自动加上。代码能工作,测试能通过,但存在安全漏洞。
更微妙的情况是,AI 可能写了一个过于宽松的断言:
def test_process_orders():
result = process_orders(orders)
assert len(result) > 0 # 只要返回了东西就算过
assert isinstance(result[0], dict) # 只要是 dict 就算过
这个测试能告诉你"函数返回了非空的 dict 列表"。但它不能告诉你"返回的数据是正确的"。如果 process_orders 内部有个 bug 导致金额算错了,这个测试照样通过。
这就是所谓的弱断言(Weak Assertion)。AI 倾向于写弱断言,因为弱断言更容易写、更容易通过、更不容易因为实现细节变化而失败。但弱断言的保护作用也弱得多。
AI 代码的真正风险在哪
说了这么多,我不是在说"AI 写不出好代码"。
恰恰相反,AI 写的代码在模式正确性、代码风格、常见错误的避免上,往往比很多初级开发者写得好。AI 不会忘记检查 null,不会写出明显的 off-by-one 错误,不会忘记关闭文件句柄。
AI 的问题不是"写不好代码",而是"不知道你在乎什么"。
具体来说:
AI 不知道你的业务约束。 你知道你的应用最多同时处理 1000 个请求,AI 不知道。你知道你的用户群体有 30% 用安卓旧版本,AI 不知道。你知道财务数据必须精确到分,AI 不知道。
AI 不知道你的基础设施限制。 你知道数据库连接池只有 20 个,AI 不知道。你知道某个外部 API 有每分钟 100 次的限流,AI 不知道。你知道你们的监控系统不支持某种日志格式,AI 不知道。
AI 不会考虑维护成本。 AI 写了一段用三个嵌套装饰器加元编程实现的逻辑,功能完全正确,测试全绿。但三个月后你的同事要加一个新功能,花了两天才看懂这段代码在干什么。AI 不会考虑"这段代码的可读性",因为它不读代码,它生成代码。
AI 不会考虑故障排查。 当线上出了问题,需要看日志定位问题时,AI 生成的代码可能没有足够的日志点、没有有意义的错误信息、没有清晰的错误传播链路。因为测试不需要这些东西——测试在受控环境下运行,不需要日志来排查问题。
怎么跟 AI 合作写出真正可靠的代码
问题不在 AI,在协作方式。
如果你把 AI 当外包——"帮我实现这个功能,写好测试"——那你会得到外包级别的质量:功能实现了,测试通过了,但深度不够。
如果你把 AI 当结对编程的搭档——你负责方向和边界,AI 负责实现和速度——那结果会好得多。
下面是几个具体的策略。
策略一:你来写测试,让 AI 写代码
这是最有效的方式。测试是需求的精确表达——当你手写测试时,你在做一件 AI 做不到的事:明确地定义什么是"正确"。
# 你来写测试——这些测试包含了你的业务知识
def test_deduct_concurrent_requests():
"""并发扣减时,不能出现超额扣减"""
user = create_user(balance=100.0)
# 模拟 10 个并发请求,每个扣 10 元
results = concurrent_map(
lambda _: deduct_balance(user.id, 10.0),
range(10)
)
# 最多只能成功 10 次
assert sum(1 for r in results if r) <= 10
# 最终余额不能为负
assert db.get_user(user.id).balance >= 0
这个测试表达了一个业务约束:并发扣减不能出现超额。AI 看到这个测试后,它会自动选择加锁或用其他并发安全的方式来实现。
关键不是谁来写代码,是谁来定义"正确"。
策略二:让 AI 写代码,你来写边界测试
如果你让 AI 同时写代码和测试,你可以做一件事:看完 AI 的代码后,自己补充边界测试。
# AI 生成的测试(它会写这些)
def test_happy_path(): ...
def test_null_input(): ...
# 你补充的测试(AI 想不到的)
def test_concurrent_access(): ... # 并发安全
def test_very_large_input(): ... # 性能边界
def test_unicode_emoji(): ... # 字符编码
def test_clock_skew(): ... # 时间同步问题
def test_partial_failure_recovery(): ... # 故障恢复
这个过程本身就是 code review。通过写测试,你在强迫自己思考"这段代码在什么情况下会出问题"。这个思考过程,比看十遍代码都有效。
策略三:Code Review 聚焦"AI 想不到的事"
AI 生成的代码,不需要审风格。AI 的代码风格通常比大部分人都好。
把 code review 的精力集中在 AI 的盲区上:
- 业务逻辑:这个实现符合业务规则吗?有没有遗漏的业务约束?
- 异常处理:如果外部服务挂了会怎样?如果数据库超时了会怎样?
- 安全性:输入做了校验吗?权限检查到位了吗?有没有注入风险?
- 性能:这个算法在大数据量下还行吗?有没有 N+1 查询?
- 可观测性:出问题时能排查吗?日志够吗?指标有吗?
这些不是 AI 的弱项,是 AI 的信息盲区。它不是想不到,是不知道你需要想到。
策略四:要求 AI 解释它的测试策略
这是一个简单但有效的方法。让 AI 写完代码和测试后,问它一个问题:
你测了哪些情况?没测哪些?为什么?
AI 会给你一个列表:
已测试:
- 正常余额扣减 ✓
- 余额不足 ✓
- 刚好扣完 ✓
- 负数金额 ✓
未测试:
- 并发请求(需要多线程测试框架)
- 数据库操作失败的情况(需要 mock)
- 超大金额(浮点精度问题)
这个列表本身就是有价值的。它告诉你哪些边界是 AI 意识到的但选择跳过的(可能是因为实现复杂度高),哪些是 AI 完全没意识到的(你可以提醒它)。
你可以接着说:"把并发和数据库失败的情况也测了。"AI 会补充相应的测试。
一个完整的工作流示例
最后,看一个实际的工作流。假设要实现一个请求限流器(Rate Limiter)。
❌ 错误的做法
你:帮我写一个限流器,每分钟最多 100 次请求
AI:(生成代码 + 测试,测试全绿)
你:(直接合并)
结果:限流器在单机上工作正常,但你的服务有三台实例,每台独立限流,实际限流效果变成了 300 次/分钟。
✅ 正确的做法
你:帮我写一个限流器,每分钟最多 100 次请求。
注意:我们有三台实例,需要分布式限流。
你先说说你会怎么设计。
AI:(给出方案:用 Redis + sliding window,解释优缺点)
你:好,写代码吧。测试要覆盖:
- 正常限流
- 刚好到阈值
- 超过阈值被拒绝
- 窗口切换
- Redis 不可用时的降级策略
AI:(生成代码 + 测试)
你:(Code Review,聚焦:)
- Redis 连接超时怎么处理?
- 时钟不同步会影响窗口计算吗?
- 如果 Redis 恢复后,旧的限流数据怎么处理?
(发现问题,要求 AI 修改)
AI:(修复)
你:(补充一个手动测试:模拟 Redis 故障,验证降级行为)
合并。
两种方式都用了 AI。但第二种方式产出的代码,质量高了不止一个档次。区别不在于 AI 的能力,在于人的参与深度。
结论
AI 写的代码通过了所有测试,然后搞崩了生产环境。
这个故事的关键不是"AI 不可靠",而是"我们太依赖测试来判断代码质量了"。
测试是必要的,但不够。它只能验证你想到的情况,不能保证你没想到的情况不会出问题。当 AI 同时负责写代码和写测试时,这个 gap 会被放大——因为 AI 的"没想到"和你的"没想到"可能是同一个方向。
AI 不是"代码质量"的替代品,是"代码产出速度"的放大器。 如果你的 code review 流程本来就不够严谨,AI 只会让有问题的代码以更快的速度进入生产环境。
把 AI 当结对编程的搭档。你负责定义正确、设定边界、审查盲区。AI 负责快速实现、处理细节、减少重复劳动。
这样合作,AI 写的代码,测试通过了,生产环境也不会崩。