OpenAIのストリームコンプリーションをDatabricksで試してみる

このような機能があるとは。

https://github.com/openai/openai-cookbook/blob/main/examples/How_to_stream_completions.ipynb

リンク先の説明の翻訳です。

# コンプリーションをストリームする方法

デフォルトでは、OpenAIからのコンプリーションをリクエストする際には、単一のレスポンスに対して返信する前に全体のコンプリーションが生成されます。

長いコンプリーションを生成する場合、レスポンスの待ち時間が数秒になることがあります。

すぐにレス尾ポンスを取得するには、コンプリーションが生成される際にコンプリーションを`ストリーム`することができます。これによって、完全なコンプリーションが完了する前にコンプリーションの初めから表示や処理を行うことができるようになります。

コンプリーションをストリームするには、チャットコンプリーションエンドポイントやコンプリーションエンドポイントを呼び出す際に`stream=True`を設定します。これによって、[データのみのサーバーが送信するイベント(data\-only server\-sent events)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format)としてレスポンスをストリーミングするオブジェクトを返却します。`message`フィールドではなく`delta`からチャンクを抽出します。

# 欠点

プロダクションアプリケーションで`stream=True`を設定すると、部分的なコンプリーションの評価がより困難となることで、[許可された用法](https://beta.openai.com/docs/usage-guidelines)に示されているようなコンプリーションのコンテンツのモデレーションが難しくなることに注意してください。

レスポンスのストリーミングにおけるもう一つの些細な欠点は、どれだけのトークンが消費されるのかを示す`usage`フィールドがストリーミングレスポンスには含まれないということです。すべてのレスポンスを受信し、組み立てた後で`tiktoken`を用いてご自身で計算することができます。

# サンプルコード

```py
import openai  # OpenAI API呼び出し
import time  # API呼び出しの時間計測

openai.api_key = dbutils.secrets.get("demo-token-takaaki.yayoi", "openai_api_key")
```

通常のコンプリーション。

```py
# OpenAI ChatCompletionリクエストの例
# https://platform.openai.com/docs/guides/chat

# リクエスト送信前の時間を記録
start_time = time.time()

# 100までカウントするChatCompletionリクエストを送信
response = openai.ChatCompletion.create(
    model='gpt-3.5-turbo',
    messages=[
        {'role': 'user', 'content': 'Count to 100, with a comma between each number and no newlines. E.g., 1, 2, 3, ...'}
    ],
    temperature=0,
)

# レスポンスを受信するまでに要した時間を計算
response_time = time.time() - start_time

# 遅延した時間と受信したテキストを表示
print(f"Full response received {response_time:.2f} seconds after request")
print(f"Full response received:\n{response}")
```

5秒弱かかります。

```py
Full response received 4.74 seconds after request
Full response received:
{
  "id": "chatcmpl-7pO6eslSU1dRRlf7NNB0ZosUREFkZ",
  "object": "chat.completion",
  "created": 1692481748,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 36,
    "completion_tokens": 299,
    "total_tokens": 335
  }
}
```

コンテンツの抽出。

```py
reply = response['choices'][0]['message']
print(f"Extracted reply: \n{reply}")

reply_content = response['choices'][0]['message']['content']
print(f"Extracted content: \n{reply_content}")
```

```py
Extracted reply: 
{
  "role": "assistant",
  "content": "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100."
}
Extracted content: 
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100.
```

コンプリーションをストリーミングするように`stream=True`を設定します。

```py
# stream=Trueを設定したOpenAI ChatCompletionリクエストの例
# https://platform.openai.com/docs/guides/chat

# ChatCompletionリクエスト
response = openai.ChatCompletion.create(
    model='gpt-3.5-turbo',
    messages=[
        {'role': 'user', 'content': "What's 1+1? Answer in one word."}
    ],
    temperature=0,
    stream=True  # 今回は　stream=True　を設定します
)

for chunk in response:
    print(chunk)
```

一括ではなくストリームとしてレスポンスが返却され、チャンクとしてアクセスできるようになります。

```py
{
  "id": "chatcmpl-7pO7NodCNRfHmtSjWQdJp338P7A3s",
  "object": "chat.completion.chunk",
  "created": 1692481793,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "delta": {
        "role": "assistant",
        "content": ""
      },
      "finish_reason": null
    }
  ]
}
{
  "id": "chatcmpl-7pO7NodCNRfHmtSjWQdJp338P7A3s",
  "object": "chat.completion.chunk",
  "created": 1692481793,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "2"
      },
      "finish_reason": null
    }
  ]
}
{
  "id": "chatcmpl-7pO7NodCNRfHmtSjWQdJp338P7A3s",
  "object": "chat.completion.chunk",
  "created": 1692481793,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "delta": {},
      "finish_reason": "stop"
    }
  ]
}
```

100までのカウントのコンプリーションをストリーミングさせます。

```py
# stream=Trueを設定したOpenAI ChatCompletionリクエストの例
# https://platform.openai.com/docs/guides/chat

# リクエスト送信前の時間を記録
start_time = time.time()

# 100までカウントするChatCompletionリクエストを送信
response = openai.ChatCompletion.create(
    model='gpt-3.5-turbo',
    messages=[
        {'role': 'user', 'content': 'Count to 100, with a comma between each number and no newlines. E.g., 1, 2, 3, ...'}
    ],
    temperature=0,
    stream=True  # ここでも　stream=True　を設定します
)

# ストリームの塊(チャンク)を収集する変数の作成
collected_chunks = []
collected_messages = []
# イベントストリームに対して繰り返し
for chunk in response:
    chunk_time = time.time() - start_time  # チャンクの遅延時間を計算
    collected_chunks.append(chunk)  # イベントのレスポンスを保存
    chunk_message = chunk['choices'][0]['delta']  # メッセージの抽出
    collected_messages.append(chunk_message)  # メッセージの保存
    print(f"Message received {chunk_time:.2f} seconds after request: {chunk_message}")  # 遅延時間とテキストの表示

# 遅延時間と受信テキストの表示
print(f"Full response received {chunk_time:.2f} seconds after request")
full_reply_content = ''.join([m.get('content', '') for m in collected_messages])
print(f"Full conversation received: {full_reply_content}")
```

```py
Message received 0.34 seconds after request: {
  "role": "assistant",
  "content": ""
}
Message received 0.34 seconds after request: {
  "content": "1"
}
Message received 0.35 seconds after request: {
  "content": ","
}
Message received 0.37 seconds after request: {
  "content": " "
}
Message received 0.39 seconds after request: {
  "content": "2"
}
Message received 0.42 seconds after request: {
  "content": ","
}
Message received 0.43 seconds after request: {
  "content": " "
}
Message received 0.44 seconds after request: {
  "content": "3"
}
:
:
Message received 5.67 seconds after request: {}
Full response received 5.67 seconds after request
Full conversation received: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100.
```

全体的な処理時間は大きくは異なりませんが、コンプリーションの生成過程でレスポンスが帰ってくるので体感速度が改善するということですね。便利そう。

次はこれをどうやってフロントエンドと連携するかですね。

### Databricksクイックスタートガイド

[Databricksクイックスタートガイド](https://www.amazon.co.jp/dp/B09V1YXFVQ/)


### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)