基于T3 Stack的AI全栈代码生成器t3code:从需求到项目

发布时间:2026/10/26 17:01:34
基于T3 Stack的AI全栈代码生成器t3code:从需求到项目

前阵子做全栈项目被脚手架折腾得够呛,光是把Next.js、数据库、API层和前端页面串起来,前后端类型还要手动同步,就浪费了一整天。后来我把技术栈固定成T3 Stack,效率确实上来了,但脑子里又冒出一个念头:既然技术栈和约定已经如此固定,能不能直接把需求描述丢给AI,让它生成一个完整的全栈项目?于是就有了t3code。

t3code是我最近在做的一个基于T3 Stack的全栈代码生成器:你输入自然语言描述的业务需求,它自动生成包含Next.js页面、tRPC路由、Prisma数据模型和Tailwind界面的完整项目骨架,并且生成完毕会在沙箱里自动跑类型检查和构建验证。这套东西最适合被类型同步和脚手架配置折磨过的全栈开发者,也适合想快速验证产品创意的独立开发者,如果你在研究AI编程工具或者做LLM应用开发,它同样可以作为一整套“约束式生成”的参考实现。

1. t3code是什么:把需求描述变成全栈项目的AI生成器

1.1 一句话说清t3code的工作方式

用最简单的话描述,t3code就是一个“输入输出极度收窄”的AI脚手架生成器。你提供给它的不是一句话的模糊需求,而是一个相对完整的业务场景描述,比如:

做一个团队任务管理工具,支持创建项目、任务、标签,任务可以指派给成员,每个任务有标题、描述、截止日期和完成状态。

t3code拿到这段描述之后,经过语义解析、数据模型抽取、模板渲染,最终输出一个可以直接pnpm dev启动的T3 Stack项目。这个项目里包含:

  • Prisma数据模型文件,以及对应的数据库迁移脚本
  • tRPC路由,前后端API调用全部带类型约束
  • Next.js App Router页面,包含基础列表页、创建表单、详情页
  • Tailwind样式,界面直接可用,不要求在视觉上多惊艳,但结构完整

我之前试过很多在线AI生成工具,大多是生成一些代码片段,或者是零散的前端组件。t3code和它们的核心差异在于:它交付的不是“代码参考”,而是一个完整可运行的项目结构。你拿到手之后可以直接基于它继续做业务逻辑,而不是重新组织目录结构和造轮子。

1.2 和市面上AI代码生成器的本质区别

我总结下来,t3code和其他AI生成工具的区别集中在三点。

第一,约束优先而不是自由发挥。市面上不少生成器会让你自由选技术栈,AI生成出来的代码有时候是React+Redux,有时候是Vue+Pinia,模板和规范完全没法统一。t3code把所有技术选型焊死,只生成T3 Stack这一套组合。对LLM来说,约束不是限制,而是质量保证。生成空间小,容错率高,模型也不容易在框架之间反复横跳。

第二,类型安全贯穿整个链路。传统的AI代码生成器给你生成前端页面对应的是手写API文档,后端的模型字段改了,前端根本不知道。t3code用tRPC做API层,Prisma做数据模型层,TypeScript做静态检查,类型从数据库一路通到前端组件,任何字段不一致在编译期就会暴露。这一点在实际使用中非常值钱,AI生成的代码本来就容易有低级错误,如果中间再多一层“前后端契约靠人肉对齐”,那基本就是灾难。

第三,生成结果做了沙箱验证。t3code不是生成完就完事,它会把生成的项目先丢进Docker沙箱,执行prisma generate、tsc --noEmit、next build这一整套流程。只有验证通过才会把结果交付给你。这算是我在实操中加的最有价值的一道关卡,它把“AI生成了一堆看起来合理但根本编译不过的代码”这类问题,挡在了交付之前。

2. 为什么把底座焊死在T3 Stack:技术选型复盘

2.1 T3 Stack的组件拆解

