コンテンツにスキップ

9.3.7 高度なツールパターン【オプション】

  • キャッシュ、再試行、バッチ、複合ツールなどの高度なパターンが、それぞれ何を解決するのか理解する
  • 「ツールのラッパー層」がなぜ重要か理解する
  • 実行可能なサンプルを通して、組み合わせ可能なツール実行器を理解する
  • ツール層を「関数の集まり」から「システム部品」へと捉える意識を身につける

なぜツール層にもパターンが必要なのか?

Section titled “なぜツール層にもパターンが必要なのか?”

同じ問題が何度も出てくるから

Section titled “同じ問題が何度も出てくるから”

例えば:

  • 毎回の呼び出しでログを残したい
  • あるAPIはよくタイムアウトするので再試行したい
  • 同じ問い合わせが短時間に何度も来るのでキャッシュしたい
  • ある作業はいつも「検索してから要約する」流れになる

こうしたロジックを毎回それぞれのツールに手書きしていると、
すぐにシステムが管理しづらくなります。

パターンの価値は「高度に見えること」ではない

Section titled “パターンの価値は「高度に見えること」ではない”

大事なのは次の点です。

  • 再利用できる
  • 一貫性を保てる
  • 重複実装を減らせる

これは、バックエンドのミドルウェアの考え方にとてもよく似ています。

たとえ話:ツール本体は家電、パターンは電源タップや電圧安定器

Section titled “たとえ話:ツール本体は家電、パターンは電源タップや電圧安定器”

買ってきた家電は、それだけでも使えます。
でも機器が増えてくると、
自然に次のようなものが必要になります。

  • 電源タップ
  • 電圧安定器
  • タイマー

ツールパターンも、Agent に対しては同じ役割をします。


よく使う4つの高度なツールパターン

Section titled “よく使う4つの高度なツールパターン”

向いている場面:

  • 一時的な失敗
  • 上流APIの一時的な不安定さ

向いている場面:

  • 短時間に同じ問い合わせが頻繁に起こる
  • 読み取り専用のツール

向いている場面:

  • 似た質問をまとめて一度に処理したい
  • 似たリクエストをまとめて処理したい

向いている場面:

  • いつも同じ複数ツールをセットで使う

例えば:

  • ドキュメント検索 -> rerank -> 要約

このような場合、毎回 Agent にその場で組み立てさせるより、
もっと高いレベルの複合ツールとしてまとめたほうがよいです。


まずは「組み合わせ可能なツールラッパー」の例を動かしてみよう

Section titled “まずは「組み合わせ可能なツールラッパー」の例を動かしてみよう”

次の例では、3つのことを行います。

  1. 下層ツールにキャッシュを付ける
  2. ツールに再試行を付ける
  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("返金"))

期待される出力:

Terminal window
{'keyword': '返金', 'raw': {'source': 'tool', 'value': '返金は7日以内かつ学習進捗が20%未満である必要があります。'}, 'summary': '要約:返金は7日以内かつ学習進捗が20%未...'}
{'keyword': '返金', 'raw': {'source': 'cache', 'value': '返金は7日以内かつ学習進捗が20%未満である必要があります。'}, 'summary': '要約:返金は7日以内かつ学習進捗が20%未...'}

高度な tool pattern の実行結果図

このコードで一番学ぶべきことは?

Section titled “このコードで一番学ぶべきことは?”

ツール層には「ツール本体」だけがあるわけではない、ということです。
実際のシステムでは、よく次のようなことを先に行います。

  • ラップする
  • 強化する
  • 組み合わせる

そして Agent が呼ぶのは、
元の関数ではなく、強化された能力であることが多いです。

なぜキャッシュは読み取り専用ツールに向いているのか?

Section titled “なぜキャッシュは読み取り専用ツールに向いているのか?”

読み取り専用ツールは、短時間に何度も呼ばれても、
返り値がすぐには変わらないことが多いからです。

例えば:

  • 返金ポリシーの確認
  • 製品説明の確認

こうしたツールは短時間キャッシュを入れると、
コストをかなり下げやすくなります。

なぜ「検索 + 要約」を複合ツールにするのか?

Section titled “なぜ「検索 + 要約」を複合ツールにするのか?”

この組み合わせは、かなり固定的だからです。
毎回 Agent に次のことを考えさせるよりも、

  • まず検索する
  • それから要約する

複合ツールとしてまとめたほうが、
速くて、ミスも減ります。


なぜバッチツールが重要なのか?

Section titled “なぜバッチツールが重要なのか?”

多くのリクエストは本質的にまとめて処理できるから

Section titled “多くのリクエストは本質的にまとめて処理できるから”

例えば:

  • 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"]))

期待される出力:

Terminal window
{'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"))

期待される出力:

Terminal window
[{'source': 'internal', 'text': '内部 SOP 証拠:返金エスカレーション SOP のポリシールールと対応済みケース'}, {'source': 'external', 'text': '外部参照:返金エスカレーション SOP の補足ベストプラクティス'}]

この例の大事な点は、コードがどれだけ複雑かではありません。
新人にまず見てもらいたいのは、次の点です。

  • 高度なツールパターンは「不思議な設計」ではない
  • プロジェクトで何度も出てくる流れを固めているだけ

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

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

誤解1:高度なパターンとは「装飾子をたくさん書くこと」

Section titled “誤解1:高度なパターンとは「装飾子をたくさん書くこと」”

違います。
大事なのは見た目の派手さではなく、
本当に重複する問題を減らせるかどうかです。

誤解2:キャッシュがあれば必ず良い

Section titled “誤解2:キャッシュがあれば必ず良い”

データの変化が速い場合、
キャッシュは古い結果のリスクを逆に増やすことがあります。

誤解3:組み合わせが多いほど強いシステムである

Section titled “誤解3:組み合わせが多いほど強いシステムである”

過度な抽象化は、システムを硬くしてしまいます。
大事なのは、組み合わせが安定しているか、そして頻繁に使われるかです。


この節で一番大切なのは、いくつかのパターン名を覚えることではありません。
もっとエンジニアリング寄りの判断を身につけることです。

ツールが増えて、同じ問題が何度も出てくるようになったら、ツール層はキャッシュ、再試行、バッチ処理、複合化によって、ただの「関数の集まり」から組み合わせ可能な能力システムへ進化させる必要がある。

この感覚が身につくと、
後でコード Agent やマルチツールシステムを作るときに、
「関数をもう1つ足す」だけでは足りないと分かるようになります。


  1. サンプルに timeout_tool ラッパーを追加し、それをどの層に置くべきか考えてみましょう。
  2. なぜキャッシュは読み取り専用ツールに向いていて、更新が多い書き込み操作には向いていないのでしょうか?
  3. 自分が作った Agent のタスクを1つ思い出し、その中で複合ツールにまとめやすい固定フローを見つけてみましょう。
  4. もしあるツールの組み合わせが安定しておらず、順番もよく変わるなら、それでも高度なツールとしてまとめますか? なぜですか?
参考実装と解説
  1. timeout_tool wrapper は通常 executor または tool middleware 層に置きます。そうすれば全ツールで同じ timeout 動作を共有できます。
  2. cache は read-only ツールに向いています。同じ入力が同じ安全な答えを返しやすいからです。書き込みは状態を変えるため、cached result が危険になることがあります。
  3. 返金チェック、レポート生成、ドキュメント取り込みのような workflow は、順序が安定して再現できるなら composite tool にできます。
  4. 順序が不安定なら、まだ advanced tool に隠さない方がよいです。パターンが信頼でき、テストできるまでステップを見える状態に保ちます。