DEV Community

codemee
codemee

Posted on

用 Python 串接 OpenAI 模擬 ChatGPT 聊天機器人

ChatGPT 引起了旋風, 如果能將 ChatGPT 嵌入自己的程式中, 應該會很好玩, 大概是每個人心中第一個想到的事, 不過實際上 ChatGPT 並沒有發布正式的 API, 只能利用 OpenAI 現有的 API 達到類似的功能。以下我們就簡單的試試看, 完成用 Python 串接 OpenAI API 的流程, 並進一步模擬聊天的功能。

申請 API 金鑰

要使用 OpenAI 的 API, 第一步就是註冊帳號, 申請 API 金鑰。請至 OpenAI API 頁面註冊帳號,你也可以使用 Google 等現有的帳號快速註冊。註冊過程會需要 email 認證以及手機認證, 完成後請在頭像下選 View API keys

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

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

如果沒有記下金鑰, 之後只能重新建立新的金鑰。

使用 HTTP POST 叫用 OpenAI API

在 API 頁面登入或是切換到 Overview 頁面, 會看到提供的功能分類, 對於 ChatGPT 這樣的聊天功能, 是屬於 Text Completion, 基本的概念就是你給定一個提示 (prompt), API 就會回應接續該提示的文句。

如果查 API 的參考文件, 會看到 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
}'
Enter fullscreen mode Exit fullscreen mode

其中最重要的就是在表頭中代入你的金鑰, 在訊息本體中必須以 JSON 格式傳遞相關的參數, 最關鍵的幾個參數的意義如下:

參數名稱 說明
model 要使用哪一個 OpenAI 訓練好的模型, 雖然有幾種選擇, 不過若是要聊天, 以 text-davinci-003 最佳。這是必要參數, 一定要提供。
prompt 要傳給 OpenAI 的提示, 以聊天來說, 就是你要說的話。
max_tokens 回覆文句的最多 token 數, 這個數量連同 prompt 的 token 數合計不能超過所使用模型的限制, text-davinci-003 加總後不能超過 4097。這裡的 token 並不一定是完整的單字, 單一單字也可能會被切分成多個 toten, 有興趣可以透過官方工具瞭解切割結果。
temperature 0~1 的冒險程度, text completion 產生文字的方式就是依據目前已經產生的內容挑選可能的下一個 token, 這個參數值越大, 挑選 token 時就越會把預測機率低的 token 考慮進去, 回覆的內容變化就越大, 設為 0 就會像是一個背答案的機器人, 永遠只選預測機率最高的那一個 token, 同樣的提示就會回答一樣的文句。
n 要回覆的語句數
stop 停止產生語句的字串陣列, 遇到指定的字串就會結束語句, 不會再產生後續的內容。最多可以設定 4 個。
presence_penalty -2.0~2.0, 值越大越會懲罰用過的 token, 也就是鼓勵產生語句時使用新的 token。
frequency_penalty -2.0~2.0, 值越大越會懲罰出現頻率高的 token, 避免重複。
suffix AI 會在 prompt 與 suffix 之間補上適合的語句, 讓 prompt 和 suffix 的邏輯關係正確。
best_of 讓 AI 產生 best_of 個回應, 然後傳回其中分數最高的

接著我們就依循上述規範, 利用 Python 來測試。首先建立一個儲存金鑰的變數:

>>> api_key = '你的金鑰'
Enter fullscreen mode Exit fullscreen mode

因為要使用 HTTP Post, 我會採用 requests 模組, 如果你還沒有安裝, 可以先用 pip install requests 安裝。

>>> import requests
Enter fullscreen mode Exit fullscreen mode

接著就可以使用 API 了:

>>> 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
...     }
... )
Enter fullscreen mode Exit fullscreen mode

我們在表頭中以 f 字串代入剛剛設定的 API 金鑰, 執行後就可以檢查 HTTP 狀態碼:

>>> response.status_code
200
Enter fullscreen mode Exit fullscreen mode

200 表示正常, 接著檢視一下傳回的內容:

>>> 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}}
Enter fullscreen mode Exit fullscreen mode

很明顯是 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
  }
}
Enter fullscreen mode Exit fullscreen mode

其中 choices 是回覆的文句, usage 是提示與回覆的個別 token 數, 這也會用來計算你帳號的用量。由於我們沒有設定其他參數, 因此只會回覆單一文句, 利用以下方式即可取出:

>>> json = response.json()
>>> print(json['choices'][0]['text'])


你好
Enter fullscreen mode Exit fullscreen mode

如果參數有錯, 例如將 max_tokens 打成 max_token, 少了 s:

>>> 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
...      }
... )
Enter fullscreen mode Exit fullscreen mode

HTTP 狀態碼會傳回 400:

>>> response.status_code
400
Enter fullscreen mode Exit fullscreen mode

