最近和朋友聊全栈开发,发现一个很有意思的现象:大家手里都有了能写代码的AI,但项目的"可控性"反而成了最稀缺的能力。有人一周就能搭出一个看起来很完整的全栈应用,但代码库三个星期后就没人敢动了——因为没人说得清楚哪条路由会触发哪次数据库变更,哪个helper函数改一次会牵连多少个页面。我自己在去年也经历过这个阶段,从纯"vibe coding"式地让AI顺手写代码,慢慢地转到"harness × SDD"这套有规格、有护栏、有技术组合的打法,项目的稳定性和交付速度才真正走上正轨。
这篇文章就把我现在使用的全栈开发技术组合,以及围绕AI辅助开发的完整工作流拆开讲清楚。不管你是刚开始接触全栈的新人,还是正在被AI生成代码折磨的个人开发者,应该都能从中找到一套可以直接抄作业的方案。
1. 从Vibe Coding说起:为什么全栈项目不能"全靠感觉"
1.1 Vibe Coding是什么,它解决什么问题
Vibe coding 这个词最近几乎成了全栈开发圈的头号热词。它描述的是一种开发方式:开发者用自然语言给AI编程工具下达意图,AI负责生成绝大部分代码,开发者像试衣服一样快速验证结果,满意就继续堆功能,不满意就重新prompt。整个过程很少深入每一行代码的细节,突出一个"顺着感觉走"。
这种模式为什么突然流行?因为工具能力已经到了一个临界点。现在主流AI编程工具在上下文窗口、多文件编辑、重构能力上都进步明显。对于一个几千行以内的原型项目,AI可以覆盖80%以上的编码量。对个人开发者来说,这是巨大红利:以前一个idea到能演示的demo至少要一周,现在可能只需要一天。
但我要先泼一盆冷水:vibe coding适合的是"不用长期维护的原型"或者"一次性的脚本工具"。如果你的目标是做一个正式的全栈产品,那从第一天起就得考虑数据模型会不会改、接口被别的模块消费时怎么兼容、权限漏洞会不会导致越权、代码库未来会不会有第二第三个人加入。这些问题,vibe coding本身完全不关心。
1.2 全栈项目失速的典型信号
全栈开发和纯前端或纯后端开发最大的区别在于:它天然跨界。一个全栈技术组合里,前端路由、后端API、数据库schema、鉴权逻辑、部署环境是五个相互咬合的齿轮,任何一个齿松动,整台机器都会响。AI生成代码时,它看到的是你当前这段prompt的上下文,它并不知道其他齿轮的受力情况,所以特别容易制造"局部正确、全局崩坏"的局面。
我见过很多项目从vibe coding走向失控,失速信号高度一致:
- 数据库字段在代码里被硬编码引用,改了schema后migration没问题,但代码库里留着一堆旧字段名,运行到某个分支才炸。
- API接口的成功路径好用,但错误路径完全没处理。用户输了个非法参数,页面直接白屏。
- 鉴权逻辑复制粘贴到各处,有的接口校验了用户身份,有的接口忘了校验。
- 没有统一的类型定义,前端一个interface、后端一个interface、数据库又是另一套结构,靠"心照不宣"维持着本就脆弱的契约。
这些信号一旦出现,项目再往里加功能,就是拆东墙补西墙。要解决这个问题,我们需要两样东西:一个是让AI"照着图纸干活"的规格(SDD),另一个是让坏代码"进不了主干"的工程护栏(Harness)。下面我会一个个展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 我当前主力使用的全栈技术组合
2.1 前端、后端与数据层:一份可以直接抄的选型表
严格的"最佳技术栈"是不存在的,每个团队有每个团队的偏好。我给出的这套组合,是过去近一年实践下来,和AI协作效率最高、工程约束最清晰、单人/小团队维护成本最低的一套:
| 层级 | 选型 | 选择理由 |
|---|---|---|
| 前端框架 | Next.js 14+ (App Router) + React + TypeScript(strict) | 一体化路由和SSR能力,API Routes能覆盖轻后端场景,TypeScript本身就是第一道护栏 |
| 样式方案 | TailwindCSS | 原子类约束样式边界,AI生成后不易散落大量不可控的CSS文件 |
| 后端 | 优先 Next.js API Routes / 复杂时拆 Hono 或 NestJS | 小项目少维护一套服务;复杂时Hono轻量且类型友好,NestJS适合强规范团队 |
| 数据库 | PostgreSQL | 关系型能力完善,JSON字段也能满足半结构化需求 |
| ORM | Prisma | schema.prisma本身就是可审查的数据规格,AI理解准确,迁移流程清晰 |
| 数据校验 | Zod | 在API入口统一校验,所有AI生成代码必须过Zod再进业务逻辑 |
| 认证 | Auth.js (NextAuth) | 中小型项目成本低,Provider声明式配置 |
| 测试 | Vitest + Playwright | 单元/组件测试与端到端测试各司其职 |
| 部署 | Docker + GitHub Actions + 一台小服务器/Fly.io | 统一开发与生产环境,CI负责硬性质量门禁 |
| 监控 | Sentry | 前后端异常聚合,开发期就能接入 |
你可以看到,我几乎没有选那些"听起来很酷但需要额外心智负担"的组件。这个组合的核心逻辑是:每个组件都要能被AI准确理解和生成,否则它就会成为团队里的黑洞。
2.2 为什么TypeScript严格模式是第一道Harness
有些开发者抵触TypeScript,觉得写类型麻烦。但在AI辅助开发的时代,类型的价值被放大了:AI生成代码时,类型约束就是它最清晰的"不可能出错"边界。如果你关闭了strict,AI可能生成大量any,表面看着没问题,等字段改名时你才知道什么叫痛苦。
我的习惯是 tsconfig.json 里开 "strict": true,再补上几个实用选项:
json复制{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true
}
}
noUncheckedIndexedAccess 和 exactOptionalPropertyTypes 是容易被忽略但对AI生成代码非常友好的两个选项:前者强制你处理数组索引可能为undefined的情况,后者会让可选类型更严谨。这两个开关一旦打开,AI生成的代码里很多潜在bug会在编译期暴露,而不是留到运行时让用户帮你去发现。
2.3 数据层为何选Prisma,而不是Drizzle
如果你去社区看,Drizzle现在的呼声很高,它的SQL风格贴近原生、打包体积小。但我在AI辅助开发这个前提下依然选择Prisma,原因很实际。
第一,schema.prisma 是一个独立的、结构化的数据规格文件,AI对它的理解成本远低于分散在代码里的SQL写法。你可以把它直接喂给AI,让它基于现有schema生成或修改migration,错误率会明显降低。
第二,Prisma Migrate 的可审查性更好。每次schema变更都会生成一个migration目录,里面是清晰的SQL。这让"数据库变更"这件事变得可回滚、可审查。AI生成代码时如果改了schema,我能在prisma diff阶段就发现问题。
第三,Prisma Client的查询API对AI来说默认就是"结构清晰"的。当然并不是说它不会生成N+1查询,而是它的模型API足够直观,便于你在review时快速定位。
2.4 认证、对象存储、任务队列:组件宜少不宜多
这里多说几句横向组件。很多全栈项目一开始就想上Redis队列、Kafka、K8s,我的建议是:不要。你什么时候需要它,你会知道的——通常是在监控里看到数据库压力飙升、用户开始投诉慢查询的时候。在那之前,一个简单的数据库任务表加定时扫描,足以支撑90%的MVP和早期产品。
认证方面,如果你不想维护用户密码重置、邮件验证、会话这些细节,直接用托管方案(Clerk、Auth0等)是更省心的选择。我个人的做法是:如果是快速验证,直接Clerk;如果希望数据完全自己掌控且愿意维护成本,再用Auth.js。全栈开发技术组合里,认证组件是最容易被AI生成代码搞乱的模块,因为涉及cookie、session、CSRF、OAuth回调等一系列安全边界,建议优先选择成熟方案。
3. 规格先行:SDD让AI照着图纸干活
3.1 SDD和传统需求文档的区别
规格驱动开发(Spec-Driven Development,SDD)不是让你写几十页Word文档,而是把需求转成机器和人能共同理解的"契约"。传统需求文档的特点是:大量自然语言、充满"尽量""应当""如果可能"这类模糊词,开发照着做都会跑偏,更别说AI。
SDD的关键在于:每一条规格都要可验证。验收标准不是"功能正常",而是"当未登录用户请求POST /api/notes时,返回401且不创建记录"。AI只看这种级别的描述,才能稳定地给你产出符合预期的代码。
在AI辅助开发场景里,SDD还有一个重要角色:它是你用来约束AI方向感的锚点。你写prompt的时候,把规格文件路径指给AI,AI就能在实现时不断对照规格,而不是自己脑补需求。这个过程我实践中发现能显著减少返工。
3.2 一份可执行的SPEC结构
以我最近做的团队知识库项目(下文统称NovaNotes)为例,一个"创建笔记"的接口规格长这样:
markdown复制# SPEC: 创建笔记
## 接口
POST /api/notes
## 认证
- 必须携带Bearer Token
- 仅登录用户可调用
## 请求体
{
"title": "string, 必填, 1~200字符",
"content": "Markdown string, 必填, 最多50000字符",
"tags": "string[] 可选, 最多10个, 每个1~30字符"
}
## 成功响应
201
{
"id": "string",
"title": "string",
"content": "string",
"author": { "id": "string", "name": "string" },
"tags": ["string"],
"createdAt": "ISO8601",
"updatedAt": "ISO8601"
}
## 错误响应
- 401: 未携带或携带无效Token
- 422: title为空/超长,content为空/超长,tags超过10个
- 413: content超过50000字符
## 业务规则
1. 写入前必须校验当前用户是否在允许创建笔记的空间内
2. 作者id必须来自Token,不能由请求体传入
3. tags去重后写入,顺序不保证
4. 每次创建操作记录审计日志
这个文件看起来不算复杂,但它把所有AI可能发挥的方向都锁死了。实际开发中,我会把这个文件丢给AI,配合现有的schema.prisma和路由文件,要求它先输出实现计划再动手。
3.3 用SPEC驱动AI的Prompt模板与工作流
从我的实践来看,最稳定的AI辅助开发工作流是"三段式":
- 规格阶段:你负责定义需求、边界、数据契约。
- 拆解阶段:让AI阅读规格,输出它理解的实现计划(涉及哪些文件、改动哪些schema、需要哪些新函数)。
- 实现阶段:确认计划合理后,再让AI逐文件实现,并跑测试。
一个可以直接复制的prompt模板:
text复制请阅读 /specs/notes.create.md 和 /prisma/schema.prisma。
你的任务:按照规格实现"创建笔记接口"。
要求:
1. 先输出实现计划,列出涉及的文件、改动点、新增的校验逻辑,不要写代码。
2. 如果计划里有歧义,先提问,不要猜测。
3. 实现时使用仓库已有的 Prisma Client、Zod schema 和错误处理中间件。
4. 不得直接修改规格文件。
计划确认后,再开始写代码。
这个模板的作用是强制AI先计划后执行。直接让AI"一步到位"生成代码,它常常会跳过规格里你不重视但必须存在的细节,比如审计日志、token鉴权。先计划再实现,相当于给AI装了一个"先想后做"的开关,实测下来质量提升非常明显。
4. 工程护栏Harness:让代码不失控的分层防线
4.1 第一道防线:类型、校验与Lint
在AI生成代码的世界里,"质量"不能靠自觉,要靠结构。我把全栈项目的工程护栏分成四层,第一层是"只要代码还躺在编辑器里,就能拦住的错误"。
- TypeScript strict:前面已经说过,不再赘述。
- Zod schema统一放在
/lib/schemas:AI生成API或表单时,强制它从这里导入校验规则,而不是在handler里随手写if判断。 - ESLint + Prettier:固定代码风格和反模式检查。比如
@typescript-eslint/no-floating-promises这个规则,能拦截掉大量AI会生成的"忘加await"问题。
package.json里我长期保留这几个脚本,并且在每次AI生成完代码后第一时间执行:
json复制{
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint . --max-warnings 0",
"format:check": "prettier --check .",
"check": "pnpm typecheck && pnpm lint && pnpm format:check"
}
}
--max-warnings 0 很重要:AI生成代码经常会带出几十个warning,如果你允许warning继续,很快代码库就全是"暂时不管"的隐患。直接打红,迫使解决。
4.2 第二道防线:数据库迁移、约束与查询规范
数据库层是AI最容易失控的地方。我见过的典型问题包括:AI为了让某个查询"跑起来"直接建议UPDATE不带where、批量导入数据时不走事务、循环里做N+1查询。这不能指望prompt约束,必须在结构和review上做文章。
首先,schema变更必须走migration,并且migration必须被review。Prisma提供了 prisma migrate dev 和 prisma migrate deploy 两条路径:开发环境用来生成迁移,生产环境只执行已经审过的迁移。如果AI建议你直接改数据库,直接拒绝。
其次,在schema里明确约束。以NovaNotes的Document模型为例:
prisma复制model User {
id String @id @default(cuid())
email String @unique
name String
createdAt DateTime @default(now())
documents Document[]
}
model Document {
id String @id @default(cuid())
title String @db.VarChar(200)
content String @db.Text
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
tags DocumentTag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}
这里的 @@index([authorId]) 是AI经常漏掉的东西——AI会生成外键关系,但不会主动为外键建索引。如果数据量大,这个遗漏会导致全表扫描,接口直接超时。所以规格里我会明确写上"所有外键字段必须有索引"。
数据库查询规范也要写进review清单:
- 禁止在循环里执行查询(N+1)
- 批量读取用
findMany加显式字段,不要select * - 分页用游标(cursor)或指定上限的offset,必须有上限
4.3 第三道防线:自动化测试与CI流水线
如果说schema约束是静态规则,那自动化测试和CI就是动态规则。我在NovaNotes项目里用Vitest做单元和集成测试,Playwright做核心链路的E2E,两者配合能把AI生成代码里的大量回归在合入前拦下。
一个典型的GitHub Actions workflow长这样:
yaml复制name: CI
on:
pull_request:
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: npx prisma generate
- run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/nova_notes_test
- run: pnpm check
- run: pnpm test
- run: pnpm build
这里有几个关键点:
- services里起了一个真实的PostgreSQL,而不是用mock。因为AI生成代码时常见的bug恰恰是对SQL行为理解不准确,用真库测试最有
