Next.js 項目接入 AI 的利器 —— Vercel AI SDK
共 20124字,需瀏覽 41分鐘
·
2024-06-19 11:50
大廠技術 高級前端 Node進階
點擊上方 程序員成長指北,關注公眾號
回復1,加入高級Node交流群
?
前言
首先我們花 10 分鐘使用 Next.js 快速部署一個 ChatGPT 聊天網(wǎng)站,效果如下:
不過巧婦難為無米之炊,首先你要有:
-
一個 ChatGPT 3.5 的 API KEY(必須,4.0 也行,修改對應的 model 名就行) -
一個好的網(wǎng)速(非必須,但可能 10 分鐘就搞不定了)
廢話不多說,讓我們直接開始吧!
??
本篇已收錄到掘金專欄《Next.js 開發(fā)指北》(https://juejin.cn/column/7343569488744611849)
系統(tǒng)學習 Next.js,歡迎入手小冊《Next.js 開發(fā)指南》?;A篇、實戰(zhàn)篇、源碼篇、面試篇四大篇章帶你系統(tǒng)掌握 Next.js!
十分鐘部署版
使用 Next.js 官方腳手架創(chuàng)建一個新項目:
npx create-next-app@latest
運行效果如下:
為了樣式美觀,我們會用到 Tailwind CSS,所以「注意勾選 Tailwind CSS」,其他隨意。
為了快速實現(xiàn),我們需要用到 ai 、@ai-sdk/openai 這兩個 npm 包,其中ai 是 Vercel 提供的用于接入 AI 產(chǎn)品、處理流式數(shù)據(jù)的庫, @ai-sdk/openai是 Vercel 基于 openAI 官方提供的 SDK openai 的封裝。
安裝一下依賴項:
npm install ai @ai-sdk/openai
?注:寫這篇文章的時候,ai 版本為 3.1.23, @ai-sdk/openai 版本為 0.0.18,未來 SDK 的用法可能會修改
?
修改 app/page.js,代碼如下:
'use client';
import { useChat } from 'ai/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
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}>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={handleInputChange}
/>
</form>
</div>
);
}
新建 app/api/chat/route.js代碼如下:
import { createOpenAI } from '@ai-sdk/openai';
import { streamText } from 'ai';
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY || '',
baseURL: "https://api.openai-proxy.com/v1"
});
export const maxDuration = 30;
export async function POST(req) {
const { messages } = await req.json();
const result = await streamText({
model: openai('gpt-3.5-turbo'),
messages,
});
return result.toAIStreamResponse();
}
新建 .env.local文件,代碼如下:
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
修改 app/globals.css,注釋掉這些部分(為了樣式美觀而已):
/* :root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
} */
命令行運行 npm run dev,瀏覽器無痕模式(為了避免插件等干擾)打開 http://localhost:3000/,運行效果如下:
接下來我們部署到 Vercel 上。巧婦再次難為無米之炊,你需要一個 Vercel 賬號并全局安裝了 Vercel Cli,具體參考 《實戰(zhàn)篇 | React Notes | Vercel 部署》(https://juejin.cn/book/7307859898316881957/section/7309114840307400714#heading-3)。
項目根目錄運行:
vercel
接下來等待 Vercel 自動部署(大概 1 分鐘左右),交互效果如下:
此時 Vercel 部署完成。打開 Vercel 平臺查看項目的線上地址:
部署地址是 https://next-chatgpt-amber.vercel.app/,頁面雖然能訪問,但此時并沒有效果,因為我們還沒有設置我們的環(huán)境變量。
打開項目的 Settings,添加 OPENAI_API_KEY環(huán)境變量的值,然后點擊 Save 按鈕添加:
添加后,為了讓環(huán)境變量生效,此時需要 Redeploy 一次:
此時再訪問 https://next-chatgpt-amber.vercel.app/,已經(jīng)能夠正常運行:
不過 Vercel 部署的地址默認國內無法訪問,但也有解決方法,參考 《實戰(zhàn)篇 | React Notes | Vercel 部署》(https://juejin.cn/book/7307859898316881957/section/7309114840307400714#heading-3)。
五分鐘部署版
是不是感覺還是有點麻煩,沒有關系,還有 5 分鐘部署版。前提是你有 Vercel 賬號以及一個 API KEY。
打開 next-openai(https://github.com/vercel/ai/tree/main/examples/next-openai),點擊 Deploy 按鈕:
然后在 Deploy 界面創(chuàng)建一個 GitHub 倉庫,配置一下環(huán)境變量,最后等待部署即可:
最后獲取一下生產(chǎn)地址:
這個是 Vercel 提供的 Next.js + OpenAI 的官方模板,除了剛才的例子,還提供了各種示例,也支持 GPT 4,從源碼中也可以看出:
除了 Next.js + OpenAI,其實 Vercel AI 還提供了其他模板和例子:
Vercel AI SDK
如果你要在 Next.js 項目中接入 AI 比如 OpenAI、Anthropic、Google、Mistral 等,尤其要使用 Stream 的時候,雖然可以手動處理,參考:
-
《如何用 Next.js v14 實現(xiàn)一個 Streaming 接口?》(https://juejin.cn/post/7344089411983802394#heading-5) -
《Next.js v14 如何實現(xiàn) SSE、接入 ChatGPT Stream?》
(https://juejin.cn/post/7372020457124659234#heading-11)
但流的處理是非常讓人頭疼的,Node 有自己的 Stream 同時也支持 Web Stream,各種類型的流牽涉到各種概念和 API,繁瑣的讓人頭疼。就連 Dan 也感到害怕(??):
所以處理 AI + Stream 的時候,最好是使用 Vercel 的 AI SDK,它針對多個 AI 模型都提供了 Providers,也支持Stream。加上是 Vercel 出品,質量有保證,屬于官方推薦產(chǎn)品,已經(jīng)成為 Next.js 項目接入 AI 的第一選擇。
本篇就以 OpenAI 為例,為大家講解如何使用 Vercel AI SDK。
1. 基礎配置
首先是安裝依賴項:
npm install ai @ai-sdk/openai
配置 OpenAI API Key:
OPENAI_API_KEY=xxxxxxxxx
創(chuàng)建一個路由處理程序:
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { generateText } from "ai"
export async function POST(req) {
const { messages } = await req.json();
const { text } = await generateText({
model: openai('gpt-3.5-turbo'),
messages
})
return Response.json({ text })
}
但如果你在國內調用,因為一些原因,需要配置代理,所以需要寫成這樣:
import { createOpenAI } from '@ai-sdk/openai';
import { generateText } from "ai"
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY || '',
baseURL: "https://api.openai-proxy.com/v1"
});
export async function POST(req) {
const { messages } = await req.json();
const { text } = await generateText({
model: openai('gpt-3.5-turbo'),
messages
})
return Response.json({ text })
}
2. AI SDK Core
2.1. 核心函數(shù)
之前的例子中,我們用的是 ai導出的 generateText 函數(shù),這就是 ai 的核心函數(shù),一共有 4 個:
-
generateText:生成文本,適合非交互式用例,例如需要編寫文本(例如起草電子郵件或總結網(wǎng)頁)的自動化任務 -
streamText:生成流文本。適合用于交互式用例,例如聊天機器人和內容流 -
generateObject:生成結構化對象,很多大模型支持返回結構化對象,比如 OpenAI(在官方文檔搜 “JSON mode”查看具體介紹) -
streamObject:生成流式結構化對象
常用的是 streamText,因為大型語言模型 (LLM) 可能需要長達一分鐘才能完成生成響應,對于聊天機器人這種交互場景來說,這種延遲是不可接受的,用戶希望立刻得到響應,所以使用 Stream 格式很重要。
streamText 的基本用法如下:
export async function POST(req) {
const { messages } = await req.json();
const result = await streamText({
model: openai('gpt-3.5-turbo'),
messages,
});
return result.toAIStreamResponse();
}
2.2. ReadableStream
其中 result.textStream 是一個 ReadableStream,你可以在瀏覽器或者 Node 中使用:
const result = await streamText({
model: openai('gpt-3.5-turbo'),
messages,
});
for await (const textPart of result.textStream) {
console.log(textPart);
}
打印結果如下:
稍微進階一點的用法,我們可以在實戰(zhàn)中體會。
新建 app/learn/page.js,代碼如下:
'use client';
import { createOpenAI } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { useEffect, useState } from 'react';
const openai = createOpenAI({
apiKey: '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',
baseURL: "https://api.openai-proxy.com/v1"
});
const fetch = async (cb) => {
const result = await streamText({
model: openai('gpt-3.5-turbo'),
prompt: '如何學習 JavaScript,請詳細描述',
});
const reader = result.textStream.getReader();
reader.read().then(function processText({ done, value }) {
if (done) {
console.log("Stream complete");
return;
}
cb(value)
return reader.read().then(processText);
});
}
export default function Chat() {
const [text, setText] = useState('');
const [charsReceived, setCharsReceived] = useState('');
const [chunk, setChunk] = useState('');
useEffect(() => {
fetch((text) => {
setChunk(text)
setText((prev) => {
const res = prev + text
setCharsReceived(res.length)
return res
})
})
}, [])
return (
<>
<p>{text}</p>
<div className="bg-cyan-300 text-xl text-white text-center fixed inset-x-0 bottom-0 p-4">已收到 {charsReceived} 字符。當前片段:{chunk}</div>
</>
)
}
瀏覽器效果如下:
2.3. 完成回調
AI SDK 同時提供了完成回調函數(shù):
const result = await streamText({
model: openai('gpt-3.5-turbo'),
messages,
onFinish({ text, toolCalls, toolResults, finishReason, usage }) {
console.log(text, finishReason, usage)
},
});
2.4. 輔助函數(shù)
streamText 的返回對象包含多個輔助函數(shù),以便更輕松地集成到 AI SDK UI 中:
-
result.toAIStream(): 返回一個 AI stream 對象,可以和 StreamingTextResponse()、 StreamData 一起使用 -
result.toAIStreamResponse(): 返回一個 AI stream response -
result.toTextStreamResponse(): 返回一個普通的文字 stream response -
result.pipeTextStreamToResponse(): 將數(shù)據(jù)寫入類似于 Node.js response 的對象 -
result.pipeAIStreamToResponse(): 將 AI 流數(shù)據(jù)寫入類似于 Node.js response 的對象
在上面的例子中,我們就是直接使用 result.toAIStreamResponse作為路由處理程序的返回。
3. AI SDK RSC
除了 ai,Vercel 針對服務端組件還提供了 ai/rsc,用于在服務端流式返回內容。借助 ai/rsc,你不再需要手動創(chuàng)建 API 接口,可直接使用 Server Actions 完成前后端交互。
AI SDK RSC 也提供了多個核心函數(shù),就比如用于處理 Stream Value 的 createStreamableValue,它可以將可序列化的 JS 值從服務器流式傳輸?shù)娇蛻舳?,例如字符串、?shù)字、對象和數(shù)組:
'use server';
import { createStreamableValue } from 'ai/rsc';
export const runThread = async () => {
const streamableStatus = createStreamableValue('thread.init');
setTimeout(() => {
streamableStatus.update('thread.run.create');
streamableStatus.update('thread.run.update');
streamableStatus.update('thread.run.end');
streamableStatus.done('thread.end');
}, 1000);
return {
status: streamableStatus.value,
};
};
readStreamableValue 搭配 createStreamableValue 使用,用于在客戶端讀取流式值,它返回一個異步迭代器,該迭代器在更新時生成新值:
import { readStreamableValue } from 'ai/rsc';
import { runThread } from '@/actions';
export default function Page() {
return (
<button
onClick={async () => {
const { status } = await runThread();
for await (const value of readStreamableValue(status)) {
console.log(value);
}
}}
>
Ask
</button>
);
}
具體在項目中怎么使用呢?我給大家舉個完整可用的例子。
修改 app/page.js,代碼如下:
'use client';
import { useState } from 'react';
import { generate } from './actions';
import { readStreamableValue } from 'ai/rsc';
export const dynamic = 'force-dynamic';
export const maxDuration = 30;
export default function Home() {
const [generation, setGeneration] = useState('');
return (
<div>
<button
onClick={async () => {
const { output } = await generate('如何學習 JavaScript?');
for await (const delta of readStreamableValue(output)) {
setGeneration(currentGeneration => `${currentGeneration}${delta}`);
}
}}
>
如何學習 JavaScript?
</button>
<div>{generation}</div>
</div>
);
}
新建 app/actions.js,代碼如下:
'use server';
import { streamText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { createStreamableValue } from 'ai/rsc';
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY || '',
baseURL: "https://api.openai-proxy.com/v1"
});
export async function generate(input) {
'use server';
const stream = createStreamableValue('');
(async () => {
const { textStream } = await streamText({
model: openai('gpt-3.5-turbo'),
prompt: input,
});
for await (const delta of textStream) {
stream.update(delta);
}
stream.done();
})();
return { output: stream.value };
}
瀏覽器效果如下:
當然 AI SDK RSC 的核心函數(shù)還有流式傳輸 UI 的 createStreamableUI、常用于查詢聊天記錄的 AI and UI State 等,具體查看 https://sdk.vercel.ai/docs/ai-sdk-rsc
4. AI SDK UI
Vercel AI SDK UI,雖然名字上帶了 UI,但其實跟框架、UI 都無關,主要是用于簡化前端管理 Stream 和 UI 的過程,更高效的開發(fā)界面。
主要提供了 3 個 hook:
-
useChat:對應 OpenAI 的 ChatCompletion,專為生成對話場景設計 -
useCompletion:對應 OpenAI 的 Completion,是一個通用的自然語言生成接口,支持生成各種類型的文本,包括段落、摘要、建議、答案等等 -
useAssistant:對應 OpenAI 的 Assistants API
簡單的來說就是用法基本類似,但背后調用的 OpenAI 的接口有所不同,實現(xiàn)的效果也不同。我們以 useChat 為例:
'use client';
import { useChat } from 'ai/react';
export default function Page() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: 'api/chat',
});
return (
<>
{messages.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
name="prompt"
value={input}
onChange={handleInputChange}
id="input"
/>
<button type="submit">Submit</button>
</form>
</>
);
}
不需要再定義其他狀態(tài),就實現(xiàn)了一個最基本的數(shù)據(jù)提交和數(shù)據(jù)展示。
除此之外還支持 loading 和 error 狀態(tài):
const { isLoading, ... } = useChat()
return <>
{isLoading ? <Spinner /> : null}
...
const { error, ... } = useChat()
useEffect(() => {
if (error) {
toast.error(error.message)
}
}, [error])
// Or display the error message in the UI:
return <>
{error ? <div>{error.message}</div> : null}
...
可大幅簡化前端界面的開發(fā)成本。
完整的文檔可以參考 https://sdk.vercel.ai/docs/ai-sdk-rsc/overview
總結
借助 Vercel AI SDK 可快捷接入 AI 產(chǎn)品,處理流式返回,構建前端界面,堪稱 Next.js 接入 AI 的第一選擇。
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
“分享、點贊、在看” 支持一下