T3 Stack是社区里一套非常流行的全栈TypeScript方案,核心成员包括Next.js、TypeScript、tRPC、Prisma和Tailwind CSS。它由Theo Brouer推广,设计理念是“以类型安全为核心,不做多余妥协”。我给这五个组件找了个生活化的类比:

  • Next.js是整个施工队的队长,负责指挥页面怎么渲染、路由怎么走、服务器端逻辑怎么执行。
  • TypeScript是施工图纸规范,所有参与协作的组件都必须按图纸上的标准来,谁违反了规范,编译阶段就能发现。
  • tRPC是统一的管道标准,它让前端调用后端接口就像调用本地函数一样,管道直径前后一致,不用额外维护接口文档。
  • Prisma是地基模板,用schema语言描述数据模型,数据库的表格怎么建、字段之间什么关系,写清楚就能自动生成迁移脚本。
  • Tailwind CSS是标准化的涂装色卡,所有类名都是原子化的,不用写大段CSS文件,生成器也不容易在样式上翻车。

这套组合对个人开发者和全栈项目来说非常友好。它不像大型微服务架构那样动辄十几个服务,也不像传统MERN栈那样前后端割裂。每一个组件都有非常明确的职责边界,组合在一起又刚好覆盖全栈应用的所有环节。

2.2 为什么“约束型技术栈”更适合AI生成代码

LLM生成代码这件事本质上是一个概率行为,它不是在执行确定的编译过程,而是在预测“下一个token最可能是什么”。既然是这样,模型在开放式任务中很容易失控:同一个需求,它可能在Next.js的页面路由和Express里犹豫,甚至在React和Vue之间反复摇摆。

T3 Stack这类约束型技术栈正好解决了这个问题。

第一,候选方案空间被大幅缩小。AI不需要在“用什么框架、要不要上状态管理库、API层怎么组织”这些问题上做选择,所有答案都在系统提示词里写死了。我实测下来,固定技术栈之后,模型的输出稳定性明显提升。

第二,模板一致性好,模型可以学到稳定的模式。因为t3code的生成逻辑是“模板+结构化数据”的方式,所有示例代码高度一致,LLM很容易从few-shot示例里学到输出的格式规律。这和让AI自由发挥完全不一样——自由发挥的模式太分散,模型学不到稳定的输出分布。

第三,所有生成结果都可以用同一套编译工具链验证。Next.js项目能不能跑通、Prisma数据模型是否合法、TypeScript类型是否收敛,这些都是确定性的校验手段,生成器可以把这些校验环节做成自动化关卡。

2.3 和其他技术栈的对比

我把几个常见技术栈放进一张表里对比过,差异非常直观:

技术栈配置复杂度类型链路API定义方式适合AI生成的程度
T3 Stack低,create-t3-app一条命令端到端类型安全tRPC代码即契约高
MERN中,前后端分离部署前后端靠手写接口对齐手写REST文档中
LAMP低,但前后端割裂无类型系统无契约低
微服务高,需要网关和服务编排跨服务类型难收敛依赖OpenAPI/契约测试低

说一下我为什么没有选MERN。MERN栈本身没什么问题,但它的类型链路是断裂的:Express的请求对象、Mongoose的Schema、React的状态管理,每一层都有自己的类型系统,层与层之间没有自动推导的桥梁。AI生成的代码稍微一多,类型就开始漂移,前端改一个字段名,后端接口对应不上,查错成本极高。tRPC在这一点上优势很明显,它的类型推导是自动的,一个router定义好,前端直接用hooks调用,类型自然同步。

LAMP和微服务就更不用说了,它们的设计目标本来就不是“快速生成全栈应用”。LAMP偏传统网站,对现代前端交互支持有限;微服务强调分布式扩展,但AI生成一个十几个服务编排的系统,验证成本和出错概率都太高。

所以t3code最终选择了T3 Stack,这里面还有一个更现实的理由:create-t3-app项目的模板已经非常成熟,生成结果有大量社区实践兜底,AI输出的内容不会偏出可运行的范围。

3. 核心实现:t3code的架构与生成管线拆解

3.1 整体架构:从输入到输出的两条链路