回覆的內容只有 error 元素, 說明錯誤的原因:

>>> print(response.text)
{
  "error": {
    "message": "Unrecognized request argument supplied: max_token",
    "type": "invalid_request_error",
    "param": null,
    "code": null
  }
}
Enter fullscreen mode Exit fullscreen mode

指定 token 數限制的 max_tokens 參數

choices 中每個元素的 finish_reason 代表結束的原因, "STOP" 代表正常終止, 如果是 "length" 表示是因為回應超過 max_tokens 指定的字數, 例如以下把 max_tokens 設為 30 ,然後問他一個比較複雜的問題:

>>> 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
...     }
... )
Enter fullscreen mode Exit fullscreen mode

執行結果如下, 你可以看到他其實話還沒說完, 但是因為超過限制, 所以結束了:

>>> json = response.json()
>>> print(json['choices'][0]['text'])


是的我看過灌籃高手它是一部
>>> print(json['choices'][0]['finish_reason'])
length
Enter fullscreen mode Exit fullscreen mode

指定回覆語句數的 n 參數

如果設定 n 參數, 就會回覆 n 個語句, 例如:

>>> 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你好很高兴认识你
Enter fullscreen mode Exit fullscreen mode

你可以看到因為 n 參數為 2, 所以回覆了 2 個語句。

指定結束語句的 stop 參數

你也可以設定 stop 參數, 指定遇到那些文字順序 (最多可指定 4 種) 時就停止, 避免 AI 回覆不適當的語句。例如:

>>> 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'])

Enter fullscreen mode Exit fullscreen mode

你可以看到上述回覆的結尾並不完整, 推測原本應該是要回覆『你好』, 但因為遇到『好』字就停止了。

你也可以注意到, 因為 text completion 實際上並不是聊天, 而是幫你補完文字, 所以當你只打一個『嗨』時, 它會先幫你補上逗號, 再接著後續的文字, 也就是說, 原本它是想補成『嗨,你好』。

用 suffix 參數插入文字

Text Completion 也可以在你提供的兩段文字間幫你補上 (insert) 文字, prompt 是第一段文字、而 suffix 參數則是第二段文字, 例如:

>>> 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
...      }
... )
Enter fullscreen mode Exit fullscreen mode

AI 傳回的結果如下:

>>> print(response.json()['choices'][0]['text'])
習人力資源管理, 因為我想提升我的職場技能, 以協助我更好的管理我的 團隊, 並更好的溝通與同事, 更重要的是要提升我的管理能力, 以便我可 以有更多的機會晉升或是更
>
Enter fullscreen mode Exit fullscreen mode

整合提供給 AI 的 prompt 和 suffix, 完整的內容就變成:

我想學習人力資源管理, 因為我想提升我的職場技能, 以協助我更好的管理我的 團隊, 並更好的溝通與同事, 更重要的是要提升我的管理能力, 以便我可 以有更多的機會晉升或是更換工作

使用 openai 模組

上述的過程因為是自己使用 requests.post, 有點瑣碎, OpenAI 有提供 openai 模組, 可以直接使用 pip 安裝。

請注意, 本文撰寫時 openai 版本為 0.26.0, 但是在 Windows 上若是 Python 3.10 的環境, 安裝會發生錯誤, 建議先降版本到 0.25.0 就可以正常安裝。

在 OpenAI API 網頁上都會提供 Playground 讓大家體驗功能, 並且可以開啟 View code 檢視對應的程式碼, 這個程式碼就以 openai 模組來達成。以下我們示範使用 openai 連接 Text Completion API, 首先就是要匯入模組, 並設定金鑰:

>>> import openai
>>> openai.api_key = f'{api_key}'
Enter fullscreen mode Exit fullscreen mode

其中的 {api_key} 請代換成你自己的金鑰。接著就可以利用現成的方法進行 HTTP post, 省去我們自己設定表頭以及 JSON 格式文檔的瑣碎細節:

>>> response = openai.Completion.create(
...     engine = 'text-davinci-003',
...     prompt = '嗨',
...     temperature = 0.7,
...     max_tokens = 300
... )
>>>
Enter fullscreen mode Exit fullscreen mode

這個方法的執行結果具有 Python 字典的特性, 可以直接以字典的方式存取內容:

>>> response['choices'][0]['text']
',大家好!\n\nHi, everyone!'
Enter fullscreen mode Exit fullscreen mode

計算 token 數

如果你想知道你的 prompt 到底會分成多少 token?OpenAI 並沒有提供 API 可以使用, 不過可以改用 transformers 套件來達成, 請先使用 pip install transformers 安裝套件, 即可產生 GPT2TokenizerFast 幫你將文字分成 token:

>>> from transformers import GPT2TokenizerFast
>>> tokenizer = GPT2TokenizerFast.from_pretrained('gpt2')
Enter fullscreen mode Exit fullscreen mode

