本記事では、OpenAIを使用したシステム構築時に遭遇しやすい問題を解説しています。具体的には、消費トークンの計算方法、AIが生成した文章が途中で途切れたときの対策、そしてタイムアウト時間の設定について詳細に説明します。
これらのテクニカルな問題を解決することで、より効果的にOpenAIを活用することが可能になります。
概要
今回の記事では、OpenAIを使用してシステム構築する際に詰まりやすいと感じたポイントを3つご紹介します。
- 消費トークンを事前もしくは送信後に計算する方法
- AIの記載した文章が途中で途切れてしまったときにはどうするのか
- タイムアウト時間の設定法と検出方法について
今回の検証は以下のバージョンで行いました。
langchain==0.0.321 openai==0.28.1
gpt-35-turbo-16k (0613)
消費トークン数の計算
消費トークン数は、リクエスト後の結果で知る方法と、事前に計算する2通りの方法があるため、順に解説します。
リクエスト前に消費トークンを計算する
社内DBに格納している過去履歴を使用する場合、送信前に適切な量になるようにプロンプトを調整する必要があります。
tiktokenを使用してトークン数の計算を行わせます。ライブラリの使用方法については以下記事をご参照ください。
現行の0613モデルであるgpt35-turboとgpt-4については、メッセージ(system, user, assistant)のトークン数に加えて以下の要素を加味する必要があります。ざっくり以下の通りでした。
- 1つのメッセージにつき4トークンを消費する
- nameスキーマ※1 を使用する場合は、さらに追加で1トークンを消費する
- 1回のrequest処理毎に3トークンを消費する
これに加えてAIが消費するトークンの合計数がモデルの制限を超えないようにする必要があります。
※1: あまり使う機会はありませんが、スクリーンショットのようにnameという機能を使ってassistant, userの名前を定義することができます。もしスキーマに組み込む場合はトークン数の計算が変化する点に注意します。
リクエスト後の消費トークンを表示する
openai
ChatCompletionメソッドの返り値を確認します。
response = openai.ChatCompletion.create( engine="gpt-35-turbo-16k", messages=[ {"role": "system", "content":"あなたは親切なアシスタントです。"}, {"role": "user", "content": "インターネットについて教えてください。"}, ], max_tokens=1000, temperature=0 ) print(response)
printしたレスポンススキーマは次の通りです。
prompt_tokensがmessagesに記載した内容で消費された合計トークン数で、completion_tokensが回答で消費されたトークン数です。total_tokensでトークンの合計数を確認できます。response["usage"]["prompt_tokens"]
などで取り出すことができます。
{ "id": "chatcmpl-XXXXXXXXXXXXXXXXXXXXXX", "object": "chat.completion", "created": 0000000000, "model": "gpt-35-turbo-16k", "choices": [ { "index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": "(※長文のため省略)" } } ], "usage": { "prompt_tokens": 42, "completion_tokens": 604, "total_tokens": 646 } }
LangChain
AzureChatOpenAIもしくはChatOpenAIを使用してメッセージを送信します。
chat = AzureChatOpenAI( deployment_name=engine_gpt35, openai_api_base=openai.api_base, openai_api_version=openai.api_version or "", openai_api_key=openai.api_key or "", max_tokens=1000, ) messages = [ SystemMessage(content="あなたは親切なアシスタントです。"), HumanMessage(content="インターネットについて教えてください。"), ] result = chat.generate([messages]) print(result)
chatオブジェクトにそのままmessagesを渡して生成することも可能ですが、上記の通りgenerateメソッドを使用することで詳細な情報を取得することができます。
generations=[[ChatGeneration(text='(※長文のため省略)'))]] llm_output={'token_usage': {'prompt_tokens': 42, 'completion_tokens': 613, 'total_tokens': 655}, 'model_name': 'gpt-3.5-turbo'} run=[RunInfo(run_id=UUID('XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'))]
トークン数はresult.llm_output['token_usage']
から取り出すことができます。
AI生成文章が途切れた時の対処法
文章が途切れたことを検知する
AIが生成した文章が途切れたことを検知するためには、finish_reasonを用います。例としてChatCompletionメソッドのmax_tokenを制限して文章を途切れさせてみます。
response = openai.ChatCompletion.create( engine="gpt-35-turbo-16k", messages=[ {"role": "system", "content":"あなたは親切なアシスタントです。"}, {"role": "user", "content": "インターネットについて教えてください。"}, ], max_tokens=20, temperature=0 )
レスポンスは次のようになります。contentの部分に途切れてしまった文章で、finish_reasonの部分が「length」となっています。
{ "id": "chatcmpl-XXXXXXXXXXXXXXXXXXXXXX", "object": "chat.completion", "created": 0000000000, "model": "gpt-35-turbo-16k", "choices": [ { "index": 0, "finish_reason": "length", "message": { "role": "assistant", "content": "もちろんです!インターネットは、世界中のコン" } } ], "usage": { "prompt_tokens": 115, "completion_tokens": 20, "total_tokens": 135 } }
finish_reasonは次の種類があります。
finish_reason名 | 意味 |
---|---|
stop | APIが完全なメッセージを返した場合 |
length | パラメーターまたはトークン制限によりメッセージが不完全な場合 |
function_call | 関数呼び出しを実施した場合 |
content_filter | 不適切な回答を生成した場合(コンテンツは省略される) |
null | 応答がまだ進行中か不完全 |
stopが正常なレスポンスです。lengthが検出された場合については後述しますが、messagesのリストにAIが返した文章を追加して再リクエストすることで続きの文章を生成できます。
content_filterに合致した場合のレスポンスを一部抜粋します。以下の通りcontentがそのままスキーマから除外される形になるようです。
"choices": [ { "index": 0, "finish_reason": "content_filter", "message": { "role": "assistant" } } ]
途切れた文章の続きを生成する
途切れた文章の続きを生成するためには、次のようにリクエストを送信します。
response = openai.ChatCompletion.create( engine="gpt-35-turbo-16k", messages=[ {"role": "system", "content":"あなたは親切なアシスタントです。"}, {"role": "user", "content": "インターネットについて教えてください。"}, {"role": "assistant", "content": "'もちろんです!インターネットは、世界中のコンピューターを相互に接続する大規模なネットワークです。これにより、情報の共有やコミュニケーシ"}, ], max_tokens=1000, temperature=0 )
上記のケースではassistantの回答が「コミュニケーシ」で途切れてしまったケースを想定します。この場合は「ョン」から始まり、続く文章を生成すれば正しく続きを生成できたといえます。
これについてのレスポンスは次の通りで、きれいに続きを生成しています。
ョンが可能になります。インターネットを利用することで、電子メールの送受信、ウェブサイトの閲覧、オンラインショッピング、ソーシャルメディアの利用など、さまざまな活動が行えます。\n\nインターネットは、TCP/IPと呼ばれるプロトコルを使用して...(以下長文のため省略)
一方で、続きをきれいに生成してくれないケースもありました。
response = openai.ChatCompletion.create( engine="gpt-35-turbo-16k", messages=[ {"role": "system", "content":"あなたは親切なアシスタントです。"}, {"role": "user", "content": "春の季語を絡めた冗談を教えてください。"}, {"role": "assistant", "content": "「春眠(しゅんみん)暁(ぎょう)を覚(さ)えず」という言葉がありますが、「春は眠くても、ア"}, ], max_tokens=1000, temperature=0 )
この場合のアウトプットは次の通りでした。
「ア」から続く言葉にせず改めて回答を生成しています。また【「】で始まっているにもかかわらず、閉じる記号を生成しませんでした。
春は眠くても、アシスタントはいつでも起きていますよ!
単純にassistantの回答を一覧の最後に記載するだけでは、続く文章の生成がうまくいかないケースもあるようです。単に前に続く文章を続けてほしい場合であっても、プロンプトエンジニアリング等の工夫が必要になりそうです。
タイムアウト時間の設定
openai
openaiのパッケージからリクエストを送信する場合、デフォルトのタイムアウトは600秒に設定されています。
変更する場合は以下の通り、引数request_timeoutを1秒単位で設定します。
response = openai.ChatCompletion.create( engine="gpt-35-turbo-16k", messages=[ {"role": "system", "name":"yuki_nishino", "content":"あなたは親切なアシスタントです。"}, {"role": "user", "content": "インターネットについて教えてください。"}, ], max_tokens=1000, temperature=0, request_timeout=120 )
LangChain
LangChainからリクエストを送信する場合、デフォルトのタイムアウトはChatCompletionに準拠するため600秒に設定されます。変更する場合は以下の通り、request_timeoutを設定することで1秒単位で設定ができます。
またmax_retriesを2以上に設定することで、タイムアウト時に自動で時間を倍にして再リトライをする挙動をとらせることもできます。
chat = AzureChatOpenAI( deployment_name=CHATGPT_ENGINE, openai_api_base=openai.api_base, openai_api_version=openai.api_version or "", openai_api_key=openai.api_key or "", temperature=0, request_timeout=120, max_retries=1, )
おわりに
今回の記事ではOpenAIを使ったシステム構築について、特に消費トークンの計算方法、AIが生成した文章が途中で途切れてしまったときの対処法、タイムアウト時間の設定法について解説しました。
OpenAIの利用には多くのテクニカルな知識が求められます。これらの情報が理解を深める一助となれば幸いです。