コンテンツにスキップ

9.3.5 よく使うツールの統合

  • Agent でよく使う代表的なツールの種類を理解する
  • 各種類のツールが何の問題に向いているかを理解する
  • 統一されたツール登録とディスパッチの例を読めるようになる
  • ツール統合でよく起きる失敗点と、実装上の注意点を理解する

なぜツールを種類ごとに見るのか?

Section titled “なぜツールを種類ごとに見るのか?”

「ツール」という言葉の範囲が広すぎるから

Section titled “「ツール」という言葉の範囲が広すぎるから”

検索もツール、計算機もツール、データベース検索もツール、ファイルの読み書きもツールです。
これらを全部まとめて「1つの関数」と考えると、すぐに混乱します。

より実用的なのは、まず次のように分類することです。

  1. 検索系
  2. 計算系
  3. データアクセス系
  4. ファイル / 環境操作系
  5. 外部サービス呼び出し系

種類ごとに注目点が違うからです。

  • 検索系は召喚率ではなく、検索結果の質を見る
  • 計算系は正確さと安全性を見る
  • データベース系は権限と絞り込みを見る
  • ファイル系はパスの境界を見る
  • 外部サービス系はタイムアウトとリトライを見る

つまり、

どれも「ツール」ではあるけれど、実装上のリスクはまったく同じではない

ということです。


向いている用途:

  • ドキュメントを探す
  • ナレッジベースを検索する
  • Webページを探す

特徴:

  • 入力は通常 クエリ
  • 出力は通常、候補の一覧

向いている用途:

  • 四則演算
  • 統計指標の計算
  • 小さなデータ変換

特徴:

  • 出力は安定して正確である必要がある
  • 安全性に特に注意する必要がある

向いている用途:

  • データベースを検索する
  • 注文情報を確認する
  • ユーザー状態を確認する

特徴:

  • パラメータと権限が最重要
  • 多くの業務ロジックがこの層で決まる

向いている用途:

  • ファイルを読む
  • ファイルを書く
  • ディレクトリを列挙する
  • コードを実行する

特徴:

  • リスクが高い
  • 境界管理が非常に重要

向いている用途:

  • メールを送る
  • 外部の API を呼ぶ
  • チケットを作成する

特徴:

  • 失敗、タイムアウト、リトライがよく発生する

実際のシステムでは、ツールをあちこちに散らすのではなく、まとめて登録することがよくあります。

