1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude API の tool_choice でツール呼び出しを強制する実装手順 — 強制したままループして終わらない・thinking 併用で 400・前置きテキストが消える、3つのハマりどころ【2026】

1
Posted at

はじめに / 対象と前提

Claude API の tool use で「ツールを定義したのに呼んでくれない」「必ずこのツールを通して JSON で返してほしい」という場面で使うのが tool_choice だ。パラメータ自体は 1 行だが、強制した途端に別の場所が壊れるので、自分がハマった順にまとめる。

  • 想定読者:Claude API の tool use を一度は動かしたことがある人
  • 前提知識:tools の定義と tool_use / tool_result の往復
  • 環境:Python 3.13 / anthropic SDK 0.6x 系 / モデルは claude-sonnet-5-5(手元のモデル ID に読み替えてほしい)

TL;DR

  • tool_choice は auto(既定)/ any(どれか必ず呼ぶ)/ tool(指定の 1 本を必ず呼ぶ)/ none(呼ばせない)の 4 種類
  • 強制(any / tool)は 最初の 1 ターンだけ にする。ループ全体に付けると永遠に終わらない
  • 強制は拡張思考と併用できず 400 になる。また強制時は tool_use の前の説明テキストが出なくなる

手順 / 動かし方

1. 4 種類の指定方法

tool_choice={"type": "auto"}                      # 呼ぶかどうかは Claude が決める(既定)
tool_choice={"type": "any"}                       # tools のどれかを必ず呼ぶ
tool_choice={"type": "tool", "name": "classify"}  # classify を必ず呼ぶ
tool_choice={"type": "none"}                      # ツールを呼ばせない

2. 特定ツールを強制して構造化された結果を受け取る

問い合わせ文を分類する例。tool で 1 本に固定すると、返ってくる content は必ず tool_use ブロックになる。

import anthropic

client = anthropic.Anthropic()  # ANTHROPIC_API_KEY を環境変数から読む

tools = [{
    "name": "classify",
    "description": "問い合わせ文をカテゴリと緊急度に分類して記録する",
    "input_schema": {
        "type": "object",
        "properties": {
            "category": {"type": "string", "enum": ["billing", "bug", "question"]},
            "urgency": {"type": "integer", "description": "1(低)〜3(高)"},
        },
        "required": ["category", "urgency"],
    },
}]

res = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "classify"},
    messages=[{"role": "user", "content": "昨日から決済画面が 500 で落ちます。至急お願いします"}],
)

print(res.stop_reason)
block = next(b for b in res.content if b.type == "tool_use")
print(block.input)

実行結果:

tool_use
{'category': 'bug', 'urgency': 3}

tool_choice を外して同じ入力を投げると、モデルの判断次第で「決済画面のエラーですね。分類します」のようなテキストだけ返して stop_reason が end_turn になる回が混ざる。分類・抽出のように ツールを通らない応答が不要な処理 では、最初から強制しておくほうが後段のコードが単純になる。

3. 強制は 1 ターン目だけにして auto に戻す

エージェントループに組み込む場合の核心部分はこうなる。

def run(messages, first_choice):
    choice = first_choice
    while True:
        res = client.messages.create(
            model="claude-sonnet-5-5", max_tokens=2048,
            tools=tools, tool_choice=choice, messages=messages,
        )
        if res.stop_reason != "tool_use":
            return res
        messages.append({"role": "assistant", "content": res.content})
        messages.append({"role": "user", "content": execute_tools(res.content)})
        choice = {"type": "auto"}  # 2 ターン目以降は必ず auto に戻す

最後の 1 行が次のハマりどころ 1 の回避策そのものだ。

ハマりどころ

1. 強制したままループを回すと永遠に終わらない

tool_choice={"type": "any"} を付けっぱなしでループを書いたところ、stop_reason が毎回 tool_use で返り続け、同じツールが同じ引数で何十回も呼ばれた。

  • 原因:any / tool は「このターンで必ずツールを呼べ」という指定で、ツール結果を受け取った後のターンにも等しく効く。Claude はテキストで締めくくる選択肢を持てないので、end_turn に到達できない
  • 回避策:強制は初回だけにして以降は auto に戻す(上のコード)。加えてループには必ず上限回数を入れる。強制の有無に関係なく保険になる

なお tool_choice を途中で変えると、プロンプトキャッシュのうち messages 部分が無効になる(tools と system のキャッシュは残る)。長い会話でキャッシュを使っている場合、切り替えたターンだけ cache_read_input_tokens が減るのは仕様どおりの挙動だ。

2. 拡張思考と併用すると 400

thinking を有効にしたリクエストに any か tool を付けると、リクエスト自体が弾かれる。

anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error':
{'type': 'invalid_request_error',
 'message': 'Thinking may not be enabled when tool_choice forces tool use.'}}
  • 原因:拡張思考と両立する tool_choice は auto と none だけ。強制系は思考ブロックを挟む余地がないため非対応になっている
  • 回避策:どちらかを諦める。自分は「思考が要る難しい判断」は auto のまま system プロンプトで「回答前に必ず classify を呼ぶこと」と明示し、「機械的な抽出」は thinking を切って強制、と用途で分けた。auto + 強い指示でも呼ばれない回はゼロにならないので、stop_reason が tool_use でなければ 1 回だけ再要求する分岐を入れておく

3. 強制すると前置きテキストが出ない/並列呼び出しの個数が変わる

auto のときは「まず天気を調べます」のようなテキストブロックが tool_use の前に付くことがある。これを進捗表示に使っていた UI が、any に変えた途端に無言になった。

  • 原因:強制時は API 側がアシスタントの応答をツール呼び出しから始めさせるので、明示的に頼んでも tool_use の前に自然文は出ない
  • 回避策:説明文が欲しいなら auto のまま、user メッセージ側で「○○ツールを使って答えて」と指示する。進捗表示は tool_use ブロックの name から自前で組み立てるほうが安定する

もう 1 つ、any / tool でも 1 ターンに複数の tool_use が返ることがある。「必ず 1 個」を前提に res.content[0] を決め打ちするとここで落ちる。1 個に固定したいときは次のように書く。

tool_choice={"type": "tool", "name": "classify", "disable_parallel_tool_use": True}

disable_parallel_tool_use は auto と組み合わせると「最大 1 個」、any / tool と組み合わせると「ちょうど 1 個」になる。

背景・補足:どれを選ぶか

やりたいこと 指定
普通のエージェント auto(既定)
分類・抽出など、必ず決まった形で受け取る tool + disable_parallel_tool_use
複数ツールのどれかには必ず振り分ける(ルーター) any
ツール定義は残したまま、このターンは文章だけ欲しい none

none は地味に便利で、ループの上限に達したときに「ここまでの結果をまとめて」と最終ターンを打つ用途に使える。tools を外すと過去の tool_use ブロックとの整合性でエラーになることがあるので、定義は残して none にするのが安全だった。

また、強制しても input がスキーマに厳密に従う保証は別の話だ。型まで保証したい場合はツール定義に strict: true を付ける構造化出力側の機能と組み合わせる。

まとめ

  • tool_choice は auto / any / tool / none。強制は「そのターンで必ず呼ぶ」という意味
  • 強制は初回ターン限定にして auto に戻す。付けっぱなしはループが終わらない
  • 拡張思考と強制は併用不可(400)。思考が要るなら auto + 明示的な指示で代替する
  • 強制時は前置きテキストが出ない。1 個に絞るなら disable_parallel_tool_use を付ける
  • 最終ターンのまとめは tools を消さずに none を使う
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?