0个 useEffect,0个状态库:如何用 Next.js 优雅地把大模型流式 UI“挤”给前端

在过去,要在前端实现一个类似 ChatGPT 的"打字机式"大模型流式对话界面,前端工程师往往需要编写大量痛苦的胶水代码:

text 复制代码
传统的客户端流式处理:
[大模型] ──(Stream)──> [业务后端] ──(SSE/WebSocket)──> [客户端 useState 频繁触发] ──> [重新渲染]

你不仅要在客户端处理 ReadableStream 的读取、处理流的闭合,还要引入 Redux、Zustand 或维护极为复杂的 useState 来缓存已经吐出来的文本。一旦涉及到 Markdown 解析或图表混排,客户端的性能和代码复杂度直接飙升。

然而,在 Next.js(App Router)与 Vercel AI SDK 的全栈新范式下,这一切技术债都不复存在了。

借助 React Server Components (RSC) 的流式渲染能力与 Server Actions 的无缝前后端通信,我们可以直接在服务端调配大模型,并将流式 UI 像"挤牙膏"一样平滑地直接注入到前端页面中,客户端甚至不需要维护任何全局状态。

接下来带你拆解这一全栈 AI 研发的最优解法。

一、 核心架构:为什么 RSC + Server Actions 是 AI 流式传输的绝配?

现代 Next.js 提供的 Server Actions 允许我们直接在客户端调用定义在服务端的异步函数。结合 Vercel AI SDK,其流式模型传输的革新工作流如下:

text 复制代码
现代 AI SDK + RSC 架构:
[Client 点击发送] ──(Server Action)──> [Server 直接调用大模型]
                                              │
                                      (React Server Stream)
                                              ▼
[Client Suspense 渐进式接收] <───────── [直接吐出渲染好的 UI/数据]
  1. 零客户端状态(Zero Client State): 文本的拼接和流式逻辑完全由 React 底层的 Suspense 框架接管,前端没有 setMessages,没有 useEffect
  2. 极佳的安全性: 大模型的 API Key 稳妥地保存在服务端环境变量中,无需担心泄露。
  3. 完美的用户体验: 利用 ai 库提供的 streamText,数据甚至能在网络链路中自动转换为流式 React 组件,即 Generative UI 的底层基石。

二、 极简实战:30 行代码实现全栈 AI 聊天流

让我们用最纯粹的代码来看看,基于 Next.js App Router 的最佳实践长什么样。

1. 服务端核心:定义 Server Action (app/actions.ts)

我们在服务端处理与 AI 的连接。利用 ai 库的 streamText,并将生成的 textStream 直接返回:

typescript 复制代码
'use server';

import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { createStreamableValue } from 'ai/rsc';

export async function generateContent(prompt: string) {
  // createStreamableValue 是 AI SDK 提供的魔法,专门用于在 Server Actions 中传输流
  const stream = createStreamableValue('');

  (async () => {
    const { textStream } = await streamText({
      model: openai('gpt-4o'),
      prompt: prompt,
    });

    for await (const delta of textStream) {
      stream.update(delta); // 实时向客户端推送新出来的字符
    }

    stream.done(); // 传输完毕
  })();

  return { output: stream.value };
}

2. 客户端核心:无状态渲染 (app/page.tsx)

在客户端,我们只需要使用 AI SDK 提供的 readStreamableValue 钩子,配合一个简单的本地局部变量去承接渲染,彻底告别复杂的全局数据流控制:

tsx 复制代码
'use client';

import { useState } from 'react';
import { readStreamableValue } from 'ai/rsc';
import { generateContent } from './actions';

export default function ChatPage() {
  const [generation, setGeneration] = useState<string>('');
  const [input, setInput] = useState<string>('');
  const [isLoading, setIsLoading] = useState<boolean>(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);
    setGeneration('');

    // 直接像调用普通函数一样调用 Server Action
    const { output } = await generateContent(input);

    // 优雅地读取流式数据,React 会自动处理视图层面的高频增量更新
    for await (const delta of readStreamableValue(output)) {
      setGeneration(prev => prev + delta);
    }
    setIsLoading(false);
  };

  return (
    <div className="max-w-xl mx-auto p-6">
      <form onSubmit={handleSubmit} className="flex gap-2 mb-4">
        <input 
          value={input} 
          onChange={e => setInput(e.target.value)}
          className="flex-1 p-2 border rounded text-black"
          placeholder="问点什么..."
        />
        <button type="submit" disabled={isLoading} className="bg-blue-600 p-2 rounded disabled:opacity-50">
          发送
        </button>
      </form>
      <div className="prose bg-gray-900 p-4 rounded min-h-[100px] whitespace-pre-wrap">
        {generation || (isLoading && 'AI 正在思考...')}
      </div>
    </div>
  );
}

三、 高阶进阶:利用 UI Stream 实现流式 React 组件(Generative UI)

上面的例子只是单纯输出了文本流。但在实际项目中,我们希望大模型不仅返回文字,还能直接返回渲染好的 React 组件。例如:用户说"帮我查一下去东京的机票",AI 吐字结束后,直接在聊天框里流式呈现一个精美的机票行程卡片。

这正是 Next.js RSC 的杀手级特征。利用 Vercel AI SDK 的 streamUI 接口,我们可以直接将服务端的 React 组件"流"回客户端:

typescript 复制代码
// app/actions.tsx (服务端)
import { streamUI } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import FlightCard from '@/components/FlightCard'; // 这是一个普通的 Server Component

export async function chatWithUI(prompt: string) {
  const result = await streamUI({
    model: openai('gpt-4o'),
    prompt: prompt,
    text: ({ content }) => <div>{content}</div>, // 默认的文本流渲染
    tools: {
      get_flight_info: {
        description: '获取机票航班信息',
        parameters: z.object({ destination: z.string() }),
        // 当模型命中 Tool 调用时,服务端直接渲染 React 组件并流式传给前端
        generate: async ({ destination }) => {
          const data = await fetchFlightData(destination);
          return <FlightCard data={data} />;
        }
      }
    }
  });

  return result.value;
}

通过这种方式,客户端不需要配置任何"如果 type 是 flight 引入对应组件"的丑陋判断逻辑,真正的做到了前端只负责挂载,服务端通过 Server Component 决定一切的高级全栈解耦。

四、 避坑指南与工程最佳实践

在将 Next.js 与 AI SDK 接入生产环境时,有几个绕不开的性能和体验大坑需要注意:

1. 警惕 Server Actions 的 Vercel 运行超时限制(Serverless Timeout)

  • 痛点: 如果你将 Next.js 部署在 Vercel 的免费版或 Pro 版上,Serverless Functions 存在严格的执行时间限制(免费版 10 秒,Pro 版 60 秒)。大模型生成长文本时非常容易触发 HTTP 超时断连。
  • 解法: 必须在涉及 AI 流式传输的 Server Action 文件头部,显式声明该路由使用 Edge Runtime 运行,Edge 环境没有运行超时限制:
typescript 复制代码
export const runtime = 'edge'; // 强制开启边缘运行时

2. 结合 React useOptimistic 优化"发送"瞬间的白屏卡顿

在大模型还没来得及吐出第一个字前,网络请求有一段短暂的延迟(TTFB)。如果完全等 Server Action 响应,界面会显得非常呆滞。

  • 解法: 引入 React 19 的 useOptimistic 钩子。当用户点击发送按钮时,立即在前端插入一条带着"流式加载中..."动画的乐观数据(Optimistic Update),一旦流式 Action 成功建立连接,再无缝替换为真实的流数据。

3. 不要将巨量历史消息(Messages)塞进 Server Actions

由于 Server Actions 的调用本质上是在发送一个 POST 请求,如果每一次对话你都把客户端维护的数百条、长达数万字的聊天历史历史记录作为参数丢给 Server Action,会导致每次点击发送时前端需要上传巨大的 Payload,极大浪费带宽。

  • 解法: 短期对话可以使用状态轻量序列化,长期或大型项目应当配合后端的 Redis 或数据库,前端仅传递一个 sessionId,由 Server Action 在服务端内部自行补全 Context。

💡 总结

Next.js 与 Vercel AI SDK 的深度结合,彻底颠覆了以往"全栈 AI 应用必定充斥着复杂前端状态"的偏见。通过 RSC 边缘流式渲染 + Server Actions 零信令通信,前端的复杂度被最大程度压缩。

把状态管理的繁琐包袱卸在服务端,把丝滑的流式体验直接留给用户------这就是目前构建企业级 AI 应用最高效、最优雅的前端工程化正文。