Vibe Coding 一时爽,线上事故火葬场:我的三次 AI 编程翻车实录

2 阅读1分钟

Vibe Coding 一时爽,线上事故火葬场:我的三次 AI 编程翻车实录

本文参与掘金活动:谁还没被AI坑过

前言

"Vibe Coding" 这个词,相信各位开发者都不陌生。自从 Andrej Karpathy 提出这个概念以来,无数开发者(包括我)开始沉迷于"让 AI 写代码,我只负责描述需求"的快感中。

说实话,第一次用 Cursor 让它帮我写完一个完整的 CRUD 接口时,我整个人都飘了——"以后写代码就是动动嘴皮子的事儿"。

那段时间我几乎魔怔了,逢人就安利:"你试试 Cursor,真的,写代码效率提升 10 倍不是梦!"甚至在团队周会上,我还专门做了一次分享,标题就叫《Vibe Coding:后端开发的未来》。

然而,现实很快给了我三记响亮的耳光。

今天不聊理论,直接分享我在真实项目中被 AI 坑得最惨的三次经历,希望能给各位提个醒。


第一坑:AI 帮我写的"优雅"代码,把数据库锁了 40 分钟

事故现场

那是一个再普通不过的需求:批量更新用户状态。我用 Cursor 描述了一下需求:

"帮我写一个批量更新用户状态的接口,要优雅一点"

AI 给出的代码看起来非常"优雅":

async def batch_update_user_status(user_ids: list, new_status: str):
    """批量更新用户状态"""
    async with get_db_session() as session:
        # AI 觉得这样写很 "Pythonic"
        users = await session.execute(
            select(User).where(User.id.in_(user_ids)).with_for_update()
        )
        for user in users.scalars():
            user.status = new_status
            user.updated_at = datetime.now()
            await asyncio.sleep(0.01)  # AI 说这是 "模拟真实业务延迟"
        await session.commit()

看着挺对的吧?with_for_update() 加行锁,逐个更新,最后提交。代码风格优雅,注释清晰,甚至还有类型标注。我当时还觉得 AI 真贴心,连异步处理都考虑到了。

问题在哪?

user_ids 有 5000 个的时候,这段代码会:

  1. 对 5000 行加排他锁
  2. 循环中 await asyncio.sleep(0.01) 让整个事务持续 50 秒
  3. 锁持有期间,所有涉及这些用户的查询全部阻塞

上线当天下午,告警群炸了——数据库连接池耗尽,大量请求超时。DBA 同事冲过来问我:"你那个批量更新接口怎么回事?数据库里全是锁等待!"

我当时还一脸懵:"不可能啊,代码是 AI 帮我写的,很优雅的..."

DBA 同事看了一眼代码,沉默了三秒钟,然后说了一句让我至今难忘的话:"AI 写的代码,你不 review 的吗?"

修复过程

# ❌ AI 给的代码:逐行更新,锁持有时间长
for user in users:
    user.status = new_status
    await asyncio.sleep(0.01)  # 无意义的延迟

# ✅ 正确做法:批量操作,减少锁持有时间
async def batch_update_user_status(user_ids: list, new_status: str):
    async with get_db_session() as session:
        await session.execute(
            update(User)
            .where(User.id.in_(user_ids))
            .values(status=new_status, updated_at=datetime.now())
        )
        await session.commit()

教训:AI 不会帮你考虑并发和性能,它只会写出"看起来对"的代码。批量操作的边界条件、锁策略、性能影响,这些都得你自己把关。


第二坑:RAG 检索出来的"正确"答案,让客服回复了错误信息

事故现场

我们做了一个基于 RAG 的客服助手,用 LangChain + Milvus 搭建。测试阶段效果惊艳,领导拍板上线。

技术方案看起来很完美:

  • 文档切片 → 向量化 → 存入 Milvus
  • 用户提问 → 向量检索 → 取 Top 5 相关文档 → 送入 LLM 生成回答

上线第一周,一切正常。直到有个用户问:"你们的退货政策是什么?"

AI 回复:

"根据我们的政策,商品签收后 90 天内 可以无理由退货,运费由买家承担。"

实际政策是 7 天

用户拿着这个回复截图发了条微博,阅读量 10 万+,标题是《XX 公司客服 AI 竟然胡说八道,90 天退货政策是真是假?》。

公关部门连夜加班处理,我则被叫到会议室"喝茶"。

问题在哪?

排查后发现,知识库里有两份文档:

  • 一份是 2024 年的退货政策(7 天)
  • 一份是某个竞品的分析报告,提到了"某平台 90 天退货政策"

向量检索时,"退货政策" 和竞品分析报告的相似度竟然更高(0.87 vs 0.82),因为那份报告里"退货政策"这个词出现的频率更高。

更坑的是,那份竞品分析报告的标题是《行业标杆:XX 平台 90 天退货政策深度解析》,里面的措辞非常肯定,AI 自然就"信以为真"了。

AI 忠实地执行了 RAG 的指令:用检索到的内容回答问题。它不知道这份文档是竞品的,也不知道 90 天和 7 天在业务上有多大的区别。

修复方案

