从 5 个文件读完一个生产级 AI Chatbot------Vercel AI Chatbot 源码拆解
版本:基于
main分支(PR #1498 之后,约 2026-05) 阅读时间:30 分钟通读 / 4 小时深读 拆解日期:2026-06-04
TL;DR(懒人速读)
- 项目本质 :Next.js 16 + AI SDK v6 的全栈对话样板,约 140 个
.ts/.tsx、20k 行。生产级工程化,不是 ChatGPT 克隆。 - 架构核心一句话 :客户端
useChat+ Context + SWR 是中枢;服务端用createUIMessageStream把 AI 主流 + 自定义data-*旁路信号塞进同一条 SSE;流同时写 Redis 实现断线续读。 - 3 个最值得抄的设计 :
- 主流 + 旁路双 channel------消息流和 UI 控制信号物理同流、语义分离
parts: Part[]异构消息容器------客户端、协议、DB 三处同构,零翻译层resumable-stream+ Redis------服务端做权威源,客户端断了重连即可
- 30 分钟最短自学路径 :
lib/types.ts(56 行)→lib/db/schema.ts→hooks/use-active-chat.tsx(301 行)→app/(chat)/api/chat/route.ts(372 行)→components/chat/data-stream-handler.tsx(91 行)。读完这 5 个文件掌握 70%。 - 附赠:通用读源码 9 步方法论(侦察 → 跑 → 入口 → 地图 → 主线 → 契约 → PR 考古 → 笔记 → 输出)。
1. 它是什么
Vercel AI Chatbot 是一个用 Next.js + Vercel AI SDK v6 构建的全栈对话样板。它的官方定位是 "free, open-source template"------但代码质量、工程化程度、产品成熟度都达到了生产级。
三句话能说清楚:
- 是什么 :Next 16 + React 19 + AI SDK v6 + Drizzle + better-auth 的对话样板,约 140 个
.ts/.tsx文件,20k 行代码。 - 给谁用:想快速搭一个自己的 AI 对话产品的开发者;以及 Vercel 自己用来展示新技术栈(AI Gateway、resumable-stream、botid)的官方"门面"。
- 差异 :相比 LibreChat / Lobe Chat 这类"功能全"的产品,它走"工程化精"路线------Artifacts 工件系统 和resumable-stream 可恢复流是两个独特价值。
2. 技术栈一览
| 层 | 选择 |
|---|---|
| 框架 | Next.js 16.2 (App Router + RSC) + React 19 |
| AI 协议 | Vercel AI SDK v6 (ai, @ai-sdk/react, @ai-sdk/provider) |
| 流式 Markdown | streamdown(CJK / code / math / mermaid 子包) |
| 可恢复流 | resumable-stream + Redis |
| 数据库 | Drizzle + Postgres |
| 鉴权 | better-auth(中间反复换过 next-auth,最终用 better-auth 的 anonymous 插件) |
| Bot 防护 / 限流 | botid + Redis IP 限流 + DB 用户级限流 |
| UI | shadcn/ui + Radix + Tailwind 4 + framer-motion |
| Artifacts 富文本 | ProseMirror(文档) + CodeMirror(代码) + react-data-grid(表格) |
| 工具链 | Biome(不是 ESLint)+ Playwright(仅 e2e) |
| 可观测 | @vercel/otel + OpenTelemetry |
几个值得注意的"非典型选择":
- 无 Zustand / Redux ------客户端状态全靠
useChat(AI SDK 自带) + SWR + 几个useState,再用 Context 下发。够用。 - 无单元测试 ------只有 4 个 e2e(~270 行)。
tests/pages/chat.ts用 POM 模式枚举了 UI 契约,比测试本身更有价值。 - React 18 类型 + React 19 运行时------已知的兼容性 hack,类型还没完全跟上。
3. 核心架构
客户端:一棵 Provider 树 + 一个中枢 hook
关键技巧 :app/(chat)/page.tsx 是空的 return null。真正的 UI 渲染在 layout 里。
这样做的好处 :用户从 /(新对话)跳到 /chat/{id}(已有对话)时,React 只换 page 不换 layout------ChatShell 不卸载,流连接不中断,滚动位置不丢。这是用 App Router 的"layout 持久性"对抗 SPA 切换不友好的经典手法。
服务端:薄路由 + 一个流容器
代码骨架对应:
ts
createUIMessageStream({
execute: ({ writer }) => {
const r = streamText({ model, tools, stopWhen: stepCountIs(5) })
writer.merge(r.toUIMessageStream()) // 主消息流
writer.write({ type: "data-chat-title" }) // 旁路 UI 信号
},
onFinish: () => saveMessages(assistant),
})
一个文件 372 行做了 8 件事------这是这个项目代码组织上最值得吐槽的一个文件,但功能逻辑非常清晰。
数据形态:parts 数组贯穿三处
ts
// 客户端
type ChatMessage = UIMessage<MessageMetadata, CustomUIDataTypes, ChatTools>
// 网络 / SSE 帧
{ type: "text", text: "..." } | { type: "tool-getWeather", state: "output-available", ... } | ...
// 数据库
Message_v2.parts: json // 同一份 JSON
消息不是字符串,是 parts: Part[] 异构数组 。同一条 assistant 消息可以交错出现:文字 / 工具调用 / 工具结果 / 推理 / 文件附件。客户端、协议、数据库三处同构,零翻译层。这是整个项目最重要的工程取舍。
4. 主线全景:从输入到流式回复
整个生命周期完成后,最终消息在 DB,最近流在 Redis。如果客户端中途断了,下次进入这个 chat → 从 DB 拿历史 + 从 Redis 续读未完的流。
5. 7 个最值钱的设计点
5.1 主流 + 旁路双 channel
服务端一个 SSE 流里塞两类事件:
- 主流 :AI SDK 标准事件,被
useChat内部消费,自动并入messages[] - 旁路 :
data-*自定义事件,被onData拦截,进入DataStreamProvider
为什么巧妙 :避免"UI 控制信号污染消息流"。data-chat-title、data-id(Artifact 创建)这些信号不是消息,硬塞进 messages 里会让数据模型变脏。
5.2 工具能拿到流的 writer
ts
tools: {
createDocument: createDocument({ session, dataStream, modelId })
}
工具内部可以自己调用 streamText,把生成内容增量写进 dataStream 。这是 Artifacts 的核心机制------AI 调 createDocument,工具内部启动子流,子流的内容通过 data-textDelta 推给客户端,客户端在 Artifact 面板里实时渲染。
这是把"工具执行"和"次级流式生成"统一起来的关键抽象。
5.3 stepCountIs(5):Agent 多步循环自动化
ts
streamText({ stopWhen: stepCountIs(5), tools, ... })
模型输出 tool-call → AI SDK 自动执行工具 → 把结果作为新消息送回模型 → 继续。最多 5 轮,对客户端完全透明。这一行就实现了完整的 Agent 行为。
5.4 resumable-stream + Redis = 服务端权威源
SSE 流返回客户端的同时 克隆一份写入 Redis。客户端刷新 / 断网时调用 GET /api/chat/{id}/stream 从 Redis 续读。
对做对话产品的启发:客户端别再轮询 + 自己维护状态------让服务端做权威源,客户端断了重连。
5.5 Document 复合主键 (id, createdAt)
ts
document = { id, createdAt, ..., pk: [id, createdAt] }
同一个 documentId 可以有多版。版本浏览、diff 对比、回到几步前------零额外代价。代价是 document 表会膨胀。
5.6 客户端先生成 chatId
ts
const newChatIdRef = useRef(generateUUID())
const chatId = chatIdFromUrl ?? newChatIdRef.current
新对话时前端先 mint 一个 UUID,发送时一起送给服务端。结果:消息即刻显示(乐观更新)+ URL 跳转后 ID 不变(可分享)------同时拿下。
5.7 双层限流(IP + 用户)
route.ts 同时跑两个限流检查:
- IP 级(Redis,10 条/24h)
- 用户级(DB,10 条/小时)
这一对设计是被攻击逼出来的------PR #1436 的描述显示,攻击者通过不断换 guest session 绕过用户限流。单层限流永远不够。
6. 我不喜欢的 5 个地方
- Provider 嵌套 7 层 ------ 应该提取
<AllProviders>收编。 ChatShell一坨解构 15+ 字段 ------ Context 设计反模式,应让子组件按需取。lib/db/queries.ts632 行平铺 ------ 没按 entity 拆。route.ts372 行做 8 件事 ------ 应拆成validate / authorize / loadContext / streamReply。CustomUIDataTypes用null表示无数据 ------ 反序列化容易出错;用空对象更安全。
7. 可借鉴的 3 件事(实用浓缩)
如果只能从这个项目拿走 3 个设计,推荐:
① 主流 + 旁路双 channel 对话主流走 messages[],UI 控制信号走独立的 DataStreamProvider。彻底解决"轮询状态丢失"、"消息流被污染"两类问题。
② parts: Part\[\] 异构容器 消息从字符串升级为数组。一条消息能包含文字 / 工具调用 / 推理 / 文件,渲染层只需 parts.map(switch type)。
③ 服务端权威源 + 可恢复流 SSE 同时写 Redis,客户端的"轮询"换成"重连续读"。彻底解决断线 / 切 tab / 刷新丢状态。
8. 核心抽象词典
读这种项目,最大的认知负担是新名词。把 7 个最关键的名词翻译成"如果团队里说人话"的版本:
| 项目名词 | 人话翻译 |
|---|---|
| UIMessage / ChatMessage | "一条消息的客户端形态"------里面 parts: Part[] 是异构数组 |
| DBMessage | "一条消息的数据库形态"------parts 字段是 JSON 列 |
| Part | "消息的一个片段"------type 决定它是文字 / 工具调用 / 推理 / 文件 |
| UIMessageStream | "一个 SSE 流容器"------里面塞 AI 标准事件 + 自定义 data-* 事件 |
| Artifact | "一个可编辑的工件"------文档/代码/表格/图片,有 kind 区分,有版本历史 |
| DataStream(旁路) | "UI 控制信号总线"------服务端推给客户端的"非消息"事件 |
| Resumable Stream | "服务端是权威源的流"------SSE 同时往 Redis 写一份,客户端断了可重连续读 |
9. 30 分钟自学路径
如果时间有限,按这个顺序读:
- 5 分钟 :README +
package.json------ 技术栈速览 - 5 分钟 :
lib/types.ts(56 行)+lib/db/schema.ts------ 数据契约 - 10 分钟 :
hooks/use-active-chat.tsx(301 行)+components/chat/shell.tsx------ 客户端中枢 - 8 分钟 :
app/(chat)/api/chat/route.ts(372 行) ------ 服务端主流 - 2 分钟 :
components/chat/data-stream-handler.tsx(91 行) ------ 旁路通道闭环
读完这 5 个文件(共约 1500 行),你已经掌握 70% 的项目。剩下的 Artifact、鉴权、限流都是"按这套架构填充的内容"。
10. 关键 PR(用来理解"为什么")
| PR | 标题 | 学到什么 |
|---|---|---|
| #1462 | feat: v1 --- persistent shell, model gateway, artifact improvements | 项目奠基,自我介绍式 PR |
| #1453 | fix(auth): migrate from next-auth to better-auth | better-auth → next-auth → better-auth 反复横跳,鉴权未稳定 |
| #1436 | fix(chat): add ip-based rate limiting | 上线被刷爆,单层限流不够 |
| #1444 | fix(ai): validate models server-side | 客户端可控字段必须在服务端二次校验 |
11. 官方链接
项目本体
- vercel/ai-chatbot --- GitHub 仓库
- chat.vercel.ai --- 官方在线 demo
- Pull Requests --- 已合并 PR(设计决策金矿)
Vercel AI SDK(最重要的依赖)
- AI SDK 官网 --- 核心文档
- sdk.vercel.ai/docs/ai-sdk... --- Core API(streamText / generateText / tool)
- sdk.vercel.ai/docs/ai-sdk... ---
useChat用法 - sdk.vercel.ai/docs/ai-sdk... ---
data-*旁路事件 - vercel/ai --- SDK 源码
- AI SDK v6 announcement --- v6 设计意图
关键周边包
- streamdown --- 流式 Markdown 渲染器(含 CJK/code/math/mermaid 子包)
- resumable-stream --- 可恢复 SSE 流(核心:断流续读)
- botid --- Vercel 自家 bot 检测
数据 / 鉴权
- Drizzle ORM --- TS 优先的 SQL ORM
- better-auth --- 当前用的鉴权框架(含 anonymous 插件)
- next-auth (Auth.js) --- 项目早期用的方案
Next.js / React
- Next.js 16 升级指南 --- 包含
middleware.ts→proxy.ts改名 - App Router 文档
- React 19 Blog --- 含 React Compiler
UI
- shadcn/ui --- 组件库
- Radix UI --- 无样式原语
- ProseMirror --- 文档工件编辑器
- CodeMirror 6 --- 代码工件编辑器
部署 / 模型
- Vercel AI Gateway --- 统一模型接入网关
- Vercel Postgres / Neon --- 数据库托管
- Vercel Blob --- 文件存储
后记
读这个项目最大的收获不是任何具体技术------是看到 Vercel 工程师用最少的代码、最直接的抽象,把对话产品该有的工程化都做到位:流式、续读、工具、Artifacts、限流、Bot 防护、版本化、可分享 URL......每一个都用最薄的胶水实现。
这种"刚刚好"的工程品味,比任何具体技术都更值得抄。
附:通用读源码方法论(这次实操的 9 步流程)
- 外围侦察 ------ README / package.json / 最近 PR / Release,30 分钟内回答"是什么/给谁用/差异"
- 跑起来 ------ 至少 clone 下来,能跑最好(建立"现象"再去解释代码)
- 找入口点 ------
package.json的bin/main、Next.js 的 layout/page、CLI 的入口 - 目录地图 + 核心 20% 文件 ------ git log 频次 / 引用次数 / 测试厚度三角验证
- 主线端到端追踪 ------ 选一条用户能感知的路径深度走完,不分岔
- 读测试 / 契约 ------ zod schema / DB schema / POM / e2e 是契约的四个来源
- git 时间机器 / PR 考古 ------ 当 git log 浅时,用 PR 描述代替
- 做笔记 ------ 全景图 + 巧妙/疑惑/不喜欢三类清单 + 私人词典
- 输出 ------ 写一篇能讲给别人听的文章(就是这篇)