コンピュータアクセスシステム

ツール使用パターン:信頼性の高いエージェント-ツールインターフェースの構築


エージェントがツールを呼び出し、40行のJSONブロブを受け取った——生のAPIレスポンス、ネストされたオブジェクト、statusフィールドに埋め込まれたエラーコード。モデルはそれを読み、もっともらしい値を選んで続行した。値は間違っていた。三ステップ後、エージェントは誤ったデータに基づいて自信満々にレポートを書いた。

ツールは機能した。インターフェースが失敗した。

ツール使用は言語モデルをエージェントに変える仕組みだ。エージェントのすべての能力——データベース検索、ファイル書き込み、API呼び出し、サービス問い合わせ——はツールインターフェースを通じて届く。インターフェースの設計が悪ければ、基盤となるサービスが正常に動作していてもモデルは悪い判断を下す。このガイドでは、正確で信頼性が高く、本番環境に対応したツールインターフェースを構築するための5つのパターンを扱う。

前提条件: PythonとClaude APIに慣れていること。MCPをツールのトランスポート層として使う背景については最初のMCPサーバーの構築を参照。


なぜインターフェース設計が重要か

エージェントがツールを選択して使用するとき、2つの決定を行う:

  1. どのツールを呼び出すか — ツールのnamedescriptionによって決まる
  2. どの引数を渡すか — ツールのinput_schemaによって決まる

曖昧な説明は誤ったツール選択につながる。ゆるいスキーマはモデルが不正な入力を渡すことを許す。非構造化された結果はモデルに何が起きたかを推測させる。エージェントのバグのほとんどは推論にあるのではなく——ツールの境界にある。


パターン1:スキーマファースト設計

実装を書く前にJSONスキーマを書く。厳密なスキーマは、何かが実行される前の入力段階でモデルの動作を制約する。

import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "search_products",
"description": (
"キーワードで商品カタログを検索する。"
"IDと名前と価格を含む一致する商品のリストを返す。"
"ユーザーが商品を探したりブラウズしたいときに使う。"
),
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "検索するキーワード"
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "food", "home", "all"],
"description": "フィルタリングする商品カテゴリ。未指定の場合は'all'を使う。"
},
"max_results": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"description": "返す結果の数。デフォルト:5"
}
},
"required": ["query", "category"]
}
}
]
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "1万円以下の電子機器を探して"}]
)

エラーを減らすスキーマルール:

  • 固定された有効値セットを持つフィールドにはenumを使う
  • 範囲外の入力を防ぐために数値フィールドにminimum/maximumを設定する
  • ツールが本当にそれなしでは実行できない場合にのみフィールドをrequiredにする
  • モデルの視点から説明を書く:「…のときに使う」はモデルにツールをいつ呼び出すかを伝える

使用するとき: すべてのツール定義で。


パターン2:構造化されたツール結果

型付けされた機械読み取り可能な結果を返す。生のAPIレスポンスや散文による説明を返してはいけない。

import json
from dataclasses import dataclass, asdict
from typing import Any, Optional
@dataclass
class ToolResult:
success: bool
data: Optional[Any] = None
error: Optional[str] = None
def to_content(self) -> str:
return json.dumps(asdict(self), ensure_ascii=False)
def search_products(
query: str,
category: str,
max_results: int = 5,
) -> ToolResult:
try:
raw_results = _query_database(query, category, limit=max_results)
products = [
{"id": r["product_id"], "name": r["title"], "price": r["price_usd"]}
for r in raw_results
]
return ToolResult(success=True, data={"products": products, "count": len(products)})
except ConnectionError as e:
return ToolResult(success=False, error=f"データベース利用不可:{e}")
except Exception as e:
return ToolResult(success=False, error=f"検索失敗:{type(e).__name__}: {e}")
def _query_database(query, category, limit):
return []

一貫した{success, data, error}エンベロープにより、モデルは常にどこを見ればいいか分かる。障害の優雅な処理についてはエージェントエラー回復パターンを参照。


パターン3:並列ツール呼び出し

Claudeは単一のレスポンスで複数のツールを要求できる。逐次ではなく並列で処理する。

from concurrent.futures import ThreadPoolExecutor, as_completed
TOOL_REGISTRY = {
"search_products": search_products,
}
def dispatch_tool(name: str, inputs: dict) -> ToolResult:
handler = TOOL_REGISTRY.get(name)
if not handler:
return ToolResult(success=False, error=f"不明なツール:{name}")
return handler(**inputs)
def process_tool_calls(response: anthropic.types.Message) -> list[dict]:
tool_uses = [
block for block in response.content
if block.type == "tool_use"
]
if not tool_uses:
return []
def execute(tool_use):
result = dispatch_tool(tool_use.name, tool_use.input)
return {
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result.to_content(),
}
with ThreadPoolExecutor(max_workers=len(tool_uses)) as executor:
futures = {executor.submit(execute, tu): tu for tu in tool_uses}
results = []
for future in as_completed(futures):
results.append(future.result())
return results
def run_agent(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, "text"):
return block.text
return ""
if response.stop_reason == "tool_use":
tool_results = process_tool_calls(response)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
else:
break
return ""