# 不是代码的问题,是架构设计的问题
# 需要增加:
# 1. 文档来源标注
# 2. 置信度阈值
# 3. 多路召回 + 交叉验证
# 4. 关键信息的人工审核机制

retriever = VectorRetriever(
    score_threshold=0.85,  # 置信度阈值
    top_k=5,
    reranker=CohereReranker()  # 二次排序
)

# 关键信息必须标注来源
response = llm.invoke(
    f"基于以下参考资料回答问题,如果不确定请说'我不确定'。\n"
    f"注意:请优先参考公司官方文档,而非第三方分析报告。\n"
    f"参考资料:{context}\n"
    f"用户问题:{query}"
)

教训:RAG 不是万能药。向量相似度不等于语义正确性。关键业务场景必须有兜底机制,不能让 AI 自由发挥。


第三坑:AI 生成的正则表达式,匹配了所有人的手机号

事故现场

这是一个看起来最简单的需求:验证用户输入的手机号格式。我让 Copilot 帮我写正则:

import re

# AI 给出的正则
PHONE_PATTERN = r'^1[3-9]\d{9}$'

def validate_phone(phone: str) -> bool:
    return bool(re.match(PHONE_PATTERN, phone))

看起来没问题对吧?标准的中国大陆手机号正则。我当时还特意测试了几个号码,都能正确匹配,就直接提交了。

问题在哪?

上线后,运营反馈:"为什么所有用户的手机号都能通过验证?"

我一看日志,发现用户输入的是各种格式:

  • 138-1234-5678(带横杠)
  • +86 13812345678(带国际区号)
  • 138 1234 5678(带空格)
  • (010)12345678(带括号的座机)
  • 13812345678@qq.com(把邮箱当手机号填了)
  • 12345678901(随便填的 11 位数字)

AI 的正则 ^1[3-9]\d{9}$ 只能匹配纯数字格式。但我没告诉它要支持哪些格式,它就按最标准的来了。

更坑的是,前端那边用的是另一个正则,两边不一致导致数据对不上。前端以为验证通过了就直接提交,后端以为格式不对就静默失败,用户则以为自己填对了。

最后排查了两天,才发现是正则不一致的问题。

修复方案

# ✅ 正确做法:先清洗再验证
import re

def normalize_phone(phone: str) -> str:
    """去除所有非数字字符"""
    digits = re.sub(r'\D', '', phone)
    # 处理国际区号
    if digits.startswith('86'):
        digits = digits[2:]
    return digits

def validate_phone(phone: str) -> tuple[bool, str]:
    """验证手机号,返回 (是否有效, 标准化后的号码)"""
    normalized = normalize_phone(phone)
    if not re.match(r'^1[3-9]\d{9}$', normalized):
        return False, ""
    return True, normalized

教训:AI 写的正则只能处理"理想输入"。真实世界的用户输入千奇百怪,你必须考虑数据清洗和边界情况。而且,前后端的校验规则必须统一定义,不能各写各的。


反思:Vibe Coding 的正确姿势

被坑了三次之后,我总结出了一套"防坑指南":

1. AI 是副驾驶,不是主驾驶

AI 擅长:                    你不该指望 AI:
✅ 快速生成代码骨架            ❌ 考虑并发和性能
✅ 处理标准化的 CRUD           ❌ 理解业务上下文
✅ 写单元测试                  ❌ 保证数据一致性
✅ 解释报错信息                ❌ 预测用户行为
✅ 生成文档和注释              ❌ 处理安全边界

2. 建立"三审"机制

# 我现在的代码审查流程
review_checklist = {
    "功能正确性": "AI 生成的代码逻辑对不对?",
    "边界条件": "空值、超大输入、并发场景?",
    "安全风险": "SQL 注入、XSS、敏感信息泄露?",
    "性能影响": "时间复杂度、内存占用、数据库查询次数?",
    "可维护性": "代码可读性、错误处理、日志记录?"
}

3. 让 AI 帮你测试,而不只是写代码

# 我现在会让 AI 生成测试用例,然后用测试来验证它写的代码
prompt = """
请为以下函数生成单元测试用例,覆盖:
1. 正常输入
2. 边界条件(空值、极大值、极小值)
3. 异常输入(类型错误、格式错误)
4. 并发场景(如果适用)

函数代码:{code}
"""

4. 关键业务逻辑,AI 写完必须人工 review

我现在的工作流是这样的:

  1. 用 AI 生成代码骨架
  2. 人工 review 核心逻辑
  3. AI 生成测试用例
  4. 跑测试,发现问题再迭代
  5. 上线前做 code review

虽然比"纯 Vibe Coding"慢了一点,但至少不会翻车。


写在最后

Vibe Coding 确实很爽,但它就像自动驾驶——你可以让它帮你处理大部分场景,但你必须随时准备接管方向盘。

被 AI 坑不可怕,可怕的是被坑了还不知道为什么。每一次翻车都都是一次学习机会,关键是你要搞清楚:是你的 prompt 写得不够好,还是 AI 本身就处理不了这种场景?

如果你也有被 AI 坑过的经历,欢迎在评论区分享,让我们一起避坑!


💡 本文所有代码示例均为简化版本,实际项目中需要根据具体场景调整。

📌 参考资料: