來 Azure 學習 OpenAI 三 - 用 Python 調用 Azure OpenAi API
Published: 2023-04-16 22:30:00

大家好，我是微軟學生大使 Jambo。在我們申請好 Azure 和 Azure OpenAI 之後，我們就可以開始使用 OpenAI 模型了。如果你還沒有申請 Azure 和 Azure OpenAI，可以參考 [註冊 Azure 和申請 OpenAI](https://dev.to/jambochen/xue-sheng-dang-kuai-lai-azure-qi-xue-xi-openai--1pe6)。
  
本文將會以 Azure 提供的 Openai 端口為例，並使用 OpenAI 提供的 [Python SDK](https://github.com/openai/openai-python ) 進行模型的調用。
  
## 創建工作區
  
進入 Azure 首頁，在搜索欄中輸入 OpenAI，點擊進入 OpenAI 頁面。
  
![1](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/1.png )
  
點擊創建。
  
![2](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/2.png )
  
選擇訂閱，資源組，工作區名稱，工作區地區，點擊創建。這裡我們地區選擇 “美東” ，因為目前只有這個地區支持 chatgpt 對話模型。如果你不需要對話模型，可以選擇其他模型種類更多的 “西歐”。
  
![3](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/3.png )
  
選擇下一步，確認無誤後點擊創建。
  
![4](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/4.png )
  
等他創建完成後，點擊 “探索” 進入工作區。
  
![5](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/5.png )
  
## 模型介紹
  
在使用模型之前，我們先來了解一下 Azure 提供了哪些 OpenAI 模型。 Azure 提供的模型從功能上可以分為三大類：補全（completion）、對話（chat）、嵌入（embeddings）。
  
補全模型可以根據輸入的文本，補全剩餘的文本。這類模型顧名思義，就是根據前文續寫後續的部分。他可以用來續寫文章，補全程序代碼。不僅如此，你其實也可以通過固定的文字格式來實現對話的效果。
  
對話模型相信有使用過 ChatGPT 的同學應該很熟悉。對話模型可以根據輸入的文本，生成對話的回复。這類模型可以用來實現聊天機器人，也可以用來實現對話式的問答系統。在調用方面，對話模型與補全模型最主要的區別是：你需要一個列表來存儲對話的歷史記錄。
  
沒接觸過過 NLP（自然語言處理） 的同學可能會對 “嵌入” 這個詞感到疑惑。 “嵌入” 實際上就是將文本轉換為向量的操作，而這個向量可以用來表示文本的語義信息，這樣就可以方便地比較語義的相似度。而嵌入模型就是用來實現這個操作的。
  
大部分模型擁有多個能力等級，能力越強能處理的文字也就越複雜，但相對的處理速度和使用成本也就越高。通常有 4 個等級：Davinci > Curie > Babbage > Ada ，其中 Davinci 最強而 Ada 是最快的（有興趣的同學可以查一下這 4 位名人）。在使用模型時，你可以根據自己的需求選擇合適的等級。
  
具體的模型介紹可以參考 [Azure OpenAI 服務模型](https://learn.microsoft.com/azure/cognitive-services/openai/concepts/models )。
  
## 部署模型
  
在了解了模型的功能和等級之後，我們就可以開始使用模型了。在使用模型之前，我們需要先部署模型。在 Azure OpenAI 工作區中，進入 “部署” 頁面。
  
![6](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/6.png )
  
選擇模型，點擊創建。這裡我部署了一個補全模型和對話模型。
  
![7](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/7.png )
  
部署後你就可以用 API 調用模型了，當然你也可以現在 Playground 中測試一下。
  
![8](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/8.png )

## API 參數

在 Playground 中測試模型時，我們可以看到 API 的參數。這裡我們來介紹一下這些參數。具體的參數細節可以參考 [API Reference](https://platform.openai.com/docs/api-reference/completions/create)。

- `model` 指定使用的模型。
- `prompt` 是輸入給模型的文本。
- `temperature` 控制了生成文本的隨機程度，值越大，生成的文本越隨機，值越小，生成的文本越穩定。這個值的範圍在 0.0 到 2.0 之間（雖然在 Playground 中最高只能設為 1）。
- `top_p` 與 `temperature` 類似，也是控制生成文本的隨機程度。但這個參數簡單的說是控制候選詞的範圍，值越大，候選詞的範圍越大，值越小，候選詞的範圍越小。這個值的範圍在 0.0 到 1.0 之間。通常來說，這兩個參數只需要設置一個就可以了。
- `max_tokens` 是模型生成的文本的最大長度，這其中的 “token” 不是指字符長度，你可以把他理解為模型眼中的 “詞”。 Token 與我們所使用的詞不一定是一一對應的。
- `stop` 是生成文本的停止條件，當生成的文本中包含這個字符串時，生成過程就會停止,最終生成的文本中將不包含這個字符串。這個參數可以是一個 string，也可以是一個長度至多為 4 的 string 列表。
- `presence_penalty` 控制生成文本的多樣性。他會懲罰那些在生成文本中已經出現過的 token，以減小未來生成這些 token 的概率。這個值的範圍在 -2.0 到 2.0 之間。如果設為負值，那麼懲罰就會變為獎勵，這樣就會增加生成這些 token 的概率。
- `frequency_penalty` 與 `presence_penalty` 類似，也是控制生成文本的多樣性。但不同的是，`presence_penalty` 是一次性懲罰，而 `frequency_penalty` 累計懲罰。如果一個詞在生成文本中出現了多次，那麼這個詞在未來生成的概率就會越來越小。這個值的範圍同樣在 -2.0 到 2.0 之間。

## 計算 Token

GPT 模型使用 token 來表示文本，而不是使用字符。模型能處理的文本長度是有限的，而這個長度指的是 token 的數量，而不是字符的數量，並且 OpenAI 使用模型的計費方式也是按照生成 token 的數量計算。因此為了能夠更好地使用模型，我們需要知道生成的文本究竟有多少 token。

OpenAI 提供了一個 Python 庫 [tiktoken](https://github.com/openai/tiktoken) 來計算 token。

```bash
pip install tiktoken
```

導入 tiktoken 庫。

```python
import tiktoken
```

不同模型使用不同的編碼來將文本轉換為 token。

| Encoding name           | OpenAI models                                       |
|-|-|
| `cl100k_base`           | `gpt-4`, `gpt-3.5-turbo`, `text-embedding-ada-002`  |
| `p50k_base`             | Codex models, `text-davinci-002`, `text-davinci-003`|
| `r50k_base` (or `gpt2`) | GPT-3 models like `davinci`  |

我們可以使用 `tiktoken.get_encoding()` 來獲取編碼對象。也可以使用 `tiktoken.encoding_for_model()` 通過模型名自動獲取編碼對象。

```python
encoding = tiktoken.get_encoding("cl100k_base")
encoding = tiktoken.encoding_for_model("gpt-3.5-turbo")
```

然後用 `.encode()` 方法將文本 token 化。返回的 token 列表的長度，就是這段文本的 token 數量。

```python
encoding.encode("tiktoken is great!")
```

```txt
[83, 1609, 5963, 374, 2294, 0]
```

我們還可以使用 `.decode()` 將 token 列表轉換為文本。

```python
encoding.decode([83, 1609, 5963, 374, 2294, 0])
```

```txt
'tiktoken is great!'
```

## 使用 Python SDK
  
我們首先需要到 Azure 的 “密鑰” 頁面獲取密鑰和終結點，兩個密鑰只要其中一個即可。
  
![9](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/9.png )
  
然後安裝 openai 庫。注意，Python 版本需要大於等於 3.7。我們這裡使用官方提供的 [Python SDK](https://github.com/openai/openai-python )，其他語言的 SDK 可以在 [OpenAI Libraries](https://platform.openai.com/docs/libraries ) 找到。
另外，因為這個庫沒有專門的文檔參考，所以我們需要查看[庫的源碼](https://github.com/openai/openai-python/tree/main/openai )和 [API 參考](https://platform.openai.com/docs/api-reference )。```bash
pip3 install openai
```
  
更具先前獲取的密鑰和終結點初始化 SDK：
  
```python
import openai

openai.api_key = "REPLACE_WITH_YOUR_API_KEY_HERE"    # Azure 的密鑰
openai.api_base = "REPLACE_WITH_YOUR_ENDPOINT_HERE"  # Azure 的終結點
openai.api_type = "azure" 
openai.api_version = "2023-03-15-preview" # API 版本，未來可能會變
model = ""  # 模型的部署名
```
  
### 調用補全模型
  
補全模型使用的是 `openai.Completion.create` 方法，使用的參數在上面已經介紹過了，但因為我使用的是 Azure 的 API，所以指定模型的參數名是 `engine`。下面是一個簡單的例子：
  
```python
prompt = "1, 2, 3, 4, "
response = openai.Completion.create(
    engine=model, prompt=prompt, max_tokens=50, temperature=0.0
)
print(response)
```

它打印出的內容就是 API 返回的 json 結果。其中 `text` 就是模型生成的文本，可以看到它將數列續寫下去。但他只停在 21，是因為我設置了 `max_tokens=50`，可以看到 `usage` 中的 `completion_tokens` 是 50，也就是說模型生成了 50 個 token，達到了最大值，而 `finish_reason` 是 `length`，表示是因為達到了最大長度而停止的。如果是因為寫完了而停止，那麼 `finish_reason` 的值會是 `stop`。
  
```json
{
  "choices": [
    {
      "finish_reason": "length",
      "index": 0,
      "logprobs": null,
      "text": "5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,"
    }
  ],
  "created": 1680628906,
  "id": "cmpl-71edm7DMgHrynaiXONWi4ufSrXiL0",
  "model": "gpt-35-turbo",
  "object": "text_completion",
  "usage": {
    "completion_tokens": 50,
    "prompt_tokens": 12,
    "total_tokens": 62
  }
}
```

此外你或許有註意到，我使用的模型是 “gpt-35-turbo”。他雖然是對話模型，但他實際上也可以通過補全 API 使用。但同樣是對話模型的 “gpt-4” 就只能通過對話 API 使用。

接下來我們在其中加個停止條件，讓他數到 11 就停止：
  
```python
prompt = "1, 2, 3, 4, "
response = openai.Completion.create(
    engine=model, prompt=prompt, temperature=0.0, stop=["11"]
)
print(response["choices"][0])
```
  
可以看到他確實是停在 11 之前，並且 `finish_reason` 的值是 `stop`。
  
```json
{
  "finish_reason": "stop",
  "index": 0,
  "logprobs": null,
  "text": "5, 6, 7, 8, 9, 10, "
}
```
  
但如果我們將 `temperature` 的值調高，它可能會生成意料之外的文本：
  
```python
prompt = "1, 2, 3, 4, "
response = openai.Completion.create(
    engine=model, prompt=prompt, temperature=0.8, stop=["11"]
)
print(response["choices"][0])
```
  
```json
{
  "finish_reason": "length",
  "index": 0,
  "logprobs": null,
  "text": "5, 6, 7, 8, 9, 10])) # 55\nprint(sum_list([0, 0, 0, 0, 0])) # 0\nprint(sum_list([1, -"
}
```
  
### 調用對話模型
  
對話模型使用的是 `openai.ChatCompletion.create` 方法，它的參數和補全模型的參數類似，但有一些不同：對話模型輸入的是一個對話歷史，而不是單個的文本。並且其參數名是 `messages`，而不是 `prompt`。
  
對話歷史是一個列表，列表中的每個元素都是一個對話，每個對話又是一個字典，字典中有兩個鍵：`role` 和 `content`。 `role` 是對話的角色，而 `content` 是對話的內容。下面是個簡單的例子：
  
```python
messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Knock knock."},
    {"role": "assistant", "content": "Who's there?"},
    {"role": "user", "content": "Orange."},
]
```
  
可以看到 `role` 的值有 `system`、`user` 和 `assistant`。 `user` 就是指用戶，`assistant` 代表的就是和你對話的 AI 模型，而 `system` 可以理解為 `assistant` 的設置、人設等等。當然 `system` 也可以省略。
  
接下來我們用此對話歷史來調用對話模型：
  
```python
response = openai.ChatCompletion.create(
    engine=model, messages=messages
)
response
```
  
```json
{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "content": "Yes, many other Azure Cognitive Services do support the use of customer managed keys for encrypting and protecting data. In fact, most Azure services that handle sensitive data can be configured to use customer managed keys for enhanced security and compliance. Some examples of Azure Cognitive Services that support customer managed keys include Azure Cognitive Services Speech Services, Azure Cognitive Services Text Analytics, and Azure Cognitive Services Translator Text.",
        "role": "assistant"
      }
    }
  ],
  "created": 1680633366,
  "id": "chatcmpl-71fnivs89GKEMKpzwuCCxbH0MNP98",
  "model": "gpt-35-turbo",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 79,
    "prompt_tokens": 58,
    "total_tokens": 137
  }
}
```
  
我們可以維護一個對話歷史，然後每次調用對話模型時，將新的對話加入到對話歷史中，以此循環來實現一個對話系統：
  
```python
conversation = [{"role": "system", "content": "You are a helpful assistant."}]
  
while True:
    user_input = input()
    conversation.append({"role": "user", "content": user_input})
  
    response = openai.ChatCompletion.create(
        engine=model,
        messages=conversation,
    )
  
    conversation.append(
        {"role": "assistant", "content": response["choices"][0]["message"]["content"]}
    )
    print("\n" + response["choices"][0]["message"]["content"] + "\n")
```
  
### 流式調用
  
所謂 “流式” ，就是服務器生成了什麼內容就立即返回給客戶端，而不是等全部都完成後再統一返回。最典型的例子就是各種視頻網站，你在播放視頻時，視頻的內容是隨著時間的推移而不斷加載的，而不是等到視頻加載完畢後再一次性播放。這樣做的好處是可以讓用戶更快的看到內容。 ChatGPT 那樣像一個字一個字寫出來的效果就是通過流式形成的。
  
但他的缺點是，服務器將不會幫你統計生成的 token，你需要自己統計。
  
以下是一個一般調用的例子：
  
```python
response = openai.ChatCompletion.create(
    engine=model,
    messages=[
        {'role': 'user', 'content': "Count to 10. E.g. 1, 2, 3, 4, ..."},
    ],
    temperature=0,
)
  
print(response)
```
  
```json
{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "content": "\n\n1, 2, 3, 4, 5, 6, 7, 8, 9, 10.",
        "role": "assistant"
      }}
  ],
  "created": 1680709601,
  "id": "chatcmpl-71zdJtUaOIhfnVsAKLyD88RtGzgQb",
  "model": "gpt-35-turbo",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 31,
    "prompt_tokens": 28,
    "total_tokens": 59
  }
}
```
  
但我們只要將 `stream` 參數設置為 `True` ，就可以使用流式調用了：
  
```python
response = openai.ChatCompletion.create(
    engine=model,
    messages=[
        {'role': 'user', 'content': "Count to 10. E.g. 1, 2, 3, 4, ..."},
    ],
    temperature=0,
    stream=True,
)
print(next(response))
for chunk in response:
    print(chunk['choices'])
