如何在聊天应用程序中使用 AI SDK、Prisma 和 Next.js
介绍
Prisma ORM 通过类型安全的查询简化了数据库访问,当与 Next.js 和 AI SDK 结合使用时,它为构建具有持久存储的 AI 驱动聊天应用程序奠定了强大的基础。
在本指南中,您将学习如何使用 AI SDK、Next.js 和 Prisma ORM 构建一个聊天应用程序,将聊天会话和消息存储在 Prisma Postgres 数据库中。您可以在 GitHub 上找到本指南的完整示例。
先决条件
- Node.js 20+
- OpenAI API 密钥或其他 AI 提供商 API 密钥
1. 设置项目
首先,您需要创建一个新的 Next.js 项目。
npx create-next-app@latest ai-sdk-prisma
它将提示你自定义设置。选择默认值
- 你想使用 TypeScript 吗?
是 - 你想使用 ESLint 吗?
是 - 你想使用 Tailwind CSS 吗?
是 - 你想将代码放在
src/目录中吗?否 - 你想使用 App Router 吗? (推荐)
是 - 你想为
next dev使用 Turbopack 吗?是 - 你想自定义导入别名吗(默认为
@/*)?否
导航到项目目录
cd ai-sdk-prisma
2. 安装和配置 Prisma
2.1. 安装依赖项
要开始使用 Prisma,您需要安装一些依赖项
npm install prisma tsx @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg dotenv pg
如果你使用的是其他数据库提供程序(MySQL、SQL Server、SQLite),请安装相应的驱动程序适配器包,而不是 @prisma/adapter-pg。有关更多信息,请参阅 数据库驱动程序。
安装后,在项目中初始化 Prisma
npx prisma init --db --output ../app/generated/prisma
在设置 Prisma Postgres 数据库时,您需要回答几个问题。选择离您最近的区域,并为您的数据库选择一个易于记忆的名称,例如“My Next.js AI SDK Project”
这将创建
- 一个包含
schema.prisma文件的prisma目录。 - 一个用于配置 Prisma 的
prisma.config.ts文件 - 一个 Prisma Postgres 数据库。
- 项目根目录中包含
DATABASE_URL的.env文件。 output字段指定生成的 Prisma Client 将存储的位置。
2.2. 定义 Prisma Schema
在 prisma/schema.prisma 文件中,添加以下模型
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "postgresql"
}
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
}
这将创建三个模型:Session、Message 和 MessageRole。
2.3 将 dotenv 添加到 prisma.config.ts
要访问 .env 文件中的变量,它们可以由您的运行时加载,或者通过使用 dotenv 加载。在 prisma.config.ts 顶部包含 dotenv 的导入
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
});
2.4. 配置 Prisma Client 生成器
现在,运行以下命令创建数据库表并生成 Prisma Client
npx prisma migrate dev --name init
npx prisma generate
3. 将 Prisma 集成到 Next.js
创建一个 /lib 目录并在其中创建一个 prisma.ts 文件。该文件将用于创建和导出您的 Prisma Client 实例。
mkdir lib
touch lib/prisma.ts
像这样设置 Prisma 客户端
import { PrismaClient } from "../app/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const prisma = globalForPrisma.prisma || new PrismaClient({
adapter,
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;
我们建议使用连接池(如 Prisma Accelerate)来有效地管理数据库连接。
如果您选择不使用一个,请**避免**在长生命周期环境中全局实例化 PrismaClient。相反,请为每个请求创建和处置客户端,以防止耗尽数据库连接。
4. 设置 AI SDK
4.1. 安装 AI SDK 并获取 API 密钥
安装 AI SDK 包
npm install ai @ai-sdk/react @ai-sdk/openai zod
要使用 AI SDK,您需要从 OpenAI 获取 API 密钥。
- 导航到 OpenAI API Keys
- 点击
Create new secret key - 填写表单
- 给您的密钥命名,例如
Next.js AI SDK Project - 选择
All访问权限
- 给您的密钥命名,例如
- 点击
Create secret key - 复制 API 密钥
- 将 API 密钥添加到
.env文件中
DATABASE_URL=<YOUR_DATABASE_URL_HERE>
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY_HERE>
4.2. 创建路由处理程序
您需要创建一个路由处理程序来处理 AI SDK 请求。此处理程序将处理聊天消息并将 AI 响应流式传输回客户端。
mkdir -p app/api/chat
touch 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();
}
此路由处理程序
- 从请求正文提取对话历史记录
- 将 UI 消息转换为 AI 模型期望的格式
- 实时将 AI 响应流式传输回客户端
要将聊天会话和消息保存到数据库,我们需要
- 向请求添加会话
id参数 - 在响应中包含
onFinish回调 - 将
id和messages参数传递给saveChat函数(我们接下来会构建它)
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 函数
在 lib/save-chat.ts 创建一个新文件,用于将聊天会话和消息保存到数据库
touch lib/save-chat.ts
首先,创建一个名为 saveChat 的基本函数,它将用于将聊天会话和消息保存到数据库。
将 messages 和 id 参数传入,分别类型为 UIMessage[] 和 string
import { UIMessage } from "ai";
export async function saveChat(messages: UIMessage[], id: string) {
}
现在,添加逻辑以使用给定的 id 创建一个会话
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");
}
添加逻辑以将消息保存到数据库。您将只保存最后两条消息(用户和助手的最后一条消息),以避免任何重叠的消息。
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,
},
});
}
}
此函数
- 使用给定的
idupsert(如果不存在则创建)一个会话 - 将消息保存到
sessionId下的数据库中
5. 创建消息 API 路由
在 app/api/messages/route.ts 创建一个新文件,用于从数据库中获取消息
mkdir -p app/api/messages
touch app/api/messages/route.ts
创建一个基本的 API 路由,用于从数据库中获取消息。
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
将 app/page.tsx 文件的内容替换为以下内容
'use client';
export default function Page() {
}
6.1. 设置基本导入和状态
首先导入所需的依赖项并设置将管理聊天界面的状态变量
'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. 加载现有消息
创建一个 useEffect 钩子,当聊天组件加载时,它将自动获取并显示任何以前保存的消息
'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]);
}
这会在组件首次挂载时从数据库加载任何现有消息,以便用户可以看到他们以前的对话历史记录。
6.3. 添加消息显示
构建 UI 组件,这些组件将在获取数据时显示加载指示器,并以适当的样式渲染聊天消息
'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 响应显示在左侧,带有浅色背景。
6.4. 添加输入表单
现在我们需要创建输入界面,允许用户输入消息并将其发送给 AI
'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. 测试您的应用程序
要测试您的应用程序,请运行以下命令
npm run dev
打开浏览器并导航到 https://:3000 以查看您的应用程序的实际运行情况。
通过向 AI 发送消息并查看其是否已保存到数据库来测试它。检查 Prisma Studio 以查看数据库中的消息。
npx prisma studio
大功告成!您刚刚使用 Next.js 和 Prisma 创建了一个 AI SDK 聊天应用程序。以下是一些可供探索的后续步骤,以及一些有助于您开始扩展项目的更多资源。
下一步
现在您已经拥有一个连接到 Prisma Postgres 数据库的 AI SDK 聊天应用程序,您可以
- 使用更多模型和关系扩展您的 Prisma schema
- 添加创建/更新/删除路由和表单
- 探索身份验证和验证
- 使用 Prisma Postgres 启用查询缓存以获得更好的性能
更多信息
与 Prisma 保持联系
通过以下方式与我们保持联系,继续你的 Prisma 之旅: 我们的活跃社区。保持信息灵通,参与其中,并与其他开发人员协作。
- 在 X 上关注我们 获取公告、直播活动和实用技巧。
- 加入我们的 Discord 提问、与社区交流,并通过对话获得积极支持。
- 在 YouTube 上订阅 获取教程、演示和直播。
- 在 GitHub 上参与 加星收藏存储库、报告问题或为问题做出贡献。