前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在 KubeGems 上部署 ChatGPT 飞书机器人

在 KubeGems 上部署 ChatGPT 飞书机器人

作者头像
云原生小白
发布2023-01-11 19:42:06
4.4K0
发布2023-01-11 19:42:06
举报
文章被收录于专栏:LokiLoki

背景

ChatGPT是由 OpenAI 开发的一个人工智能聊天机器人程序,于2022年11月一经推出,就凭借优秀的对话体验刷爆了全网,并获得地表最强 AI 聊天机器人的称号。目前ChatGPT有很多应用场景,不限于 搜索引擎辅助生成代码语言翻译文字创作等等,当下甚至已经出现很多个人或公司开始基于 ChatGPT 开发出一些特定÷场景的应用例如 客服药品分类等等。虽然 ChatGPT 目前存在一些 胡编逻辑混乱的问题,但和它的其它同行相比已远远领先。作为一个天然适合聊天的 AI 服务,本篇文章自然也将指导用户在 KubeGems 中部署 ChatGPT API 并将其接入到飞书机器人中为个人和企业快速提供简单的对话服务来体验 ChatGPT。

开始之前,我们先看下效果

注意:由于OpenAI 目前还没有开放api,同时它近期还接入了Cloudflare的防火墙,来阻止部分bot的调用,所以文中的时效性仅适合当前,不代表以后

总体概览

但是由于Cloudflare防火墙限制,首先我们需要找到一个可以绕过防火墙的方法。在GitHub上我们找到了这个项目

GitHub - transitive-bullshit/chatgpt-api: Node.js client for the unofficial ChatGPT API. 🔥

它基于 puppeteer, 并模拟一个正常的用户登陆到 OpenAI, 然后在浏览器中嵌入脚本来发起对话请求;

Puppeteer 是一个 Node.js 库,它提供了一组用于控制 Chrome 浏览器的 API。你可以使用 Puppeteer 自动化浏览器操作,如页面导航、表单提交、JavaScript 执行等

但是这个项目有些限制,它只能一个一个账号启动一个实例,不支持账号池,所我们还需要自己完成账号池的功能;

既然有了账号池,我们还需要完成对话和账号的关联保持,例如:id 为 xxx-xxx 的的会话发生在账号 account1上,如果与这个会话的消息发到了 account2的实例上,那就会发生上下文错落的情况;

此外,还需要尽可能让账号池的账号关联的会话,尽可能保持均衡,避免某个实例的请求过多导致OpenAI限流。

由此,打造自己个人或者企业的ChatGPT 飞书机器人,我们需要对 chatgpt-api 这个工程进行以下的改造.

  1. 将 chatgpt-api 改造成支持容器化,需封装一个 http 或者其他可达的服务接口
  2. 支持账号池,我们决定基于kubernetes statefulset来实现,让每个pod 实例拥有独立的 OpenAI 账号
  3. 处理Cloudflare 防火墙验证码逻辑
  4. 提供业务层代理来保持具体 conversation_id和Pod 实例之间的关联,并支持负载均衡保持会话
  5. 开发飞书机器人程序,响应群内@会话事件,并将ChatGPT结果返回给用户

最终改造后的架构如下:

改造流程

第一步、封装 http 服务

基于express, 很容易支持将 chatgpt-api 暴露成为http服务, 我们直接在demos目录下添加一个 server.ts文件

Express.js 是一个基于 Node.js 的 Web 应用框架。它提供了一组强大的特性,帮助你创建各种 Web 应用和 API。

添加一个service,这非常简单!

