Vibecoding写了3天代码,上线1小时就炸了——5个血泪教训

Vibecoding写了3天代码,上线1小时就炸了——5个血泪教训

上周三下午,我用Claude Code vibecoding了一个内部工具——需求管理看板,类似简化版Linear。3天时间,从零到能跑的全栈应用,React前端+FastAPI后端+PostgreSQL。本地demo效果炸裂,同事看了都说"这速度牛逼"。

周五晚上部署上线。周六早上9点,收到告警:API响应时间从200ms飙到12秒,数据库连接池打满,前端白屏。

修了4个小时。

这篇文章不是来劝退vibecoding的。恰恰相反,我现在每天还在用。但那次翻车让我想明白了几件事,都是AI生成代码时特别容易踩、人工review又很难一眼看出来的坑。


坑一:AI写的数据库查询,N+1是标配

我的看板有个功能:展示每个需求的评论数和最新评论时间。AI生成的代码大概长这样:

@app.get("/requirements")
async def list_requirements(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Requirement))
    requirements = result.scalars().all()
    
    items = []
    for req in requirements:
        # 每个需求单独查一次评论
        count_result = await db.execute(
            select(func.count(Comment.id))
            .where(Comment.requirement_id == req.id)
        )
        comment_count = count_result.scalar()
        
        latest_result = await db.execute(
            select(Comment.created_at)
            .where(Comment.requirement_id == req.id)
            .order_by(Comment.created_at.desc())
            .limit(1)
        )
        latest_comment = latest_result.scalar()
        
        items.append({
            "id": req.id,
            "title": req.title,
            "comment_count": comment_count,
            "latest_comment": latest_comment,
        })
    return items

本地跑,5条需求,毫秒级响应,完全没问题。

线上有300条需求。每条需求触发2次数据库查询,加上主查询,总共601次SQL。连接池默认10个连接,请求排队,响应时间指数级增长。

修法:

@app.get("/requirements")
async def list_requirements(db: AsyncSession = Depends(get_db)):
    # 一次查询搞定:子查询聚合评论数据
    comment_stats = (
        select(
            Comment.requirement_id,
            func.count(Comment.id).label("comment_count"),
            func.max(Comment.created_at).label("latest_comment"),
        )
        .group_by(Comment.requirement_id)
        .subquery()
    )
    
    query = (
        select(Requirement, comment_stats)
        .outerjoin(comment_stats, Requirement.id == comment_stats.c.requirement_id)
    )
    
    result = await db.execute(query)
    rows = result.all()
    
    return [
        {
            "id": req.id,
            "title": req.title,
            "comment_count": row.comment_count or 0,
            "latest_comment": row.latest_comment,
        }
        for req, row in rows
    ]

300条需求,1次查询,耗时从12秒降到45ms。

教训:AI默认生成的是"能跑"的代码,不是"能扛"的代码。凡是涉及列表+关联数据的场景,必须手动检查SQL执行次数。


坑二:前端状态管理,AI特别喜欢全局store

看板需要拖拽排序。AI给我生成的方案:一个巨大的zustand store,所有状态都在里面。

// AI生成的store(简化版)
const useBoardStore = create((set, get) => ({
  requirements: [],
  columns: [],
  comments: {},
  users: {},
  filters: { status: 'all', assignee: 'all', priority: 'all' },
  sortBy: 'created_at',
  dragState: null,
  selectedIds: [],
  modalState: null,
  
  fetchRequirements: async () => { /* ... */ },
  updateRequirement: async (id, data) => { /* ... */ },
  moveRequirement: async (id, columnId, position) => { /* ... */ },
  addComment: async (reqId, content) => { /* ... */ },
  // 还有15个方法...
}));

本地跑,一切正常。但拖拽排序的时候,整个看板在抖——每次拖拽触发store更新,所有依赖store的组件全部重渲染。40个卡片,每个卡片有头像、标签、进度条,一次拖拽触发40次组件渲染。

修法:拆store,用selector做精准订阅。

