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)会截断修法是加一个环境检查脚本,在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
踩完这些坑之后,我调整了工作流:
3天的vibecoding + 1天的五关检查,比从零手写快3倍,比纯vibecoding直接上线稳定10倍。
Vibecoding最迷人的地方,是它让"开始做一个东西"变得特别轻。但很多人也是从这里开始翻车的——代码跑起来了,看起来能用了,就直接部署了。
别急。多花一天过五关,省得周末修4个小时。
关注「安全值班室」公众号
每天一篇AI安全早报 + 实战攻防案例
浙公网安备 33010602011771号