这是之前在 Laf 中疾速搭建 ChatGPT 的例子「优化版 流式更快」三分钟搭建本人的ChatGPT。

外面用到的 laf 模板是这样的:

import cloud from '@lafjs/cloud'const apiKey = 'your apikey'export default async function (ctx: FunctionContext) {  const { ChatGPTAPI } = await import('chatgpt')  const { body, response } = ctx  // get chatgpt api  let api = cloud.shared.get('api')  if (!api) {    api = new ChatGPTAPI({ apiKey })    cloud.shared.set('api', api)  }  // set stream response type  response.setHeader('Content-Type', 'application/octet-stream');  // send message  const res = await api.sendMessage(body.message, {    onProgress: (partialResponse) => {      if (partialResponse?.delta != undefined)        response.write(partialResponse.delta)    },    parentMessageId: body.parentMessageId || ''  })  response.end("--!" + res.id)}

这里用到了一个 nodejs 包:const { ChatGPTAPI } = await import('chatgpt');但它实际上并不是 openAI 的官网包。(这里附上它的我的项目地址:chatgpt-api,感兴趣的同学能够理解一下)

其实,咱们还有另外一个抉择:只应用 OpenAI 的原生接口。

这样,既缩小了导入内部包时带来的不必要的依赖,也不必再放心内部包降级时可能导致的莫名其妙的报错。还能够从零实现 ChatGPT 的外围性能,十分洁净,十分清新。

如果,你也想这么干的话,那么你能够尝试一下这份代码:登录 laf.dev, 点击函数市场,抉择这个函数模板:

设置一下环境变量 OPENAI_API_KEY:

小小测试一下:

OK,没问题。

这里的运行后果是由两局部组成的:{回复}--!{id}

如果发动的 POST 申请不带参数 parentMessageId(即上一条信息的 id),就会开始一个新的对话;如果带上了 parentMessageId,就会接着上一条信息持续往下聊。就像这样:

而后!点击公布(你必定找失去这个按钮),这个函数就能够外网拜访了。

laf小小解释一下这份代码在做什么

模板代码如下:

import cloud from '@lafjs/cloud'import util from "util"const db = cloud.database()export default async function (ctx: FunctionContext) {  const { v4: uuidv4 } = require('uuid')  const { getEncoding } = require('js-tiktoken')  const maxConversationTokens = 13000  let curConversationTokens = 0  const maxReplyToken = 1000  let encoding = cloud.shared.get('encoding')  if (!encoding) {    encoding = getEncoding('cl100k_base')    cloud.shared.set('encoding', encoding)  }  const { body, response } = ctx  response.setHeader('Content-Type', 'application/octet-stream')  const curQuestion = { "role": "user", "content": body.message }  curConversationTokens += CountMessagesTokens(encoding, [curQuestion])  const parentMessageId = body?.parentMessageId || ''  const messageId = uuidv4()  let conversationHistory = []  let parentMessageIdTmp = parentMessageId  while (parentMessageIdTmp !== '') {    const parentMessageRes = await db.collection('messages').where({      messageId: parentMessageIdTmp,    }).getOne()    if (curConversationTokens + parentMessageRes.data.tokens < maxConversationTokens) {      conversationHistory.unshift(...parentMessageRes.data.message);      parentMessageIdTmp = parentMessageRes.data.parentMessageId;      curConversationTokens += parentMessageRes.data.tokens;    } else {      break    }  }  conversationHistory.push(curQuestion)  const data = {    model: "gpt-3.5-turbo-16k",    messages: conversationHistory,    max_tokens: maxReplyToken,    stream: true,  }  await streamFetch({    data, onMessage: (partialResponse) => {      response.write(partialResponse)    }  }).then((responseText) => {    const reply = { "role": "assistant", "content": responseText };    const message = [curQuestion, reply];    const tokens = CountMessagesTokens(encoding, message);    db.collection('messages').add({      parentMessageId,      messageId,      message,      tokens,    })  }).catch((error) => {    console.error('Error:', error);  })  response.end("--!" + messageId)}export const streamFetch = ({ data, onMessage }) =>  new Promise(async (resolve, reject) => {    let responseText = '';    try {      const response = await fetch("https://api.openai.com/v1/chat/completions", {        method: "POST",        headers: {          "Content-Type": "application/json",          Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,        },        body: JSON.stringify(data),      });      const reader = response.body?.getReader();      if (!reader) {        console.error('Response aborted.')        return reject("Response aborted.");      }      const decoder = new util.TextDecoder('utf-8');      const read = async () => {        try {          const { done, value } = await reader?.read();          if (done) {            return resolve(responseText);          }          const chunk = decoder.decode(value);          const lines = chunk.split("\n");          const parsedLines = lines            .map((line) => line.replace(/^data: /, "").trim())            .filter((line) => line !== "" && line !== "[DONE]")            .map((line) => JSON.parse(line));          for (const parsedLine of parsedLines) {            const { choices } = parsedLine;            const { delta } = choices[0];            const { content } = delta;            if (content) {              onMessage(content);              responseText += content;            }          }          read();        } catch (error) {          console.error('Response aborted.')          return reject("Response aborted.");        }      };      read();    } catch (error) {      console.error("Error:", error);      return reject(typeof error === 'string' ? error : error?.message || 'Request aborted.');    }  });function CountMessagesTokens(encoding, messages) {  const tokens_per_message = 3  const tokens_per_name = 1  let numTokens = 0;  for (const message of messages) {    numTokens += tokens_per_message    for (const [key, value] of Object.entries(message)) {      numTokens += encoding.encode(value).length      if (key === 'name') {        numTokens += tokens_per_name      }    }  }  return numTokens;}

首先小小解释一下这份代码的外围:OpenAI 的原生接口

目前业内曾经有大量的 gpt 相干工具;但归根结底,大家都是在调用 OpenAI 的这个 API:

这个 API 的外围参数是 messages;ChatGPT 之所以记得你说过什么,是因为咱们发送的 messages 带上了过来的对话记录;messages 格局如下:

//messages[    {“role”:"system", "content": "$ 提醒词"},    {“role”:"user", "content": "$ 用户说的第一句话"},    {“role”:"assistant", "content": "$AI的第一句回复"},    ...    {“role”:"user", "content": "$ 用户说的第N-1句话"},    {“role”:"assistant", "content": "$AI的第N-1句回复"},    {“role”:"user", "content": "$ 用户说的第N句话"},]

发送过来后,OpenAI 就会返回给你一条最新的音讯:{“role”:"assistant", "content": "$AI的第N句回复"}

了解了这个概念后,这份代码就好了解了:

首先,取出 POST 申请中的 message, 小小拼装一下:

curQuestion = { "role": "user", "content": body.message }curConversationTokens += CountMessagesTokens(encoding, [curQuestion])const parentMessageId = body?.parentMessageId || ''const messageId = uuidv4()

申请中若带有 parentMessageId,就阐明是有历史对话的;咱们得去云数据库中递归查找,把所有历史对话串起来:

//递归查找所有历史对话记录//若对话记录已超过 maxConversationToken,则进行let conversationHistory = []let parentMessageIdTmp = parentMessageIdwhile (parentMessageIdTmp !== '') {  const parentMessageRes = await db.collection('messages').where({    messageId: parentMessageIdTmp,  }).getOne()  if (curConversationTokens + parentMessageRes.data.tokens < maxConversationTokens) {    conversationHistory.unshift(...parentMessageRes.data.message);    parentMessageIdTmp = parentMessageRes.data.parentMessageId;    curConversationTokens += parentMessageRes.data.tokens;  } else {    break  }}conversationHistory.push(curQuestion)

这里有一行代码:if (curConversationTokens + parentMessageRes.data.tokens < maxConversationTokens),这是在干什么? 这是因为发送的 conversationHistory 不可能是有限长的;最新版的 gpt-3.5-turbo-16k 的 tokens 限度是 16k。 所以,咱们得保障 conversationHistory 应用的 tokens 不大于 maxConversationTokens;文中的 CountMessagesTokens 函数就是用来计算每条 Message 应用的 tokens。

串起来后的会话记录 conversationHistory 长这样:

//conversationHistory[    {“role”:"user", "content": "$ 用户说的第N-X句话"}, // N-X 最小为 1    {“role”:"assistant", "content": "$AI的第N-X句回复"},    ...    {“role”:"user", "content": "$ 用户说的第N-1句话"},    {“role”:"assistant", "content": "$AI的第N-1句回复"},    {“role”:"user", "content": "$ 用户说的第N句话"},]

而后,咱们应用 streamFetch 函数向 openAI 发动申请,并接管它的流式输入,再将它的流式输入再流式返回给咱们的前端(狠狠套娃);对于 streamFetch的实现这里不开展,就叨一嘴咱们该咋用:

await streamFetch({  data, onMessage: (partialResponse) => {    response.write(partialResponse)  }}).then((responseText) => {  const reply = { "role": "assistant", "content": responseText };  const message = [curQuestion, reply];  const tokens = CountMessagesTokens(encoding, message);  db.collection('messages').add({    parentMessageId,    messageId,    message,    tokens,  })}).catch((error) => {  console.error('Error:', error);})

onMessage 是一个委托函数,能够了解为:openAI 每流式输入一个字,你都能够用 onMessage 去解决这个字;咱们的解决也很简略,间接将这个字写回 response,就实现了流式输入~

responseText 是 openAI 响应完结后,输入的残缺内容;咱们将它拼装一下:const message = [curQuestion, reply], 就失去了上面这个货色:

// message[    {“role”:"user", "content": "$ 用户说的第N句话"},    {“role”:"assistant", "content": "$AI的第N句回复"}]

计算一下它的 tokens,将message、messageId、parentMessageId、 tokens 存入云数据库中,完结!期待下一次用户申请的号召 ~

laf搭个前端吧!

能够间接应用这个我的项目 chatGPT demo

批改我的项目中 src/views/chat/index.vue 的这两行代码,别离是 117 行 和 236 行:将 url 替换为你方才公布的函数的 url~

在本地测试一下:npm run dev

十分丝滑,兄弟。

而后执行:npm run build,在当前目录下就会多出一个 dist 文件夹。

点击存储——创立Bucket(留神是公共读)——上传文件夹(将 dist 文件传上去)——开启网站托管,就能够拜访这个网站了!

laf完结了吗?

咱们只用 OpenAI 的原生接口,就从零搭建了本人的 ChatGPT。预计大家也能看到,最近市面常常有角色扮演、或者接入知识库的 ChatGPT;如果你认真看了下面的内容,预计你也能猜到:

messages = [    {“role”:"system", "content": "$ 提醒词"},    {“role”:"user", "content": "$ 用户说的第一句话"},    {“role”:"assistant", "content": "$AI的第一句回复"},    ...    {“role”:"user", "content": "$ 用户说的第N-1句话"},    {“role”:"assistant", "content": "$AI的第N-1句回复"},    {“role”:"user", "content": "$ 用户说的第N句话"},]

只有在 messages 适合的地位中插入 role 为 system 的 message,咱们就能够设置提醒词去疏导 GPT,让它成为本人想要的形态~


关注咱们,下一期持续教大家用最低老本,从零让咱们的 GPT 表演角色、接入知识库噢~

援用链接
[1]
chatgpt-api: https://github.com/transitive-bullshit/chatgpt-api

[2]
laf.dev: https://laf.dev/

[3]
chatGPT demo: https://github.com/lifu963/chatgpt-demo

对于 Laf
Laf 是一款为所有开发者打造的集函数、数据库、存储为一体的云开发平台,助你像写博客一样写代码,随时随地公布上线利用!3 分钟上线 ChatGPT 利用!

GitHub:https://github.com/labring/laf

官网(国内):https://laf.run

官网(海内):https://laf.dev

开发者论坛:https://forum.laf.run

关注 Laf 公众号与咱们一起成长

浏览 1217

Laf 开发者

发消息
人划线

sealos 以kubernetes为内核的云操作系统发行版,让云原生简略遍及

laf 写代码像写博客一样简略,什么docker kubernetes通通不关怀,我只关怀写业务!