###### tags: `OpenAI` `ChatGPT` `Python`
# 用 Python 串接 OpenAI 模擬 ChatGPT 聊天機器人
[ChatGPT](https://chat.openai.com/chat) 引起了旋風, 如果能將 ChatGPT 嵌入自己的程式中, 應該會很好玩, 大概是每個人心中第一個想到的事, 不過實際上 ChatGPT 並沒有發布正式的 API, 只能利用 OpenAI 現有的 API 達到類似的功能。以下我們就簡單的試試看, 完成用 Python 串接 OpenAI API 的流程, 並進一步模擬聊天的功能。
## 申請 API 金鑰
要使用 [OpenAI](https://openai.com/) 的 API, 第一步就是註冊帳號, 申請 API 金鑰。請至 [OpenAI API](https://openai.com/api/) 頁面註冊帳號,你也可以使用 Google 等現有的帳號快速註冊。註冊過程會需要 email 認證以及手機認證, 完成後請在頭像下選 **View API keys**:

按一下 **Create new secret key** 就會產生你專屬的金鑰:

不過要特別留意這個金鑰只會顯示一次, 複製後按下 **OK** 就不會再顯示完整的金鑰, 也不會有複製的按鈕:

如果沒有記下金鑰, 之後只能重新建立新的金鑰。
## 使用 HTTP POST 叫用 OpenAI API
在 API 頁面登入或是切換到 [Overview](https://beta.openai.com/overview) 頁面, 會看到提供的功能分類, 對於 ChatGPT 這樣的聊天功能, 是屬於 [Text Completion](https://beta.openai.com/docs/guides/completion), 基本的概念就是你給定一個**提示 (prompt)**, API 就會回應接續該提示的文句。
如果查 [API 的參考文件](https://beta.openai.com/docs/api-reference/completions/create?lang=curl), 會看到 Text Completion 必須以 HTTP Post 方法取用, 以 curl 指令可以如下測試:
```
curl https://api.openai.com/v1/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer 你的金鑰' \
-d '{
"model": "text-davinci-003",
"prompt": "Say this is a test",
"max_tokens": 7,
"temperature": 0
}'
```
其中最重要的就是在表頭中代入你的金鑰, 在訊息本體中必須以 JSON 格式傳遞相關的參數, 最關鍵的幾個參數的意義如下:
|參數名稱|說明|
|---|---|
|[model](https://beta.openai.com/docs/api-reference/completions/create#completions/create-model)|要使用哪一個 OpenAI [訓練好的模型](https://beta.openai.com/docs/models/overview), 雖然有幾種選擇, 不過若是要聊天, 以 text-davinci-003 最佳。這是必要參數, 一定要提供。|
|[prompt](https://beta.openai.com/docs/api-reference/completions/create#completions/create-prompt)|要傳給 OpenAI 的提示, 以聊天來說, 就是你要說的話。|
|[max_tokens](https://beta.openai.com/docs/api-reference/completions/create#completions/create-max_tokens)|回覆文句的最多 token 數, 這個數量連同 prompt 的 token 數合計不能超過所使用模型的限制, text-davinci-003 加總後不能超過 4097。這裡的 token **並不一定是完整的單字**, 單一單字也可能會被切分成多個 toten, 有興趣可以透過[官方工具](https://beta.openai.com/tokenizer)瞭解切割結果。|
|[temperature](https://beta.openai.com/docs/api-reference/completions/create#completions/create-temperature)|0~1 的冒險程度, text completion 產生文字的方式就是依據目前已經產生的內容挑選可能的下一個 token, 這個參數值越大, 挑選 token 時就越會把預測機率低的 token 考慮進去, 回覆的內容變化就越大, 設為 0 就會像是一個背答案的機器人, 永遠只選預測機率最高的那一個 token, 同樣的提示就會回答一樣的文句。|
|[n](https://beta.openai.com/docs/api-reference/completions/create#completions/create-n)|要回覆的語句數|
|[stop](https://beta.openai.com/docs/api-reference/completions/create#completions/create-stop)|停止產生語句的字串陣列, 遇到指定的字串就會結束語句, 不會再產生後續的內容。最多可以設定 4 個。|
|[presence_penalty](https://beta.openai.com/docs/api-reference/completions/create#completions/create-presence_penalty)|-2.0~2.0, 值越大越會懲罰用過的 token, 也就是鼓勵產生語句時使用新的 token。|
|[frequency_penalty](https://beta.openai.com/docs/api-reference/completions/create#completions/create-frequency_penalty)|-2.0~2.0, 值越大越會懲罰出現頻率高的 token, 避免重複。|
|[suffix](https://beta.openai.com/docs/api-reference/completions/create#completions/create-suffix)|AI 會在 prompt 與 suffix 之間補上適合的語句, 讓 prompt 和 suffix 的邏輯關係正確。
|[best_of](https://beta.openai.com/docs/api-reference/completions/create#completions/create-best_of)|讓 AI 產生 best_of 個回應, 然後傳回其中分數最高的|
接著我們就依循上述規範, 利用 Python 來測試。首先建立一個儲存金鑰的變數:
```python
>>> api_key = '你的金鑰'
```
因為要使用 HTTP Post, 我會採用 requests 模組, 如果你還沒有安裝, 可以先用 pip install requests 安裝。
```python
>>> import requests
```
接著就可以使用 API 了:
```python
>>> response = requests.post(
... 'https://api.openai.com/v1/completions',
... headers = {
... 'Content-Type': 'application/json',
... 'Authorization': f'Bearer {api_key}'
... },
... json = {
... 'model': 'text-davinci-003',
... 'prompt': '你好',
... 'temperature': 0.4,
... 'max_tokens': 300
... }
... )
```
我們在表頭中以 f 字串代入剛剛設定的 API 金鑰, 執行後就可以檢查 HTTP 狀態碼:
```python
>>> response.status_code
200
```
200 表示正常, 接著檢視一下傳回的內容:
```python
>>> print(response.text)
{"id":"cmpl-6X3PDJv9VwTsoVGSonytEFYWnjAip","object":"text_completion","created":1673335935,"model":"text-davinci-003","choices":[{"text":"\n\n你好!","index":0,"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":4,"completion_tokens":9,"total_tokens":13}}
```
很明顯是 JSON 格式的資料, 重整內容如下:
```json
{
"id": "cmpl-6X3PDJv9VwTsoVGSonytEFYWnjAip",
"object": "text_completion",
"created": 1673335935,
"model": "text-davinci-003",
"choices": [
{
"text": "\n\n你好!",
"index": 0,
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 4,
"completion_tokens": 9,
"total_tokens": 13
}
}
```
其中 choices 是回覆的文句, usage 是提示與回覆的個別 token 數, 這也會用來計算你[帳號的用量](https://openai.com/api/pricing/#prices)。由於我們沒有設定其他參數, 因此只會回覆單一文句, 利用以下方式即可取出:
```python
>>> json = response.json()
>>> print(json['choices'][0]['text'])
你好!
```
如果參數有錯, 例如將 max_tokens 打成 max_token, 少了 s:
```python
>>> response = requests.post(
... 'https://api.openai.com/v1/completions',
... headers = {
... 'Content-Type': 'application/json',
... 'Authorization': f'Bearer {api_key}'
... },
... json = {
... 'model': 'text-davinci-003',
... 'prompt': '嗨',
... 'temperature': 0,
... 'max_token': 300
... }
... )
```
HTTP 狀態碼會傳回 400:
```python
>>> response.status_code
400
```
回覆的內容只有 error 元素, 說明錯誤的原因:
```python
>>> print(response.text)
{
"error": {
"message": "Unrecognized request argument supplied: max_token",
"type": "invalid_request_error",
"param": null,
"code": null
}
}
```
### 指定 token 數限制的 max_tokens 參數
choices 中每個元素的 finish_reason 代表結束的原因, "STOP" 代表正常終止, 如果是 "length" 表示是因為回應超過 max_tokens 指定的字數, 例如以下把 max_tokens 設為 30 ,然後問他一個比較複雜的問題:
```python
>>> response = requests.post(
... 'https://api.openai.com/v1/completions',
... headers = {
... 'Content-Type': 'application/json',
... 'Authorization': f'Bearer {api_key}'
... },
... json = {
... 'model': 'text-davinci-003',
... 'prompt': '你有看過灌籃高手嗎?',
... 'temperature': 0.4,
... 'max_tokens': 30
... }
... )
```
執行結果如下, 你可以看到他其實話還沒說完, 但是因為超過限制, 所以結束了:
```python
>>> json = response.json()
>>> print(json['choices'][0]['text'])
是的,我看過灌籃高手。它是一部
>>> print(json['choices'][0]['finish_reason'])
length
```
### 指定回覆語句數的 n 參數
如果設定 n 參數, 就會回覆 n 個語句, 例如:
```python
>>> response = requests.post(
... 'https://api.openai.com/v1/completions',
... headers = {
... 'Content-Type': 'application/json',
... 'Authorization': f'Bearer {api_key}'
... },
... json = {
... 'model': 'text-davinci-003',
... 'prompt': '嗨',
... 'temperature': 0.4,
... 'max_tokens': 300,
... 'n': 2
... }
... )
>>> json = response.json()
>>> for item in json['choices']:
... print(f"{item['index']}{item['text']}")
0,你好!很高兴见到你!有什么可以帮到你的吗?
1,你好!很高兴认识你!
```
你可以看到因為 n 參數為 2, 所以回覆了 2 個語句。
### 指定結束語句的 stop 參數
你也可以設定 stop 參數, 指定遇到那些文字順序 (最多可指定 4 種) 時就停止, 避免 AI 回覆不適當的語句。例如:
```python
>>> response = requests.post(
... 'https://api.openai.com/v1/completions',
... headers = {
... 'Content-Type': 'application/json',
... 'Authorization': f'Bearer {api_key}'
... },
... json = {
... 'model': 'text-davinci-003',
... 'prompt': '嗨',
... 'temperature': 0.4,
... 'max_tokens': 300,
... 'stop':['迎', '好']
... }
... )
>>> json = response.json()
>>> print(json['choices'][0]['text'])
,你
```
你可以看到上述回覆的結尾並不完整, 推測原本應該是要回覆『你好』, 但因為遇到『好』字就停止了。
你也可以注意到, 因為 text completion 實際上並不是聊天, 而是幫你補完文字, 所以當你只打一個『嗨』時, 它會先幫你補上逗號, 再接著後續的文字, 也就是說, 原本它是想補成『嗨,你好』。
### 用 suffix 參數插入文字
Text Completion 也可以在你[提供的兩段文字間幫你補上 (insert) 文字](https://beta.openai.com/docs/guides/completion/inserting-text), prompt 是第一段文字、而 suffix 參數則是第二段文字, 例如:
```python
>>> response = requests.post(
... 'https://api.openai.com/v1/completions',
... headers = {
... 'Content-Type': 'application/json',
... 'Authorization': f'Bearer {api_key}'
... },
... json = {
... 'model': 'text-davinci-003',
... 'prompt': '我想學',
... 'suffix': '換工作',
... 'temperature': 0.7,
... 'max_tokens': 3000
... }
... )
```
AI 傳回的結果如下:
```python
>>> print(response.json()['choices'][0]['text'])
習人力資源管理, 因為我想提升我的職場技能, 以協助我更好的管理我的 團隊, 並更好的溝通與同事, 更重要的是要提升我的管理能力, 以便我可 以有更多的機會晉升或是更
>
```
整合提供給 AI 的 prompt 和 suffix, 完整的內容就變成:
>我想學習人力資源管理, 因為我想提升我的職場技能, 以協助我更好的管理我的 團隊, 並更好的溝通與同事, 更重要的是要提升我的管理能力, 以便我可 以有更多的機會晉升或是更換工作
## 使用 openai 模組
上述的過程因為是自己使用 requests.post, 有點瑣碎, OpenAI 有提供 [openai 模組](https://beta.openai.com/docs/libraries/python-bindings), 可以直接使用 pip 安裝。
:::warning
注意, 本文撰寫時 openai 版本為 0.26.0, 但是在 Windows 上若是 Python 3.10 的環境, 安裝會發生錯誤, 建議先降版本到 0.25.0 就可以正常安裝。
:::
在 OpenAI API 網頁上都會提供 [Playground](https://beta.openai.com/playground/p/8P6JA6XEx74NTvcRUngWKEYW?model=text-davinci-003) 讓大家體驗功能, 並且可以開啟 **View code** 檢視對應的程式碼, 這個程式碼就以 openai 模組來達成。以下我們示範使用 openai 連接 Text Completion API, 首先就是要匯入模組, 並設定金鑰:
```python
>>> import openai
>>> openai.api_key = f'{api_key}'
```
其中的 `{api_key}` 請代換成你自己的金鑰。接著就可以利用現成的方法進行 HTTP post, 省去我們自己設定表頭以及 JSON 格式文檔的瑣碎細節:
```python
>>> response = openai.Completion.create(
... engine = 'text-davinci-003',
... prompt = '嗨',
... temperature = 0.7,
... max_tokens = 300
... )
>>>
```
這個方法的執行結果具有 Python 字典的特性, 可以直接以字典的方式存取內容:
```python
>>> response['choices'][0]['text']
',大家好!\n\nHi, everyone!'
```
## 計算 token 數
如果你想知道你的 prompt 到底會分成多少 token?OpenAI 並沒有提供 API 可以使用, 不過可以改用 [transformers](https://huggingface.co/docs/transformers/model_doc/gpt2#transformers.GPT2TokenizerFast) 套件來達成, 請先使用 pip install transformers 安裝套件, 即可產生 [GPT2TokenizerFast]() 幫你將文字分成 token:
```python
>>> from transformers import GPT2TokenizerFast
>>> tokenizer = GPT2TokenizerFast.from_pretrained('gpt2')
```
這裡使用預先訓練好的模型 gpt2, 以下可取得指定文句解析後的結果:
```python
>>> t = tokenizer("hamburger")
>>> print(t)
{'input_ids': [2763, 6236, 1362], 'attention_mask': [1, 1, 1]}
>>>
```
有了 token id 的清單, 就可以利用清單的長度取得 token 數量了。你也可以讓他直接[傳回數量](https://huggingface.co/docs/transformers/v4.24.0/en/internal/tokenization_utils#transformers.PreTrainedTokenizerBase.__call__.return_length)即可:
```python
>>> t = tokenizer("hamburger", return_attention_mask=False, retu
... rn_length=True)
>>> t
{'input_ids': [2763, 6236, 1362], 'length': [3]}
```
這裡同時指定不要傳回 [attention mask](https://huggingface.co/docs/transformers/v4.24.0/en/glossary#attention-mask), 減少資料量, 不過 token id 就無法省去。
## 讓對話過程更像是聊天
使用 Text Completion 時 OpenAI 端並不會像是 ChatGPT 那樣依照對談記錄回答, 所以每一次問答都像是獨立事件, 例如利用以下的範例連續對答:
```python
import openai
# Set the API key
openai.api_key = "你的金鑰"
while True:
# Read a message from the user
message = input("You: ")
# Use GPT-3 to generate a response
response = openai.Completion.create(
engine="text-davinci-003",
prompt = message,
max_tokens=2048,
temperature=0.9,
)
print("Bot: ", response.choices[0].text)
```
你會發現連續的問答之間並沒有什麼關連性, 像是這樣:
```
# py chat.py
You: 海洋污染對哪個地區影響最大
Bot:
答:大西洋地區
You: 台灣呢?
Bot:
台灣是亞洲島嶼國家,位於中國東南部,是一個文化多元、經濟成長急速 的國家。台灣是世界上成長最快的經濟體之一,擁有先進的科技和自由的 市場,經濟及社會健康發展水平較高。台灣擁有東方文化與西方現代化的 完美結合,也是體育、教育及人文氣息濃厚的好地方。
You:
```
如果希望讓連續的對答可以有連慣性, 可以把之前的對話內容一併送回, 例如:
```python
import openai
# Set the API key
openai.api_key = "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"
prev_prompt = ''
prev_ans = ''
while True:
# Read a message from the user
message = input("You: ")
# Use GPT-3 to generate a response
response = openai.Completion.create(
engine="text-davinci-003",
prompt=prev_prompt + "\n"+ prev_ans + "\n" + message,
max_tokens=2048,
temperature=0.9,
)
# Print GPT-3's response
print("Bot: ", response.choices[0].text)
prev_prompt = message
prev_ans = response.choices[0].text
```
上述範例採取比較簡單的方式, 只將前一次對話的問與答納入, 對話內容就會比較有相關性了:
```
# py chat.py
You: 海洋污染對哪一個地區影響最大?
Bot:
海洋污染對各大洋的海洋生物影響最大,而歐洲、北美洋和南美洋的生物受到的污染影響最大。特別是1990年以來,東北太平洋和加勒比海洋中主 要由非法倒塑料所導致的污染增加,以及地中海、大西洋和北冰洋中漁業殘留物對海洋生物的植物和動物,特別是魚類造成很大的污染。
You: 那台灣呢?
Bot:
台灣位於西太平洋及華南海域,受到來自亞洲和大洋洲旁邊區域的污染影 響較大,例如:河口污染、油汙、藥物殘留物等。此外,台灣也受到漁業殘留物的影響,許多漁業活動對海洋生態系統造成嚴重的污染衝擊。另外,台灣的沿海廢棄物排放也造成了很大的污染和對海洋生態系統的影響。
You:
```
又或是像這一串對答:
```
You: 日本的空污嚴重嗎
Bot: ?
空氣汙染在日本是一個嚴重的問題。根據世界衛生組織(WHO)2018年出版的污染圖表,日本在空氣污染指數上是世界上數一數二的地方,總指數高 出全球平均。雖然日本的政府在抗擊空氣汙染方面做出了努力,但仍有極 大的改進空間。
You: 那台灣呢?
Bot:
台灣的空氣污染情況也是嚴重的。根據世界衛生組織的數據顯示,台灣的 空氣污染指數高出全球平均水平,在空氣污染指數上也是高出全球平均水 平的地方之一。雖然台灣政府也做出了努力,但在抗擊空氣汙染方面仍存 在很大的改善空間。
You:
```
在有關台灣的回覆中, 它會加上『也』字, 明確展現出他知道這是接續剛剛日本的問題。
如果希望提升效果, 可以將過去的對答歷史都納入, 而不僅僅像是上述範例只納入前一次的對答。如果擔心對答歷史過長, 超過 token 限制, 也可以使用先前介紹過計算 token 數量的方法, 在數量過多時先請 OpenAI 幫你把對答歷史摘要, 用摘要當成對答歷史, 就可以省下不少資料量。