それぞれがツールを呼び出す複数のエージェントのオーケストレーションについてはマルチエージェントパターンを参照。


パターン4:安全なツール呼び出しラッパー

ツールの例外がエージェントループに未処理のまま到達することを絶対に許してはいけない。

import signal
def timeout_handler(signum, frame):
raise TimeoutError("ツール実行がタイムアウト")
def safe_tool_call(
name: str,
inputs: dict,
timeout_seconds: int = 30,
) -> ToolResult:
"""
タイムアウト付きでツールを実行し、すべての例外をキャッチする。
常にToolResultを返す——例外を投げることはない。
"""
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout_seconds)
try:
return dispatch_tool(name, inputs)
except TimeoutError:
return ToolResult(
success=False,
error=f"ツール'{name}'が{timeout_seconds}秒後にタイムアウト"
)
except Exception as e:
return ToolResult(
success=False,
error=f"ツール'{name}'が{type(e).__name__}を発生:{e}"
)
finally:
signal.alarm(0)

ツールがsuccess: falseを返すとき、モデルは再試行するか、代替を試みるか、失敗を報告するかを決定できる。より幅広いリトライ戦略についてはエラー回復パターンを参照。


パターン5:結果の検証と切り詰め

ツール結果をモデルに返す前に検証する。

MAX_TOOL_RESULT_CHARS = 8000
def validate_result(result: ToolResult, expected_keys: list[str]) -> ToolResult:
if not result.success or not isinstance(result.data, dict):
return result
missing = [k for k in expected_keys if k not in result.data]
if missing:
return ToolResult(
success=False,
error=f"ツールレスポンスに期待されるフィールドが欠けている:{missing}"
)
return result
def truncate_result(result: ToolResult) -> ToolResult:
content = result.to_content()
if len(content) <= MAX_TOOL_RESULT_CHARS:
return result
truncated_data = {
"truncated": True,
"chars_omitted": len(content) - MAX_TOOL_RESULT_CHARS,
"content": content[:MAX_TOOL_RESULT_CHARS],
}
return ToolResult(
success=result.success,
data=truncated_data,
error="結果を切り詰めた——コンテキストウィンドウに対して大きすぎる",
)
def safe_tool_call_validated(
name: str,
inputs: dict,
expected_keys: list[str] | None = None,
) -> ToolResult:
result = safe_tool_call(name, inputs)
if expected_keys:
result = validate_result(result, expected_keys)
result = truncate_result(result)
return result

本番でこれらの失敗を観察してデバッグするにはデバッグと可観測性を参照。


よくある間違い

間違い1:生のAPIレスポンスを返す

モデルは30のフィールドを持つネストされたオブジェクトを受け取り、ほとんどが無関係だ。間違ったものを選ぶ。

解決策: 返す前にレスポンスを整形する。モデルが次の決定を下すために必要なものだけを返す。

間違い2:確認なしの副作用を持つツール

メールを送ったり、レコードを削除したり、課金したりするツールは無言で実行されるべきではない。

解決策: 不可逆なアクションには2ツールパターンを使う:plan_emailはプレビューを返し、send_emailが実際に送信する。

間違い3:ツールの責任が重複している

似たようなことをする2つのツールはモデルにどちらを使うか推測させる。

解決策: 各ツールは明確で重複しない目的を持つべきだ。

間違い4:外部ツールにタイムアウトがない

遅いサードパーティのAPI呼び出しがエージェントループ全体を無期限にブロックする。

解決策: 常にタイムアウトを設定する(パターン4)。


本番チェックリスト

  • 各ツールにモデルの視点からの説明がある(「…のときに使う」)
  • すべての固定値入力にenumフィールド
  • 各ツールが{success, data, error}を返す——生のレスポンスは返さない
  • ツール呼び出しがsafe_tool_callでラップされている
  • マルチツールレスポンスの並列実行
  • 各外部ツールにタイムアウトが設定されている
  • 可変サイズのペイロードへの結果の切り詰め
  • 外部APIを呼び出すツールの検証

次のステップ

  1. スキーマから始める — 関数本体の前にinput_schemaを書く
  2. ToolResultラッパーを追加する — 既存のすべてのツールに
  3. safe_tool_callを導入する — エージェントループを強化する
  4. 結果検証を設定する — 制御できないAPIを呼び出すツールに

関連ガイド: