【从0开发一个 Agent】第四章:实现第一个 AI Chat
在上一章中,我们成功搭建了 Next.js + Prisma + HeroUI 的项目骨架。但此时的项目只是一个空壳,缺乏与 AI 交互的灵魂。
本章的目标是实现最基础的 AI 对话功能。我们将打通前后端,实现流式响应(Streaming)、Markdown 渲染以及代码高亮,让你的应用真正具备“ChatGPT 级别”的交互体验。
1. 为什么需要 Streaming(流式响应)?
在开始写代码之前,我们必须理解一个核心概念:为什么 AI 聊天必须是流式的?
传统请求的痛点:
在普通的 Web 请求中,用户发送消息后,需要等待服务端生成完整的回复,再一次性返回给前端。对于 LLM 来说,生成一段长文本可能需要 5-10 秒。在这期间,用户只能面对一个旋转的 Loading 图标,体验极其割裂。
Streaming 的优势:
大模型本质上是“逐字(Token)生成”的。Streaming 技术利用了 Server-Sent Events (SSE) 协议,将大模型生成的每一个 Token 实时推送到前端。用户能像看“打字机”一样看到文字逐字出现。这不仅极大地缓解了用户的等待焦虑,还让交互感觉更加自然和智能。
2. 核心架构与数据流转
在 Next.js App Router 中实现 AI Chat,我们需要遵循前后端分离的流式架构:

关键组件解析:
streamText (AI SDK):核心函数,负责与 LLM 通信并将复杂的流式协议转换为标准的 Web 响应。
useChat (AI SDK React Hook):前端魔法,自动管理消息列表、输入框状态、加载状态,并无缝对接后端的 SSE 流。
3. 代码实现:后端 API Route
首先,我们在 src/app/api/chat/route.ts 中创建对话接口。
// src/app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@/lib/ai/config'; // 引入上一章配置的 AI SDK 实例
// 允许流式响应最长持续 30 秒(Vercel 等 Serverless 环境需要)
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'), // 或 deepseek-chat
system: '你是一个专业的 AI 助手,回答请尽量简洁、准确,支持 Markdown 格式。',
messages,
});
// 将 AI SDK 的结果转换为标准的流式 HTTP 响应
return result.toDataStreamResponse();
}
设计思考:
为什么不直接返回 JSON?因为 toDataStreamResponse() 内部封装了 Vercel AI SDK 自定义的 Data Stream 协议,它不仅传输文本,还能传输工具调用状态、错误信息等结构化数据,为后续实现 Tool Calling 打下基础。
4. 代码实现:前端 UI 与 Markdown 渲染
4.1 消息气泡组件
AI 的回复通常包含代码块和列表,我们需要 react-markdown 配合 remark-gfm 来渲染。为了美观,我们引入 react-syntax-highlighter 进行代码高亮。
npm install react-syntax-highlighter @types/react-syntax-highlighter
// src/components/chat/MessageBubble.tsx
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 function MessageBubble({ role, content }: { role: string; content: string }) {
const isUser = role === 'user';
return (
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'} mb-4`}>
<div className={`max-w-[80%] p-3 rounded-lg ${isUser ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-800'}`}>
{isUser ? (
<p>{content}</p>
) : (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter style={oneDark} language={match} PreTag="div" {...props}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>{children}</code>
);
}
}}
>
{content}
</ReactMarkdown>
)}
</div>
</div>
);
}
```<websource>source_group_web_1</websource>
### 4.2 聊天主页面
使用 `useChat` Hook 接管所有状态:
```tsx
// src/app/page.tsx
"use client";
import { useChat } from 'ai/react';
import { MessageBubble } from '@/components/chat/MessageBubble';
import { Input, Button } from '@heroui/react';
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
});
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-4"> AI Agent Enterprise</h1>
{/* 消息列表区 */}
<div className="flex-1 overflow-y-auto space-y-2 mb-4">
{messages.map((m) => (
<MessageBubble key={m.id} role={m.role} content={m.content} />
))}
{isLoading && <div className="text-gray-400">AI 正在思考...</div>}
</div>
{/* 输入区 */}
<form onSubmit={handleSubmit} className="flex gap-2">
<Input
value={input}
onChange={handleInputChange}
placeholder="输入你的问题..."
className="flex-1"
/>
<Button type="submit" color="primary" isLoading={isLoading}>
发送
</Button>
</form>
</div>
);
}
5. 测试验证
启动项目 npm run dev,打开浏览器。
验证清单:
- 输入一段包含代码的请求(例如:“用 Python 写一个快速排序”)。
- 观察文字是否像打字机一样逐字流出(Streaming 验证)。
- 观察代码块是否有语法高亮,且排版整齐(Markdown 验证)。
- 在 AI 回复过程中,点击发送按钮,检查是否被正确禁用(Loading 状态验证)。
6. 常见问题与踩坑分析
问题 1:Next.js 报错 Dynamic server usage
原因:useChat 是一个依赖浏览器 window 和 fetch 的 Hook,不能在 Server Component 中使用。
解决:确保包含 useChat 的页面或组件顶部有 “use client”; 指令。
问题 2:流式响应中断或报错 maxDuration exceeded
原因:Serverless 环境(如 Vercel Hobby 计划)默认限制函数执行时间为 10 秒。
解决:在 API Route 中显式声明 export const maxDuration = 30;,并确保你的 AI 提供商套餐支持长连接。
问题 3:代码高亮样式丢
原因:react-syntax-highlighter 的样式在 SSR 时可能无法正确注入。
解决:确保在 Client Component 中引入样式,或者改用 shiki(Next.js 官方推荐的高亮库,对 SSR 更友好)。
本章总结
- 我们理解了 Streaming 对于 AI 应用体验的决定性作用。
- 实现了基于 Vercel AI SDK 的 streamText 后端流式接口。
- 利用 useChat Hook 在前端实现了零样板代码的状态管理。
- 集成了 react-markdown 与 react-syntax-highlighter,实现了生产级的富文本渲染。
至此,你的项目已经具备了与 ChatGPT 媲美基础对话能力。
但这仅仅是开始。目前的还仅仅是具备了聊天能力。从下一章开始,我们将完善聊天体验,达到生产级效果。
更多推荐


所有评论(0)