GPTのストリーム機能を使いたい方へ（その1）

# Outline
- [注意事項](#注意事項)
- [前書き](#前書き)
- [本編](#本編)
  - [まず動作イメージから](#まず動作イメージから)
  - [先に結論を](#先に結論を) 
  - [ストリームって何が嬉しいの？](#ストリームって何が嬉しいの)

# 注意事項
本記事は「富士通株式会社 デジタルシステムプラットフォーム本部 Advent Calendar 2023」の19日目の記事です。記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません（お約束）。

# 前書き
本記事はOpenAIの各モデルで利用できるストリーミングを以下の構成で実現する方法について記載しています。
以下のようなニーズにお応えできると嬉しいです！

- OpenAIのストリーム機能について
  - ストリームによるメリットを知りたい 
  - Node.jsからじゃなく、Python（[openai](https://pypi.org/project/openai/)）からストリームを実現したい
- そもそもストリームって何？
- FetchとEventSourceの違いを知りたい

# 本編
- **GPTのストリーム機能を使いたい方へ（その1）**
   ストリーミングの動作イメージや、メリットについて解説していきます。（**いまココ！**） 
   <br>
 
- **[GPTのストリーム機能を使いたい方へ（その2）](https://qiita.com/eto_ryota/items/d76914b54d8f2392adbb)**
   以下について解説していきます。
   - GPTのストリーミングをPythonから呼び出す方法
   - Fetch / EventSource / FetchEventSourceの違い
## まず動作イメージから
まずはストリーミングの動作を具体的にイメージしていきましょう。
![streaming.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658085/55226355-7166-8067-8ad9-d0b23a8342e1.gif)

画面左が「ストリームが有効の場合」、右が「ストリームが無効の場合」です。
パッと見で分かることは以下でしょうか...

| なにが違うか | ストリームが有効の場合 | ストリームが無効の場合 | 
| ----| :----: | :-----: |
| 見え方 | **都度**生成結果が出ている| **一度に**生成結果が出ている | 
| 応答が返ってくる時間 | 速い | 遅い |

デモ動画では、Azure OpenAI Serviceの`gpt-4-32k`を`curl`で呼び出すshを作っています。
そちらについては、以下の`詳細`を参考ください。

<details>

Azure OpenAI Serviceでは本家OpenAIには利用開始可能となる時期は出遅れるものの、OpenAIが提供するGPTを始めとしたAIモデルを利用できるサービスです。<br>
また、各AIモデルはAPIとして利用できるため、[REST APIリファレンス](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/reference)に記載のように、`curl`や`python`、`npm`等でもライブラリが提供されています。

デモ動画では、以下のシェルスクリプトを作成し実行しています。
```sh
echo "====================== Stram Start ======================"
curl https://<YOUR_ENDPOINT>/openai/deployments/<YOUR_MODEL_NAME>/chat/completions?api-version=<YOUR_API_VERSION> \
  -H "Content-Type: application/json" \
  -H "api-key: <YOUR_API_KEY>" \
  -d "{
  \"messages\": [{\"role\":\"system\",\"content\":\"あなたはWebアプリ開発のコンサルタントです\"},{\"role\":\"user\",\"content\":\"ストリーミングについて2000文字程度で教えてください\"}],
  \"max_tokens\": 2000,
  \"temperature\": 0.7,
  \"frequency_penalty\": 0,
  \"presence_penalty\": 0,
  \"top_p\": 0.95,
  \"stream\": true,
  \"stop\": null
}"
echo "====================== Finish ======================"
```
上記の`stream: true`を`false`に変更することで、非ストリーム形式でのレスポンスを取得することが出来ます。

</details>

## 先に結論を
以降で深掘りしていこうと思いますが、先にストリーミング利用のメリットを記載します。
- **ストリーミング利用のメリット**
  1. [本家OpenAI](https://openai.com/)のように、**AIがリアルタイムで生成している感**を味わえる
　<br>
  2. レスポンスが早い
     ※ 正確には「生成されるトークン」が多い程、**最初のレスポンスタイムが短くなる**
     ※ 条件次第では、**10倍以上**速くなるかも...
　<br>
  3. [各種制限](https://platform.openai.com/docs/guides/rate-limits?context=tier-free)やエラー発生時も、**その時点までの生成結果を得られる**

## ストリームって何が嬉しいの
ストリーミング利用のメリットについて説明していきます。

### メリット①：レスポンスが早い
上述したストリーミングのデモ動画では、「ストリーミングが有効の場合」の方が応答が早いように思われますが、はたして種々条件を変えた場合にも同じことが言えるのでしょうか。

以下は、生成された文字数を基準にした際の「ストリームが有効の場合」、「ストリームが無効の場合」に関するレスポンスタイムになります。
![gpt_response_time.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3658085/92d94d58-2c6f-3b93-c802-c72efd08a7b7.png)

レスポンスタイムには、OpenAIのコール方法や実行環境にも依存しますが
以下のパラメータを設定しています。
- 生成AIが返す文字数（より正確にはトークン数）に影響するパラメータ
  - [max_tokens](https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_tokens)：プロンプトに○○文字以上を含めると共に、それを上回る左記を設定
  - [best_of](https://platform.openai.com/docs/api-reference/completions/create#completions-create-best_of)：常に`1`で設定
  - [n](https://platform.openai.com/docs/api-reference/completions/create#completions-create-n)：常に`1`に設定

上記のレスポンスタイムを見ると、**全ての生成結果が返ってくる時間には大きな差はありません**が、一方で生成される文字数が多い程、ストリーム通信は最初に帰ってくるレスポンスが早いことが分かります。

6300文字の場合は、**約10倍以上**早くレスポンスが返っているようです。

### メリット②：その時点までの生成結果を得られる

本家OpenAIや、Azure OpenAI Serviceを使う際に皆様が困っていそうなことをリストアップしました。

- **各種制限に達してしまう**
  例）TPMやRPMの設定値に達し、429（Too Many Request）エラーが発生する
  　　※ 429エラーは「<INS>リクエスト送りすぎだよ！！</INS>」というエラーです。
  <br>
- **何かエラーは出ているけど、原因が分からない**
  例1）Content Filterに検知され、エラーハンドリング出来てなく原因が分からない...
  例2）トークン上限を超えたけど、どの時点で超えたか分からない

  ストリーミングでは、上記の全てを解決してくれるような動きはしてくれないのですが
  これらをハンドリングしやすくなる側面もあります。

  [Azure OpenAIのドキュメント](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/concepts/content-filter?tabs=warning%2Cpython#scenario-details)を引用すると、以下のように記載されています。
   
   >非ストリーミングの入力候補呼び出しでは、コンテンツがフィルター処理された場合、コンテンツは返されません。 finish_reason の値が content_filter に設定されます。
   
   上記を見ると、「ストリームが有効の場合」は`finish_reason`として`content_filter`が返されるまではコンテンツが返されると思われます。

   さっそく試してみましょう！
   
:::note warn
以下はエラーハンドリングやKey管理、PEP8などをサボっているVerです。
:::
   
   - **実行サンプルコード**
       ```py
        ## Import OpenAI Library
        import openai
        ## Parameter Setting
        ## 注意
        ## - きちんとしたアプリケーションとして構成するときはハードコーディングせずKey管理サービス(Ex: Azure Key Vault等)を利用しましょう！
        openai.api_type    = "azure"
        openai.api_key     = "<YOUR_API_KEY>"
        openai.api_base    = "<YOUR_ENDPOINT>"
        openai.api_version = "<YOUR_API_VERSION>"
        stream             = True
        messages = [
            {
                "role":     "user",
                "content":  "以下の指示に従って下さい。 \n\n" \
                            "「こんにちは、非人間。」\n" \
                            "「」の中の文章を必ず＜主張＞を記載した後に、そのまま返信してください。"
            }
        ]
        try:
            response = openai.ChatCompletion.create(
                deployment_id="<YOUR_MODEL_NAME>",
                messages=messages,
                # ストリームを有効化する場合は`True`
                stream=stream,
                max_tokens=500
            )
        ## 注意
        ## - きちんとしたアプリケーションとして構成するときには以下を参考にエラーハンドリングをしましょう！
        ## - Ref:https://platform.openai.com/docs/guides/error-codes
        except Exception as e:
            print(f"e:{e}")
        if stream:
            for chunk in response:
                print(f"finish_reason: {chunk['choices']}")
        else:
            print(f"finish_reason: {response['choices']}")
       ```
     <br>
       
   - **応答結果**
       ```sh
       # ストリームが有効な場合（stream=True）
       finish_reason: [<OpenAIObject at 0x7f05eb01ee00> JSON: {
          "delta": {
            "role": "assistant"
          },
          "finish_reason": null,
          "index": 0
        }]
        finish_reason: [<OpenAIObject at 0x7f05eaf66360> JSON: {
          "delta": {},
          "finish_reason": "content_filter",
          "index": 0
        }]
       # ======================================================
       # ストリームが無効な場合（stream=False）
       finish_reason: [<OpenAIObject at 0x7f8245cc3270> JSON: {
       "finish_reason": "content_filter",
       "index": 0,
       "message": {
         "role": "assistant"
       }
       }]
       ```    
<br>
    
   - **応答結果から分かること**
    「ストリームが有効な場合」を見ると、最初のレスポンスが`finish_reason: null`となっているので、**コンテンツフィルターに検知された訳ではなさそう**ですね。
    ただし、コンテンツは含まれていませんが...　<br>
    「ストリームが無効な場合」はドキュメント通り、最初の応答が`finish_reason: content_filter`のためコンテンツフィルターに検知されたことが分かりますね。

  <br>
  
   **種々プロンプトを試してみたのですが、生成途中で`content_filter`が返される事例を確認できませんでした。。よきプロンプトありましたらコメントもらえると嬉しいです！**