在上一章中,我们成功搭建了 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 媲美基础对话能力。

但这仅仅是开始。目前的还仅仅是具备了聊天能力。从下一章开始,我们将完善聊天体验,达到生产级效果。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