← 文章 / 编程开发
freeCodeCamp 1小时前 · 2026-09-13 09:28:26 · 2 阅读

使用 Vercel AI SDK 和 shadcn/ui 构建 AI 聊天应用界面

如今随便打开一个 AI 产品,看到的都是同样的界面:一列消息、底部的输入框,还有逐 token 流式输出的文字。看起来简单,但做好并不容易。

你需要处理流式状态、不完整的 token、tool 调用、重试、markdown 渲染、滚动位置,还有一堆细节体验问题……同时还得保证界面可访问且流畅。工具选不对,你会发现时间都耗在修状态 bug 上,而不是做自己的产品。

本教程将带你用两个天生一对的工具搭建一个真实的 AI 聊天界面:Vercel AI SDK 负责流式传输和模型逻辑,shadcn/ui 负责界面本身。

读完本文,你将得到一个能正常工作的聊天界面:支持流式响应、markdown 渲染,而且成品拿来就能上线。

你还会学到如何借助 MCP server 进一步加速 UI 开发,以及如果想完全跳过搭建步骤,去哪里找现成的生产级聊天模板。

我们要做的成品 - AI 聊天界面截图

目录

前置条件

你需要:

  • Node.js 18 或更高版本

  • 对 React 和 Next.js 有基本了解,特别是 App Router

  • 来自 OpenAI、Anthropic 或 Google 等 LLM 提供商的 API 密钥。即使没有密钥也可以跟着做,稍后会详细说明。

你将构建的内容

我们将构建一个基于 Next.js 的聊天应用,包含以下功能:

  • 一个流式 API 路由,用于调用 LLM 提供商

  • 使用 useChat 构建的客户端聊天界面

  • 消息气泡、自动扩展输入框和可滚动的对话记录,均使用 shadcn/ui 组件进行样式设计

  • 一个简单的工具调用,让模型不仅能对话,还能执行更多操作

  • 一种回退状态,即使未添加 API 密钥也能正常工作,允许你先构建 UI,之后再连接模型

让我们从一个空文件夹开始,逐步构建出一个足以向同事展示的应用。

第一步:搭建 Next.js 应用骨架

创建一个新的 Next.js 项目,启用 TypeScript 和 Tailwind:

npx create-next-app@latest ai-chat-app --typescript --tailwind --eslint --app
cd ai-chat-app

对于 CLI 询问的其他选项,保持默认设置即可。你几乎将在 app 目录内完成所有工作。

第二步:安装 Vercel AI SDK

Vercel AI SDK 在这里承担了主要工作。它提供了一致 API 来调用不同的模型提供商,支持流式传输文本和结构化数据,并处理工具调用,这样在切换模型时就不必重写聊天逻辑。

安装核心包、React 绑定以及 OpenAI 兼容提供商:

npm install ai @ai-sdk/react @ai-sdk/openai-compatible

特别要提一下 @ai-sdk/openai-compatible:你无需为每个提供商单独安装不同的包,只需切换 base URL,就能指向任何支持 OpenAI 风格 API 的提供商(如 OpenAI 本身、Gemini、Groq,以及许多自托管环境)。配合 AI_PROVIDER 环境变量,你甚至不用修改路由处理器就能切换提供商,这正是下一步要实现的模式。

步骤 3:为什么 shadcn/ui 特别适合 AI 聊天界面

在编写 UI 代码之前,不妨先了解一下,为什么许多 AI 聊天产品选择 shadcn/ui 而非传统组件库。

大多数组件库提供一个编译后的包,并通过 props 隐藏内部实现。这在设置页面中没问题,但用在聊天界面上效果很差。聊天场景需要精细控制消息气泡在流式输出时的动画、「思考中」指示器的状态,以及工具调用与普通文本的不同渲染方式。

shadcn/ui 采取了不同的方法:不安装包,而是直接将组件源码复制到你的项目中。你完全拥有这份代码,不必与抽象层搏斗来适应你的需求,也不用等维护者为你暴露某个特定 prop。这种所有权模式正是聊天界面所需要的,因为几乎没有两个 AI 产品会以完全相同的方式渲染消息、推理过程或工具输出。

