如今你打开的几乎所有 AI 产品都呈现相同的界面:底部有一个消息列表、一个文本框,以及逐 token 流式出现的文字。看起来简单,但要做好并不容易。
你需要管理流式状态、部分 token、工具调用、重试、Markdown 渲染、滚动位置以及十几个细微的 UX 细节……同时还要保持界面的可访问性和高速响应。如果使用了错误的工具,你会花更多时间在修复状态错误上,而不是构建实际产品。
在本教程中,你将使用两种天生相配的工具构建一个真实的 AI 聊天界面:Vercel AI SDK 负责流式处理和模型逻辑,而 shadcn/ui 负责界面本身。
学完后,你将拥有一个可运行的聊天界面,能够流式返回结果、渲染 Markdown,并且看起来像是可以直接交付的产品。
你还将学习如何通过 MCP 服务器进一步提升 UI 开发速度,以及如果想直接跳过搭建过程,可以在哪里获取一个生产就绪的聊天模板。
目录
先决条件
您需要:
Node.js 18 或更高版本
对 React 和 Next.js 有基本了解,特别是 App Router
来自 OpenAI、Anthropic 或 Google 等 LLM 提供商的 API 密钥。即使没有也可以跟随学习,稍后会详细说明。
您将构建的内容
您将构建一个具有以下功能的 Next.js 聊天应用:
一个与 LLM 提供商通信的流式 API 路由
使用
useChat构建的客户端聊天界面消息气泡、自动增长的输入框以及可滚动的对话,全部使用 shadcn/ui 组件进行样式设计
一个简单的工具调用,使模型能够做得不仅仅是聊天
即使在添加 API 密钥之前也能工作的后备状态,这样您可以先构建 UI,随后再连接模型
让我们从一个空文件夹开始,逐步构建出您愿意向队友展示的内容。
步骤 1:搭建 Next.js 应用
创建一个启用 TypeScript 和 Tailwind 的新 Next.js 项目:
npx create-next-app@latest ai-chat-app --typescript --tailwind --eslint --app
cd ai-chat-app
保持 CLI 询问的其他选项的默认值。您将几乎完全在 app 目录中工作。
步骤 2:安装 Vercel AI SDK
The Vercel AI SDK 负责这里的主要工作。它提供了一个统一的 API,用于调用不同的模型提供商、流式传输文本和结构化数据以及处理工具调用,这样您在切换模型时就不必重新编写聊天逻辑。
安装核心包、React 绑定以及一个 OpenAI 兼容的提供商:
npm install ai @ai-sdk/react @ai-sdk/openai-compatible
@ai-sdk/openai-compatible 值得特别提一下:与其为每个提供商安装独立的软件包,不如只需更换基础 URL,即可指向任何遵循 OpenAI 风格 API 的提供商(包括 OpenAI 本身、Gemini、Groq 以及大量自托管方案)。结合 AI_PROVIDER 环境变量,您可以在不触碰路由处理程序的情况下实现提供商切换——这正是您在下一步将要构建的模式。
步骤 3:为什么 shadcn/ui 与 AI 聊天界面如此契合
在编写任何 UI 代码之前,先了解为何众多 AI 聊天产品倾向于使用 shadcn/ui,而非传统组件库,是很有价值的。
大多数组件库会直接给你一个编译好的包,并通过 props 隐藏内部实现。这对设置页面来说已经足够好,但对聊天界面则不然。在聊天场景中,你需要精确控制消息气泡在流式输出时的动画、“思考中”指示器的行为,以及工具调用与纯文本的渲染方式不同。
shadcn/ui 采取了另一种方式:无需安装软件包,而是将组件的实际源码复制到你的项目中。你完全拥有它,无需与抽象层作斗争来适应你的用例,也不必等待维护者暴露你所需的那个 prop。这种所有权模式正是聊天界面所需要的,因为几乎没有两款 AI 产品在消息、推理或工具输出的渲染方式上完全相同。
这也是为什么围绕它形成了一个完整的生态系统。如果你想要超出默认注册表的更多生产就绪的区块和模板——包括仪表盘、营销区块以及完整的聊天界面——位于 Shadcn Space 的 shadcn/ui 社区中心值得收藏。在本教程后续你会再次回到这里。
步骤 4:在你的项目中设置 shadcn/ui
由于你已经在步骤 1 中拥有了一个 Next.js 项目,因此可以直接在此项目上应用 Shadcn Space 预设:
npx shadcn@latest apply --preset b0
这会在你的现有项目中创建 components.json、Tailwind 配置以及 lib/utils.ts。接着,拉取聊天界面需要的组件:
npx shadcn@latest add button input textarea scroll-area avatar separator
每次调用 add 会把真实可读的组件源码复制到 components/ui/ 目录,随时可以像项目中的其他文件一样导入和编辑,之后无需再与已编译的包打交道。
如果你不想从单独的原始组件手动拼装消息列表和作曲器,Shadcn Space 还提供了现成的 AI 聊天块,你可以直接添加:
npx shadcn@latest add @shadcn-space/ai-chat-01
npx shadcn@latest add @shadcn-space/ai-chat-03
ai-chat-01 提供了带有欢迎屏幕、建议提示、可滚动消息线程以及带有附件和模型选择器的作曲器的对话界面。
AI Chat 01 实时预览:
ai-chat-03 提供了带有可折叠侧边栏、固定和最近聊天、搜索以及顶部栏的周围应用程序外壳。
AI Chat 03 实时预览:
您可以单独安装任意一个块,或者如果想要完整的聊天布局而不必从头构建周围界面,则可以同时安装两者。
两个块均为付费功能。如果您希望为聊天界面获得免费侧边栏,则标准 shadcn sidebar-07 块是一个轻量级且免费的替代方案:
npx shadcn@latest add sidebar-07
步骤 5:构建流式 API 路由
创建 app/api/chat/route.ts。此服务器端部分负责与模型通信,并将响应流式传输回浏览器。
// app/api/chat/route.ts
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { convertToModelMessages, streamText, type UIMessage } from "ai";
const PROVIDERS: Record = {
openai: {
baseURL: "https://api.openai.com/v1",
model: "gpt-4o-mini",
},
gemini: {
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
model: "gemini-2.5-flash",
},
groq: {
baseURL: "https://api.groq.com/openai/v1",
model: "llama-3.3-70b-versatile",
},
};
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const providerName =
process.env.AI_PROVIDER?.trim().toLowerCase() ?? "openai";
const { baseURL, model } =
PROVIDERS[providerName] ?? PROVIDERS.openai;
const provider = createOpenAICompatible({
name: providerName,
baseURL: process.env.AI_BASE_URL ?? baseURL,
apiKey: process.env.AI_API_KEY,
});
const result = streamText({
model: provider(process.env.AI_MODEL ?? model),
system: "You are a concise, helpful assistant.",
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
以下几点值得注意:
createOpenAICompatible提供一个可用于任何 OpenAI 风格 API 的提供程序实例。在openai、gemini或groq之间切换AI_PROVIDER,您的路由处理程序完全不需要更改。convertToModelMessages在客户端发送的 UI 消息格式与模型提供程序期望的格式之间架起桥梁。streamText启动模型生成并返回一个可以直接管道到客户端的流。toUIMessageStreamResponse()将该流封装成客户端useChat钩子能够逐标记消费的响应。
在 .env 中设置您的提供程序和密钥:
# .env
AI_PROVIDER=openai
AI_API_KEY=
如果你还没有密钥,也可以先构建 UI。只需让此路由返回一个预设的流式响应,直到你准备好连接真正的提供者。下面的客户端代码不关心流来自哪里。
步骤 6:使用 useChat 连接客户端
现在,让我们构建聊天界面。创建一个 components/chat.tsx 文件:
// components/chat.tsx
"use client";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
const isLoading =
status === "submitted" || status === "streaming";
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ text: input });
setInput("");
};
return (
{messages.map((message) => (
{message.role !== "user" && (
AI
)}
{message.parts.map((part, i) =>
part.type === "text" ? (
{part.text}
) : null
)}
))}
);
}
将 放入 app/page.tsx 中,然后运行 npm run dev。此时你拥有一个可用的流式聊天界面。模型返回的每条消息会逐字出现,而不是一次性全部显示,而 status 提供了一种在响应进行时禁用输入的简洁方式。
请注意,useChat 在这里完成了大量幕后工作:它负责消息列表,在数据块到达时处理流式重新组装,并管理已提交、流式和就绪的生命周期,这样你就不需要自己跟踪这些状态。
实时预览:
步骤 7:让模型调用工具
仅能说话的聊天框功能有限。AI SDK 允许你的模型在代码中调用真实函数,并使用 JSON 模式描述其被允许发送的输入。
将此添加到你的路由处理程序中,紧随步骤 5 中的提供程序设置之后:
// app/api/chat/route.ts
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import {
convertToModelMessages,
jsonSchema,
streamText,
tool,
type UIMessage,
} from "ai";
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const provider = createOpenAICompatible({
name: "openai",
baseURL: "https://api.openai.com/v1",
apiKey: process.env.AI_API_KEY,
});
const result = streamText({
model: provider("gpt-4o-mini"),
messages: convertToModelMessages(messages),
tools: {
getWeather: tool({
description: "Get the current weather for a city",
inputSchema: jsonSchema<{ city: string }>({
type: "object",
properties: {
city: {
type: "string",
description: "The city to get the weather for",
},
},
required: ["city"],
}),
execute: async ({ city }) => {
// Call a real weather API here in production
return {
city,
temperature: 22,
condition: "clear",
};
},
}),
},
});
return result.toUIMessageStreamResponse();
}
模型决定何时调用 getWeather,SDK 将该调用路由到你的 execute 函数,结果作为消息的一部分流式返回到对话中。除非你想以不同于纯文本的方式渲染工具部分,否则在客户端只需检查 part.type,无需额外的管道。
步骤 8:在没有后端的情况下原型化 UI
你会不断遇到的一个问题是:在后端或 API 密钥甚至还没准备好之前,你就想打磨聊天 UI,包括间距、动画以及推理块如何折叠。每次调整一个像素都要用真实模型重建该 UI,既慢又会消耗大量 token。
这就是 shadcn AI SDK 辅助工具 包所解决的问题。它允许你编写一个假的对话,并通过你已经在使用的相同 useChat 钩子进行流式传输,过程中不需要服务器、模型或 API 密钥:
npm install @shadcn/helpers
import { createChat } from "@shadcn/helpers";
import { useChat } from "@ai-sdk/react";
const chat = createChat()
.user("What changed in the last release?")
.assistant("The release added keyboard shortcuts and faster search.");
function ChatPreview() {
const { messages } = useChat({
messages: chat.get(0),
transport: chat.transport(),
});
// render `messages` exactly like you would with a real backend
}
它支持 AI SDK 所理解的所有部件类型,包括推理、工具调用、文件和数据源,并且每次都能确定性地流式传输。这也使其在编写可重现的演示或 UI 测试时变得真正有用。
您可以这样构建和完善整个界面,待后端准备好时,立即替换为真实的 /api/chat 路由。
将您的聊天转变为完整产品
聊天窗口很少单独发货。一旦您的聊天窗口能够工作,您通常需要一个用于过去对话的侧边栏、一个用于模型选择的设置面板,以及可能的管理视图来查看所有用户的使用情况。这与流式传输文本是不同的问题。这属于应用程序外壳和数据表领域。
与其手动构建该外壳,大多数团队会选择使用预构建的管理布局。一个现成的 shadcn 仪表盘(如 Shadcn Space 提供的那个),内置了内部工具所需的布局、数据表、图表和导航模式,这样您就无需从零开始重新构建侧边栏和设置页面,只是为了让聊天功能有个容身之所。
而且如果您还想省去构建聊天屏幕本身的步骤,这也是一个真实可行的选择。一些团队直接从一个现成的 shadcn AI 聊天应用 模板开始,该模板已经内置了对话侧边栏、Markdown 和代码渲染以及工具调用可视化功能。稍后我们会回到这一点。
完整聊天应用的实时预览:
使用 MCP 服务器加速 shadcn 开发
无论您如何构建聊天 UI,都有一种比从文档复制粘贴更快的获取组件的方式:MCP(模型上下文协议)服务器,它为您的 AI 编码助手提供对组件注册表的实时访问。
Shadcn MCP 服务器将 Claude Code、Cursor 和 Windsurf 等工具直接连接到 Shadcn Space 组件目录。您无需再搜索文档并粘贴安装命令,只需向助手说明您需要的内容,例如“添加一个带有头像和时间戳的消息气泡组件”,它就会拉取真实的当前组件定义,而不是依赖过时的训练数据进行猜测。
设置它只需为 Claude Code 输入一行命令:
claude mcp add shadcnspace-mcp -- npx -y shadcnspace-mcp@latest
其他编辑器只需将相同的命令放入其 MCP 配置文件中,例如 Cursor 中的 .cursor/mcp.json。完整的 MCP 服务器入门指南 逐步介绍了每个受支持编辑器的配置,而 Shadcn MCP 页面详细说明了连接后它可以搜索、安装和生成的内容。
如果您更喜欢观看设置过程而非阅读,这里有一段简短的演示,完整地讲解了相同的步骤。
发布前需要处理的几件事
上面的教程版本故意保持最小化。在将其投入生产之前,请添加:
速率限制 在您的
/api/chat路由上。没有限制的聊天端点很容易导致模型费用飙升。中止处理 让用户能够在响应流中途停止,
useChat通过内置的stop()函数直接支持。错误边界 包裹聊天组件,以防止流中断或服务提供者故障导致整个页面崩溃。
身份验证,如果需要将响应或对话历史限定为特定用户。
这些并非高深莫测的技术。它们是您会为任何 API 路由采用的相同生产基础。聊天端点只是让人更容易忽略它们,因为在开发过程中‘幸福路径’看起来异常顺畅。
使用现成模板跳过样板代码
上面的内容已经能够得到一个真实可用的聊天界面,但这只是教程版本。生产级聊天产品通常还需要会话侧边栏、项目分组、Markdown 以及语法高亮的代码块、推理面板、语音输入以及用于切换模型的设置页面。从零开始构建所有这些功能可能本身就是一个耗时数周的工作。
如果你不想自己构建这个外壳,值得看看 Shadcn Space 提供的 shadcn AI 聊天应用 模板。它基于本文介绍的同一套基础构建(Next.js、Vercel AI SDK 和 shadcn ui),但已经内置了侧边栏、推理和工具调用 UI、文件附件以及多提供商模型切换。你只需设置 AI_PROVIDER 和 AI_API_KEY,即可通过成熟的界面与真实模型对话。
你可以查看 实时演示,以便在决定是自己构建还是从模板开始之前,准确看到侧边栏、流式传输和工具调用的行为。
总结
你现在拥有一个能够流式返回真实响应、调用工具的聊天界面,它完全基于你拥有且可以自由编辑的组件构建。对于 AI 产品而言,这些功能的重要性远超你的想象。Vercel AI SDK 负责处理复杂的流式传输和模型逻辑,而 shadcn/ui 则让你完全控制该逻辑在屏幕上的呈现方式。
从这里开始,自然的下一步是接入真实提供商,添加上述的生产基础,并决定是继续扩展自己的 UI,还是依赖成品模板来更快地构建周边产品。无论哪种方式,你现在已经了解了底层实际发生的事情,这将使两条路径都变得更加容易。
资源
我在 Ashutosh Rada(高级前端开发者)的帮助下撰写了这篇文章。在 LinkedIn 上联系我.



