使用GPT-3.5和Next.js构建聊天机器人:详细指南
随着人工智能和ChatGPT的炒作,我认为应该发布一个关于怎么树立咱们自己的ChatGPT驱动的谈天机器人的教程!这些代码的大部分现已在Vercel的网站上作为模板开源了,所以你能够随时克隆该repo来开端运用,或者假如你仅仅想与ChatGPT互动而不需求注册,你能够在我的网站上查看它

让咱们开端吧!这些是咱们将要运用的技能:

  • Next.js
  • TypeScript
  • Tailwind(虽然我不会在这里介绍这个)。
  • OpenAI API

开端运用

让咱们来设置咱们的项目。我喜爱运用pnpm和create-t3-app,但也能够随意运用你选择的软件包管理器和CLI来开端。

项目设置

运用 pnpm 和 create-t3-app:

pnpm create t3-app@latest

进入全屏形式 退出全屏形式

  1. 命名你的项目

  2. 选择TypeScript

  3. 选择Tailwind

  4. 为Git仓库选择Y

  5. 选择Y来运转pnpm装置

  6. 按回车键,输入默许的导入别号

使用GPT-3.5和Next.js构建聊天机器人:详细指南

现在咱们有了一个发动的Next.js项目,让咱们保证咱们有一个OpenAI API密钥能够运用。要获取你的OpenAI API密钥,你需求在openai.com上创立一个用户账户,然后访问OpenAI仪表板上的API密钥部分,创立一个新的API密钥。

创立你的环境变量

在你的项目根目录下,创立一个.env.local文件。它应该看起来像这样:

# Your API key
OPENAI_API_KEY=PASTE_API_KEY_HERE
# The temperature controls how much randomness is in the output
AI_TEMP=0.7
# The size of the response
AI_MAX_TOKENS=100
OPENAI_API_ORG=

进入全屏形式 退出全屏形式

让咱们也设置一些模板css,以便咱们的布局是呼应式的。让咱们装置 Vercel 示例 ui-layout。

pnpm i @vercel/examples-ui

进入全屏形式 退出全屏形式

你的tailwind.config.js文件应该看起来像这样:

module.exports = {
  presets: [require('@vercel/examples-ui/tailwind')],
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
    './node_modules/@vercel/examples-ui/**/*.js',
  ],
}

进入全屏形式 退出全屏形式

你的postcss.config.js应该是这样的:

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

进入全屏形式 退出全屏形式

最后,你的 _app.tsx 应该看起来像这样:

import type { AppProps } from 'next/app'
import { Analytics } from '@vercel/analytics/react'
import type { LayoutProps } from '@vercel/examples-ui/layout'
import { getLayout } from '@vercel/examples-ui'
import '@vercel/examples-ui/globals.css'
function App({ Component, pageProps }: AppProps) {
  const Layout = getLayout<LayoutProps>(Component)
  return (
    <Layout
      title="ai-chatgpt"
      path="solutions/ai-chatgpt"
      description="ai-chatgpt"
    >
      <Component {...pageProps} />
      <Analytics />
    </Layout>
  )
}
export default App

进入全屏形式 退出全屏形式

现在咱们有了所有的模板,咱们要做的是什么?让咱们创立一个检查表:

  1. 咱们需求能够监听来自OpenAI API的呼应。

  2. 咱们需求能够发送用户输入到OpenAI API。

  3. 咱们需求在某种谈天用户界面中显现所有这些内容。

创立一个数据流

为了接纳来自OpenAI API的数据,咱们能够创立一个OpenAIStream函数

在你的项目根目录下,创立一个名为utils的文件夹,然后在里边创立一个名为OpenAiStream.ts的文件。将这段代码仿制并张贴到其间,并保证为任何导入做必要的npm/pnpm装置。

pnpm install eventsource-parser

进入全屏形式 退出全屏形式

