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 这个工程进行以下的改造.
需封装一个 http 或者其他可达的服务接口
支持账号池
,我们决定基于kubernetes statefulset来实现,让每个pod 实例拥有独立的 OpenAI 账号Cloudflare 防火墙
与验证码
逻辑conversation_id
和Pod 实例之间的关联,并支持负载均衡
和保持会话
飞书机器人
程序,响应群内@会话事件,并将ChatGPT结果返回给用户最终改造后的架构如下:
基于express, 很容易支持将 chatgpt-api 暴露成为http服务, 我们直接在demos目录下添加一个 server.ts
文件
Express.js 是一个基于 Node.js 的 Web 应用框架。它提供了一组强大的特性,帮助你创建各种 Web 应用和 API。
添加一个service,这非常简单!
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
对应的密码,以下是核心的实现逻辑
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中
OPENAI_USER_0=user0@email.com
OPENAI_USER_1=user1@email.com
...more
OPENAI_PASSWROD_0=password0
OPENAI_PASSWROD_1=password1
...more
运行 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_KE
Y的环境变量的时候,gptchat-api 会自动安装插件并启用这个服务,遇到有验证码的界面的时候,它会自动帮我们处理,完全不用我们操心验证码了
NopeCHA 是一个基于AI的验证码自动识别服务提供商,它目前提供了浏览器插件的支持
由于需要支持账号池,我们启动了多个实例,且会话的上下文是通过 conversation_id
来保持的,我们需要一个proxy来将请求发送到关联的实例,也需要它帮我们将新的对话请求自动分配给"最闲"的节点;
为了实现负载均衡,我们需要在代理上保存转发记录表
,它记录了每个节点的会话详情,开始时间和最后活跃时间,有了这些数据,我们便可以实现负载均衡
,会话保持的功能
(这很像路由表的功能)。
{
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
}
具体实现逻辑:
conversation_id
的请求进来时,我们就认为这是个一个新的会话,负载均衡从 endpoints
中找到 conversations
数最少的节点转发请求,并且从 response headers
中获取 conversation_id
, 将这个 conversation
记录在节点的conversations
中conversation_id
时,则找到这个 conversation_id
所在节点转发ChatGPT API节点注册则直接利用了Kubernetes 的 Endpoint。Proxy 服务启用了一个协程专门用于 watch endpoints
, 它负责维护节点的状态,当一个节点不健康的时候,转发记录表中的节点的 online 状态会被标记为 false
,当请求来的时候,只会选择 online 为 true
的节点进行筛选, 即使请求带了 conversation_id
, 这儿也不会将请求转发给不健康的节点,这种请求将转发到一个新节点,并且会将 conversationd_id
重置。
我们简单开发一个飞书机器人并对接上 chatgpt api,这样就可以在飞书的个人或群组上对它进行聊天交互。那么它具体的设计如下:
FeishuSession
,如果不存在,就新建一个FeishuSession
,并且让这个Session
开始执行对话机制; 这个Session
的对话机制就是从Session单独的消息队列中取消息,访问chatgpt-appi,获取对应的响应,然后通过飞书发给用户,如果存在了Session,那就直接讲对话放入这个Session的订阅队列中。简单的说就是订阅聊天消息事件,识别出 @机器人
的消息,将消息放入队列中FeishuSession
维持了一个对话过期时间,每次有消息传递的时候,这个时间都会重置到预先设定的超时时间段之后的时刻前面讲了很多我们的开发设计,但如果你仅仅只想快速部署体验的话,可以尝试在本地部署运行起来。我们已经将应用用 Helm 打包并发布到了 KubeGems 在线应用商店,用户可以在 KubeGems 中实现一键部署。
app_id
和 app_secret
。在应用管理后台 -> "事件订阅" 页面,拿到 Verification Token
以上三个变量需要在部署应用的时候使用
注册 OpenAI 账号,并取得账号
和密码
因为一些众所周知的原因,本文不会介绍注册流程,读者可在网上自行寻找方法
gptchat-api-feishubot
这个应用,将它部署到你的指定环境中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
将上述配置粘贴在应用部署过程中的配置框中,点击部署,等待服务运行
OpenAI 的API返回的是一个EventSource
,chatgpt-api 项目是将 EventSource 的 stream 读取完成后才返回内容
,所以这儿有很大的延迟,返回的内容越长,延迟越大。我们可以想办法将EventSource的内容转发给下游(但是我不太熟悉puppteer😄,所以我还没解决这个问题)
现阶段还有另外一个项目 https://github.com/ChatGPT-Hackers 可以考虑。它的做法是在浏览器内部部署agent,反向注册到代理服务上,有兴趣的同学可以试试。