前端技术栈全链路实战

目标

我们要做一个类 ChatGPT 的聊天界面,具备:

  • 流式输出(逐字打印)
  • 支持多轮对话(维持历史)
  • 支持工具调用(让 AI 能查天气等)
  • Markdown 渲染 + 代码高亮
  • 暗黑模式(顺手)

用到的技术:Next.js 15 + React 19 + Vercel AI SDK

为什么选 Vercel AI SDK

作为前端开发者,强烈推荐 ai 这个库:

-统一的接口:OpenAI、Anthropic、Google、本地模型写法一样,换供应商改一行 -内置 React HooksuseChat / useCompletion 开箱即用 -流式响应:完整解决 SSE、缓冲、中断、错误 -工具调用、多模态、RAG全覆盖

  • 背靠 Vercel,生态和维护都稳

项目初始化

npx create-next-app@latest ai-chat --typescript --tailwind --app
cd ai-chat
npm i ai @ai-sdk/openai zod
npm i react-markdown remark-gfm react-syntax-highlighter
npm i -D @types/react-syntax-highlighter

环境变量:

# .env.local
OPENAI_API_KEY=sk-xxx
# 或用 DeepSeek
# OPENAI_BASE_URL=https://api.deepseek.com/v1

后端:一个文件搞定

// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai'
import { streamText, tool } from 'ai'
import { z } from 'zod'

export async function POST(req: Request) {
 const { messages } = await req.json()

 const result = await streamText({
 model: openai('gpt-4o-mini'),
 system: '你是一个友善、简洁的助手。',
 messages,
 tools: {
 getWeather: tool({
 description: '查询城市的当前天气',
 parameters: z.object({
 city: z.string().describe('城市名'),
 }),
 execute: async ({ city }) => {
 // 调真实 API 的地方,这里假数据
 return { city, temperature: 22, condition: '晴' }
 },
 }),
 },
 maxSteps: 5, // 允许多轮工具调用
 })

 return result.toDataStreamResponse()
}

就这样,后端完成了。它负责:

  • 接收前端发来的 messages
  • 调用 LLM(带工具)
  • 以流式格式返回数据

前端:useChat Hook

// app/page.tsx
'use client'

import { useChat } from 'ai/react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'

export default function Home() {
 const {
 messages,
 input,
 handleInputChange,
 handleSubmit,
 isLoading,
 stop,
 reload,
 } = useChat({
 api: '/api/chat',
 })

 return (
 <div className="max-w-3xl mx-auto p-6 h-screen flex flex-col">
 <h1 className="text-2xl font-bold mb-4">AI 聊天</h1>

 <div className="flex-1 overflow-y-auto space-y-4 mb-4">
 {messages.map((m) => (
 <div
 key={m.id}
 className={`p-3 rounded-lg ${
 m.role === 'user'
 ? 'bg-blue-100 dark:bg-blue-900 ml-auto max-w-[80%]'
 : 'bg-gray-100 dark:bg-gray-800 mr-auto max-w-[80%]'
 }`}
 >
 <div className="text-xs opacity-60 mb-1">
 {m.role === 'user' ? '你' : 'AI'}
 </div>

 {m.parts?.map((part, i) => {
 if (part.type === 'text') {
 return (
 <ReactMarkdown
 key={i}
 remarkPlugins={[remarkGfm]}
 components={{
 code({ inline, className, children }) {
 const match = /language-(\w+)/.exec(className || '')
 return !inline && match ? (
 <SyntaxHighlighter
 language={match[1]}
 style={oneDark}
 PreTag="div"
 >
 {String(children).replace(/\n$/, '')}
 </SyntaxHighlighter>
 ) : (
 <code className={className}>{children}</code>
 )
 },
 }}
 >
 {part.text}
 </ReactMarkdown>
 )
 }
 if (part.type === 'tool-invocation') {
 return (
 <div
 key={i}
 className="text-xs bg-yellow-50 dark:bg-yellow-900 p-2 rounded mt-2 font-mono"
 >
 调用: {part.toolInvocation.toolName}
 <pre>{JSON.stringify(part.toolInvocation.args, null, 2)}</pre>
 {part.toolInvocation.state === 'result' && (
 <pre>{JSON.stringify(part.toolInvocation.result, null, 2)}</pre>
 )}
 </div>
 )
 }
 })}
 </div>
 ))}
 </div>

 <form onSubmit={handleSubmit} className="flex gap-2">
 <input
 value={input}
 onChange={handleInputChange}
 placeholder="说点什么..."
 disabled={isLoading}
 className="flex-1 px-4 py-2 border rounded-lg dark:bg-gray-900"
 />
 {isLoading ? (
 <button type="button" onClick={stop} className="px-4 bg-red-500 text-white rounded-lg">
 停止
 </button>
 ) : (
 <button type="submit" className="px-4 bg-blue-500 text-white rounded-lg">
 发送
 </button>
 )}
 </form>
 </div>
 )
}

跑起来

npm run dev

打开 http://localhost:3000,就能聊天了。试试:

  • "你好"(普通对话)
  • "北京现在天气"(触发工具调用)
  • "写个快排"(会 Markdown + 代码高亮)

加一层 RAG

如果你想让它回答你自己的数据,把第 5 篇的 RAG 集成进来:

// app/api/chat/route.ts
import { retrieveRelevantDocs } from '@/lib/rag'

export async function POST(req: Request) {
 const { messages } = await req.json()
 const lastUser = messages.filter((m) => m.role === 'user').pop()

 // 检索
 const docs = await retrieveRelevantDocs(lastUser.content, 3)
 const context = docs.map((d, i) => `[${i + 1}] ${d.text}`).join('\n\n')

 const result = await streamText({
 model: openai('gpt-4o-mini'),
 system: `你是一个助手,优先使用下面的资料回答:\n${context}`,
 messages,
 })

 return result.toDataStreamResponse()
}

生产要加什么

这个 Demo 能跑,但离上线还差:

1. 持久化— 现在刷新就没了。用 Postgres 存 conversation + messages。

2. 鉴权— 用 NextAuth / Clerk,不然谁都能用你的 API。

3. 速率限制Upstash Ratelimit 限制每个用户每分钟请求数。

4. 成本监控— 记录每次调用的 Token 数,用户维度统计。

5. 错误处理— 网络抖动、LLM 超时、工具失败……UI 都要有反馈。

6. 安全— 过滤 Prompt 注入、敏感内容审核(用 OpenAI Moderation 或国内的内容安全 API)。

可以直接参考的开源项目

这些是大厂级别的实现,抄作业吧:

动手作业

基于上面的 Demo:

  1. 加持久化(SQLite + Drizzle ORM)
  2. 加上对话列表侧栏(新建、重命名、删除)
  3. 加 5 个有意思的工具(搜索、计算器、翻译、生成二维码、查汇率)
  4. 部署到 Vercel

参考资料

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:构建你的第一个 AI 聊天应用

本文链接:https://www.sshipanoo.com/blog/ai/ai-for-frontend/09-构建AI聊天应用/

本文最后一次更新为 天前,文章中的某些内容可能已过时!