这也是围绕它形成完整生态系统的原因。如果你想在默认注册表之外获得更多生产级区块和模板(包括仪表盘、营销章节和完整的聊天 UI),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 chat blocks,可以直接添加:

npx shadcn@latest add @shadcn-space/ai-chat-01

npx shadcn@latest add @shadcn-space/ai-chat-03

ai-chat-01 提供对话界面,包含欢迎屏、建议提示词、可滚动的消息列表,以及支持附件和模型选择器的输入框。

AI Chat 01 实时预览:

Live Preview of AI Chat 01

ai-chat-03 提供外层应用框架,包含可折叠侧边栏、置顶和最近会话、搜索功能以及顶栏。

AI Chat 03 实时预览:

Live Preview of AI Chat 03

这两个 block 可以单独安装,也可以一起安装,这样就能直接获得完整的聊天布局,无需从零搭建界面。

两个 block 都是付费的。如果你想给聊天界面配一个免费的侧边栏,标准 shadcn 的 sidebar-07 block 是一个轻量的免费替代方案:

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<string, { baseURL: string; model: string }> = {
  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 的 Provider 实例。只需切换 AI_PROVIDER 环境变量为 openaigeminigroq,路由处理逻辑无需任何改动。

  • convertToModelMessages 用于转换消息格式,将客户端发送的 UI 消息格式转为模型服务方所需的格式。

  • streamText 启动模型生成流程,返回一个可直接转发给客户端的流。

  • toUIMessageStreamResponse() 将该流封装为响应,客户端的 useChat Hook 可据此逐 Token 解析消费。

.env 中配置 Provider 和密钥:

# .env
AI_PROVIDER=openai
AI_API_KEY=

如果尚未获取密钥,仍可先构建 UI。此时可让路由返回预设的流式响应,待准备就绪后再接入真实 Provider。下方客户端代码不关心流的来源。

第 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 (
    <div className="flex h-screen flex-col">
      <ScrollArea className="flex-1 p-4">
        <div className="mx-auto flex max-w-2xl flex-col gap-4">
          {messages.map((message) => (
            <div
              key={message.id}
              className={cn(
                "flex gap-3",
                message.role === "user" && "justify-end"
              )}
            >
              {message.role !== "user" && (
                <Avatar className="h-8 w-8">
                  <AvatarFallback>AI</AvatarFallback>
                </Avatar>
              )}

              <div
                className={cn(
                  "max-w-[75%] rounded-2xl px-4 py-2 text-sm",
                  message.role === "user"
                    ? "bg-primary text-primary-foreground"
                    : "bg-muted"
                )}
              >
                {message.parts.map((part, i) =>
                  part.type === "text" ? (
                    <span key={i}>{part.text}</span>
                  ) : null
                )}
              </div>
            </div>
          ))}
        </div>
      </ScrollArea>

      <form onSubmit={handleSubmit} className="border-t p-4">
        <div className="mx-auto flex max-w-2xl items-end gap-2">
          <Textarea
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="向助手发送消息..."
            className="min-h-11 flex-1 resize-none"
            disabled={isLoading}
          />

          <Button
            type="submit"
            disabled={isLoading || !input.trim()}
          >
            发送
          </Button>
        </div>
      </form>
    </div>
  );
}


<Chat /> 组件放入 app/page.tsx,然后运行 npm run dev。现在你就拥有了一个可以流式输出的聊天界面。模型返回的每条消息都会逐字显示,而不是整段一次性输出;同时 status 状态让你能在响应处理期间干净地禁用输入框。

注意 useChat 在这里默默承担了大量工作:它管理消息列表,处理数据块到达时的流式重组,并维护 submitted、streaming 和 ready 的生命周期,省去你手动跟踪这些状态的麻烦。

实时预览:

Live Preview of Chat

Step 7: Let the Model Call Tools

只能聊天的输入框功能有限。AI SDK 允许模型调用代码中的实际函数,并通过 JSON schema 描述允许传入的参数。

在 Step 5 的 provider 配置旁,往路由处理程序里加上这段代码:

// 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,过程既缓慢又耗费 token。

