OpenAI APIで "finish_reason"が "length"(回答が途切れる)後に、続きを取得できるか試す
はじめに
ChatGPTだと回答が途中で途切れた時に「続けてください」と入力することで続きが読めますが、APIの時("finish_reason"が "length"で終了したケース)はどうなのか気になったので検証してみました。
先に結論を書くと、トークン数の余力を確保できれば、という条件付きですが。途切れた回答をrole
をassistant
として追加し、再度APIを実行すれば、拍子抜けするくらいシンプルに続きを返してくれます。
(書きかけの文章の続きを書くなど生成AIが一番得意な作業なので拍子抜けも何もないのですが)
"トークン数の余力を確保できれば"というのは、例えばmax_tokens
を指定して返信に利用できるトークン数を絞っているケースのように、モデル毎に設定されているトークン上限に余裕がある場合です。
あるいは、会話のやり取りが積み重なり、プロンプトや履歴に直近の回答とは関係ない文章が含まれ、それを削除することでトークン数の空きを確保できるようなケースです。
そのため、目新しい内容はなく、当たり前なことを構えて文章にしているだけなのですが、自分と似た思い込み(続きを取得するのに複雑なロジックやプロンプトが必要なのではetc)や疑問を感じたことがある方の参考になればと思います。
前提
開発言語はNode.js(v18)でライブラリはopenai-nodeを使用しました。
modelは gpt-3.5-turbo-0613
を主に使っています。
max_tokensについて
検証ではmax_tokens
を指定して、故意に回答を途切れさせようと思いますが、先にmax_tokens
とは何かについて軽く触れておきます。
The maximum number of tokens to generate in the chat completion.
The total length of input tokens and generated tokens is limited by the model's context length.
API呼び出し時に指定できるパラメタで、回答の出力トークン数を、指定したトークン数で制限できます。
一見便利なパラメタですが、指定したトークン数に収まるように、回答を作成してくれるわけではないです。
出力中の文章が途中であっても、指定した長さで出力を中断するパラメタとなっています。
ここでは具体例を示しませんが、色々なプロンプトと組み合わせたところmax_tokens
によって作成しようとする回答の長さが変わるケースもありましたが、全く考慮せず回答を作成しようとするケースの方が多かったです。
文面を作成に影響を与えているものの優先順位はかなり低いように見えます。プロンプト内で直接トークン数について指示を与えた方が、完成した場合の文章の長さはコントロールしやすかったです。
max_tokensを使って途切れさせた回答の続きを取得する
少し脱線しましたが、本題に戻って、少なめのmax_tokens
を指定することで、故意に回答を途切れさせ、その続きの取得を試します。
messages
に以下を指定し、max_tokens
は200を指定します。
1回目
{
role: "user",
content: "夏目漱石について教えてください",
},
途中で途切れて"finish_reason"は"length"となります。
2回目
取得した結果をmessages
に追加します。role
はassistant
とします。
{
role: "user",
content: "夏目漱石について教えてください",
},
{
role: "assistant",
content:
"夏目漱石(なつめ・そうせき、1867年-1916年)は、日本の文学者であり、明治時代から大正時代の文学運動で重要な役割を果たしました。彼は、日本の現代文学の創始者として広く称えられています。\n" +
"\n" +
"漱石は、東京で生まれ育ちました。彼は早くから文学に興味を持ち、明治学院中学部で文学を学びました。大学では哲学を専攻しましたが、文学の方に興味を持ち始め、小説の執筆を始めました。\n" +
"\n" +
"漱石の代表作品は多",
},
※漱石に関する記述に間違いがありますが、あくまでOpenAIが返した結果なのでご容赦ください。。。
引き続きfinish_reason
はlength
で回答が途切れますが、続きが出力されていることがわかります。
3回目〜
同じ要領でmessages
の追加を繰り返し、finish_reason
がstop
になるまで繰り返します。
以下のように回答を全て取得することができました。
{
role: "user",
content: "夏目漱石について教えてください",
},
{
role: "assistant",
content:
"夏目漱石(なつめ・そうせき、1867年-1916年)は、日本の文学者であり、明治時代から大正時代の文学運動で重要な役割を果たしました。彼は、日本の現代文学の創始者として広く称えられています。\n" +
"\n" +
"漱石は、東京で生まれ育ちました。彼は早くから文学に興味を持ち、明治学院中学部で文学を学びました。大学では哲学を専攻しましたが、文学の方に興味を持ち始め、小説の執筆を始めました。\n" +
"\n" +
"漱石の代表作品は多",
},
{
role: 'assistant',
content: 'く、『吾輩は猫である』や『坊っちゃん』などがあります。これらの作品は、漱石の自伝的な要素を含んでおり、彼自身の経験や観察を元にしたものです。漱石の作品は、独特な文体と風刺的な描写が特徴で、社会や人間の内面を鋭く描写しています。\n' +
'\n' +
'漱石はまた、明治時代から大正時代にかけての日本の社会情勢にも大きく関与しました。彼は教育者としても活動し、教師として働く傍ら、'
},
{
role: 'assistant',
content: '進歩的な教育改革を提案しました。また、漱石は文学のみならず政治や社会問題にも関心を持ち、国家主義や社会主義などの思想についても考察しました。\n' +
'\n' +
'漱石の文学的な貢献は、彼の文体の先駆的な試みやリアリズムの追求に見られます。彼は日本の伝統的な文学の形式にとらわれることなく、自由な文体で物語性とリアリティを追求しました。彼の作品は、日本の文学史において大きな影響'
},
{
role: 'assistant',
content: 'を与え、多くの後続の作家に影響を与えたとされています。\n' +
'\n' +
'漱石は、その作品や思想によって多くの読者や学者から高く評価されています。彼の作品は、人間の孤独や自己の探究、社会の変革など、普遍的なテーマを扱っており、現代の読者にも深い感銘を与えることがあります。\n' +
'\n' +
'また、漱石の文学的な功績を称えるために、彼の名を冠した文学賞(夏目漱石賞)も設けられています。この賞は、優'
},
{
role: 'assistant',
content: 'れた文学作品に与えられるものであり、漱石の文学的遺産を継承する役割を果たしています。\n' +
'\n' +
'夏目漱石は、その短い生涯の中で多くの作品を残し、日本の文学史において偉大な存在とされています。彼の作品は、人間の内面や社会の問題を鋭く描写し、読者に深い洞察を与えるものです。そのため、彼の作品を読むことは、日本の文学や社会、人間の理解を深める一助となるでしょう。'
}
あっさり目的を達成できてしまいました。
実は、この手順で実行する前に以下の2パターンも試していました。
1つ目は、assistant
の回答に加え、「続きをお願いします」とuser
の発話も挿入する方法です。こちらでも続きの回答を得られるのですが、assistant
が「承知しました」と言った無駄な受け答えを加えたり(会話なので当たり前ですが)、読みやすいよう少し前の回答から続きを出力したり、受け取った値の処理が複雑になるのでメリットはありませんでした。
2つ目はassistant
の回答に加え、2回目以降はsystem
で「途切れてしまったので続きを回答してください」と指示を加える方法です。こちらは無駄な回答なく続きを出力してくれますが、すでに紹介したassistant
による履歴追加に比べてメリットは特にありませんでした。
補足
上記はmax_tokens
で出力を制限したために全体のトークン上限に空きがありました。
そのため単純に途切れた回答を追加して再実行すれば回答を取得できました。
しかし、トークンに空きがない状態では同様の対処はできません。残りトークンが0になるまで途中までの回答が出力され、超過した時点でエラーとなります。
エラー例:
This model's maximum context length is 4097 tokens. However, your messages resulted in 4563 tokens. Please reduce the length of the messages.
プロンプトやプロンプトで取得しようとしている回答の種類によるので、一概には言えませんが、追加で以下のような対処法があると思います。
より上限が大きいモデルを利用する
以下の表にモデル毎のトークン数をModelsから抜粋しました。
モデル名 | トークン上限 |
---|---|
gpt-3.5-turbo-0613 | 4,097 |
gpt-3.5-turbo-16k-0613 | 16,385 |
gpt-4-0613 | 8,192 |
gpt-4-32k-0613 | 32,768 |
費用は若干費用は増加しますが一番簡単な方法かと思います。
履歴を削除、要約する
2つ目は、プロンプトや履歴を削除、抜粋、要約してトークン数の余力を確保する方法です。
日常会話のように話題が転々する一方で、積み上がった履歴が溢れた場合は、回答に無関係な履歴は削除可能な場合が多いです。
逆に話題に変化がなかったり、省略不能な数回のやりとりでトークンが超過するようなケースは、履歴や回答の一部を別途要約させ、それを履歴として扱うことでトークンの余力を捻出できるかもしれません。
中断した回答に一定の長さがあれば回答単体をLLMに渡す
回答だけを渡して、それを補完させるのも選択肢の1つです。
特に回答に一定の長さがある場合は有効かもしれません。
ただし、不足している内容次第ではハルシネーションが発生しやい印象があります。
最後に
結果が当たり前すぎて、途中で記事にするのをやめようかと思ったのですが、冒頭にも書いたように、自分と同じ思い込み(シンプルに考えず、複雑な設計・実装をしようとする)をする人もいるかもしれないので記事にしました。
一方でmax_tokens
指定による回答の長さへの影響や、回答の長さを正確に指定する方法、トークンの余力がない状態での続きの回答の取得方法など、実用的な部分は課題として残っています。
補足として簡単に触れましたが、LangChainの実装なども参考にしつつ、機会があればこれらも記事にできればと思います。
Discussion