t3code整体可以拆成两条链路来理解。

第一条是交互链路,做的事情是把用户的自然语言需求变成结构化的、可校验的中间表示。用户的输入首先被发送到LLM接口,LLM在预设的prompt约束下,输出一个结构化JSON对象,里面包含应用名、数据模型定义、页面描述、API调用需求等。这个JSON不是直接拿来用的,它要过一道zod校验,确保字段名称、类型、范围都符合预期,校验不通过就会要求模型重新生成。

第二条是生产链路,做的事情是把结构化JSON变成真实可运行的代码。t3code拿到通过校验的JSON后,会把它作为渲染数据传给一套提前写好的模板引擎。模板引擎负责把JSON里的模型定义渲染成Prisma的schema文件,把页面描述渲染成Next.js的tsx文件,把API需求渲染成tRPC的router文件。所有文件生成完毕后,整个项目会被丢进Docker沙箱做编译验证,验证通过才交付。

我之所以把生成过程设计成“JSON中间表示+模板渲染”,而不是让LLM直接输出整个代码文件,原因很现实:直接让LLM输出完整代码文件的成本高、出错率也高,而且每个文件之间的关联性很难保证。模板是提前写好、经过实际运行验证的,模型只需要负责填充业务的差异化部分,成功率会高很多。

3.2 Prompt工程:输出协议定了,AI才不乱跑

t3code的核心prompt设计里,最重要的不是提示词写得有多长,而是给模型规定了一个非常严格的输出协议。我让LLM必须返回类似这样的JSON结构:

{ "appName": "taskflow", "appDescription": "团队任务管理工具", "models": [ { "name": "Project", "fields": [ { "name": "name", "type": "String", "required": true }, { "name": "description", "type": "String", "optional": true } ] }, { "name": "Task", "fields": [ { "name": "title", "type": "String", "required": true }, { "name": "status", "type": "Enum", "enumValues": ["TODO", "IN_PROGRESS", "DONE"] }, { "name": "project", "type": "Relation", "relationTo": "Project" } ] } ], "pages": [ { "route": "/", "type": "projectList" }, { "route": "/projects/[id]", "type": "projectDetail" } ] }

在系统提示词里,我做了几个硬性约束:

  • 只能使用给定的技术栈组件,不允许引入React Query、Formik这类额外依赖
  • 数据模型的数量控制在5个以内,字段类型只允许String、Int、Float、Boolean、DateTime、Enum、Relation
  • 页面类型从预设的列表页、详情页、创建表单、编辑表单、登录注册页中选择
  • 输出必须是合法JSON,不允许输出任何Markdown代码块包裹

这里有一个很重要的细节:我要求模型输出“描述性JSON”而不是“代码结果”。描述性JSON的结构更稳定,模型理解起来更容易,zod校验也更方便。等JSON校验通过,再由模板引擎把它渲染成代码,这样脏活累活都由确定性的程序完成,模型的发挥空间被约束在正确的轨道里。

另外,我在调用LLM时尽量使用JSON Mode或者Function Calling,而不是普通的对话补全。实测下来,不用JSON模式的时候,模型大概有20%的概率返回带解释文字、Markdown格式或者其他非结构化内容;用了JSON模式之后,结构化失败率降到3%以内。这个差距对生成器来说就是能不能自动化跑通的差别。

3.3 三道关卡:格式关、类型关、运行关

生成器不能只看“模型输出好像能用”就完事,我在t3code里设置了三个强制校验关卡。

第一道是格式关。LLM返回的JSON必须能通过zod schema的校验。zod schema里定义了每个字段的类型、必填性、取值范围。校验失败时,系统会把错误信息回传给模型,让它修正后重新生成。这里有个小技巧:回传错误信息时一定要带上具体的字段路径和期望值,比如“字段models[2].fields[0].type取值Enum,但缺少enumValues”,模型修正的成功率会高很多。