// 拆分后的store
const useDragStore = create((set) => ({
  dragState: null,
  setDragState: (state) => set({ dragState: state }),
}));

const useFilterStore = create((set) => ({
  filters: { status: 'all', assignee: 'all', priority: 'all' },
  setFilters: (filters) => set({ filters }),
}));

// 组件只订阅自己需要的slice
function RequirementCard({ id }) {
  // 只在这一条数据变化时重渲染
  const requirement = useBoardStore(
    (state) => state.requirements.find(r => r.id === id),
    shallow
  );
  const dragState = useDragStore((s) => s.dragState);
  
  // ...
}

拖拽丝滑,重渲染从40次降到2次(拖拽源和目标)。

教训:AI生成状态管理代码时,倾向于"一个store管所有",因为这样最简单。数据量小的时候看不出问题,一旦列表超过20项,性能问题立刻暴露。


坑三:错误处理?AI说"我忘了"

这是最隐蔽的坑。AI生成的异步代码,错误处理经常是这样的:

@app.post("/requirements/{req_id}/comments")
async def add_comment(
    req_id: int, 
    comment: CommentCreate, 
    db: AsyncSession = Depends(get_db)
):
    new_comment = Comment(
        requirement_id=req_id,
        content=comment.content,
        author_id=comment.author_id,
    )
    db.add(new_comment)
    await db.commit()
    return {"id": new_comment.id}

看起来没问题对吧?但线上出现了这种情况:前端发了请求,后端commit成功了,但返回响应时网络断了。前端收到超时错误,认为没创建成功,用户又点了一次。结果同一条评论出现了两次。

更惨的是,没有try-except,数据库constraint violation直接返回500,前端拿到的是HTML格式的报错页面,JSON解析失败,白屏。

修法:

from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError

@app.post("/requirements/{req_id}/comments")
async def add_comment(
    req_id: int, 
    comment: CommentCreate, 
    db: AsyncSession = Depends(get_db)
):
    # 1. 先校验需求是否存在
    req = await db.get(Requirement, req_id)
    if not req:
        raise HTTPException(status_code=404, detail="需求不存在")
    
    # 2. 校验作者是否存在
    author = await db.get(User, comment.author_id)
    if not author:
        raise HTTPException(status_code=400, detail="作者不存在")
    
    # 3. 幂等性:检查是否已存在相同内容的评论(防重复提交)
    existing = await db.execute(
        select(Comment).where(
            Comment.requirement_id == req_id,
            Comment.author_id == comment.author_id,
            Comment.content == comment.content,
            Comment.created_at > func.now() - text("interval '1 minute'"),
        )
    )
    if existing.scalar():
        raise HTTPException(status_code=409, detail="请勿重复提交")
    
    try:
        new_comment = Comment(
            requirement_id=req_id,
            content=comment.content,
            author_id=comment.author_id,
        )
        db.add(new_comment)
        await db.commit()
        await db.refresh(new_comment)
        return {"id": new_comment.id, "created_at": new_comment.created_at}
    except IntegrityError:
        await db.rollback()
        raise HTTPException(status_code=400, detail="数据完整性错误")

教训:AI生成的代码默认假设"一切顺利"。网络会断、数据库会冲突、用户会双击。凡是写操作,必须手动补上:输入校验、幂等性、异常捕获、事务回滚。


坑四:AI不知道你的环境差异

本地用SQLite开发,线上用PostgreSQL。AI生成的代码里藏着这种:

# AI生成的日期查询
from datetime import datetime, timedelta

# 这行在SQLite上跑得通,PostgreSQL上会报错
recent = await db.execute(
    select(Requirement).where(
        Requirement.created_at > datetime.now() - timedelta(days=7)
    )
)