这正是 shadcn AI SDK helper 包解决的问题。它允许你脚本化一个假对话,并通过你已在使用的同一个 useChat hook 将其流式传输,全程无需服务器、模型或 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 dashboard(比如 Shadcn Space 提供的那款)自带内部工具所需的布局、数据表格、图表和导航模式,你不用为了让聊天功能有个落脚点,就重新造一遍侧边栏和设置页。

如果你连聊天界面本身也不想写,这也是完全可行的。有些团队会直接从一个现成的 shadcn AI chat app 模板起步,它已经内置了会话侧边栏、markdown 和代码渲染、工具调用可视化等功能。文末我们会再聊到这个。

完整聊天应用在线预览:

完整聊天应用在线预览

用 MCP Server 加速 shadcn 开发

不管你怎么搭建聊天 UI,都有比从文档里复制粘贴更快的组件引入方式:用 MCP(Model Context Protocol)server 让你的 AI 编程助手直接访问组件仓库。

Shadcn MCP server 能把 Claude Code、Cursor、Windsurf 这类工具直接连到 Shadcn Space 组件目录。你不用自己去翻文档、粘贴安装命令,只需要告诉助手你需要什么,比如"加一个带头像和时间戳的消息气泡组件",它就会拉取真实、最新的组件定义,而不是凭过时的训练数据瞎猜。

在 Claude Code 里只需一行命令即可完成配置:

claude mcp add shadcnspace-mcp -- npx -y shadcnspace-mcp@latest

其他编辑器只需把同样的命令写进各自的 MCP 配置文件,比如 Cursor 的 .cursor/mcp.jsonMCP server 入门指南详细介绍了各支持编辑器的配置方法,Shadcn MCP 页面则说明了它连接后具体能搜索、安装和生成哪些内容。

如果比起阅读教程,你更倾向于观看视频,这里有一段简短的演示视频,完整覆盖了上述所有步骤。

上线前需要处理的几个事项

上述教程版本刻意保持极简。在将其推向生产环境之前,请补充以下内容:

  • /api/chat 路由添加速率限制。没有限制的聊天端点是产生巨额模型账单的最快途径。

  • 实现中止处理,允许用户在流式传输过程中停止响应。通过 useChatstop() 函数即可开箱即用。

  • 为聊天组件添加错误边界,因为流中断或服务商故障不应导致整个页面崩溃。

  • 添加身份验证,如果需要将响应或对话历史限定在特定用户范围内。

这些都不是什么高深技术。它们就是应用于任何 API 路由的标准生产级配置。聊天端点容易让人忽略这些基础项,因为在开发阶段一切看起来都格外顺滑。

使用现成模板跳过样板代码

上述步骤能让你拥有一个真正可用的聊天界面,但这仅是教程版本。生产级聊天产品通常还需要会话侧边栏、项目分组、Markdown 和带语法高亮的代码块、推理面板、语音输入以及用于切换模型的设置界面。从零构建所有这些功能本身就是一项耗时数周的工作。

如果你不想自己搭建这套外壳,建议看看 Shadcn Space 上的shadcn AI 聊天应用模板。它基于本文介绍的技术栈(Next.js、Vercel AI SDK 和 shadcn ui)构建,但已预集成侧边栏、推理和工具调用 UI、文件附件以及多服务商模型切换功能。只需设置 AI_PROVIDERAI_API_KEY,即可通过完善的界面与真实模型进行对话。

你可以访问在线演示,查看侧边栏、流式传输和工具调用的具体表现,再决定是自行开发还是基于模板启动。

总结

现在你已经拥有一个可以流式输出真实回复、调用工具的聊天界面,所有组件均由你自主掌握,可自由编辑。对于 AI 产品而言,这一点比听起来更重要。Vercel AI SDK 负责处理复杂的流式传输和模型逻辑,而 shadcn/ui 则让你完全掌控这些逻辑在屏幕上的视觉呈现。

接下来,自然的步骤是接入真实的服务提供商、添加上述生产环境所需的基础功能,并决定是继续扩展自定义 UI,还是依赖现成的模板来加速周边产品的构建。无论选择哪条路径,由于你现在已透彻理解底层运行机制,实施起来都会轻松许多。

资源

本文撰写过程中得到了 Ashutosh Rada(高级前端开发)的协助。在 LinkedIn 上连接

原始来源: freeCodeCamp

评论 (0)