第二道是类型关。JSON格式正确不代表代码能编译。t3code会把JSON里的字段类型映射成Prisma类型,再映射成tRPC输入输出的zod schema。这一步的难点在于类型映射不是一一对应的,比如JSON里的String可能在Prisma里是String或Text,Enum在SQLite上要退化成String。生成完毕会执行tsc --noEmit,确保所有类型都收敛。

第三道是运行关。这步把生成的项目目录放进Docker容器,执行完整的构建流程:安装依赖、生成Prisma Client、执行数据库迁移、跑next build。只有成功通过构建,t3code才会把项目交付给用户。如果AI生成的代码里有任何潜在语法错误、缺失依赖、配置文件问题,都会在运行关暴露出来。

3.4 模板渲染:为什么模板要提前写死

t3code的模板不是随手写的,而是在实际开发中反复打磨过的。我把常用场景的页面结构、API调用方式、数据模型组织方式都沉淀成了模板,模型只负责填充业务字段。

举个例子,项目列表页的模板是这样组织的:自动生成一个ProjectList组件,调用api.project.list.useQuery()获取数据,用Tailwind网格布局展示卡片,每张卡片点击跳转到详情页。这个过程完全不需要模型参与具体的代码编写,模型只需要提供“有哪些字段”“页面的标题是什么”这些信息。

这样做的好处是显而易见的:模板代码每一次生成都经过编译验证,不存在“这次能跑、下次崩了”的不确定性。模型输出的JSON如果合法,那么渲染出来的代码大概率是可靠的,剩下的风险都由运行关兜底。

4. 实操复盘:从零搭建一个最小可用的t3code

4.1 第一步:用create-t3-app初始化宿主项目

t3code本身就是一个T3 Stack应用,所以我直接用官方脚手架初始化宿主:

pnpm create t3-app@latest t3code

初始化时勾选Next.js、TypeScript、tRPC、Prisma、Tailwind,数据库选SQLite。为什么先选SQLite?因为t3code的沙箱验证环境希望“零配置、启动快”,SQLite不需要单独的数据库服务,生成项目的迁移和连接都简单。后续如果要支持PostgreSQL,可以在模板和配置层面做切换,但开发初期SQLite足够。

宿主项目搭建好之后,需要在src/server目录下预留几个模块:llm目录放模型调用逻辑,generator目录放模板渲染逻辑,sandbox目录放沙箱验证逻辑。这几个模块互相解耦,后续单独修改某一个不会影响其他部分。

4.2 第二步:接入LLM并定义输出协议

我的实现里用OpenAI的Node SDK,核心逻辑很简单:

import OpenAI from "openai"; import { z } from "zod"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); const GenerationSchema = z.object({ appName: z.string(), appDescription: z.string(), models: z.array( z.object({ name: z.string(), fields: z.array( z.object({ name: z.string(), type: z.enum(["String", "Int", "Float", "Boolean", "DateTime", "Enum", "Relation"]), required: z.boolean().optional(), enumValues: z.array(z.string()).optional(), relationTo: z.string().optional(), }) ), }) ), pages: z.array( z.object({ route: z.string(), type: z.enum(["list", "detail", "create", "edit", "auth"]), }) ), }); async function analyzeRequest(userInput: string) { const response = await openai.chat.completions.create({ model: "gpt-4o", response_format: { type: "json_object" }, messages: [ { role: "system", content: `你是全栈项目架构师。请根据用户需求输出结构化JSON。只允许输出JSON,不要输出任何解释。...`, }, { role: "user", content: userInput }, ], }); const json = JSON.parse(response.choices[0].message.content ?? "{}"); const parsed = GenerationSchema.safeParse(json); if (!parsed.success) { // 把校验错误回传给模型,让它修正后重新生成 } return parsed.data; }

这段代码里有三个关键决策。第一,response_format强制JSON输出,避免模型返回解释文字。第二,zod schema在运行时兜底,即使模型输出格式正确但字段值非法,也会被拦下来。第三,生成失败时采用“重新生成”而不是“自己尝试修复JSON”,因为让程序改JSON的容错逻辑很容易越弄越乱,直接让模型重新输出反而更可靠。

4.3 第三步:写生成器核心——从JSON到Prisma schema

生成器核心模块做的事情可以概括为:把中间表示里的models数组渲染成Prisma schema文件。

我写了一个模板渲染函数,逻辑大致是:

function renderPrismaSchema(data: GenerationResult): string { const models = data.models.map((model) => { const fields = model.fields.map((field) => { if (field.type === "Enum") { return `${field.name} ${field.name}Enum ${field.required ? "" : "?"}`; } if (field.type === "Relation") { return `${field.name} ${field.relationTo} ${field.required ? "" : "?"} @relation(...)`; } return `${field.name} ${field.type} ${field.required ? "" : "?"}`; }); return `model ${model.name} {\n id Int @id @default(autoincrement())\n ${fields.join("\n ")}\n}`; }); return `datasource db { provider = "sqlite" url = "file:./dev.db" }\n\ngenerator client { provider = "prisma-client-js" }\n\n${models.join("\n\n")}`; }

实际项目里的渲染比这个复杂,要处理Enum定义、关系字段的@relation参数、软删除字段、时间戳默认值等。但核心思路是一致的:模板渲染是确定性过程,输入JSON合法,输出Prisma schema就合法。

比如之前那个“团队任务管理工具”的需求,最终生成的Prisma数据模型是这样的:

model Project { id Int @id @default(autoincrement()) name String description String? tasks Task[] createdAt DateTime @default(now()) } model Task { id Int @id @default(autoincrement()) title String description String? status TaskStatus @default(TODO) dueDate DateTime? project Project? @relation(fields: [projectId], references: [id]) projectId Int? createdAt DateTime @default(now()) } enum TaskStatus { TODO IN_PROGRESS DONE }

看到这个结果,我第一反应是松了口气。AI生成的模型命名规范、关系合理、枚举定义完整,这就是输出协议约束的价值——模型只需要做决策,具体的代码语法全部由模板保证。

4.4 第四步:在沙箱里做“能跑吗”测试

生成器最后一步是沙箱验证。我用Docker跑一个Node环境,把生成的项目挂载进去:

docker run --rm \ -v $(pwd)/generated:/workspace \ -w /workspace \ node:20-alpine \ sh -c "pnpm install && pnpm prisma generate && pnpm prisma db push && pnpm tsc --noEmit && pnpm next build"

这一步把整个验证流程变成了一条命令,任何一步失败都会导致退出码非零,生成器据此判断项目不合格。prisma db push而不是prisma migrate,是因为生成的初始项目不需要保留迁移历史,直接用DDL同步建表即可,速度快很多。

沙箱跑一次大概需要40到90秒,视依赖安装速度而定。这个时间成本是值得的,因为用户在本地打开一个编译不过的项目,排查起来比等待这几十秒痛苦得多。

4.5 一次完整的生成演示

为了让整个流程更直观,我把一次真实的生成过程放出来。输入需求是:

做一个博客应用,包含文章、标签两个核心模型。文章有标题、内容、发布时间,可以关联多个标签。页面包含文章列表、文章详情、创建文章表单。

t3code生成的目录结构大概是:

blog-app/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── server/ │ │ ├── api/ │ │ │ ├── routers/ │ │ │ │ ├── post.ts │ │ │ │ └── tag.ts │ │ │ └── root.ts │ │ └── db.ts │ └── app/ │ ├── layout.tsx │ ├── page.tsx │ ├── posts/ │ │ ├── [id]/page.tsx │ │ └── new/page.tsx │ └── tags/ │ └── page.tsx

生成的文章列表页会自动调用api.post.list.useQuery(),用Tailwind渲染卡片式布局;文章详情页通过router.push实现跳转。整套代码拿来就能启动,再填充真实的业务逻辑,一个博客应用的基础骨架就完成了。

5. 踩坑记录与排查速查表

5.1 Prisma的“方言”坑:同一个schema在不同数据库表现不一样

第一个让我头疼的问题是Prisma在不同数据库上的行为差异。SQLite不支持原生枚举类型,生成enum字段时会退化成String约束;自增主键在SQLite、PostgreSQL、MySQL上的写法也略有差异。最开始t3code生成的结果在SQLite上能跑,用户切到PostgreSQL就报错。

解决方案是在生成器里维护一个“方言映射表”,根据目标数据库把JSON里的字段类型翻译成对应的Prisma类型。这个映射规则不复杂,但必须在生成阶段处理,而不是留给用户在拿到项目之后自行修改。

另外,prisma db push和prisma migrate在冲突处理上也不一样。db push会直接同步数据库结构,适合初始模型;migrate适合已经有历史数据的项目。t3code生成的初始项目统一用db push,可以避免一堆没用的迁移文件污染用户项目。

5.2 tRPC的类型要自己收敛:AI生成的数据模型不能直接进API

第二个坑在tRPC的类型推导上。tRPC虽然能自动推导前后端类型,但AI生成的数据模型里经常包含可选字段、DateTime类型、Decimal类型,这些类型在JSON序列化前后不一致。比如Prisma返回的DateTime在前端拿到的是ISO字符串,Decimal在JSON里会被序列化成字符串,如果tRPC的输入输出类型没有做显式转换,页面一加载数据就会崩。

我的解决办法是给所有tRPC router套一层统一的类型包装:列表返回字段显式映射成前端可用类型,日期字段统一转ISO字符串,Decimal字段统一转number或string。这层转换在生成模板时就固定写好,模型不需要理解这些细节。

5.3 版本锁定的教训:dependencies不能用latest

这是个血泪教训。早期t3code生成的package.json里依赖版本写的是latest,结果某个版本的Next.js升级后,AI生成的页面突然全屏白屏,排查了很久才发现是App Router和tRPC的适配配置变了。从那以后,我把所有依赖版本全部锁定到经过验证的组合,模板里写死的版本号,不到万不得已不升级。

依赖锁定版本
next14.2.x
react18.3.x
@trpc/server10.45.x
@trpc/client10.45.x
prisma5.x
@prisma/client5.x
tailwindcss3.4.x

为什么要锁版本而不是用~或者^?因为AI生成的代码是基于特定版本的API写的,版本一跨大版本,API可能就变了。锁版本虽然牺牲了一些灵活性,但换来了“生成结果永远可跑”的确定性。对于生成器工具来说,确定性比最新版本更重要。

5.4 常见问题速查表

我在持续使用中整理了一张问题排查表,遇到问题直接对号入座:

现象可能原因解决办法
生成后next build失败模板里依赖版本和本地缓存不一致删除node_modules和lockfile重新安装
prisma db push提示表已存在schema和本地数据库状态冲突删除dev.db后重新执行
tRPC调用返回404router合并路径和前端调用路径不一致检查root router里router挂载的key是否匹配
LLM返回JSON解析失败模型输出被Markdown包裹启用JSON Mode或Function Calling,并用zod兜底
生成页面白屏Server Components与tRPC配置不匹配确认模板使用官方适配器版本,锁定依赖版本
生成项目启动后没有样式Tailwind版本与PostCSS配置不匹配使用3.4.x版本,避免Tailwind v4配置文件差异

这些坑都踩过一遍之后,t3code的稳定性才算是真正到了可以放心交付的水平。说实话,做这个工具最大的收获不是“AI能生成代码”这个结果,而是理解了怎么用约束、协议、模板、校验把AI的概率性输出变成确定性产物。

我个人在实际使用中最强烈的一个体会是:t3code这类工具的最佳定位不是“替代程序员写代码”,而是“干掉从零搭建项目的重复劳动”。用它生成骨架,把时间花在业务逻辑和产品设计上,这个工作流相当舒服。最后再分享一个小技巧:让AI先输出完整的数据模型,再让它根据模型生成页面,顺序对了,生成结果的稳定程度会提升一个档次。如果你也在做类似的AI生成工具,或者在用LLM生成代码的路上踩过坑,欢迎一起交流。