```
  
```json
{
  "choices": [
    {
      "delta": {
        "role": "assistant"
      },
      "finish_reason": null,
      "index": 0
    }
  ],
  "created": 1680710012,
  "id": "chatcmpl-71zjwZk3EB5fLgVup9S1BPZo73JUk",
  "model": "gpt-35-turbo",
  "object": "chat.completion.chunk",
  "usage": null
}
[<OpenAIObject at 0x1e2ba8fdc10> JSON: {
  "delta": {
    "content": "\n\n"
  },
  "finish_reason": null,
  "index": 0
}]
[<OpenAIObject at 0x1e2ba8fe870> JSON: {
  "delta": {
...
  "delta": {},
  "finish_reason": "stop",
  "index": 0
}]
```
  
因為長度原因，除了第一個塊，其他都只打印了 `choices` 的內容，但其餘的部分都是一樣的，甚至是 `id`。
可以看到，第一個塊的 `delta` 裡面只有 `role` ，而後面的塊裡面的 `delta` 裡面有 `content` ，這就是服務器生成的內容。我們只要把這些內容拼接起來就可以了：
  
```python
response = openai.ChatCompletion.create(
    engine=model,
    messages=[
        {"role": "user", "content": "Count to 10. E.g. 1, 2, 3, 4, ..."},
    ],
    temperature=0,
    stream=True,
)
  
message = next(response)["choices"][0]["delta"]
content = "".join(
    [chunk["choices"][0]["delta"].get("content", "") for chunk in response]
)
message["content"] = content
print(message)
```
  
```python
{
  "content": "\n\n1, 2, 3, 4, 5, 6, 7, 8, 9, 10.",
  "role": "assistant"
}
```
  
### 異步調用
  
在我們寫諸如網站、爬蟲之類的程序時，經常會遇到類似調用數據庫、API 的情況，這些操作都是需要漫長的等待，在等待的過程中，程序會處於阻塞狀態，無法繼續執行後面的代碼。可這些操作的瓶頸通常不在於程序本身，而是在於網絡或者硬盤的速度，這時候我們就可以使用異步來解決這個問題。
  
異步可以讓我們等待結果的同時，繼續執行其他的代碼，這樣就不會阻塞程序的執行了。我們在這裡就不多介紹 Python 的異步和其語法，這超出了本文的範圍，有興趣的同學可以自行學習。本文將著重於 SDK 的異步調用。
  
OpenAI SDK 的異步調用和同步調用的區別在於，異步調用需要使用 `acreate` 方法，而不是 `create` 方法。這裡的 `a` 表示異步。 `acreate` 方法的返回值是一個 `coroutine` 對象，而不是一個 `OpenAIObject` 對象，所以我們不能直接使用 `print` 來打印它，而是需要使用 `await` 來等待它的結果。
  
```python
async def async_completion():
    response = await openai.ChatCompletion.acreate(
        engine=model,
        messages=[
            {"role": "user", "content": "Count to 10. E.g. 1, 2, 3, 4, ..."},
        ],
        temperature=0,
    )
    return response["choices"][0]["message"]
print(await async_completion())
```
  
```python
{
  "content": "\n\n1, 2, 3, 4, 5, 6, 7, 8, 9, 10.",
  "role": "assistant"
}
```
  
OpenAI SDK 的異步請求默認是使用 `aiohttp` 第三方異步請求庫的 session。 SDK 在每次請求時都會新建一個 session，這會造成一部分的開銷，因此如果你的程序需要頻繁的異步請求，那麼你可以自己傳入一個 `aiohttp.ClientSession` 對象，這樣就可以復用 session 了。另外要注意的是，如果你使用了自己的 session，那麼你需要在程序結束時手動關閉它，以免造成其他問題：
  
```python
from aiohttp import ClientSession
  
openai.aiosession.set(ClientSession())
res = await asyncio.gather(*[async_completion() for _ in range(10)])
await openai.aiosession.get().close()
```
  
## 提示技巧
  
OpenAI 的模型基於你輸入的文本，也就是提示（Prompt）來生成後續的文字，它可以做很多事情，正因如此，你需要明確的表達你想要什麼。在一些時候，僅僅是描述是不夠的，你還需要給模型展示出你想要的結果，比如列舉幾個例子。下面三點是創建提示的基本準則：
  
- **展示和講述。 ** 通過指示、示例或兩者結合，清楚地表明你的需求。
- **提供優質數據。 ** 如果您試圖構建分類器或讓模型遵循某種模式，請確保有足夠的示例。 。
- **檢查設置。 ** `Temperature` 和 `top_p` 控制模型在生成響應時的確定性程度。如果你想要模型輸出確定性的答案，將值設低一些；如果你想要模型輸出更多的選擇，將值設高一些。通常這兩個值同時只需要調整一個就可以了。在生成文字時，你可能需要經常調整這些設置，以確保輸出的正確性。
  
另外，雖然以下例子都是使用完成模型，但這些提示原則同樣適用於聊天模型。
  
### 生成
  
產生文本和想法幾乎是 OpenAI 的最基本功能，它可以用來生成代碼、文章、故事、歌詞、對話等等。在這裡，我們將使用 `Completion` 類來完成這個任務。下面是個很簡單的例子：

![10](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/10.png)

這雖然只是個非常簡單的例子，但其中還是有些值得注意的細節：
  
1. 我們首先描述了我們的意圖，即生成一段關於 Python 優勢的列表。
2. 我們提供了一個示例，這樣模型就知道我們想要的是一個列表。並且只是說明優勢，而不用具體闡述。
3. 我們留了一個 “2. ”，以此引導模型繼續生成。

### 對話

生成模型在經過引導後也可以用於對話。當然，只是簡單的提示可能不足以讓模型理解你的意思，你可以給他提供一些示例對話，這樣他就可以跟著對話續寫下去：

![12](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/12.png)

為了避免讓他生成過頭，把我們的話也一起生成了，我們可以使用 `stop` 參數來進行限制。比如在這裡，我把 `stop` 參數設置的是 “博士：”，這樣當他生成到這個字符前，就會停止。
  
### 分類
  
下面的例子中，我在《巫師3》的評論區中選了 5 條，並讓他分類為好評或差評：

![11](https://raw.githubusercontent.com/JamboChen/Jambo-blog/master/img/azure-openai/3.openai-guide/11.png)