AI SDK 开发超入门
目录
最后更新时间:2024-07-19, Vercel AI SDK 3.2.29
本页主要资料来源:https://sdk.vercel.ai/docs/getting-started/nextjs-app-router
AI SDK 是什么?
(待补)
(初次运行 AI SDK)
工作方式:
- 服务端
- 页面组件
分成如下3 步:
- 创建 Next.js 项目、安装 AI SDK
- 服务端
- 页面组件
1. 准备工作:创建项目、做基础设置
1.1 创建一个 Next.js 项目
我们将创建一个采用 APP 路由(app route)的 Next.js (V14)新项目。
bash
npm create next-app@latest aiapp
创建 Next.js 项目时的选择与调整
选择
✔ Would you like to use TypeScript? … No / Yes ✔ Would you like to use ESLint? … No / Yes ✔ Would you like to use Tailwind CSS? … No / Yes ✔ Would you like to use `src/` directory? … No / Yes ✔ Would you like to use App Router? (recommended) … No / Yes ✔ Would you like to customize the default import alias (@/*)? … No / Yes ✔ What import alias would you like configured? … @/*
Initializing project with template: app-tw
- CSS 清理
https://github.com/vercel/next.js/tree/canary/packages/create-next-app/templates/app-tw
进行一些必要的清理: global.css 去掉渐变
app/globals.css
css
body {
color: rgb(var(--foreground-rgb));
/* background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb)); */
}
- 更正 punycode 警告(未能解决)
"eslint": "^8.0.0", 就不需要特别处理了。
1.2 安装必要的依赖库
我们将使用 Vercel AI SDK,及 OpenAI Provider。运行如下命令安装:
bash
npm install ai @ai-sdk/openai zod
其中,我们安装了三个依赖库:
- ai, https://sdk.vercel.ai/docs/introduction
- @ai-sdk/openai, https://sdk.vercel.ai/providers/ai-sdk-providers/openai
- zod https://zod.dev
查看 package.json
中的版本信息
json
"dependencies": {
"@ai-sdk/openai": "^0.0.36",
"ai": "^3.2.29",
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
"zod": "^3.23.8"
},
1.3 设置 OpenAI API Key
我们将使用 OpenAI 公司的相关模型,请获取 OpenAI API Key,并在文件 .env.local
中设置:
OPENAI_API_KEY=xxxxxxxxx
AI SDK 将直接从 .env.local
中读取。
2. 用 AI SDK 编写 AI Chatbot
AI SDK 是一个中间模块,如下图所示:
- 它在页面前端接受用户的输入提问,并执行提问;
- 调用
/api/chat
,它将调用模型,并向前端返回结果; - 然后,前端组件接收结果,并在界面显示出来。
通常,我们将使用 streaming
形式,也就是逐步的显示模型结果,而非等到回答结束再显示。
2.1 编写 API 路由
我们首先编写 app/chat
这个 API。
新建 app/api/chat
目录,并创建 route.ts
文件。在其中,我们调用模型,并将模型的输出转换为 OpenAIStream
,然后用 StreamingTextResponse
返回。
/app/api/chat/route.ts
tsx
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
// Set the runtime to edge for best performance
export const runtime = 'edge';
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
});
return result.toAIStreamResponse();
}
streamText
的文档见(AI SDK Core):https://sdk.vercel.ai/docs/reference/ai-sdk-core/stream-text
2.2 编写 Chat 界面组件
为了在页面前端显示对话,我们创建 Chat 界面组件。它包括对话信息的显示、用户的提问输入框。
在这里,我们使用的是 useChat
这个 Hook (AI SDK UI),文档参见:链接。
新建文件 src/components/chat.tsx
如下:
tsx
'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>
);
}
2.3 在页面显示 Chat 组件
最后,我们在首页显示这个 Chat 界面组件。代码如下:
app/page.tsx
tsx
import Chat from '@/components/chat';
export const runtime = 'edge';
export default function Page() {
return <Chat />;
}
运行后的界面如下,上方是对话历史,页面底端是用户的对话输入框。
终端显示的信息:调用 /api/chat
npm run dev
> aiapp@0.1.0 dev
> next dev
▲ Next.js 14.2.5
- Local: http://localhost:3000
- Environments: .env.local
✓ Starting...
✓ Ready in 1848ms
○ Compiling /api/chat ...
✓ Compiled /api/chat in 552ms (224 modules)
POST /api/chat 200 in 6503ms
3. 深入探索: 7 个场景
3.1 预设对话的初始内容
将components/Chat.tsx
修改成如下,你将会看到,对话框中已经有了一句话:「你好很高兴见到你,你有任何问题都欢迎提问。」
tsx
'use client';
import { useChat, Message } from 'ai/react';
import { generateId } from 'ai';
export default function Chat() {
const initialMessages: Message[] = [
{
role: "assistant",
content: '你好很高兴见到你,你有任何问题都欢迎提问。',
id: generateId()
}
]
const {
messages,
input,
handleInputChange,
handleSubmit
} = useChat({
initialMessages:initialMessages,
});
return (
...
);
}
3.2 在用户侧增加系统提示语
我们也可以在用户侧直接增加系统提示语。我们将该 Message 的角色设定为 'sysmtem'。为了让它不在用户界面上显示出来,在显示部分,我们将它过滤掉了,见 33 行。
tsx
'use client';
import { useChat, Message } from 'ai/react';
import { generateId } from 'ai';
export default function Chat() {
const id = generateId()
const initialMessages: Message[] = [
{
role: 'system',
content: '我的名字叫 Mona, 我是一个乐于助人的 AI 助手。',
id: id,
},
{
role: "assistant",
content: '你好很高兴见到你,你有任何问题都欢迎提问。',
id: id,
}
]
const {
messages,
input,
handleInputChange,
handleSubmit
} = useChat({
initialMessages:initialMessages,
});
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages
.filter(message => message.role !== 'system')
.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>
);
}
很显然,在组件 Chat
中增加系统提示语并不是一个最佳选择,我们可以将这个逻辑放到 /api/chat
中。
3.3 在 /api/chat
增加系统提示语
首先,我们去掉组件 Chat
中的提示语。然后,我们修改 app/api/chat/route.ts
如下:
ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
// Set the runtime to edge for best performance
export const runtime = 'edge';
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
system: '我的名字叫 Mona, 我是一个乐于助人的 AI 助手。'
});
return result.toAIStreamResponse();
}
3.4 前后端配合,调用不同的 API
我们可以在后端设置多个 API 端点,然后前后端配合,根据需要调用不同的端点。
假设,我们增加一个新的端点:app/api/chat-david
ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
// Set the runtime to edge for best performance
export const runtime = 'edge';
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
system: '我的名字叫大卫, 我专门帮您将想法变成 140 字以内的简短 Tweet。'
});
return result.toAIStreamResponse();
}
在组件 Chat
中,我们这样调用,我们设定此次要调用的端口为 /api/chat-david
。
tsx
'use client';
import { useChat, Message } from 'ai/react';
import { generateId } from 'ai';
export default function Chat() {
const id = generateId()
const initialMessages: Message[] = [
{
role: "assistant",
content: '请输入你想要编写 Tweet 的想法',
id: id,
}
]
const {
messages,
input,
handleInputChange,
handleSubmit
} = useChat({
api:'/api/chat-david',
initialMessages:initialMessages,
});
return (
...
);
}
3.5 从前端传送额外的数据到 API 端点
有时,我们希望从从前端传送额外的数据到 API 端点。比如,我们希望传送一个名字,在 Chat
中,我们可以这样做:
tsx
export default function Chat() {
...
const {
messages,
input,
handleInputChange,
handleSubmit
} = useChat({
api:'/api/chat-david',
initialMessages:initialMessages,
body:{'name':'蒙娜'}
});
...
}
在 app/api/chat-david
中,我们可以这样获取 name
,并将用模板字面变量(template literals)之用在系统提示语里:
ts
export async function POST(req: Request) {
const { messages,name } = await req.json();
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
system: `我的名字叫${name}, 我专门帮您将想法变成 140 字以内的简短 Tweet。`
});
return result.toAIStreamResponse();
}
3.6 从 API 端点传送额外数据到前端
我们将 app/api/chat/route.ts
修改为:
ts
import { openai } from '@ai-sdk/openai';
import { streamText, StreamData } from 'ai';
// Set the runtime to edge for best performance
export const runtime = 'edge';
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const data = new StreamData();
data.append({ test: 'value' });
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
onFinish() {
data.close();
},
});
return result.toAIStreamResponse({ data });
}
然后,我们在 Chat
中获取并显示 data
tsx
'use client';
import { useChat, Message } from 'ai/react';
import { generateId } from 'ai';
export default function Chat() {
const id = generateId()
const initialMessages: Message[] = [
{
role: "assistant",
content: '你好很高兴见到你,你有任何问题都欢迎提问。',
id: id,
}
]
const {
messages,
input,
handleInputChange,
handleSubmit,
data,
} = useChat({
initialMessages:initialMessages,
});
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
{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>
);
}
从API端点传输数据,参考了 Vercel AI SDK 文档。
技术细节:先传 Data
根据源代码,stream 是先传输 data,再传输 AI 返回的结果。
ts
const stream = data
? mergeStreams(data.stream, this.toAIStream())
: this.toAIStream();
3.7 在 API 端点中,删节部分 message
AI SDK 的工作方式是:
- 在前端,
useChat
维护一个messages
数组,用于记录聊天记录。每次向 API 端点提交时,messages 被同时提交。
随着对话的次数越来越多,每次在 API 端点向 AI 模型提交时,如果每次提交所有 messages
,那么会导致 Token 用量的大幅增加。
我们可以在其中简单地做如下删节:如果 messages
长度大于等于10 ,则删除第 5 到倒数第 3 个元素。在实际工程中,你可能要采用更为复杂的逻辑。
app/api/chat/route.ts
ts
const { messages } = await req.json();
if (messages.length >= 10) {
// 删除第 5 到倒数第 3 个元素
messages.splice(4, messages.length - 7);
}
console.log(messages)
console.log(messages.length)
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
});
return result.toAIStreamResponse();
}
为了在前端观察,我们在组件 Chat
在中做了如下修改:
tsx
const {
messages,
input,
handleInputChange,
handleSubmit,
} = useChat({
initialMessages:initialMessages,
onFinish(message) {
console.log("new messages",[...messages, message])
},
});
4. 前端组件中的错误处理
接下来,我们在前端组件中增加错误处理。
https://sdk.vercel.ai/docs/ai-sdk-ui/error-handling
我们可以在 app/api/chat/route.ts
直接抛出错误,用于测试:
ts
throw new Error('This is a test error');
4.1 尝试了解错误处理:keepLastMessageOnErrors
useChat
的错误处理有两个方面:
keepLastMessageOnErrors
onError
在发生错误时,我们在 Console 打印出错误。特别地,如果设置 keepLastMessageOnErrors
为 false,上图中的 User: hello
将不被保留在 messages
中。
我们进行如下测试,编写:components/ChatWithError.tsx
tsx
'use client';
import { useChat, Message } from 'ai/react';
import { generateId } from 'ai';
export default function ChatWithError() {
const id = generateId()
const initialMessages: Message[] = [
{
role: "assistant",
content: '你好很高兴见到你,你有任何问题都欢迎提问。',
id: id,
}
]
const {
messages,
input,
handleInputChange,
handleSubmit,
} = useChat({
initialMessages:initialMessages,
keepLastMessageOnError: true,
onFinish(message) {
console.log("new messages",[...messages, message])
},
onError: error => {
console.error(error);
console.log("messages",messages)
},
});
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>
);
}
4.2 实际进行错误处理
我们将代码修改为如下:
代码来自,并进行了改编:https://sdk.vercel.ai/docs/ai-sdk-ui/error-handling
tsx
'use client';
import { useChat, Message } from 'ai/react';
import { generateId } from 'ai';
export default function ChatWithError() {
const id = generateId()
const initialMessages: Message[] = [
{
role: "assistant",
content: '你好很高兴见到你,你有任何问题都欢迎提问。',
id: id,
}
]
const {
messages,
input,
handleInputChange,
handleSubmit,
error,
reload,
} = useChat({
initialMessages:initialMessages,
keepLastMessageOnError: true,
onError: error => {
console.error(error);
console.log("messages",messages)
},
});
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>
))}
{error && (
<>
<div className='text-red-500'>An error occurred.</div>
<button type="button" onClick={() => reload()}>
Retry
</button>
</>
)}
<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}
disabled={error != null}
/>
</form>
</div>
);
}
4.3 换一种方式进行错误处理
tsx
'use client';
import { useChat, Message } from 'ai/react';
import { generateId } from 'ai';
export default function ChatWithError() {
const id = generateId()
const initialMessages: Message[] = [
{
role: "assistant",
content: '你好很高兴见到你,你有任何问题都欢迎提问。',
id: id,
}
]
const {
messages,
input,
handleInputChange,
handleSubmit,
error,
reload,
setMessages
} = useChat({
initialMessages:initialMessages,
keepLastMessageOnError: true,
onError: error => {
console.error(error);
console.log("messages",messages)
},
});
function customSubmit(event: React.FormEvent<HTMLFormElement>) {
if (error != null) {
setMessages(messages.slice(0, -1)); // remove last message
}
handleSubmit(event);
}
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>
))}
{error && <div className='text-red-500'>An error occurred.</div>}
<form onSubmit={customSubmit}>
<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>
);
}
相关资料: