9.3.7 高度なツールパターン【オプション】
ツールが2つか3つしかないなら、
そのまま登録して振り分けるだけでも、たいていは十分です。
でもツールが増えてくると、すぐに新しい問題が出てきます。
- 同じツールが何度も呼ばれる
- ある呼び出しはまとめて処理したほうがよい
- よくある処理フローは、いつも同じツールの組み合わせになる
この段階になると、ツール層にも「デザインパターン」が必要だと分かってきます。
この節で学ぶのは、次のことです。
ツールをただのバラバラな関数から、組み合わせ可能で再利用できる能力レイヤーへ引き上げる方法。
学習目標
- キャッシュ、再試行、バッチ、複合ツールなどの高度なパターンが、それぞれ何を解決するのか理解する
- 「ツールのラッパー層」がなぜ重要か理解する
- 実行可能なサンプルを通して、組み合わせ可能なツール実行器を理解する
- ツール層を「関数の集まり」から「システム部品」へと捉える意識を身につける
なぜツール層にもパターンが必要なのか?
同じ問題が何度も出てくるから
例えば:
- 毎回の呼び出しでログを残したい
- あるAPIはよくタイムアウトするので再試行したい
- 同じ問い合わせが短時間に何度も来るのでキャッシュしたい
- ある作業はいつも「検索してから要約する」流れになる
こうしたロジックを毎回それぞれのツールに手書きしていると、
すぐにシステムが管理しづらくなります。
パターンの価値は「高度に見えること」ではない
大事なのは次の点です。
- 再利用できる
- 一貫性を保てる
- 重複実装を減らせる
これは、バックエンドのミドルウェアの考え方にとてもよく似ています。
たとえ話:ツール本体は家電、パターンは電源タップや電圧安定器
買ってきた家電は、それだけでも使えます。
でも機器が増えてくると、
自然に次のようなものが必要になります。
- 電源タップ
- 電圧安定器
- タイマー
ツールパターンも、Agent に対しては同じ役割をします。
よく使う4つの高度なツールパターン
再試行ラッパー
向いている場面:
- 一時的な失敗
- 上流APIの一時的な不安定さ
キャッシュラッパー
向いている場面:
- 短時間に同じ問い合わせが頻繁に起こる
- 読み取り専用のツール
バッチツール
向いている場面:
- 似た質問をまとめて一度に処理したい
- 似たリクエストをまとめて処理したい
複合ツール
向いている場面:
- いつも同じ複数ツールをセットで使う
例えば:
- ドキュメント検索 -> rerank -> 要約
このような場合、毎回 Agent にその場で組み立てさせるより、
もっと高いレベルの複合ツールとしてまとめたほうがよいです。
まずは「組み合わせ可能なツールラッパー」の例を動かしてみよう
次の例では、3つのことを行います。
- 下層ツールにキャッシュを付ける
- ツールに再試行を付ける
- さらに複合ツールとして組み合わせる
from functools import wraps
def cache_tool(fn):
cache = {}
@wraps(fn)
def wrapper(*args):
if args in cache:
return {"source": "cache", "value": cache[args]}
value = fn(*args)
cache[args] = value
return {"source": "tool", "value": value}
return wrapper
def retry_tool(fn, retries=2):
@wraps(fn)
def wrapper(*args):
last_error = None
for _ in range(retries + 1):
try:
return fn(*args)
except Exception as e:
last_error = str(e)
return {"error": f"retry_failed:{last_error}"}
return wrapper
@cache_tool
def search_docs(keyword):
docs = {
"返金": "返金は7日以内かつ学習進捗が20%未満である必要があります。",
"修了証": "すべての必修項目を完了し、テストに合格すると修了証を取得できます。",
}
return docs.get(keyword, "関連する文書が見つかりません")
def summarize(text):
return f"要約:{text[:18]}..."
def search_and_summarize(keyword):
doc = search_docs(keyword)
if "error" in doc:
return doc
return {
"keyword": keyword,
"raw": doc,
"summary": summarize(doc["value"]),
}
print(search_and_summarize("返金"))
print(search_and_summarize("返金"))
期待される出力:
{'keyword': '返金', 'raw': {'source': 'tool', 'value': '返金は7日以内かつ学習進捗が20%未満である必要があります。'}, 'summary': '要約:返金は7日以内かつ学習進捗が20%未...'}
{'keyword': '返金', 'raw': {'source': 'cache', 'value': '返金は7日以内かつ学習進捗が20%未満である必要があります。'}, 'summary': '要約:返金は7日以内かつ学習進捗が20%未...'}

