9.3.7 高度なツールパターン【オプション】
- キャッシュ、再試行、バッチ、複合ツールなどの高度なパターンが、それぞれ何を解決するのか理解する
- 「ツールのラッパー層」がなぜ重要か理解する
- 実行可能なサンプルを通して、組み合わせ可能なツール実行器を理解する
- ツール層を「関数の集まり」から「システム部品」へと捉える意識を身につける
なぜツール層にもパターンが必要なのか?
Section titled “なぜツール層にもパターンが必要なのか?”同じ問題が何度も出てくるから
Section titled “同じ問題が何度も出てくるから”例えば:
- 毎回の呼び出しでログを残したい
- あるAPIはよくタイムアウトするので再試行したい
- 同じ問い合わせが短時間に何度も来るのでキャッシュしたい
- ある作業はいつも「検索してから要約する」流れになる
こうしたロジックを毎回それぞれのツールに手書きしていると、
すぐにシステムが管理しづらくなります。
パターンの価値は「高度に見えること」ではない
Section titled “パターンの価値は「高度に見えること」ではない”大事なのは次の点です。
- 再利用できる
- 一貫性を保てる
- 重複実装を減らせる
これは、バックエンドのミドルウェアの考え方にとてもよく似ています。
たとえ話:ツール本体は家電、パターンは電源タップや電圧安定器
Section titled “たとえ話:ツール本体は家電、パターンは電源タップや電圧安定器”買ってきた家電は、それだけでも使えます。
でも機器が増えてくると、
自然に次のようなものが必要になります。
- 電源タップ
- 電圧安定器
- タイマー
ツールパターンも、Agent に対しては同じ役割をします。
よく使う4つの高度なツールパターン
Section titled “よく使う4つの高度なツールパターン”再試行ラッパー
Section titled “再試行ラッパー”向いている場面:
- 一時的な失敗
- 上流APIの一時的な不安定さ
キャッシュラッパー
Section titled “キャッシュラッパー”向いている場面:
- 短時間に同じ問い合わせが頻繁に起こる
- 読み取り専用のツール
バッチツール
Section titled “バッチツール”向いている場面:
- 似た質問をまとめて一度に処理したい
- 似たリクエストをまとめて処理したい
向いている場面:
- いつも同じ複数ツールをセットで使う
例えば:
- ドキュメント検索 -> rerank -> 要約
このような場合、毎回 Agent にその場で組み立てさせるより、
もっと高いレベルの複合ツールとしてまとめたほうがよいです。
まずは「組み合わせ可能なツールラッパー」の例を動かしてみよう
Section titled “まずは「組み合わせ可能なツールラッパー」の例を動かしてみよう”次の例では、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_tooldef 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%未...'}
このコードで一番学ぶべきことは?
Section titled “このコードで一番学ぶべきことは?”ツール層には「ツール本体」だけがあるわけではない、ということです。
実際のシステムでは、よく次のようなことを先に行います。
- ラップする
- 強化する
- 組み合わせる
そして Agent が呼ぶのは、
元の関数ではなく、強化された能力であることが多いです。
なぜキャッシュは読み取り専用ツールに向いているのか?
Section titled “なぜキャッシュは読み取り専用ツールに向いているのか?”読み取り専用ツールは、短時間に何度も呼ばれても、
返り値がすぐには変わらないことが多いからです。
例えば:
- 返金ポリシーの確認
- 製品説明の確認
こうしたツールは短時間キャッシュを入れると、
コストをかなり下げやすくなります。
なぜ「検索 + 要約」を複合ツールにするのか?
Section titled “なぜ「検索 + 要約」を複合ツールにするのか?”この組み合わせは、かなり固定的だからです。
毎回 Agent に次のことを考えさせるよりも、
- まず検索する
- それから要約する
複合ツールとしてまとめたほうが、
速くて、ミスも減ります。
なぜバッチツールが重要なのか?
Section titled “なぜバッチツールが重要なのか?”多くのリクエストは本質的にまとめて処理できるから
Section titled “多くのリクエストは本質的にまとめて処理できるから”例えば:
- 10件の注文状況を一度に確認する
- まとまった価格計算を一度に行う
- 一組のドキュメント要約を一度に取る
1件ずつ呼び出すと、
次のようなコストが無駄になりがちです。
- ネットワーク往復
- モデルのステップ数
- スケジューリングのオーバーヘッド
最小限のバッチツールの例
Section titled “最小限のバッチツールの例”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回あたりの呼び出しコストが高い
いつ一連のツールを「高度なツール」としてまとめるべきか?
Section titled “いつ一連のツールを「高度なツール」としてまとめるべきか?”組み合わせが十分に固定されているとき
Section titled “組み合わせが十分に固定されているとき”もし処理の流れがいつも次のようなら:
search -> rerank -> summarize
これは複合ツールにするのにとても向いています。
Agent に細かいことをあまり考えさせたくないとき
Section titled “Agent に細かいことをあまり考えさせたくないとき”Agent の仕事が、いつも低レベル操作にとどまる必要はありません。
基本動作が安定しているなら、
高レベルのツールにまとめることで、Agent は次のことに集中できます。
- より高いレベルの判断
システムをより安定・高速・テストしやすくしたいとき
Section titled “システムをより安定・高速・テストしやすくしたいとき”複合ツールは、一般に次のことがしやすくなります。
- 単体テスト
- 観測
- レート制限
境界がはっきりするからです。
「知識ベース駆動の SOP 文書アシスタント」を作るなら、どの組み合わせを先にまとめるべきか?
Section titled “「知識ベース駆動の SOP 文書アシスタント」を作るなら、どの組み合わせを先にまとめるべきか?”この種のプロジェクトでは、ツールは自然と次のような種類に増えていきます。
- 内部資料を検索する
- 外部資料を検索する
- 重複を除いて並べ替える
- SOP スキーマを生成する
- Word に出力する
各ステップを毎回 Agent にその場で決めさせると、
たいてい次のような問題が起こります。
- 順番が安定しない
- 内部資料の確認を忘れることがある
- 先に出力してから、あとで内容を補うことがある
なので、最初の段階で特に先にまとめる価値が高いのは、
次のような頻出の固定フローです。
| 複合ツール | 何を固定してくれるか |
|---|---|
retrieve_sop_materials | まず内部 SOP を検索し、次に外部参照を補足し、最後にまとめて重複を除く |
build_sop_schema | ポリシールール、対応済みケース、チェックリスト項目を先に抽出してからスキーマに整理する |
export_sop_doc | まず SOP スキーマを検証し、それからテンプレートを使って Word に出力する |
まずは、次のように理解するとよいです。
よく一緒に起こる動作を、あらかじめ1つの安定した手順にまとめる。
もう少し実際のプロジェクトに近い最小複合ツールの例
Section titled “もう少し実際のプロジェクトに近い最小複合ツールの例”def retrieve_internal_docs(topic): return [{"source": "internal", "text": f"内部 SOP 証拠:{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_sop_materials(topic): internal_docs = retrieve_internal_docs(topic) external_docs = retrieve_external_docs(topic) return merge_materials(internal_docs, external_docs)
print(retrieve_sop_materials("返金エスカレーション SOP"))期待される出力:
[{'source': 'internal', 'text': '内部 SOP 証拠:返金エスカレーション SOP のポリシールールと対応済みケース'}, {'source': 'external', 'text': '外部参照:返金エスカレーション SOP の補足ベストプラクティス'}]この例の大事な点は、コードがどれだけ複雑かではありません。
新人にまず見てもらいたいのは、次の点です。
- 高度なツールパターンは「不思議な設計」ではない
- プロジェクトで何度も出てくる流れを固めているだけ
このページを終えたら、この証拠カードを残します。
- ツール契約
- 名前、説明、入力スキーマ、出力スキーマ
- 権限
- ツールが読み取りまたは変更を許可されている範囲
- 呼び出しトレース
- 引数、結果、エラー、再試行、またはフォールバック
- 失敗確認
- 間違ったツール、不適切な引数、危険な操作、または観測不足
- 安全対策
- 検証、確認、サンドボックス化、レート制限、またはロールバック
よくある誤解
Section titled “よくある誤解”誤解1:高度なパターンとは「装飾子をたくさん書くこと」
Section titled “誤解1:高度なパターンとは「装飾子をたくさん書くこと」”違います。
大事なのは見た目の派手さではなく、
本当に重複する問題を減らせるかどうかです。
誤解2:キャッシュがあれば必ず良い
Section titled “誤解2:キャッシュがあれば必ず良い”データの変化が速い場合、
キャッシュは古い結果のリスクを逆に増やすことがあります。
誤解3:組み合わせが多いほど強いシステムである
Section titled “誤解3:組み合わせが多いほど強いシステムである”過度な抽象化は、システムを硬くしてしまいます。
大事なのは、組み合わせが安定しているか、そして頻繁に使われるかです。
この節で一番大切なのは、いくつかのパターン名を覚えることではありません。
もっとエンジニアリング寄りの判断を身につけることです。
ツールが増えて、同じ問題が何度も出てくるようになったら、ツール層はキャッシュ、再試行、バッチ処理、複合化によって、ただの「関数の集まり」から組み合わせ可能な能力システムへ進化させる必要がある。
この感覚が身につくと、
後でコード Agent やマルチツールシステムを作るときに、
「関数をもう1つ足す」だけでは足りないと分かるようになります。
- サンプルに
timeout_toolラッパーを追加し、それをどの層に置くべきか考えてみましょう。 - なぜキャッシュは読み取り専用ツールに向いていて、更新が多い書き込み操作には向いていないのでしょうか?
- 自分が作った Agent のタスクを1つ思い出し、その中で複合ツールにまとめやすい固定フローを見つけてみましょう。
- もしあるツールの組み合わせが安定しておらず、順番もよく変わるなら、それでも高度なツールとしてまとめますか? なぜですか?
参考実装と解説
timeout_toolwrapper は通常 executor または tool middleware 層に置きます。そうすれば全ツールで同じ timeout 動作を共有できます。- cache は read-only ツールに向いています。同じ入力が同じ安全な答えを返しやすいからです。書き込みは状態を変えるため、cached result が危険になることがあります。
- 返金チェック、レポート生成、ドキュメント取り込みのような workflow は、順序が安定して再現できるなら composite tool にできます。
- 順序が不安定なら、まだ advanced tool に隠さない方がよいです。パターンが信頼でき、テストできるまでステップを見える状態に保ちます。