ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション(LDI)の技術ブログです

Claude(LLM)の回答が途中で終了する場合の stop_reason を用いた回答の継続取得

はじめに

モバイルアプリエンジニアの庄司です。

Claudeを API 経由で利用していると、
回答が途中で終了してしまうケースに遭遇することがあります。

文末が不自然に途切れていたり、
明らかに続きがありそうな状態で生成が終了しているように見える場合です。

当初は、

  • 続きがありそうなら「続き」の要求を明示的にプロンプトとして入力して、得られた出力を結合する
  • 回答が大きくならないようにプロンプトを調整する

といった形で対処していました。

しかし実際には、続きがあるかどうかは API のレスポンスで判断でき、「続き」の要求を明示的にプロンプトで入力しなくても継続した回答を得られることがわかりました。
本記事では、その中でもレスポンスの stop_reason を用いた対処方法について紹介します。

本記事は、Claude の API を利用した開発経験がある方を主な対象としています。
また、下記の利用を前提としています。

  • Claude 4.5 Sonnet / 4.5 Haiku (AWS Bedrock)
  • Python3

回答の続きを得るには

回答が途中で切れる理由について

Claude の回答が途中で終了する代表的な理由としては以下があります。

  • トークン上限に達した
  • モデル内部の制御による停止
  • セーフティフィルタの適用

どのような理由で終了したかは、 API のレスポンス中のstop_reasonというプロパティで確認することができます。


stop_reason を確認する

Claudeの API では、
レスポンスに stop_reason というプロパティが含まれます。

概念的なレスポンスは以下のようになります。

{
  "content": [
    { "type": "text", "text": "途中までの回答..." }
  ],
  "stop_reason": "max_tokens"
}

stop_reason は、生成が停止した理由を表します。

代表的な値は以下の通りです。

  • max_tokens
    → トークン上限に達したため生成が終了した
  • end_turn
    → モデルが自然に生成を完了した

本記事では、PoC や簡易ツールでの利用を想定し、この 2 つについて扱います。

stop_reasonとして返るそのほかの値については下記の公式のドキュメントを参照してください。

停止理由の処理 - Claude API Docs


max_tokens の場合のみ続きを取得する

基本的な考え方は非常に単純です。

回答を取得する
↓
stop_reason を確認する
↓
max_tokens であれば続きを取得する
↓
それ以外(end_turn)であれば終了する

生成された文章の内容から判断するのではなく、
stop_reason の値に基づいて処理を分岐することがポイントです。

Claude の公式ドキュメントでも、 stop_reasonmax_tokens の場合に、ユーザーから「続きを要求するメッセージ」を送信する例が紹介されていますが、 「続きを要求するメッセージ」を送信せずとも続きを得ることができます。


実装例(PoC)

以下は、Claude のモデルを用いた PoC 実装の一部です。 実際には CLI や会話管理を含むクライアントとして実装していますが、 ここでは stop_reason による継続判定に関係する部分のみを抜粋しています。

    MAX_CONTINUATION_REQUESTS = 10

    def stream_response(self, messages: list[dict]):
        current_messages = messages.copy()
        accumulated_response = ""

        for _ in range(self.MAX_CONTINUATION_REQUESTS):
            response_body = self.invoke_model(current_messages)

            text = self._extract_text(response_body)
            # Claudeの返答テキストを加工せず累積
            accumulated_response += text

            # Claudeの回答の停止理由を判定する
            stop_reason = response_body.get("stop_reason", "end_turn")
            needs_continuation = stop_reason == "max_tokens"
            if not needs_continuation:
                return

            # Claudeへ再要求するためメッセージを再初期化する
            current_messages = messages.copy()
            current_messages.append({
                "role": "assistant",
                 # 累積したテキストをClaude(assistant)の返答としてcurrent_messagesに追加する
                "content": accumulated_response.rstrip()
            })

この実装では、レスポンスのstop_reason"max_tokens" の場合のみ継続リクエストを送信しています。

※ 本記事では説明を簡略化するため、 エラーハンドリングや CLI 周りの実装は省略しています。


messages を更新している理由

継続リクエストを送信する際は、 これまでに生成された回答を assistant メッセージとして messages に追加しています。

current_messages.append({
    "role": "assistant",
    "content": accumulated_response.rstrip()
})

これにより、モデルは同一回答の続きとして回答を生成します。


安全対策について

この実装では、MAX_CONTINUATION_REQUESTS による上限を設けています。

MAX_CONTINUATION_REQUESTS = 10

実際には、モデルごとに最大コンテキスト長が定められており、 それを超えた場合は model_context_window_exceeded で停止し、大量のループが発生するわけではありません。

ですが、コスト面での安全性を担保のため継続回数に上限を設けました。

まとめ

・Claude の回答が途中で終了した場合、 stop_reasonを見れば理由がわかる。
・stop_reasonがmax_tokensの場合は、回答の継続をリクエストすれば続きの回答を得られる。
・継続をリクエストする場合はこれまでに生成された回答を assistant メッセージとしてmessages に追加してリクエストすることで 「続きを要求するメッセージ」を送信せずとも続きの回答を得ることができる。


最後に

Claude の回答が途中で終了する場合に、継続した回答を得る方法について紹介しました。 Claude の利用においては初歩的な内容かもしれませんが、公式のドキュメントからはこうした挙動をとることは分かりにくいため、 同様の問題にハマった方もいらっしゃるのではないでしょうか。

Claude をAPIで利用する方の一助となれれば幸いです。

今後もローソンデジタルイノベーションでは技術ブログを更新していきますので、是非「読者になる」で応援をお願いします。