Skip to content

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 项目时的选择与调整
  1. 选择

     ✔ 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

  1. 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)); */
}
  1. 更正 punycode 警告(未能解决)

"eslint": "^8.0.0", 就不需要特别处理了。

1.2 安装必要的依赖库

我们将使用 Vercel AI SDK,及 OpenAI Provider。运行如下命令安装:

bash
npm install ai @ai-sdk/openai zod

其中,我们安装了三个依赖库:

查看 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 返回的结果。

https://github.com/vercel/ai/blob/32e9f36fbcf09dfea3ffccc5c8a1ff7cd8a0ad5d/packages/core/core/generate-text/stream-text.ts#L767C1-L769C27

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

相关资料:

Alang.AI - Make Great AI Applications