🚀 Vercel AI SDK 使用指南: 聊天记录持久化 (Chatbot Message Persistence)

6 阅读4分钟

在开发 AI 聊天应用时,消息持久化 (Message Persistence) 是一个绕不开的核心功能。默认情况下,useChat 的状态是保存在内存中的,一旦用户刷新页面,聊天记录就会消失。

为了构建生产级别的应用,我们需要将聊天记录保存到数据库中,并在用户重新进入时加载它们。本文将基于 Vercel AI SDK 的最新文档,带你一步步实现这一功能。

核心思路

Vercel AI SDK 提供了一套优雅的机制来处理持久化,主要分为两部分:

  1. 服务端 (保存) :利用 streamTextonFinish 回调,在 AI 回答完成后,将完整的对话历史(包含用户的新消息和 AI 的回答)保存到数据库。
  2. 客户端 (加载) :在页面加载时(通常在 Next.js 的 Server Component 中)从数据库获取历史记录,并通过 initialMessages 属性传递给 useChat

1. 模拟数据库层 (Mock Database)

为了演示方便,我们先创建一个简单的文件存储工具。在实际生产环境中,请将其替换为你的数据库逻辑(如 PostgreSQL, MySQL, MongoDB 等)。

新建 util/chat-store.ts

TypeScript

import { generateId } from 'ai';
import { UIMessage } from 'ai';

// 模拟数据库:保存聊天记录
// 在实际项目中,这里应该是数据库的 INSERT/UPDATE 操作
const chats: Record<string, UIMessage[]> = {};

export async function createChat(): Promise<string> {
  const id = generateId();
  chats[id] = [];
  return id;
}

export async function loadChat(id: string): Promise<UIMessage[]> {
  return chats[id] || [];
}

export async function saveChat({
  chatId,
  messages,
}: {
  chatId: string;
  messages: UIMessage[];
}): Promise<void> {
  chats[chatId] = messages;
  console.log(`Chat ${chatId} saved with ${messages.length} messages.`);
}

2. 服务端实现:生成流并保存 (Route Handler)

app/api/chat/route.ts 中,我们使用 streamText 生成流,并利用 onFinish 钩子进行存储。

关键点:

  • 使用 toUIMessageStreamResponse 返回响应。
  • onFinish 会在流结束时触发,此时你可以拿到完整的 messages 数组。
  • 防止中断丢失:使用 result.consumeStream() 确保即使客户端断开连接(如关闭标签页),服务端也能继续处理并完成保存。

TypeScript

import { openai } from '@ai-sdk/openai'; // 也可以替换为阿里通义千问等兼容 SDK 的 Provider
import { convertToModelMessages, streamText, UIMessage } from 'ai';
import { saveChat } from '@/util/chat-store'; // 引入上面的模拟数据库方法

export async function POST(req: Request) {
  // 从请求体中获取消息列表和当前聊天 ID
  const { messages, chatId }: { messages: UIMessage[]; chatId: string } = await req.json();

  // 1. 生成流式文本
  const result = streamText({
    model: openai('gpt-4o'), // 选择你的模型
    messages: convertToModelMessages(messages),
  });

  // 2. 关键:消费流以防止客户端断开导致处理中断
  // 即使客户端关闭页面,服务端也会继续执行直到流结束并触发 onFinish
  result.consumeStream();

  // 3. 返回 UI 友好的流响应,并在结束时保存
  return result.toUIMessageStreamResponse({
    originalMessages: messages, // 传入原始消息以便 SDK 合并
    onFinish: async ({ messages }) => {
      // 这里的 messages 包含了:之前的历史 + 用户新消息 + AI 新生成的回答
      await saveChat({ chatId, messages });
    },
  });
}

3. 客户端实现:加载历史记录 (Page & Component)

接下来我们在前端加载这些数据。

步骤 A: 创建聊天组件

ui/chat.tsx 中,我们从 props 接收 initialMessages 并传给 useChat

TypeScript

'use client';

import { UIMessage, useChat } from '@ai-sdk/react';

export default function Chat({
  id,
  initialMessages,
}: {
  id: string;
  initialMessages: UIMessage[];
}) {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    id,
    // 核心:使用从服务端加载的消息初始化状态
    initialMessages, 
    // 传递 chatId 给后端,以便后端知道保存到哪条记录
    body: { chatId: id }, 
  });

  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {messages.map(m => (
        <div key={m.id} className="whitespace-pre-wrap">
          {m.role === 'user' ? 'User: ' : 'AI: '}
          {m.content}
        </div>
      ))}

      <form onSubmit={handleSubmit} className="fixed bottom-0 w-full max-w-md p-2 bg-white border-t">
        <input
          className="w-full p-2 border border-gray-300 rounded shadow-xl"
          value={input}
          placeholder="Say something..."
          onChange={handleInputChange}
        />
      </form>
    </div>
  );
}

步骤 B: 在 Server Component 中获取数据

在 Next.js 的页面文件 app/chat/[id]/page.tsx 中,我们在服务端直接读取数据库,避免了额外的 API 请求,提升首屏速度。

TypeScript

import { loadChat } from '@/util/chat-store';
import Chat from '@/ui/chat';

export default async function Page(props: { params: Promise<{ id: string }> }) {
  const params = await props.params;
  
  // 从数据库加载历史消息
  const messages = await loadChat(params.id);

  // 将消息传递给客户端组件
  return <Chat id={params.id} initialMessages={messages} />;
}

进阶优化:只发送最后一条消息

随着聊天记录变长,每次请求都把所有历史记录从客户端发给服务端会浪费带宽。我们可以优化为:客户端只发新消息,服务端负责拼接历史记录

1. 客户端修改 (ui/chat.tsx)

使用 prepareSendMessagesRequest 自定义请求体:

TypeScript

import { DefaultChatTransport } from 'ai';

// ... inside Chat component
const { messages, input, handleInputChange, handleSubmit } = useChat({
  // ... other props
  transport: new DefaultChatTransport({
    api: '/api/chat',
    // 仅发送最后一条消息(即用户刚输入的那条)
    prepareSendMessagesRequest({ messages, id }) {
      return { 
        body: { 
          message: messages[messages.length - 1], 
          chatId: id 
        } 
      };
    },
  }),
});

2. 服务端修改 (app/api/chat/route.ts)

服务端需要先加载历史,再拼接新消息:

TypeScript

export async function POST(req: Request) {
  // 此时接收到的只是单条 message
  const { message, chatId } = await req.json();

  // 1. 从数据库加载之前的历史记录
  const previousMessages = await loadChat(chatId);

  // 2. 拼接完整的消息列表
  // 注意:这里可以加入 validateUIMessages 进行数据验证
  const allMessages = [...previousMessages, message];

  const result = streamText({
    model: openai('gpt-4o'),
    messages: convertToModelMessages(allMessages),
  });
  
  // ... 后续代码不变 (consumeStream, toUIMessageStreamResponse 等)
}

总结 📝

通过 Vercel AI SDK 的 onFinishinitialMessages,我们可以轻松实现聊天记录的持久化。

  • 服务端:使用 onFinish 捕获完整会话并存库,记得调用 consumeStream() 防止断连丢失数据。
  • 客户端:使用 Server Component 预加载数据,并通过 initialMessages 恢复会话状态。
  • 优化:在生产环境中,建议仅传输增量消息,并在服务端进行历史记录的合并与校验。

希望这篇教程对你有帮助!如有疑问,欢迎在评论区交流。👏