このエントリは2023/09/12現在の情報に基づいています。将来の機能追加や変更に伴い、記載内容からの乖離が発生する可能性があります。
問い合わせ
いつもの人から以下のような問い合わせが届いた。
Azure API Management (以下、APIM) をAzure OpenAI Service (以下、AOAI) 組み合わせて、複数システムからの呼び出しをサポートできるようにしている(バックエンドではApplication Gatewayを使ったAOAIへの負荷分散をしている)。AOAIのStreamが有効になるようにしたリクエストでAPIMでホストしているAPIを呼び出したところ、HTTP 500が返ってきた。これを避けるにはどうしたらいいか?
AOAIのStreamモードとは、Server Sent Events (SSE) を使ってレスポンスを返すようにするもので、OpenAIにも同じ仕組みがある。
Streaming
https://platform.openai.com/docs/guides/production-best-practices/streaming
この問い合わせ主のポリシー構成は以下の通り。また、診断ログ設定で、Azure Monitorにリクエスト・レスポンスのログを出すようにしているとのこと。
<policies>
<inbound>
<base />
<authentication-managed-identity resource="https://cognitiveservices.azure.com"
output-token-variable-name="msi-access-token" ignore-error="false" />
<set-header name="Authorization" exists-action="override">
<value>@("Bearer " + (string)context.Variables["msi-access-token"])</value>
</set-header>
<set-backend-service base-url="{Endpoint for Application Gateway}" />
</inbound>
<backend>
<retry condition="@(context.Response.StatusCode >= 500)" count="3" interval="0" first-fast-retry="true">
<forward-request buffer-request-body="true" timeout="120" buffer-response="false"
fail-on-error-status-code="true" />
</retry>
</backend>
<outbound>
<base />
</outbound>
<on-error />
</policies>
原因
問い合わせ主はAzure Monitorでログを取ろうとしていたが、SSEを受け取る場合にはAPIMではバッファリングしてはいけない。これが邪魔をしていて500が発生していた。APIMに詳しい方はすぐにわかったかもしれない。
APIMからリクエストを投げてから、APIMとAOAI間でHTTP通信が確立され、AOAIからのレスポンスが垂れ流しになる。このとき、APIMのOutboundセクションでバッファリングするような仕組みを使うと500が発生する。具体的には、Azure Monitorにログ出力したり、set-body
ポリシーでポリシー式を使ったり、など。ドキュメントにも記載がある。
SSE のガイドライン / Guidelines for SSE
https://learn.microsoft.com/azure/api-management/how-to-server-sent-events#guidelines-for-sse
というわけで、診断ログの設定は解除してもらった。
おまけ
上記提案をした後、以下のような更問が届いた。
prompt tokenとcompletion tokenの個数はどこに現れるのか?
非Streamの場合、usageで確認できる。
{
"id": "chatcmpl-7xqbvCK1E147BjRaOfg1LDlr8bFYY",
"object": "chat.completion",
"created": 1694497943,
"model": "gpt-35-turbo-16k",
"prompt_annotations": [...],
"choices": [...],
"usage": {
"completion_tokens": 509,
"prompt_tokens": 19,
"total_tokens": 528
}
}
Streamの場合、レスポンスのusageは常にnullなのでどうしたものか、というわけ。
{
"id": "chatcmpl-7xr2XYKrXSel2qiK2opU43Qf02OND",
"object": "chat.completion.chunk",
"created": 1694499593,
"model": "gpt-35-turbo-16k",
"choices": [
{
"index": 0,
"finish_reason": null,
"delta": {
"content": "。"
},
"content_filter_results": {
"hate": {
"filtered": false,
"severity": "safe"
},
"self_harm": {
"filtered": false,
"severity": "safe"
},
"sexual": {
"filtered": false,
"severity": "safe"
},
"violence": {
"filtered": false,
"severity": "safe"
}
}
}
],
"usage": null
}
これは、レスポンス本体、ヘッダーのいずれにもトークン数は含まれないため、レスポンスをTokenizerとよばれるライブラリに通して算出するしかない。Tokenizerの有名どころは以下のあたり。
tiktoken (Python)
https://github.com/openai/tiktoken
JTokkit (Java)
https://github.com/knuddelsgmbh/jtokkit
Tokenizer (C#, TypeScript)
https://github.com/microsoft/Tokenizer
回避策
現時点では、APIMではStreamが有効なリクエストだと、バッファリングの問題でプロンプトログの採取に失敗してしまう。これを回避策するには、APIMとAOAIの間にFunctionsなどを配置し、そのFunctionsにStreamが有効な際のAOAIの呼び出しやトークン個数の集計を任せる、という方法がある。Streamが有効か否かの判断はリクエスト本文からわかるので、APIMでStreamが有効なリクエストであれば、set-backend-service
ポリシーを使ってFunctionsをバックエンドサービスとして指定するというもの。ただこれ、「Streamを有効にした意味があるのか?🤔」となるのはその通りなので、Streamを使うのであれば、現状ではOrchestratorに類するアプリケーションでコントロールするのが望ましい。