跳至主要內容

如何使用 AI SDK、Prisma 和 Next.js 建構聊天應用程式

20 分鐘

簡介

Prisma ORM 透過型別安全的查詢簡化了資料庫存取,當它與 Next.jsAI SDK 結合使用時,它為建構具備持久化儲存功能的 AI 聊天應用程式提供了強大的基礎。

在本指南中,您將學習如何使用 AI SDK、Next.js 和 Prisma ORM 建構聊天應用程式,並將聊天對話和訊息儲存在 Prisma Postgres 資料庫中。您可以在 GitHub 上找到本指南的完整範例。

先決條件

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 檔案中,新增以下模型

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
}

這會建立三個模型:SessionMessageMessageRole

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:

lib/prisma.ts
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 金鑰。

  1. 導覽至 OpenAI API 金鑰頁面
  2. 點擊 Create new secret key
  3. 填寫表單
    • 為您的金鑰命名,例如 Next.js AI SDK Project
    • 選擇 All 存取權限
  4. 點擊 Create secret key
  5. 複製 API 金鑰
  6. 將 API 金鑰加入 .env 檔案中
.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

設定基本路由處理器

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();
}

此路由處理器:

  1. 從請求主體 (request body) 中提取對話紀錄
  2. 將 UI 訊息轉換為 AI 模型預期的格式
  3. 將 AI 回覆即時串流傳輸回客戶端

若要將聊天對話和訊息儲存到資料庫中,我們需要:

  1. 在請求中加入一個會話 id 參數
  2. 在回覆中加入 onFinish 回呼 (callback)
  3. idmessages 參數傳遞給 saveChat 函式 (我們接下來會建立它)
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 函式

lib/save-chat.ts 建立一個新檔案,用於將聊天對話和訊息儲存至資料庫

touch lib/save-chat.ts

首先,建立一個名為 saveChat 的基本函式,用於將聊天對話和訊息儲存至資料庫。

傳入型別分別為 UIMessage[]stringmessagesid 參數

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

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

}

現在,加入使用給定 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");
}

加入將訊息儲存到資料庫的邏輯。您將只儲存最後兩則訊息 (使用者和 AI 的最後一則訊息),以避免訊息重疊。

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,
},
});
}
}

此函式會:

  1. 以給定的 id 執行 Upsert (更新或插入),若會話不存在則建立新會話
  2. 將訊息儲存到該 sessionId 下的資料庫中

5. 建立訊息 API 路由

app/api/messages/route.ts 建立一個新檔案,用於從資料庫獲取訊息

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

建立一個基本的 API 路由以從資料庫獲取訊息。

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)

使用以下內容替換 app/page.tsx 檔案的內容

app/page.tsx
'use client';

export default function Page() {

}

6.1. 設定基本的匯入項目與狀態

首先匯入所需的相依項目,並設定將管理聊天介面的狀態變數

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. 載入現有訊息

建立一個 useEffect Hook,當聊天元件載入時,它會自動獲取並顯示先前儲存的訊息

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]);
}

這會在元件首次掛載時從資料庫載入任何現有的訊息,讓使用者可以看到他們之前的對話紀錄。

6.3. 加入訊息顯示

建構 UI 元件,在獲取資料時顯示載入指示器,並以正確的樣式渲染聊天訊息

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 回覆顯示在左側且背景為淺色。

6.4. 加入輸入表單

現在我們需要建立輸入介面,讓使用者可以輸入並傳送訊息給 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. 測試您的應用程式

若要測試您的應用程式,請執行以下指令

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 旅程: 我們的活躍社群。保持資訊靈通、參與其中,並與其他開發者合作

我們衷心感謝您的參與,並期待您成為我們社群的一份子!

© . This site is unofficial and not affiliated with Prisma Data, Inc.