代码语言:javascript
复制
 app.get('/', async (req, res) => {
    const result = await api.sendMessage(q, {
        req.query.conversationId,
        req.query.parentMessageId,
        req.query.messageId 
    })
    res.send({ instance: hostname(), ...result })
}
第二步、支持账号池

chatgpt-api 目前仅支持单个 OpenAI 账号,如果有账号池需求,我们就需要启动多个实例。为了支持账号池,我们计划通过 StatefulSet的方式启动多个实例,每个实例获取以自己ID后缀结尾的账号和密码,这样多个实例启动的时候,每个实例就使用它自己的id对应的账号,例如 gptchat-api-0 就会使用配置中的 OPENAI_EMAIL_0对应的账号 和 OPENAPI_PASSWORD_0对应的密码,以下是核心的实现逻辑

代码语言:javascript
复制
import dotenv from 'dotenv-safe'
import express from 'express'
import { hostname } from 'os'

import { ChatGPTAPIBrowser } from '../src'

dotenv.config()

async function getapi() {
  const host = hostname()
  const seps = host.split('-')
  const idx = seps[seps.length - 1]
  const email =
    process.env['OPENAI_EMAIL'] || process.env[`OPENAI_EMAIL_${idx}`]
  const password =
    process.env['OPENAI_PASSWORD'] || process.env[`OPENAI_PASSWORD_${idx}`]

  console.log(`account ${idx} ${email} used`)
  const api = new ChatGPTAPIBrowser({
    email,
    password,
    debug: false,
    minimize: true
  })
  await api.initSession()
  return api
}

async function server() {
  const api = await getapi()

  const app = express()
  const port = 3000
  app.get('/', async (req, res) => {
    const q = req.query.q
    const start = Date.now()
    const conversationId = req.query.conversationId
    const parentMessageId = req.query.parentMessageId
    const messageId = req.query.messageId
    console.log(q, conversationId, parentMessageId, messageId)
    let result = {
      conversationId,
      response: '',
      messageId: ''
    }
    try {
      result = await api.sendMessage(q, {
        conversationId,
        parentMessageId,
        messageId
      })
    } catch (error) {
      console.table({
        error
      })
      res.set('instance', hostname())
      res.send({ error })
      return
    }
    const millis = Date.now() - start
    console.table({
      timeused: Math.floor(millis / 1000),
      instance: hostname(),
      ...req.query
    })
    if (result != undefined) {
      res.set('instance', hostname())
      res.set(
        'conversationId',
        req.query.conversationId || result.conversationId
      )
    }
    console.table({ instance: hostname(), ...result })
    res.send({ instance: hostname(), ...result })
  })

  app.get('/ready', async (req, res) => {
    res.send(`ok, ${hostname()}`)
  })

  app.listen(port, () => {
    console.log(`Example app listening on port ${port}`)
  })
}

server()
第三步、通过环境变量传递账号

由于 dotenv 会读取 .env 下的内容作为环境变量,所以我们将OpenAI账号按照以下格式,放到 secret 中,将其作为 .env 文件挂载到 pod中

代码语言:javascript
复制
OPENAI_USER_0=user0@email.com
OPENAI_USER_1=user1@email.com
...more
OPENAI_PASSWROD_0=password0
OPENAI_PASSWROD_1=password1
...more
第四步、运行X SERVER

运行 gptchat-api还需要有 X SERVER,不然启动的时候会报错(pupepteer在非headless环境下需要),在容器环境下,使用 xvfb来运行应用

xvfb-run -n1 -f /tmp/authvnc npx tsx demos/local-server.ts

第五步、处理验证码

ChatGPT 在登录账号的时候会触发验证码,我们使用 nopecha插件来帮助自动完成这个过程(当然,这是一个付费服务,最低$5/月),如果你想通过远程vnc手动去浏览器中输入验证码也是可以的。不过我们在这里直接使用 NopeCHA 的服务,毕竟多账号的时候,挨个去容器中认证很麻烦,还有在容器重启的时候处理也非常繁琐。

当我们提供了NOPECHA_KEY的环境变量的时候,gptchat-api 会自动安装插件并启用这个服务,遇到有验证码的界面的时候,它会自动帮我们处理,完全不用我们操心验证码了

NopeCHA 是一个基于AI的验证码自动识别服务提供商,它目前提供了浏览器插件的支持

第六步、代理 (负载均衡 + 会话保持 + 节点注册)

由于需要支持账号池,我们启动了多个实例,且会话的上下文是通过 conversation_id来保持的,我们需要一个proxy来将请求发送到关联的实例,也需要它帮我们将新的对话请求自动分配给"最闲"的节点;

为了实现负载均衡,我们需要在代理上保存转发记录表,它记录了每个节点的会话详情,开始时间和最后活跃时间,有了这些数据,我们便可以实现负载均衡会话保持的功能(这很像路由表的功能)。

代码语言:javascript
复制
{
    chatgpt-api-0: {
        name: chatgpt-api-0,
        conversations: [{
            conversation_id: "xxx-xxx-xxx",
            begintime: "2023-01-01 20:00"
            latesttime: "2023-01-01 20:20"
        }, {
            conversation_id: "xxx-xxx-xxx",
            begintime: "2023-01-01 20:00"
            latesttime: "2023-01-01 20:20"
        }],
        online: true
    },
    ... more endpoints
}

具体实现逻辑:

  1. 当一个没有 conversation_id 的请求进来时,我们就认为这是个一个新的会话,负载均衡从 endpoints中找到 conversations数最少的节点转发请求,并且从 response headers中获取 conversation_id, 将这个 conversation记录在节点的conversations
  2. 当请求带着 conversation_id时,则找到这个 conversation_id所在节点转发

ChatGPT API节点注册则直接利用了Kubernetes 的 Endpoint。Proxy 服务启用了一个协程专门用于 watch endpoints, 它负责维护节点的状态,当一个节点不健康的时候,转发记录表中的节点的 online 状态会被标记为 false,当请求来的时候,只会选择 online 为 true 的节点进行筛选, 即使请求带了 conversation_id, 这儿也不会将请求转发给不健康的节点,这种请求将转发到一个新节点,并且会将 conversationd_id重置。

第七步、对接飞书机器人

我们简单开发一个飞书机器人并对接上 chatgpt api,这样就可以在飞书的个人或群组上对它进行聊天交互。那么它具体的设计如下:

  1. 飞书机器人订阅发给它或者它所在的群里的消息
  2. 飞书机器人后端收到订阅事件后,先检查是否是机器人关注的类型(单聊消息和群聊@机器人的消息)
  3. 如果是机器人关注的消息,那么机器人检查是否和发消息的人存在了一个 FeishuSession,如果不存在,就新建一个FeishuSession,并且让这个Session开始执行对话机制; 这个Session的对话机制就是从Session单独的消息队列中取消息,访问chatgpt-appi,获取对应的响应,然后通过飞书发给用户,如果存在了Session,那就直接讲对话放入这个Session的订阅队列中。简单的说就是订阅聊天消息事件,识别出 @机器人 的消息,将消息放入队列中
  4. FeishuSession 维持了一个对话过期时间,每次有消息传递的时候,这个时间都会重置到预先设定的超时时间段之后的时刻
  5. 飞书机器人在启动的时候还有有一个协程,每过几秒执行一次扫描,将过期的会话删除,并且在删除前,先指定的用户发送会话过期的提示

部署流程

前面讲了很多我们的开发设计,但如果你仅仅只想快速部署体验的话,可以尝试在本地部署运行起来。我们已经将应用用 Helm 打包并发布到了 KubeGems 在线应用商店,用户可以在 KubeGems 中实现一键部署。

  1. 在 KubeGems 的管理员后台,进入应用商店添加仓库地址 https://charts.kubegems.io/kubegemsapp
  1. 创建飞书机器人应用,在飞书开放平台中,创建企业自建应用,并启用机器人,这一步需要拿到 飞书 app_idapp_secret。在应用管理后台 -> "事件订阅" 页面,拿到 Verification Token

以上三个变量需要在部署应用的时候使用

  1. 准备OpenAI的账号

注册 OpenAI 账号,并取得账号密码

因为一些众所周知的原因,本文不会介绍注册流程,读者可在网上自行寻找方法

  1. 进入应用商店,选择刚刚添加的 KubeGems 在线商店,并进入 gptchat-api-feishubot这个应用,将它部署到你的指定环境中
  1. 应用配置,准备values.yaml,将里面的配置内容换成你的个人数据
代码语言:javascript
复制
proxy:
  image: kubegems/chatgpt-api-proxy:latest
chatgpt:
  # 副本数,和账号的数量一致
  replicas: 1
  # 处于某些原因,中国大陆需要代理服务器才能访问到openai,
  PROXY_SERVER: "1.2.3.4:5678"
  # 验证码破解插件的key, 如果没有这个插件,需要在pod启动的时候,kubectl port-forward 将pod的5900端口转发到本地,用vnc客户端打开后手动点击
  NOPECHA_KEY: "abcdefg"
  image: kubegems/chatgpt-api:latest
  # .env 的内容文件当前目录下, 如果没提供,就用envContent的内容
  localenv: ""
  # 如果没有文件,就贴内容到这儿
  envContent: |-
    OPEN_AI_EMAIL=test@test.com
    OPEN_AI_PASSWORD=pass
feishubot:
  image: kubegems/chatgpt-api-feishubot:latest
  # 飞书 appid
  FeishuAppID: "cli_xxxx"
  # 飞书 appsecret
  FeishuAppSecret: "xxxx"
  # 飞书机器人名字
  FeishuBotName: "name"
  # 飞书的验证token,如果搞不明白是啥,可以去看飞书文档
  FeishuVerificationToken: "verifytoken"
  # 飞书的一个消息加密的key,如果搞不明白是啥,可以去看飞书文档
  FeishuEventEncryptKey: ""
  # 会话过期时间
  ConversationExpireSeconds: 3600

将上述配置粘贴在应用部署过程中的配置框中,点击部署,等待服务运行

  1. 配置飞书机器人的服务地址。将飞书应用后台的事件订阅地址,修改成刚才部署的feishu-bot service的地址
  1. 最后发布飞书机器人即可完成 🚀

缺陷和总结

OpenAI 的API返回的是一个EventSource,chatgpt-api 项目是将 EventSource 的 stream 读取完成后才返回内容,所以这儿有很大的延迟,返回的内容越长,延迟越大。我们可以想办法将EventSource的内容转发给下游(但是我不太熟悉puppteer😄,所以我还没解决这个问题)

现阶段还有另外一个项目 https://github.com/ChatGPT-Hackers 可以考虑。它的做法是在浏览器内部部署agent,反向注册到代理服务上,有兴趣的同学可以试试。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-01-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 云原生小白 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 总体概览
  • 改造流程
    • 第一步、封装 http 服务
      • 第二步、支持账号池
        • 第三步、通过环境变量传递账号
          • 第四步、运行X SERVER
            • 第五步、处理验证码
              • 第六步、代理 (负载均衡 + 会话保持 + 节点注册)
                • 第七步、对接飞书机器人
                • 部署流程
                • 缺陷和总结
                相关产品与服务
                访问管理
                访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档