本文將會教你如何用GPT-3的API製作一個Discord聊天機器人。
前言
首先,本文介紹的GPT-3 API並不是各位在Open AI網站的ChatGPT 示範網頁,而是透過OpenAI的API 存取GPT-3模型,效果會不會跟ChatGPT一樣好取決你設定的參數,也可以對模型進行Fine-tuning(微調),如果想要直接使用ChatGPT製作,可以參考非官方Node.js套件chatgpt 存取ChatGPT API(非官方)。
GPT-3
GPT-3是一個由人工智慧公司OpenAI所訓練的自然語言模型,其中的強大之處也不需要我多做介紹了,自己去ChatGPT 試過就知道了。
另外要提一下,GPT-3沒有提供模型檔案下載,只能透過API存取,這方面的話OpenAI一點也不Open,不過也是情有可原啦,畢竟訓練這個模型的過程中花了好幾億,怎麼可能說給就給呢。API的部分也有存取的次數限制,免費試用的情況下只有18美元給你用,每1000 tokens是0.02元,你可以把tokens想成是文字的數量,每1000 tokens大約是750字,詳細的資訊可以參考OpenAI的Pricing 頁面。
前置準備
Node.js
本次要使用Node.js來進行開發,請先至Node.js 官網下載並安裝Node.js,安裝完後用npm建立一個新專案。
先創一個資料夾,我這裡命名為gpt-3-chatbot-discord,然後打開終端機,輸入:
# cd /path/to/gpt-3-chatbot-discord
# npm init
然後他會要求你輸入一些設定,我這裡全部都讓他用預設的(全部enter)
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help init` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (gpt-3-chatbot-discord)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
之後會讓你確認package.json的內容,沒問題的話直接按enter
About to write to G:\Documents\t-gpt-3-chatbot\gpt-3-chatbot-discord\package.json:
{
"name": "gpt-3-chatbot-discord",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Is this OK? (yes)
你會發現資料夾裡多了一個package.json,就代表專案初始化成功。
然後安裝一些要用到的套件,輸入:
# npm i discord.js
# npm i openai
另外我個人習慣用.env把一些敏感的資料(如API的金鑰等)跟程式碼隔離開來,一方面方便修改這些資料,另一方面就是如果要把檔案傳給別人使用時可以避免把敏感資料傳送給別人,如果對方沒有程式設計基礎的話,也可以請他直接修改.env檔案裡面的設定,改成他自己的金鑰之類的。
因此再多安裝一個dotenv套件:
# npm i dotenv
Discord API
接下來要創建一個Discord的APP,前往Discord Developer Portal 的Applications頁面,點擊New Application創建一個新的APP。
輸入好名稱後再把底下同意事項打勾,點擊Create。
進到Applications的畫面後,點進左邊Bot的標籤,然後按Add Bot,之後再按下Yes, do it!
然後再按名字底下的Reset Token,Yes, do it!後,你就會得到一串Token。
在專案的資料夾下創建一個.env檔案,然後新增一行:
DISCORD_TOKEN=<你的Discord Token>
把<你的Discord Token>替換成剛剛產生的Token。
然後再到Bot頁面,有一個選項是MESSAGE CONTENT INTENT,把它打開來,不然會讀不到訊息。
設定完記得Save Changes。
將機器人加入Discord伺服器
創好APP後,再進入Developer Portal左邊OAuth2,URL Generator的地方,SCOPES的地方勾選bot,BOT PERMISSIONS勾選GENERAL PERMISSIONS的Read Messages/View Channels、TEXT PERMISSIONS的Send Messages,詳細的權限說明可以參考文件的Permissions 頁面。
然後底下會有一串網址,直接貼在瀏覽器打開。
選擇要新增進去的伺服器,要注意的是,你只能把機器人新增到你有管理權限的伺服器中,如果沒有的話要請伺服器管理員幫你加入。這邊會建議各位自己創一個伺服器來測試。
選好後點選繼續,會讓你確認要給機器人哪些權限,這邊都不要管,直接按授權。
看到已授權之後就代表加入成功了,可以到伺服器中看有沒有出現你的機器人。
OpenAI API
接下來換OpenAI的部分,進入OpenAI的API Keys 頁面,點擊+Create new secret key新增一個API Key。
建立完成後會跳出一個視窗給你複製API Key,一樣把它加入.env中。
OPENAI_API_KEY=<你的OpenAI API Key>
開始開發
先做一些設定
由於我們才都使用預設的設定建立專案,所以Node.js的main檔案是index.js,當然你也可以自己取名字,不過不要忘了進去package.json把main的選想改成你設的檔名。
"main": "<你設定的檔名>.js",
接下來新建index.js檔案,先引入要用的套件:
const dotenv = require('dotenv');
const { Configuration, OpenAIApi } = require('openai');
const { Client, GatewayIntentBits } = require('discord.js');
然後引入.env設定
dotenv.config();
Discord Client
我們可以用以下程式碼新建一個Discord Client:
const DiscordClient = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
其中intents的部分是要使用到什麼權限,這邊原本是要用數字表示,但是Discord有提供GatewayIntentBits可以幫你產生,只要呼叫相對應的變數即可,詳細的資訊可以到文件的Gateway Intents 查看。
然後可以用DiscordClient.on來確認是否登入成功
DiscordClient.on('ready', () => {
console.log(`Logged in as ${DiscordClient.user.tag}!`);
});
我們可以建立一個簡單的Ping-Pong功能
DiscordClient.on('messageCreate', (message) => {
if (message.content === 'ping') {
message.reply('Pong!');
}
});
最後再登入Bot
DiscordClient.login(process.env.DISCORD_TOKEN);
process.env.DISCORD_TOKEN是呼叫前面我們放在.env的設定,如果你不想要用dotenv的話,可以直接在這邊輸入你的token
DiscordClient.login('<你的Discord Token>');
這邊是這個段落的完整程式碼:
const dotenv = require('dotenv');
const { Configuration, OpenAIApi } = require('openai');
const { Client, GatewayIntentBits } = require('discord.js');
dotenv.config();
const DiscordClient = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
DiscordClient.on('ready', () => {
console.log(`Logged in as ${DiscordClient.user.tag}!`);
});
DiscordClient.on('messageCreate', (message) => {
if (message.content === 'ping') {
message.reply('Pong!');
}
});
DiscordClient.login(process.env.DISCORD_TOKEN);
然後到package.json,在scripts中新增一段
"start": "node index.js",
之後打開終端機,輸入
# npm start
如果看到Logged in as <bot的名字>的話,就代表登入成功,接下來可以到Discord中發送ping。
如果機器人有回應你Pong!的話,就代表你到這邊都沒有問題了。
OpenAI API
然後我們要開始把GPT-3整合進Bot中。
先建立OpenAIApi的相關設定。
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
這裡的process.env.OPENAI_API_KEY與前面的DISCORD_TOKEN同理
然後再建立OpenAIApi的Client:
const openai = new OpenAIApi(configuration);
可以建立一個變數來儲存之前的聊天內容,因為GPT-3是透過前後文來推論下一句話的
let prompt = `你是一個聊天機器人Chatbot,你必須陪同一個人聊天\n
Chatbot: 你好,我是一個聊天機器人\n`;
這裡先預置一些內容,讓GPT-3了解自己的身分,當然,這裡算是蠻重要的部分,因為這會決定GPT-3要成為一個什麼樣的角色。
你也可以讓他帶入角色的性格,比如說:你可以寫「你是一個彪悍大叔,名叫大壯,你現在在跟你的小姪子們聊天」,這樣GPT-3就會帶入性格來產生對話內容,當然這部分就看你的prompt和參數下得好不好,就跟你用Stable Diffusion 來產生圖畫是差不多的道理了。
第二行的話是先預置一段由機器人Chatbot發出的訊息,寫了名字會讓GPT-3更好的認識自己。
接下來修改回覆訊息的部分
DiscordClient.on('messageCreate', async (message) => {
# 接下來有關回覆訊息的程式都要寫在這裡
});
當有人傳送訊息時,都會進來這段Listener,所以要把相關的程式碼寫在裡面,另外這裡也把回應函數變成async(非同步),因為待會呼叫OpenAIApi的時候,由於它是非同步的函數,為了在回傳訊息時可以先等待API回傳結果,因此會加上await(同步),讓程式等API回傳資料後再繼續執行。
先把傳訊人是bot的訊息給排除掉
if (message.author.bot) return;
這樣如果訊息的發送者身分為bot的話,會直接離開這個函數,這樣會避免機器人讀到自己的訊息後開始秀逗。
之後,把用戶傳入的訊息加入prompt中:
prompt += `${message.author.username}: ${message.content}\n`;
這邊是以「傳訊者名字: 訊息」的格式來新增訊息的,這是為了讓GPT能夠辨識誰是誰,以及知道你的名字。
另外,這邊的訊息是直接加進prompt中,與前面的資料結合在一起,就如同前面所介紹的,GPT-3是透過前後文來推測下一句話。
接下來用OpenAIApi發送請求獲得推測的訊息。
const gptResponse = await openai.createCompletion({
model: 'text-davinci-002',
prompt: prompt,
max_tokens: 100,
temperature: 0.9,
});
這邊要注意的是,要在呼叫openai.createCompletion()前加上await,不然程式就不會等到資料回傳就繼續進行下去,這樣會導致後面的程式出錯,當然,你也可以用promise的寫法來寫,不過這裡為了方便展示就以這樣為主。
其中的model是text-davinci-002,這是GPT-3的模型代號之一,是GPT-3系列中最完整的版本,也有其他版本的模型可以選擇,詳細資訊可以看官方文件的GPT-3 頁面。
prompt是要用於預測的資料,就是前面程式中的prompt變數。
max_tokens是限制GPT-3最多能回覆多少字,如果你有試玩過ChatGPT的話,你應該會經常遇到它講話講一半的狀況,就是這個原因,如果出現斷句的話可以嘗試將max_tokens設高;但也不是越高越好,因為請求有最大的Tokens上限,如果使用text-davinci-002的話上限是4000 tokens,超過上限的話程式會出錯,待會會提到。
temperature是關於採樣的相關參數,詳細的說明不是本文的重點,我也沒有信心可以講得比網路上其他大佬好,關於這個參數你可以直接拿「sampling temperature」去餵狗。另外這個參數在做bot的情況下官方文件中有提到「Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer.」,也就是說,bot的話可以直接使用0.9作為採樣溫度,因為bot需要更多的創造力,而0的話就是更精準的答案。不過還是要經過實驗才能得到最佳的結果,所以盡量嘗試不同的值,找到最好的。
另外,temperature有一個替代品叫做top_p,官方建議這兩種要設定其中一種,但是不建議都設。
其他關於參數的相關資料可以參考官方文件的Completion 頁面,請盡量嘗試不同參數,尋找最好的設定值。
得到回傳結果之後,回傳得到的結果
message.reply(gptResponse.data.choices[0].text.split(':')[1]);
gptResponse.data.choices是得到的預測結果,因為它是一個陣列,所以我們直接取它第一個[0],然後陣列中的元素是dict,text欄位即是它的預測內容。
另外,這裡用split(‘:’)將句子用「:」分割成名字和訊息內容的部分,[1]則是他的訊息部分。
之後再往prompt中加入預測結果作為prompt的一部分,這樣才會讓GPT了解前面的對話經過。
prompt += `Chatbot: ${gptResponse.data.choices[0].text.split(':')[1]}\n`;
以下是到目前為止的完整程式碼
const dotenv = require('dotenv');
const { Configuration, OpenAIApi } = require('openai');
const { Client, GatewayIntentBits } = require('discord.js');
dotenv.config();
const DiscordClient = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
DiscordClient.on('ready', () => {
console.log(`Logged in as ${DiscordClient.user.tag}!`);
});
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);
let prompt = `你是一個聊天機器人Chatbot,你必須陪同一個人聊天\n
Chatbot: 你好,我是一個聊天機器人\n`;
DiscordClient.on('messageCreate', async (message) => {
if (message.author.bot) return;
prompt += `${message.author.username}: ${message.content}\n`;
const gptResponse = await openai.createCompletion({
model: 'text-davinci-002',
prompt: prompt,
max_tokens: 100,
temperature: 0.9,
});
message.reply(gptResponse.data.choices[0].text.split(':')[1]);
prompt += `Chatbot: ${gptResponse.data.choices[0].text.split(':')[1]}\n`;
});
DiscordClient.login(process.env.DISCORD_TOKEN);
接下來重啟程式,就可以到Discord試玩它了。
這是我經過調整參數得到的結果,雖然對話上沒什麼問題,但是由於它對時事的了解不是即時的,所以不要想它會給你什麼很正確的解答。
不過其實這也是取決於參數的設定,我嘗試改用top_p: 0.3之後,它終於知道現在(2022年12月23日)世界盃還沒結束了。
關於Prompt
前面有提到,每次送出的請求中,Prompt的上限是4000 tokens,如果超過的話會請求失敗,最簡單的解法就是新增一個指令,比如說clear,讓使用者手動清除,不過聊得太開心的話也會忘記這件事情,如果要真正上線運作的話,這方法肯定行不通。也可以套一層try,讓它出錯時先把prompt重設(又或是取最後幾行),然後再重送,這也是其中一種解法,也能保證對話的持續性。
我這邊還有一個我自己玩的機器人的作法提供給大家:就是讓使用者想要機器人回答時要標註它才會回應,然後幫每個使用者設一個專屬的prompt變數,這樣一方面機器人就不會每句話都回,一方面也不會混雜一堆人的訊息導致文本預測的不準確。不過就它的能力其實群聊也不是不行啦。
結論
GPT-3無疑是目前最好的文本預測模型了,當時出來時我們系上的教授還很興奮的跟我們分享,網路上也就ChatGPT的出現造成了很大的騷動,這個場面絕對不比Stable Diffusion還不盛大。
除了對參數進行調整之外,還能對模型進行Fine-tuning(微調),如果想了解這部分可以到官方文件中的Fine-tuning 頁面查看。通過Fine-tuning可以讓模型更貼近使用情境。
當然,我更希望OpenAI能夠將GPT-3的模型開源釋出,這樣就有更多花樣可以玩了。
最後,希望這篇文章能夠幫助你完整你理想中的Discord機器人囉,也希望能夠讓你更了解GPT-3這套模型。
秉持著我都會遇事別人怎麼不會的精神分享解決事情的經過。