這裡使用預先訓練好的模型 gpt2, 以下可取得指定文句解析後的結果:

>>> t = tokenizer("hamburger")
>>> print(t)
{'input_ids': [2763, 6236, 1362], 'attention_mask': [1, 1, 1]}
>>>
Enter fullscreen mode Exit fullscreen mode

有了 token id 的清單, 就可以利用清單的長度取得 token 數量了。你也可以讓他直接傳回數量即可:

>>> t = tokenizer("hamburger", return_attention_mask=False, retu
... rn_length=True)
>>> t
{'input_ids': [2763, 6236, 1362], 'length': [3]}
Enter fullscreen mode Exit fullscreen mode

這裡同時指定不要傳回 attention mask, 減少資料量, 不過 token id 就無法省去。

讓對話過程更像是聊天

使用 Text Completion 時 OpenAI 端並不會像是 ChatGPT 那樣依照對談記錄回答, 所以每一次問答都像是獨立事件, 例如利用以下的範例連續對答:

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)

Enter fullscreen mode Exit fullscreen mode

你會發現連續的問答之間並沒有什麼關連性, 像是這樣:

# py chat.py
You: 海洋污染對哪個地區影響最大
Bot:

答:大西洋地區
You: 台灣呢?
Bot:

台灣是亞洲島嶼國家,位於中國東南部,是一個文化多元、經濟成長急速 的國家。台灣是世界上成長最快的經濟體之一,擁有先進的科技和自由的 市場,經濟及社會健康發展水平較高。台灣擁有東方文化與西方現代化的 完美結合,也是體育、教育及人文氣息濃厚的好地方。
You:
Enter fullscreen mode Exit fullscreen mode

如果希望讓連續的對答可以有連慣性, 可以把之前的對話內容一併送回, 例如:

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
Enter fullscreen mode Exit fullscreen mode

上述範例採取比較簡單的方式, 只將前一次對話的問與答納入, 對話內容就會比較有相關性了:

# py chat.py
You: 海洋污染對哪一個地區影響最大?
Bot:

海洋污染對各大洋的海洋生物影響最大,而歐洲、北美洋和南美洋的生物受到的污染影響最大。特別是1990年以來,東北太平洋和加勒比海洋中主 要由非法倒塑料所導致的污染增加,以及地中海、大西洋和北冰洋中漁業殘留物對海洋生物的植物和動物,特別是魚類造成很大的污染。
You: 那台灣呢?
Bot:

台灣位於西太平洋及華南海域,受到來自亞洲和大洋洲旁邊區域的污染影 響較大,例如:河口污染、油汙、藥物殘留物等。此外,台灣也受到漁業殘留物的影響,許多漁業活動對海洋生態系統造成嚴重的污染衝擊。另外,台灣的沿海廢棄物排放也造成了很大的污染和對海洋生態系統的影響。
You:
Enter fullscreen mode Exit fullscreen mode

又或是像這一串對答:

You: 日本的空污嚴重嗎
Bot:  ?

空氣汙染在日本是一個嚴重的問題。根據世界衛生組織(WHO)2018年出版的污染圖表,日本在空氣污染指數上是世界上數一數二的地方,總指數高 出全球平均。雖然日本的政府在抗擊空氣汙染方面做出了努力,但仍有極 大的改進空間。
You: 那台灣呢?
Bot:

台灣的空氣污染情況也是嚴重的。根據世界衛生組織的數據顯示,台灣的 空氣污染指數高出全球平均水平,在空氣污染指數上也是高出全球平均水 平的地方之一。雖然台灣政府也做出了努力,但在抗擊空氣汙染方面仍存 在很大的改善空間。
You:
Enter fullscreen mode Exit fullscreen mode

在有關台灣的回覆中, 它會加上『也』字, 明確展現出他知道這是接續剛剛日本的問題。

如果希望提升效果, 可以將過去的對答歷史都納入, 而不僅僅像是上述範例只納入前一次的對答。如果擔心對答歷史過長, 超過 token 限制, 也可以使用先前介紹過計算 token 數量的方法, 在數量過多時先請 OpenAI 幫你把對答歷史摘要, 用摘要當成對答歷史, 就可以省下不少資料量。

Top comments (2)

Collapse
 
frankshih profile image
frankShih

請問一下,您在此 DEMO 中所使用的,是近期最熱門、幾乎萬能的那個版本嗎? 那個似乎沒有開放 API,要自己另外包

有 API 文件的我記得都是版本比較舊的 chatGPT,我沒理解錯吧?

Collapse
 
codemee profile image
codemee

內文中有寫 ChatGPT 自己目前沒有開放 API, 本文使用的是 ChatGPT 開發商 OpenAI 的 API, 並如標題所示, 以此 API 模擬 ChatGPT。