SQLite对timedelta减法有隐式支持,PostgreSQL不行。更隐蔽的还有:

  • String类型的字段,SQLite不区分长度,PostgreSQL的VARCHAR(255)会截断
  • 布尔值,SQLite用0/1,PostgreSQL用true/false
  • JSON字段操作语法完全不同
  • 修法是加一个环境检查脚本,在CI里跑:

    # scripts/check_db_compat.py
    """检查代码中的数据库兼容性问题"""
    
    import ast
    import sys
    
    ISSUES = []
    
    class DBChecker(ast.NodeVisitor):
        def visit_Call(self, node):
            # 检查 timedelta 直接用于SQL比较
            if isinstance(node.func, ast.Attribute):
                if node.func.attr in ('scalar', 'all', 'fetchall'):
                    # 检查父节点中是否有timedelta减法
                    pass
            self.generic_visit(node)
    
    def check_file(filepath):
        with open(filepath) as f:
            try:
                tree = ast.parse(f.read())
                DBChecker().visit(tree)
            except SyntaxError:
                pass
    
    if __name__ == "__main__":
        for f in sys.argv[1:]:
            check_file(f)
        if ISSUES:
            for issue in ISSUES:
                print(f"⚠️  {issue}")
            sys.exit(1)
        print("✅ 数据库兼容性检查通过")

    教训:AI不知道你本地和线上的环境差异。每次vibecoding生成数据库相关代码后,必须在目标数据库上跑一遍。docker-compose起个PostgreSQL容器,5分钟搞定,能省5小时debug。


    坑五:安全?AI默认你是内网

    AI生成的API,默认没有认证、没有限流、没有CORS限制。我的看板API长这样:

    # AI生成的:谁都能调
    @app.get("/requirements")
    async def list_requirements():
        ...
    
    @app.delete("/requirements/{req_id}")
    async def delete_requirement(req_id: int):
        ...

    部署到线上,任何人知道URL就能删数据。

    修法——加上认证和限流中间件:

    from fastapi import Depends, HTTPException, Request
    from fastapi.middleware.cors import CORSMiddleware
    from slowapi import Limiter
    from slowapi.util import get_remote_address
    
    limiter = Limiter(key_func=get_remote_address)
    app.state.limiter = limiter
    
    # CORS:只允许你的前端域名
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["https://your-board.example.com"],
        allow_methods=["GET", "POST", "PUT", "DELETE"],
        allow_headers=["*"],
        allow_credentials=True,
    )
    
    # 认证依赖
    async def verify_token(request: Request):
        token = request.headers.get("Authorization", "").replace("Bearer ", "")
        if not token:
            raise HTTPException(status_code=401, detail="未登录")
        user = await validate_token(token)  # 你的token验证逻辑
        if not user:
            raise HTTPException(status_code=401, detail="Token无效")
        return user
    
    # 所有写操作都要认证
    @app.post("/requirements")
    @limiter.limit("30/minute")
    async def create_requirement(
        request: Request,
        data: RequirementCreate,
        user = Depends(verify_token),
    ):
        ...

    教训:AI生成的代码默认信任所有请求。部署前必须过一遍安全检查清单:认证、限流、CORS、SQL注入(ORM通常能防)、输入长度限制。


    现在怎么用Vibecoding

    踩完这些坑之后,我调整了工作流:

  • Vibecoding出初版——让AI快速生成框架代码,这步不变,效率确实高
  • 手动过五关——N+1查询、状态管理、错误处理、环境差异、安全配置
  • 用真实数据测试——至少用100条以上的数据量跑一遍
  • Docker环境验证——本地docker-compose模拟线上环境
  • 3天的vibecoding + 1天的五关检查,比从零手写快3倍,比纯vibecoding直接上线稳定10倍。

    Vibecoding最迷人的地方,是它让"开始做一个东西"变得特别轻。但很多人也是从这里开始翻车的——代码跑起来了,看起来能用了,就直接部署了。

    别急。多花一天过五关,省得周末修4个小时。



    关注「安全值班室」公众号

    每天一篇AI安全早报 + 实战攻防案例

    关注安全值班室

    posted on 2026-05-22 09:01  明.Sir  阅读(20)  评论(0)    收藏  举报

    导航