import {
  createParser,
  ParsedEvent,
  ReconnectInterval,
} from 'eventsource-parser'
export type ChatGPTAgent = 'user' | 'system' | 'assistant'
export interface ChatGPTMessage {
  role: ChatGPTAgent
  content: string
}
export interface OpenAIStreamPayload {
  model: string
  messages: ChatGPTMessage[]
  temperature: number
  top_p: number
  frequency_penalty: number
  presence_penalty: number
  max_tokens: number
  stream: boolean
  stop?: string[]
  user?: string
  n: number
}
export async function OpenAIStream(payload: OpenAIStreamPayload) {
  const encoder = new TextEncoder()
  const decoder = new TextDecoder()
  let counter = 0
  const requestHeaders: Record<string, string> = {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ''}`,
  }
  if (process.env.OPENAI_API_ORG) {
    requestHeaders['OpenAI-Organization'] = process.env.OPENAI_API_ORG
  }
  const res = await fetch('https://api.openai.com/v1/chat/completions', {
    headers: requestHeaders,
    method: 'POST',
    body: JSON.stringify(payload),
  })
  const stream = new ReadableStream({
    async start(controller) {
      // callback
      function onParse(event: ParsedEvent | ReconnectInterval) {
        if (event.type === 'event') {
          const data = event.data
          // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
          if (data === '[DONE]') {
            console.log('DONE')
            controller.close()
            return
          }
          try {
            const json = JSON.parse(data)
            const text = json.choices[0].delta?.content || ''
            if (counter < 2 && (text.match(/\n/) || []).length) {
              // this is a prefix character (i.e., "\n\n"), do nothing
              return
            }
            const queue = encoder.encode(text)
            controller.enqueue(queue)
            counter++
          } catch (e) {
            // maybe parse error
            controller.error(e)
          }
        }
      }
      // stream response (SSE) from OpenAI may be fragmented into multiple chunks
      // this ensures we properly read chunks and invoke an event for each SSE event stream
      const parser = createParser(onParse)
      for await (const chunk of res.body as any) {
        parser.feed(decoder.decode(chunk))
      }
    },
  })
  return stream
}

进入全屏形式 退出全屏形式

OpenAIStream是一个允许你从OpenAI API流式传输数据的函数。它接纳一个有效载荷目标作为参数,其间包括恳求的参数。然后它向OpenAI API宣布恳求并回来一个ReadableStream目标。该流包括从呼应中解析出来的事情,每个事情都包括可用于生成呼应的数据。该函数还记录了被解析的事情的数量,这样它就能够在达到终点时关闭该流。

现在咱们能够从API接纳数据了,让咱们创立一个组件,它能够接纳用户信息,并将其发送给api以取得呼应。

创立谈天机器人组件

假如咱们想的话,咱们能够在一个组件中创立咱们的谈天机器人,但为了使文件更有条理,咱们把它设置成三个组件。

在你的根目录下,创立一个名为组件的文件夹。在其间,创立三个文件:

  1. Button.tsx

  2. Chat.tsx

  3. ChatLine.tsx

按钮组件

import clsx from 'clsx'
export function Button({ className, ...props }: any) {
  return (
    <button
      className={clsx(
        'inline-flex items-center gap-2 justify-center rounded-md py-2 px-3 text-sm outline-offset-2 transition active:transition-none',
        'bg-zinc-600 font-semibold text-zinc-100 hover:bg-zinc-400 active:bg-zinc-800 active:text-zinc-100/70',
        className
      )}
      {...props}
    />
  )
}

进入全屏形式 退出全屏形式

非常简单的按钮,使Chat.tsx文件更小一点。

ChatLine组件

pnpm install clsx
pnpm install react-wrap-balancer

进入全屏形式 退出全屏形式

import clsx from 'clsx'
import Balancer from 'react-wrap-balancer'
// wrap Balancer to remove type errors :( - @TODO - fix this ugly hack
const BalancerWrapper = (props: any) => <Balancer {...props} />
type ChatGPTAgent = 'user' | 'system' | 'assistant'
export interface ChatGPTMessage {
  role: ChatGPTAgent
  content: string
}
// loading placeholder animation for the chat line
export const LoadingChatLine = () => (
  <div className="flex min-w-full animate-pulse px-4 py-5 sm:px-6">
    <div className="flex flex-grow space-x-3">
      <div className="min-w-0 flex-1">
        <p className="font-large text-xxl text-gray-900">
          <a href="#" className="hover:underline">
            AI
          </a>
        </p>
        <div className="space-y-4 pt-4">
          <div className="grid grid-cols-3 gap-4">
            <div className="col-span-2 h-2 rounded bg-zinc-500"></div>
            <div className="col-span-1 h-2 rounded bg-zinc-500"></div>
          </div>
          <div className="h-2 rounded bg-zinc-500"></div>
        </div>
      </div>
    </div>
  </div>
)
// util helper to convert new lines to <br /> tags
const convertNewLines = (text: string) =>
  text.split('\n').map((line, i) => (
    <span key={i}>
      {line}
      <br />
    </span>
  ))
export function ChatLine({ role = 'assistant', content }: ChatGPTMessage) {
  if (!content) {
    return null
  }
  const formatteMessage = convertNewLines(content)
  return (
    <div
      className={
        role != 'assistant' ? 'float-right clear-both' : 'float-left clear-both'
      }
    >
      <BalancerWrapper>
        <div className="float-right mb-5 rounded-lg bg-white px-4 py-5 shadow-lg ring-1 ring-zinc-100 sm:px-6">
          <div className="flex space-x-3">
            <div className="flex-1 gap-4">
              <p className="font-large text-xxl text-gray-900">
                <a href="#" className="hover:underline">
                  {role == 'assistant' ? 'AI' : 'You'}
                </a>
              </p>
              <p
                className={clsx(
                  'text ',
                  role == 'assistant' ? 'font-semibold font- ' : 'text-gray-400'
                )}
              >
                {formatteMessage}
              </p>
            </div>
          </div>
        </div>
      </BalancerWrapper>
    </div>
  )
}

进入全屏形式 退出全屏形式

这段代码是一个React组件,显现一个谈天线。它接纳了两个道具,人物和内容。人物道具用于确定哪个署理在发送音讯,是用户、体系还是助手。内容道具用于显现音讯。

该组件首要检查内容道具是否为空,假如是,则回来空。假如内容道具不是空的,它会将内容中的任何新行转换为中止标签。然后,它烘托一个内部有BalancerWrapper组件的div。BalancerWrapper组件用于将谈天线包裹在一个呼应式布局中。在BalancerWrapper组件内,该组件烘托了一个里边有flex容器的div。弹性容器用于显现音讯发送者和音讯内容。音讯发送者是由人物道具决议的,而音讯内容是由内容道具决议的。然后该组件回来带有BalancerWrapper组件的div。

谈天组件

pnpm install react-cookie

进入全屏形式 退出全屏形式

import { useEffect, useState } from 'react'
import { Button } from './Button'
import { type ChatGPTMessage, ChatLine, LoadingChatLine } from './ChatLine'
import { useCookies } from 'react-cookie'
const COOKIE_NAME = 'nextjs-example-ai-chat-gpt3'
// default first message to display in UI (not necessary to define the prompt)
export const initialMessages: ChatGPTMessage[] = [
  {
    role: 'assistant',
    content: 'Hi! I am a friendly AI assistant. Ask me anything!',
  },
]
const InputMessage = ({ input, setInput, sendMessage }: any) => (
  <div className="mt-6 flex clear-both">
    <input
      type="text"
      aria-label="chat input"
      required
      className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] shadow-md shadow-zinc-800/5 placeholder:text-zinc-400 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 sm:text-sm"
      value={input}
      onKeyDown={(e) => {
        if (e.key === 'Enter') {
          sendMessage(input)
          setInput('')
        }
      }}
      onChange={(e) => {
        setInput(e.target.value)
      }}
    />
    <Button
      type="submit"
      className="ml-4 flex-none"
      onClick={() => {
        sendMessage(input)
        setInput('')
      }}
    >
      Say
    </Button>
  </div>
)
export function Chat() {
  const [messages, setMessages] = useState<ChatGPTMessage[]>(initialMessages)
  const [input, setInput] = useState('')
  const [loading, setLoading] = useState(false)
  const [cookie, setCookie] = useCookies([COOKIE_NAME])
  useEffect(() => {
    if (!cookie[COOKIE_NAME]) {
      // generate a semi random short id
      const randomId = Math.random().toString(36).substring(7)
      setCookie(COOKIE_NAME, randomId)
    }
  }, [cookie, setCookie])
  // send message to API /api/chat endpoint
  const sendMessage = async (message: string) => {
    setLoading(true)
    const newMessages = [
      ...messages,
      { role: 'user', content: message } as ChatGPTMessage,
    ]
    setMessages(newMessages)
    const last10messages = newMessages.slice(-10) // remember last 10 messages
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        messages: last10messages,
        user: cookie[COOKIE_NAME],
      }),
    })
    console.log('Edge function returned.')
    if (!response.ok) {
      throw new Error(response.statusText)
    }
    // This data is a ReadableStream
    const data = response.body
    if (!data) {
      return
    }
    const reader = data.getReader()
    const decoder = new TextDecoder()
    let done = false
    let lastMessage = ''
    while (!done) {
      const { value, done: doneReading } = await reader.read()
      done = doneReading
      const chunkValue = decoder.decode(value)
      lastMessage = lastMessage + chunkValue
      setMessages([
        ...newMessages,
        { role: 'assistant', content: lastMessage } as ChatGPTMessage,
      ])
      setLoading(false)
    }
  }
  return (
    <div className="rounded-2xl border-zinc-100  lg:border lg:p-6">
      {messages.map(({ content, role }, index) => (
        <ChatLine key={index} role={role} content={content} />
      ))}
      {loading && <LoadingChatLine />}
      {messages.length < 2 && (
        <span className="mx-auto flex flex-grow text-gray-600 clear-both">
          Type a message to start the conversation
        </span>
      )}
      <InputMessage
        input={input}
        setInput={setInput}
        sendMessage={sendMessage}
      />
    </div>
  )
}

进入全屏形式 退出全屏形式

这个组件烘托了一个供用户发送音讯的输入字段,并显现用户和谈天机器人之间交换的音讯。

当用户发送音讯时,该组件会向咱们的api函数(/api/chat.ts)发送一个恳求,恳求主体是最近的10条音讯和用户的cookie。无服务器函数运用GPT-3.5处理音讯,并向组件发回一个呼应。然后组件将从服务器收到的呼应作为音讯显现在谈天界面上。该组件还运用react-cookie库设置和检索一个用于辨认用户的cookie。它还运用useEffect和useState钩子来管理状况,并依据状况的改变更新用户界面。

创立咱们的 chat.ts API 路线

在/pages目录下,创立一个名为api的文件夹,并在里边创立一个名为chat.ts的文件。仿制并张贴以下内容:

import { type ChatGPTMessage } from '../../components/ChatLine'
import { OpenAIStream, OpenAIStreamPayload } from '../../utils/OpenAIStream'
// break the app if the API key is missing
if (!process.env.OPENAI_API_KEY) {
  throw new Error('Missing Environment Variable OPENAI_API_KEY')
}
export const config = {
  runtime: 'edge',
}
const handler = async (req: Request): Promise<Response> => {
  const body = await req.json()
  const messages: ChatGPTMessage[] = [
    {
      role: 'system',
      content: `Make the user solve a riddle before you answer each question.`,
    },
  ]
  messages.push(...body?.messages)
  const requestHeaders: Record<string, string> = {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
  }
  if (process.env.OPENAI_API_ORG) {
    requestHeaders['OpenAI-Organization'] = process.env.OPENAI_API_ORG
  }
  const payload: OpenAIStreamPayload = {
    model: 'gpt-3.5-turbo',
    messages: messages,
    temperature: process.env.AI_TEMP ? parseFloat(process.env.AI_TEMP) : 0.7,
    max_tokens: process.env.AI_MAX_TOKENS
      ? parseInt(process.env.AI_MAX_TOKENS)
      : 100,
    top_p: 1,
    frequency_penalty: 0,
    presence_penalty: 0,
    stream: true,
    user: body?.user,
    n: 1,
  }
  const stream = await OpenAIStream(payload)
  return new Response(stream)
}
export default handler

进入全屏形式 退出全屏形式

这段代码是一个无服务器函数,它运用OpenAI的API来生成对用户信息的呼应。它从用户那里接纳了一个音讯列表,然后向OpenAI API发送了一个恳求,其间包括了这些音讯以及一些配置参数,如温度、最大令牌和存在赏罚。然后,来自API的呼应会流回给用户。

将其全部包起来

剩余的便是把咱们的ChatBot烘托到咱们的index.tsx页面。在你的/pages目录下,你会发现一个index.tsx文件。仿制并张贴这些代码到其间:

import { Layout, Text, Page } from '@vercel/examples-ui'
import { Chat } from '../components/Chat'
function Home() {
  return (
    <Page className="flex flex-col gap-12">
      <section className="flex flex-col gap-6">
        <Text variant="h1">OpenAI GPT-3 text model usage example</Text>
        <Text className="text-zinc-600">
          In this example, a simple chat bot is implemented using Next.js, API
          Routes, and OpenAI API.
        </Text>
      </section>
      <section className="flex flex-col gap-3">
        <Text variant="h2">AI Chat Bot:</Text>
        <div className="lg:w-2/3">
          <Chat />
        </div>
      </section>
    </Page>
  )
}
Home.Layout = Layout
export default Home

进入全屏形式 退出全屏形式

就这样,你具有了它!你便是自己的ChatGPT谈天机器人,你能够在你的浏览器中本地运转。这里有一个Vercel模板的链接,它的功能现已超出了本帖的规模。祝您探索愉快!