1回目の同じ query は tool から返り、2回目は cache から返ります。後半の例では、いつ batch 化し、いつ「内部資料 + 外部資料」を安定した workflow にまとめるかを見ます。
このコードで一番学ぶべきことは?
ツール層には「ツール本体」だけがあるわけではない、ということです。
実際のシステムでは、よく次のようなことを先に行います。
- ラップする
- 強化する
- 組み合わせる
そして Agent が呼ぶのは、
元の関数ではなく、強化された能力であることが多いです。
なぜキャッシュは読み取り専用ツールに向いているのか?
読み取り専用ツールは、短時間に何度も呼ばれても、
返り値がすぐには変わらないことが多いからです。
例えば:
- 返金ポリシーの確認
- 製品説明の確認
こうしたツールは短時間キャッシュを入れると、
コストをかなり下げやすくなります。
なぜ「検索 + 要約」を複合ツールにするのか?
この組み合わせは、かなり固定的だからです。
毎回 Agent に次のことを考えさせるよりも、
- まず検索する
- それから要約する
複合ツールとしてまとめたほうが、
速くて、ミスも減ります。
なぜバッチツールが重要なのか?
多くのリクエストは本質的にまとめて処理できるから
例えば:
- 10件の注文状況を一度に確認する
- まとまった価格計算を一度に行う
- 一組のドキュメント要約を一度に取る
1件ずつ呼び出すと、
次のようなコストが無駄になりがちです。
- ネットワーク往復
- モデルのステップ数
- スケジューリングのオーバーヘッド
最小限のバッチツールの例
def get_order_status_batch(order_ids):
mock_db = {
"A001": "未発送",
"A002": "発送済み",
"A003": "受領済み",
}
return {order_id: mock_db.get(order_id, "不明な注文") for order_id in order_ids}
print(get_order_status_batch(["A001", "A002", "A009"]))
期待される出力:
{'A001': '未発送', 'A002': '発送済み', 'A009': '不明な注文'}
このパターンは特に次のような場合に向いています。
- バックエンド自体がバッチAPIをサポートしている
- 1回あたりの呼び出しコストが高い
いつ一連のツールを「高度なツール」としてまとめるべきか?
組み合わせが十分に固定されているとき
もし処理の流れがいつも次のようなら:
search -> rerank -> summarize
これは複合ツールにするのにとても向いています。
Agent に細かいことをあまり考えさせたくないとき
Agent の仕事が、いつも低レベル操作にとどまる必要はありません。
基本動作が安定しているなら、
高レベルのツールにまとめることで、Agent は次のことに集中できます。
- より高いレベルの判断
システムをより安定・高速・テストしやすくしたいとき
複合ツールは、一般に次のことがしやすくなります。
- 単体テスト
- 観測
- レート制限
境界がはっきりするからです。
「知識ベース駆動の教材生成アシスタント」を作るなら、どの組み合わせを先にまとめるべきか?
この種のプロジェクトでは、ツールは自然と次のような種類に増えていきます。
- 内部資料を検索する
- 外部資料を検索する
- 重複を除いて並べ替える
- 教材 schema を生成する
- Word に出力する
各ステップを毎回 Agent にその場で決めさせると、
たいてい次のような問題が起こります。
- 順番が安定しない
- 内部資料の確認を忘れることがある
- 先に出力してから、あとで内容を補うことがある
なので、最初の段階で特に先にまとめる価値が高いのは、
次のような頻出の固定フローです。
| 複合ツール | 何を固定してくれるか |
|---|---|
retrieve_teaching_materials | まず内部を検索し、次に外部を補足し、最後にまとめて重複を除く |
build_courseware_outline | 概念、例題、練習問題を先に抽出してから schema に整理する |
export_courseware_doc | まず schema を検証し、それからテンプレートを使って Word に出力する |
まずは、次のように理解するとよいです。
よく一緒に起こる動作を、あらかじめ1つの安定した手順にまとめる。
もう少し実際のプロジェクトに近い最小複合ツールの例
def retrieve_internal_docs(topic):
return [{"source": "internal", "text": f"内部資料:{topic} の知識点と例題"}]
def retrieve_external_docs(topic):
return [{"source": "external", "text": f"外部資料:{topic} の補足説明"}]
def merge_materials(internal_docs, external_docs):
return internal_docs + external_docs
def retrieve_teaching_materials(topic):
internal_docs = retrieve_internal_docs(topic)
external_docs = retrieve_external_docs(topic)
return merge_materials(internal_docs, external_docs)
print(retrieve_teaching_materials("割引の応用問題"))
期待される出力:
[{'source': 'internal', 'text': '内部資料:割引の応用問題 の知識点と例題'}, {'source': 'external', 'text': '外部資料:割引の応用問題 の補足説明'}]
この例の大事な点は、コードがどれだけ複雑かではありません。
新人にまず見てもらいたいのは、次の点です。
- 高度なツールパターンは「不思議な設計」ではない
- プロジェクトで何度も出てくる流れを固めているだけ
よくある誤解
誤解1:高度なパターンとは「装飾子をたくさん書くこと」
違います。
大事なのは見た目の派手さではなく、
本当に重複する問題を減らせるかどうかです。
誤解2:キャッシュがあれば必ず良い
データの変化が速い場合、
キャッシュは古い結果のリスクを逆に増やすことがあります。
誤解3:組み合わせが多いほど強いシステムである
過度な抽象化は、システムを硬くしてしまいます。
大事なのは、組み合わせが安定しているか、そして頻繁に使われるかです。
まとめ
この節で一番大切なのは、いくつかのパターン名を覚えることではありません。
もっとエンジニアリング寄りの判断を身につけることです。
ツールが増えて、同じ問題が何度も出てくるようになったら、ツール層はキャッシュ、再試行、バッチ処理、複合化によって、ただの「関数の集まり」から組み合わせ可能な能力システムへ進化させる必要がある。
この感覚が身につくと、
後でコード Agent やマルチツールシステムを作るときに、
「関数をもう1つ足す」だけでは足りないと分かるようになります。
演習
- サンプルに
timeout_toolラッパーを追加し、それをどの層に置くべきか考えてみましょう。 - なぜキャッシュは読み取り専用ツールに向いていて、更新が多い書き込み操作には向いていないのでしょうか?
- 自分が作った Agent のタスクを1つ思い出し、その中で複合ツールにまとめやすい固定フローを見つけてみましょう。
- もしあるツールの組み合わせが安定しておらず、順番もよく変わるなら、それでも高度なツールとしてまとめますか? なぜですか?