Skip to main content

如何在聊天应用中使用 AI SDK、Prisma 和 Next.js

20 min

介绍

¥Introduction

Prisma ORM 通过类型安全查询简化了数据库访问,与 Next.jsAI SDK 结合使用,为构建具有持久存储的 AI 聊天应用奠定了坚实的基础。

¥Prisma ORM streamlines database access with type-safe queries, and when paired with Next.js and AI SDK, it creates a powerful foundation for building AI-powered chat applications with persistent storage.

在本指南中,你将学习如何使用 AI SDK、Next.js 和 Prisma ORM 构建聊天应用,将聊天会话和消息存储在 Prisma Postgres 数据库中。你可以在 GitHub 上找到本指南的完整示例。

¥In this guide, you'll learn to build a chat application using AI SDK with Next.js and Prisma ORM to store chat sessions and messages in a Prisma Postgres database. You can find a complete example of this guide on GitHub.

先决条件

¥Prerequisites

1. 设置你的项目

¥ Set up your project

首先,你需要创建一个新的 Next.js 项目。

¥To get started, you'll need to create a new Next.js project.

npx create-next-app@latest ai-sdk-prisma

它会提示你自定义设置。选择默认值:

¥It will prompt you to customize your setup. Choose the defaults:

信息
  • 你想使用 TypeScript 吗?Yes

    ¥Would you like to use TypeScript? Yes

  • 你想使用 ESLint 吗?Yes

    ¥Would you like to use ESLint? Yes

  • 你想使用 Tailwind CSS 吗?Yes

    ¥Would you like to use Tailwind CSS? Yes

  • 你想将代码放在 src/ 目录中吗?No

    ¥Would you like your code inside a src/ directory? No

  • 你想使用 App Router 吗?(推荐)Yes

    ¥Would you like to use App Router? (recommended) Yes

  • 你想为 next dev 使用 Turbopack 吗?Yes

    ¥Would you like to use Turbopack for next dev? Yes

  • 你想自定义导入别名(默认为 @/*)吗?No

    ¥Would you like to customize the import alias (@/* by default)? No

导航到项目目录:

¥Navigate to the project directory:

cd ai-sdk-prisma

2. 安装和配置 Prisma

¥ Install and Configure Prisma

2.1.安装依赖

¥2.1. Install dependencies

要开始使用 Prisma,你需要安装一些依赖:

¥To get started with Prisma, you'll need to install a few dependencies:

npm install prisma tsx --save-dev
npm install @prisma/extension-accelerate @prisma/client dotenv

安装完成后,请在你的项目中初始化 Prisma:

¥Once installed, initialize Prisma in your project:

npx prisma init --db --output ../app/generated/prisma
信息

在设置 Prisma Postgres 数据库时,你需要回答几个问题。选择离你最近的区域,并为数据库选择一个容易记住的名称,例如 "我的 Next.js AI SDK 项目"

¥You'll need to answer a few questions while setting up your Prisma Postgres database. Select the region closest to your location and a memorable name for your database like "My Next.js AI SDK Project"

这将造成:

¥This will create:

  • 一个包含 schema.prisma 文件的 prisma 目录。

    ¥A prisma directory with a schema.prisma file.

  • A prisma.config.ts file for configuring Prisma

  • 一个 Prisma Postgres 数据库。

    ¥A Prisma Postgres database.

  • 一个位于项目根目录、包含 DATABASE_URL 文件的 .env 文件。

    ¥A .env file containing the DATABASE_URL at the project root.

  • output 字段指定生成的 Prisma 客户端的存储位置。

    ¥The output field specifies where the generated Prisma Client will be stored.

2.2.定义 Prisma Schema

¥2.2. Define your Prisma Schema

prisma/schema.prisma 文件中,添加以下模型:

¥In the prisma/schema.prisma file, add the following models:

prisma/schema.prisma
generator client {
provider = "prisma-client-js"
output = "../app/generated/prisma"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model Session {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages Message[]
}

model Message {
id String @id @default(cuid())
role MessageRole
content String
createdAt DateTime @default(now())
sessionId String
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
}

enum MessageRole {
USER
ASSISTANT
}

这将创建三个模型:SessionMessageMessageRole

¥This creates three models: Session, Message, and MessageRole.

2.3 Add dotenv to prisma.config.ts

To get access to the variables in the .env file, they can either be loaded by your runtime, or by using dotenv.Include an import for dotenv at the top of the prisma.config.ts

import 'dotenv/config'
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
engine: 'classic',
datasource: {
url: env('DATABASE_URL'),
},
});

2.4.配置 Prisma 客户端生成器

¥2.4. Configure the Prisma Client generator

现在,运行以下命令创建数据库表并生成 Prisma 客户端:

¥Now, run the following command to create the database tables and generate the Prisma Client:

npx prisma migrate dev --name init

3. 将 Prisma 集成到 Next.js

¥ Integrate Prisma into Next.js

在其中创建一个 /lib 目录和一个 prisma.ts 文件。此文件将用于创建和导出你的 Prisma 客户端实例。

¥Create a /lib directory and a prisma.ts file inside it. This file will be used to create and export your Prisma Client instance.

mkdir lib
touch lib/prisma.ts

按如下方式设置 Prisma 客户端:

¥Set up the Prisma client like this:

lib/prisma.ts
import { PrismaClient } from "../app/generated/prisma/client";
import { withAccelerate } from "@prisma/extension-accelerate";

const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};

const prisma = globalForPrisma.prisma || new PrismaClient().$extends(withAccelerate());

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

export default prisma;
警告

我们建议使用连接池(例如 Prisma 加速)来高效管理数据库连接。

¥We recommend using a connection pooler (like Prisma Accelerate) to manage database connections efficiently.

如果你选择不使用 PrismaClient,请避免在长期存在的环境中全局实例化 PrismaClient。请改为按请求创建并释放客户端,以防止耗尽数据库连接。

¥If you choose not to use one, avoid instantiating PrismaClient globally in long-lived environments. Instead, create and dispose of the client per request to prevent exhausting your database connections.

4. 设置 AI SDK

¥ Set up AI SDK

4.1.安装 AI SDK 并获取 API 密钥

¥4.1. Install AI SDK and get an API key

安装 AI SDK 包:

¥Install the AI SDK package:

npm install ai @ai-sdk/react @ai-sdk/openai zod

要使用 AI SDK,你需要从 OpenAI 获取 API 密钥。

¥To use AI SDK, you'll need to obtain an API key from OpenAI.

  1. 导航到 OpenAI API 密钥

    ¥Navigate to OpenAI API Keys

  2. 点击 Create new secret key

    ¥Click on Create new secret key

  3. 填写表单:

    ¥Fill in the form:

    • 给你的密钥起一个类似 Next.js AI SDK Project 的名字

      ¥Give your key a name like Next.js AI SDK Project

    • 选择 All 访问权限

      ¥Select All access

  4. 点击 Create secret key

    ¥Click on Create secret key

  5. 复制 API 密钥

    ¥Copy the API key

  6. 将 API 密钥添加到 .env 文件:

    ¥Add the API key to the .env file:

.env
DATABASE_URL=<YOUR_DATABASE_URL_HERE>
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY_HERE>

4.2.创建路由处理程序

¥4.2. Create a route handler

你需要创建一个路由处理程序来处理 AI SDK 请求。此处理程序将处理聊天消息并将 AI 响应流式传输回客户端。

¥You need to create a route handler to handle the AI SDK requests. This handler will process chat messages and stream AI responses back to the client.

mkdir -p app/api/chat
touch app/api/chat/route.ts

设置基本路由处理程序:

¥Set up the basic route handler:

app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText, UIMessage, convertToModelMessages } from 'ai';

export const maxDuration = 300;

export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();

const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
});

return result.toUIMessageStreamResponse();
}

此路由处理程序:

¥This route handler:

  1. 从请求正文中提取对话历史记录

    ¥Extracts the conversation history from the request body

  2. 将 UI 消息转换为 AI 模型所需的格式

    ¥Converts UI messages to the format expected by the AI model

  3. 将 AI 响应实时流式传输回客户端

    ¥Streams the AI response back to the client in real-time

要将聊天会话和消息保存到数据库,我们需要:

¥To save chat sessions and messages to the database, we need to:

  1. 在请求中添加会话 id 参数

    ¥Add a session id parameter to the request

  2. 在响应中包含 onFinish 回调

    ¥Include an onFinish callback in the response

  3. idmessages 参数传递给 saveChat 函数(我们将在下一步构建该函数)

    ¥Pass the id and messages parameters to the saveChat function (which we'll build next)

app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText, UIMessage, convertToModelMessages } from "ai";
import { saveChat } from "@/lib/save-chat";

export const maxDuration = 300;

export async function POST(req: Request) {
const { messages, id }: { messages: UIMessage[]; id: string } = await req.json();

const result = streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(messages),
});

return result.toUIMessageStreamResponse({
originalMessages: messages,
onFinish: async ({ messages }) => {
await saveChat(messages, id);
},
});
}

4.3.创建 saveChat 函数

¥4.3. Create a saveChat function

lib/save-chat.ts 文件夹下创建一个新文件,用于将聊天会话和消息保存到数据库:

¥Create a new file at lib/save-chat.ts to save the chat sessions and messages to the database:

touch lib/save-chat.ts

首先,创建一个名为 saveChat 的基本函数,用于将聊天会话和消息保存到数据库。

¥To start, create a basic function called saveChat that will be used to save the chat sessions and messages to the database.

messagesid 参数分别类型为 UIMessage[]string 传递给它:

¥Pass into it the messages and id parameters typed as UIMessage[] and string respectively:

lib/save-chat.ts
import { UIMessage } from "ai";

export async function saveChat(messages: UIMessage[], id: string) {

}

现在,添加逻辑以使用给定的 id 创建会话:

¥Now, add the logic to create a session with the given id:

lib/save-chat.ts
import prisma from "./prisma";
import { UIMessage } from "ai";

export async function saveChat(messages: UIMessage[], id: string) {
const session = await prisma.session.upsert({
where: { id },
update: {},
create: { id },
});

if (!session) throw new Error("Session not found");
}

添加将消息保存到数据库的逻辑。你将只保存最后两条消息(用户和助手的最后一条消息),以避免任何消息重叠。

¥Add the logic to save the messages to the database. You'll only be saving the last two messages (Users and Assistants last messages) to avoid any overlapping messages.

lib/save-chat.ts
import prisma from "./prisma";
import { UIMessage } from "ai";

export async function saveChat(messages: UIMessage[], id: string) {
const session = await prisma.session.upsert({
where: { id },
update: {},
create: { id },
});

if (!session) throw new Error("Session not found");

const lastTwoMessages = messages.slice(-2);

for (const msg of lastTwoMessages) {
let content = JSON.stringify(msg.parts);
if (msg.role === "assistant") {
const textParts = msg.parts.filter((part) => part.type === "text");
content = JSON.stringify(textParts);
}

await prisma.message.create({
data: {
role: msg.role === "user" ? "USER" : "ASSISTANT",
content: content,
sessionId: session.id,
},
});
}
}

此函数:

¥This function:

  1. 如果会话不存在,则使用给定的 id 更新会话以创建会话。

    ¥Upserts a session with the given id to create a session if it doesn't exist

  2. 将消息保存到 sessionId 下的数据库中

    ¥Saves the messages to the database under the sessionId

5. 创建消息 API 路由

¥ Create a messages API route

app/api/messages/route.ts 文件夹下创建一个新文件,用于从数据库获取消息:

¥Create a new file at app/api/messages/route.ts to fetch the messages from the database:

mkdir -p app/api/messages
touch app/api/messages/route.ts

创建一个基本的 API 路由,用于从数据库获取消息。

¥Create a basic API route to fetch the messages from the database.

app/api/messages/route.ts
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";

export async function GET() {
try {
const messages = await prisma.message.findMany({
orderBy: { createdAt: "asc" },
});

const uiMessages = messages.map((msg) => ({
id: msg.id,
role: msg.role.toLowerCase(),
parts: JSON.parse(msg.content),
}));

return NextResponse.json({ messages: uiMessages });
} catch (error) {
console.error("Error fetching messages:", error);
return NextResponse.json({ messages: [] });
}
}

6. 创建 UI

¥ Create the UI

app/page.tsx 文件的内容替换为以下内容:

¥Replace the content of the app/page.tsx file with the following:

app/page.tsx
'use client';

export default function Page() {

}

6.1.设置基本导入和状态

¥6.1. Set up the basic imports and state

首先导入所需的依赖并设置用于管理聊天界面的状态变量:

¥Start by importing the required dependencies and setting up the state variables that will manage the chat interface:

app/page.tsx
'use client';

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

export default function Chat() {
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(true);

const { messages, sendMessage, setMessages } = useChat();
}

6.2.加载现有消息

¥6.2. Load existing messages

创建一个 useEffect 钩子,用于在聊天组件加载时自动获取并显示所有之前保存的消息:

¥Create a useEffect hook that will automatically fetch and display any previously saved messages when the chat component loads:

app/page.tsx
'use client';

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

export default function Chat() {
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(true);

const { messages, sendMessage, setMessages } = useChat();

useEffect(() => {
fetch('/api/messages')
.then(res => res.json())
.then(data => {
if (data.messages && data.messages.length > 0) {
setMessages(data.messages);
}
setIsLoading(false);
})
.catch(() => setIsLoading(false));
}, [setMessages]);
}

当组件首次安装时,这会从数据库中加载所有现有消息,以便用户可以查看他们之前的对话历史记录。

¥This loads any existing messages from your database when the component first mounts, so users can see their previous conversation history.

6.3.添加消息显示

¥6.3. Add message display

构建 UI 组件,这些组件将在获取数据时显示加载指示器,并以适当的样式渲染聊天消息:

¥Build the UI components that will show a loading indicator while fetching data and render the chat messages with proper styling:

app/page.tsx
'use client';

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

export default function Chat() {
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(true);

const { messages, sendMessage, setMessages } = useChat();

useEffect(() => {
fetch('/api/messages')
.then(res => res.json())
.then(data => {
if (data.messages && data.messages.length > 0) {
setMessages(data.messages);
}
setIsLoading(false);
})
.catch(() => setIsLoading(false));
}, [setMessages]);

if (isLoading) {
return <div className="flex justify-center items-center h-screen">Loading...</div>;
}

return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map(message => (
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} mb-4`}>
<div className={`max-w-[80%] rounded-lg px-4 py-3 ${
message.role === 'user'
? 'bg-neutral-600 text-white'
: 'bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100'
}`}>
<div className="whitespace-pre-wrap">
<p className="text-xs font-extralight mb-1 opacity-70">{message.role === 'user' ? 'YOU ' : 'AI '}</p>
{message.parts.map((part, i) => {
switch (part.type) {
case 'text':
return <div key={`${message.id}-${i}`}>{part.text}</div>;
}
})}
</div>
</div>
</div>
))}

消息渲染逻辑处理不同的消息类型并应用适当的样式。 - 用户消息以深色背景显示在右侧,而 AI 响应以浅色背景显示在左侧。

¥The message rendering logic handles different message types and applies appropriate styling - user messages appear on the right with a dark background, while AI responses appear on the left with a light background.

6.4.添加输入表单

¥6.4. Add the input form

现在我们需要创建输入界面,允许用户输入并向 AI 发送消息:

¥Now we need to create the input interface that allows users to type and send messages to the AI:

app/page.tsx
'use client';

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

export default function Chat() {
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(true);

const { messages, sendMessage, setMessages } = useChat();

useEffect(() => {
fetch('/api/messages')
.then(res => res.json())
.then(data => {
if (data.messages && data.messages.length > 0) {
setMessages(data.messages);
}
setIsLoading(false);
})
.catch(() => setIsLoading(false));
}, [setMessages]);

if (isLoading) {
return <div className="flex justify-center items-center h-screen">Loading...</div>;
}

return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map(message => (
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} mb-4`}>
<div className={`max-w-[80%] rounded-lg px-4 py-3 ${
message.role === 'user'
? 'bg-neutral-600 text-white'
: 'bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100'
}`}>
<div className="whitespace-pre-wrap">
<p className="text-xs font-extralight mb-1 opacity-70">{message.role === 'user' ? 'YOU ' : 'AI '}</p>
{message.parts.map((part, i) => {
switch (part.type) {
case 'text':
return <div key={`${message.id}-${i}`}>{part.text}</div>;
}
})}
</div>
</div>
</div>
))}

<form
onSubmit={e => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
}}
>
<input
className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={e => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}

7. 测试你的应用

¥ Test your application

要测试你的应用,请运行以下命令:

¥To test your application, run the following command:

npm run dev

打开浏览器并导航到 http://localhost:3000 以查看你的应用的运行情况。

¥Open your browser and navigate to http://localhost:3000 to see your application in action.

通过向 AI 发送消息来测试它是否已保存到数据库中。检查 Prisma Studio 以查看数据库中的消息。

¥Test it by sending a message to the AI and see if it's saved to the database. Check Prisma Studio to see the messages in the database.

npx prisma studio

你已完成!你刚刚使用 Next.js 和 Prisma 创建了一个 AI SDK 聊天应用。以下是一些后续步骤,以及一些可帮助你开始扩展项目的资源。

¥You're done! You've just created a AI SDK chat application with Next.js and Prisma. Below are some next steps to explore, as well as some more resources to help you get started expanding your project.

后续步骤

¥Next Steps

现在你已经拥有一个连接到 Prisma Postgres 数据库的 AI SDK 聊天应用,你可以:

¥Now that you have a working AI SDK chat application connected to a Prisma Postgres database, you can:

  • 使用更多模型和关系扩展你的 Prisma 模式

    ¥Extend your Prisma schema with more models and relationships

  • 添加创建/更新/删除路由和表单

    ¥Add create/update/delete routes and forms

  • 探索身份验证和验证

    ¥Explore authentication and validation

  • 使用 Prisma Postgres 启用查询缓存以获得更好的性能

    ¥Enable query caching with Prisma Postgres for better performance

更多信息

¥More Info


Stay connected with Prisma

Continue your Prisma journey by connecting with our active community. Stay informed, get involved, and collaborate with other developers:

We genuinely value your involvement and look forward to having you as part of our community!