前端技术栈全链路实战
目标
我们要做一个类 ChatGPT 的聊天界面,具备:
- 流式输出(逐字打印)
- 支持多轮对话(维持历史)
- 支持工具调用(让 AI 能查天气等)
- Markdown 渲染 + 代码高亮
- 暗黑模式(顺手)
用到的技术:Next.js 15 + React 19 + Vercel AI SDK。
为什么选 Vercel AI SDK
作为前端开发者,强烈推荐 ai 这个库:
-统一的接口:OpenAI、Anthropic、Google、本地模型写法一样,换供应商改一行
-内置 React Hooks:useChat / 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)。
可以直接参考的开源项目
这些是大厂级别的实现,抄作业吧:
- Vercel AI Chatbot — AI SDK 官方示例,生产级
- LobeChat — 多模型、插件、支持 MCP
- Open WebUI — Ollama 官方 UI
- Chatbot UI — 最小化实现
动手作业
基于上面的 Demo:
- 加持久化(SQLite + Drizzle ORM)
- 加上对话列表侧栏(新建、重命名、删除)
- 加 5 个有意思的工具(搜索、计算器、翻译、生成二维码、查汇率)
- 部署到 Vercel
参考资料
- Vercel AI SDK 文档 — 权威参考
- AI SDK Examples
- Next.js 官方文档
- shadcn/ui — 配套的高质量组件
- Streamdown — 流式 Markdown 优化
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:构建你的第一个 AI 聊天应用
本文链接:https://www.sshipanoo.com/blog/ai/ai-for-frontend/09-构建AI聊天应用/
本文最后一次更新为 天前,文章中的某些内容可能已过时!