如何使用 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 嗎?
Yes - 您想使用 ESLint 嗎?
Yes - 您想使用 Tailwind CSS 嗎?
Yes - 您想將程式碼放在
src/目錄中嗎?No - 您想使用 App Router 嗎? (推薦)
Yes - 您想在
next dev中使用 Turbopack 嗎?Yes - 您想自訂匯入別名 (預設為
@/*) 嗎?No
導覽至專案目錄
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 在 prisma.config.ts 加入 dotenv
要存取 .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 client:
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 金鑰頁面
- 點擊
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. 建立路由處理器 (Route Handler)
您需要建立一個路由處理器來處理 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();
}
此路由處理器:
- 從請求主體 (request body) 中提取對話紀錄
- 將 UI 訊息轉換為 AI 模型預期的格式
- 將 AI 回覆即時串流傳輸回客戶端
若要將聊天對話和訊息儲存到資料庫中,我們需要:
- 在請求中加入一個會話
id參數 - 在回覆中加入
onFinish回呼 (callback) - 將
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 的基本函式,用於將聊天對話和訊息儲存至資料庫。
傳入型別分別為 UIMessage[] 和 string 的 messages 與 id 參數
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");
}
加入將訊息儲存到資料庫的邏輯。您將只儲存最後兩則訊息 (使用者和 AI 的最後一則訊息),以避免訊息重疊。
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,
},
});
}
}
此函式會:
- 以給定的
id執行 Upsert (更新或插入),若會話不存在則建立新會話 - 將訊息儲存到該
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 Hook,當聊天元件載入時,它會自動獲取並顯示先前儲存的訊息
'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
- 加入建立/更新/刪除 (CRUD) 的路由與表單
- 探索驗證與身份驗證
- 啟用 Prisma Postgres 的查詢快取以獲得更好的效能
更多資訊
與 Prisma 保持聯繫
透過以下方式與我們聯繫,繼續您的 Prisma 旅程: 我們的活躍社群。保持資訊靈通、參與其中,並與其他開發者合作
- 在 X 上關注我們 以獲取公告、現場活動和實用技巧。
- 加入我們的 Discord 提出問題、與社群對話,並透過對話獲得積極支援。
- 在 YouTube 上訂閱 查看教學、演示和直播。
- 在 GitHub 上交流 透過為存放庫加星標、報告問題或為 issue 做出貢獻。