import ast
import operator
OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
}
def safe_calculate(expression):
def visit(node):
if isinstance(node, ast.Expression):
return visit(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in OPS:
return OPS[type(node.op)](visit(node.left), visit(node.right))
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
return -visit(node.operand)
raise ValueError("unsupported_expression")
return visit(ast.parse(expression, mode="eval"))
def search_docs(keyword):
docs = {
"返金": "コース購入後 7 日以内に返金申請ができます",
"証明書": "プロジェクトを完了し、テストに合格すると証明書を取得できます"
}
return docs.get(keyword, "関連ドキュメントが見つかりませんでした")
def calculator(expression):
return safe_calculate(expression)
def get_user_status(user_id):
mock_db = {
1: {"name": "Alice", "progress": 0.15},
2: {"name": "Bob", "progress": 0.35}
}
return mock_db.get(user_id, {"error": "user_not_found"})
TOOLS = {
"search_docs": search_docs,
"calculator": calculator,
"get_user_status": get_user_status
}
print(TOOLS.keys())

期待される出力:

Terminal window
dict_keys(['search_docs', 'calculator', 'get_user_status'])

後で次のようなことが必要になるからです。

  • スキーマ を統一して説明する
  • 権限管理をまとめて行う
  • ログを統一して取る
  • ディスパッチと集計を一元化する

ツールに登録表がないと、システムはどんどん保守しづらくなります。


def dispatch(call):
name = call["name"]
arguments = call["arguments"]
if name not in TOOLS:
return {"error": "unknown_tool"}
try:
result = TOOLS[name](**arguments)
return {"result": result}
except Exception as e:
return {"error": str(e)}
calls = [
{"name": "search_docs", "arguments": {"keyword": "返金"}},
{"name": "calculator", "arguments": {"expression": "12 * 7"}},
{"name": "get_user_status", "arguments": {"user_id": 1}}
]
for call in calls:
print(call, "->", dispatch(call))

期待される出力:

Terminal window
{'name': 'search_docs', 'arguments': {'keyword': '返金'}} -> {'result': 'コース購入後 7 日以内に返金申請ができます'}
{'name': 'calculator', 'arguments': {'expression': '12 * 7'}} -> {'result': 84}
{'name': 'get_user_status', 'arguments': {'user_id': 1}} -> {'result': {'name': 'Alice', 'progress': 0.15}}

このコードが教えてくれるのは、次の点です。

  • 異なるツールでも、同じ呼び出し口を共有できる
  • プログラム側でエラー処理をまとめられる
  • 後からツールを増やしても構造が崩れにくい

種類ごとに何へ注意すべきか?

Section titled “種類ごとに何へ注意すべきか?”

重点ポイント:

  • クエリ を書き換える必要があるか
  • 何件返すか
  • 結果を rerank する必要があるか

重点ポイント:

  • 安全性
  • 精度
  • 式が正しいかどうか

安全な計算機の簡単な例:

import ast
import operator
OPS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
}
def safe_calculate(expression):
def visit(node):
if isinstance(node, ast.Expression):
return visit(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in OPS:
return OPS[type(node.op)](visit(node.left), visit(node.right))
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
return -visit(node.operand)
raise ValueError("unsupported_expression")
return visit(ast.parse(expression, mode="eval"))
def safe_calculator(expression):
allowed = set("0123456789+-*/(). ")
if not set(expression) <= allowed:
return {"error": "invalid_expression"}
return {"result": safe_calculate(expression)}
print(safe_calculator("3 * (4 + 5)"))
print(safe_calculator("__import__('os').system('rm -rf /')"))

期待される出力:

Terminal window
{'result': 27}
{'error': 'invalid_expression'}

重点ポイント:

  • 権限
  • パラメータの完全性
  • クエリの境界

たとえば、モデルに任意の SQL を自由に生成させて、そのまま実行するのは避けるべきです。

重点ポイント:

  • パスのホワイトリスト
  • 書き込み権限
  • 人の確認が必要かどうか

重点ポイント:

  • タイムアウト
  • リトライ
  • 冪等性

Agent らしいツール組み合わせの例

Section titled “Agent らしいツール組み合わせの例”

シナリオ:ユーザーが返金できるか判断する

Section titled “シナリオ:ユーザーが返金できるか判断する”

この処理には、次の 2 つのツールが必要かもしれません。

  1. ユーザーの学習進捗を確認する
  2. 返金ポリシーを確認する
def refund_eligibility_agent(user_id):
status = get_user_status(user_id)
if "error" in status:
return {"error": "ユーザーが存在しません"}
policy = search_docs("返金")
progress = status["progress"]
can_refund = progress < 0.2
return {
"user": status["name"],
"progress": progress,
"policy": policy,
"can_refund": can_refund
}
print(refund_eligibility_agent(1))
print(refund_eligibility_agent(2))

期待される出力:

Terminal window
{'user': 'Alice', 'progress': 0.15, 'policy': 'コース購入後 7 日以内に返金申請ができます', 'can_refund': True}
{'user': 'Bob', 'progress': 0.35, 'policy': 'コース購入後 7 日以内に返金申請ができます', 'can_refund': False}

Agent の一般的な tool dispatch 実行結果図

このコードが本当に示していること

Section titled “このコードが本当に示していること”

このコードが示しているのは、

ツール統合とは、各ツールを単独で置くことではなく、複数のツールを協力させて 1 つの目的を達成すること

という点です。

だからこそ、今後の Agent はツールのオーケストレーション能力にますます依存していきます。


たとえば:

  • ツールは user_id を必要とする
  • でもモデルは id を送ってしまう

戻り値の形式が統一されていない

Section titled “戻り値の形式が統一されていない”

あるツールは文字列、別のツールは dict、さらに別のツールは list を返すと、システムはだんだん接続しづらくなります。

エラー処理が統一されていない

Section titled “エラー処理が統一されていない”

あるツールは None を返し、別のツールは例外を投げ、さらに別のツールは "failed" を返す。
これでは後続の処理がすぐに崩れます。

本番で問題が起きたとき、どの種類のツールに問題があったのか分かりません。


実用的な提案:ツールの戻り値形式を統一する

Section titled “実用的な提案:ツールの戻り値形式を統一する”

もっとも安定しやすい方法の1つは、ツールの出力構造を統一することです。たとえば、すべて次の形式にそろえます。

{
"ok": True,
"data": ...
}

または:

{
"ok": False,
"error": ...
}

簡単な例:

def wrapped_search(keyword):
try:
result = search_docs(keyword)
return {"ok": True, "data": result}
except Exception as e:
return {"ok": False, "error": str(e)}
print(wrapped_search("返金"))

期待される出力:

Terminal window
{'ok': True, 'data': 'コース購入後 7 日以内に返金申請ができます'}

こうしておくと、後の Agent 層で統一的に判定しやすくなります。


初心者がよくつまずくポイント

Section titled “初心者がよくつまずくポイント”

とりあえず全部のツールをつなぐ

Section titled “とりあえず全部のツールをつなぐ”

ツールが増えるほど、システムは複雑になります。
より安定したやり方は、

  • まず本当に必要な 2〜3 個だけつなぐ

ことです。

高リスクなツールと低リスクなツールを区別しない

Section titled “高リスクなツールと低リスクなツールを区別しない”

ファイル削除、支払い操作、データベース書き込みは、ドキュメント検索とは危険度がまったく違います。

ツール API の約束事が統一されていない

Section titled “ツール API の約束事が統一されていない”

これは、Agent システムがだんだん混乱していく大きな原因の1つです。


このページを終えたら、この証拠カードを残します。

ツール契約
名前、説明、入力スキーマ、出力スキーマ
権限
ツールが読み取りまたは変更を許可されている範囲
呼び出しトレース
引数、結果、エラー、再試行、またはフォールバック
失敗確認
間違ったツール、不適切な引数、危険な操作、または観測不足
安全対策
検証、確認、サンドボックス化、レート制限、またはロールバック

この節で一番大切なのは、「どんなツールがあるか」を覚えることではなく、次の点を理解することです。

よく使うツール統合のポイントは、ツールを接続することだけではなく、それらを統一されたインターフェース、統一されたエラー処理、統一された境界制御でまとめることにある。

こうして初めて、ツール層は Agent の能力を広げる存在になり、障害を増やす存在にはなりません。


  1. この節のツール登録表に get_weather(city) ツールを追加してください。
  2. すべてのツールの戻り値を {"ok": ..., "data": ..., "error": ...} の形式に統一してください。
  3. 考えてみましょう。なぜデータベース書き込みツールと検索ツールを同じ権限レベルにしてはいけないのでしょうか?
  4. 自分の言葉で説明してください。なぜツール登録表と統一ディスパッチャが Agent 実装でとても重要な 2 つの構造だと言えるのでしょうか?
参考実装と解説
  1. get_weather(city) は registry に入れ、schema、risk level、timeout、正規化された戻り値形式を持たせます。
  2. {ok, data, error} に統一すると下流ロジックが単純になります。成功時は data を読み、失敗時は自然言語を解析せず error で分岐できます。
  3. データベース書き込みツールは記録を変更するため、検索ツールより強い権限、確認、rollback ルールが必要です。
  4. registry はツールメタデータの単一の参照元です。dispatcher は validation、permission check、retry、logging、error handling を集中管理します。