エージェントや MCP サーバーを組むと、ほぼ必ず「ツールを呼ぶ/呼ばない」のさじ加減に悩みます。期待した場面で呼んでくれない、あるいは何でもない応答にまでツールを叩きにいく。この境界は乱数ではなく、system プロンプトの一文で動かせます。本稿は、空のディレクトリから動くエージェントまでを 1 本通しで作り、誘導 → 強制の順に調整の引き出しを実装します(検証日: 2026-10-26)。
判定の仕組みと調整の2段構え
Claude はユーザーのリクエストとツールの説明をもとに、そのターンでツールを呼ぶか直接答えるかを毎回判断します。デフォルトの tool_choice は {"type": "auto"} で、各ターンごとに「呼ぶ/直接応答」を選びます。
調整は 2 段で考えます。1 段目は system プロンプトによる誘導です。
This boundary is steerable through your system prompt.
境界は system プロンプトで操作できる、と公式ドキュメントは明言しています。具体的には、"Use the tools to investigate before responding." のような軽い指示で呼び出し頻度が上がり、"Always call a tool first before responding." でさらに積極的になります。逆に "Use your judgment about whether to call a tool or respond directly." にすると保守的なままに保てます。
2 段目は tool_choice による強制です。プロンプト誘導は確率的な押し引きですが、tool_choice を any や tool にすれば、そのターンで必ずツールを呼ばせられます。ただし強制には後述のコストがあります。
完成させるもの
1 つのファイル tool_steering.py に、ツール定義・3 プロンプトの比較・ツールループをまとめます。
mkdir claude-tool-steering && cd claude-tool-steering
python3 -m venv .venv
source .venv/bin/activate
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."
python -c "import anthropic; print(anthropic.__version__)" # SDK 版の確認
前提: Python 3.11+ / macOS・Linux / anthropic Python SDK(2026-10-26 時点の最新)。
# tool_steering.py
from anthropic import Anthropic
client = Anthropic() # ANTHROPIC_API_KEY を環境変数から読む
MODEL = "claude-sonnet-5-5"
TOOLS = [
{
"name": "search_docs",
"description": "Search internal product documentation and return matching passages.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural-language search query",
},
},
"required": ["query"],
},
}
]
DOCS = {
"refund": "Refunds are processed within 5 business days to the original payment method.",
"shipping": "Standard shipping takes 3-5 business days.",
}
def search_docs(query):
q = query.lower()
for key, text in DOCS.items():
if key in q:
return text
return "No matching passage found."
def create(system_prompt, messages, tool_choice=None):
kwargs = dict(
model=MODEL,
max_tokens=1024,
system=system_prompt,
tools=TOOLS,
messages=messages,
)
if tool_choice is not None:
kwargs["tool_choice"] = tool_choice
return client.messages.create(**kwargs)
PROMPTS = {
"conservative": "Use your judgment about whether to call a tool or respond directly.",
"light": "Use the tools to investigate before responding.",
"strong": "Always call a tool first before responding.",
}
def compare(question):
for label, sp in PROMPTS.items():
resp = create(sp, [{"role": "user", "content": question}])
called = resp.stop_reason == "tool_use"
print(f"{label:12s} stop_reason={resp.stop_reason:9s} tool_called={called}")
def agent_loop(system_prompt, question, tool_choice=None):
messages = [{"role": "user", "content": question}]
while True:
resp = create(system_prompt, messages, tool_choice)
tool_choice = None # 2 回目以降は強制を外す(後述の落とし穴)
if resp.stop_reason != "tool_use":
return "".join(b.text for b in resp.content if b.type == "text")
messages.append({"role": "assistant", "content": resp.content})
results = []
for block in resp.content:
if block.type == "tool_use":
out = search_docs(**block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": out,
})
messages.append({"role": "user", "content": results})
if __name__ == "__main__":
print("--- behavior comparison ---")
compare("What is our refund policy?")
print("--- full loop (light prompt) ---")
print(agent_loop(PROMPTS["light"], "What is our refund policy?"))
実行と確認
python tool_steering.py で実行します。印字フォーマットはコードから決まりますが、stop_reason の値は呼び出しが確率的なので実行ごとに変わり得ます。次は「想定される形」で、stop_reason 列と最終行の文面は変動します。
--- behavior comparison ---
conservative stop_reason=end_turn tool_called=False
light stop_reason=tool_use tool_called=True
strong stop_reason=tool_use tool_called=True
--- full loop (light prompt) ---
Refunds are processed within 5 business days to the original payment method.
成功の判定: 終了コードが 0、比較が 3 行印字され、ループが空でない回答(refund の内容を含む)を返すこと。失敗の見分け方: 認証エラー(API キー未設定)なら先頭で例外停止、tool_called=False ばかりなら誘導が効いていないサインです。
ループの核心:stop_reason を見て tool_result を返す
Claude responds with stop_reason: "tool_use" and one or more tool_use blocks
stop_reason が tool_use なら、tool_use ブロックを処理し tool_result を送り返す、というのがクライアントツールの基本契約です。agent_loop はこれを実装しています。tool_use_id は対応する tool_use ブロックの id と一致させる必要があります。
tool_choice で強制する——ただし1ターン目だけ
必ず呼ばせたいときは tool_choice を渡します。
agent_loop(
"You are a support agent.",
"What is our refund policy?",
tool_choice={"type": "any"}, # any=何かのツール必須 / {"type":"tool","name":...}=特定ツール必須
)
落とし穴は、強制を毎ターン維持すると無限ループになることです。強制下では最終回答を出せず必ずツールを呼ぶため、ループが止まりません。agent_loop は 1 ターン目で tool_choice = None に戻し、2 ターン目以降は auto に任せて回答させています(これは設計判断です)。
強制のコスト:トークンと「確認してくる挙動」
ツール使用時、API はツール使用を有効化する専用の system プロンプトを自動で加えます。tool_choice が auto / none のとき、Claude Opus 5.5・Claude Sonnet 5.5 ではこのオーバーヘッドは 286 トークンです(取得日: 2026-10-26)。Claude Haiku 5.5 の例では、auto / none が 286 トークン、any / tool が 406 トークンに増えます(同日取得)。Opus 5.5・Sonnet 5.5 の any / tool 時の値はこの出典には無いため、ここでは断定しません。強制はオーバーヘッドを増やし得る、と押さえてください。
もう 1 つのコストは挙動面です。
Claude Opus is much more likely to recognize that a parameter is missing
必須パラメータが欠けているとき、Opus はそれに気づいて確認を求める可能性が高いとされています。Sonnet は、ツールリクエストを出す前に思考するよう促されると確認する場合がある一方、合理的な値を推論することもあります。ここが重要で、tool_choice を any / tool に固定すると、モデルは必ずツールを呼ばざるを得ず「引数が足りないので確認する」経路が塞がれます。強制はこの安全弁を手放す取引だ、というのが私の見立てです(設計上の意見)。
私ならこう組む:決定ラダー
出典の引き写しではない運用指針を 1 つ。調整は下から上へ段階的に上げるのが安全だと考えます(以下は設計判断・意見です)。(1) まず auto + 軽い誘導の一文から始める。(2) stop_reason == "tool_use" の発生率をログで測る。(3) 足りなければ強い誘導に差し替える。(4) それでも取りこぼすクリティカルな経路に限って tool_choice を 1 ターン目だけ強制する。いきなり全ターン強制から入ると、無限ループ・トークン増・確認挙動の喪失を一度に抱え込みます。強制は「最後の手段を局所的に」が実務的です。
既知の限界
プロンプト誘導は確率的で、同じ入力でも結果はぶれます(各ターンで判断されるため)。トークン数とモデルごとの挙動差は 2026-10-26 時点の取得値で、将来変わり得ます。strict 系のスキーマ厳格化については本稿の検証範囲で裏が取れなかったため扱いません。
References
[1] https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview