メインコンテンツへスキップ

9.3.6 ツールの安全性とエラー処理

この節の位置づけ

ツールによって Agent は「話せる」から「実行できる」に変わります。
でも、一度「実行できる」ようになると、リスクもすぐに上がります。

たとえば:

  • 間違ったデータを検索しても、まだ取り返しがつく
  • ファイルを書き間違えると、すぐに問題になる
  • 削除系の API を間違って呼ぶと、もっと深刻

だからこの節の核心は「ツールが動くかどうか」ではなく、

ツールがエラー、タイムアウト、誤用、さらには権限越えを起こしたときに、システムがきちんと受け止められるか。

学習目標

  • ツールのリスクが、なぜ純テキスト回答より高いのかを理解する
  • 権限の段階分け、パラメータ検証、エラー返却を設計できるようにする
  • リトライ、タイムアウト、冪等性、手動確認がそれぞれ何を防ぐのかを理解する
  • 実行可能な例を通して、安全ガード付きのツール実行器を理解する

なぜツールの安全性は Agent の最重要ラインなのか?

純粋な回答のミスは、たいてい「言い間違い」

もしモデルがテキストだけを返すなら、
間違いの影響はたいてい次のようなものです。

  • 情報が正確ではない
  • 表現が誤解を招く

もちろんこれも重要ですが、
多くの場面ではまだ「出力の問題」にとどまります。

ツール呼び出しのミスは、「実行ミス」になる

一度ツールに実行能力があると、
リスクは次のように変わります。

  • 見てはいけないデータを見てしまう
  • ファイルを壊してしまう
  • 外部 API を間違って呼ぶ
  • 注文を二重に出す、二重に課金する

つまり、

ツールは、エラーを言語レベルから行動レベルへと拡大してしまうのです。

例えで言うと:チャットボットと研修中の作業担当者は同じリスクではない

処理の流れを説明するだけのボットと、
実際にボタンを押したり、データベースを更新したり、メールを送ったりできる担当者は、
リスクの大きさがまったく違います。

Agent がツール層に入るときも、まったく同じです。


ツールの安全性でよく使う 4 つの防御線

パラメータ検証

まず確認するのは:

  • パラメータはそろっているか
  • 型は合っているか
  • 値は正しいか

権限の段階分け

ツールごとにリスクは異なります。
よくある分け方は次のようなものです。

  • read_only
  • write_limited
  • destructive

実行制約

たとえば:

  • タイムアウト
  • 最大リトライ回数
  • レート制限
  • 冪等 key

監査と再現

最低でも、次の情報は記録すべきです。

  • 誰が呼び出したか
  • どのツールを選んだか
  • パラメータは何か
  • 成功したかどうか
  • 何が返ったか

まずはガード付きの最小実行器を動かしてみよう

次の例では、3 種類のツールをシミュレーションします。

  • 低リスクの読み取り専用ツール
  • 中リスクの書き込みツール
  • 高リスクの削除ツール

そして実行前に次を行います。

  • ホワイトリスト確認
  • パラメータ検証
  • 権限確認
  • タイムアウトのシミュレーション
ALLOWED_TOOLS = {
"search_docs": {"risk": "read_only", "required_args": ["keyword"]},
"update_profile": {"risk": "write_limited", "required_args": ["user_id", "city"]},
"delete_file": {"risk": "destructive", "required_args": ["path"]},
}


def run_tool(name, arguments, user_role):
if name not in ALLOWED_TOOLS:
return {"ok": False, "error": "unknown_tool"}

meta = ALLOWED_TOOLS[name]

for field in meta["required_args"]:
if field not in arguments:
return {"ok": False, "error": f"missing_arg:{field}"}

if meta["risk"] == "destructive" and user_role != "admin":
return {"ok": False, "error": "permission_denied"}

if name == "search_docs":
return {"ok": True, "data": {"result": f"{arguments['keyword']} に関連するドキュメントが見つかりました"}}

if name == "update_profile":
return {
"ok": True,
"data": {"message": f"ユーザー {arguments['user_id']} の都市を {arguments['city']} に更新しました"},
}

if name == "delete_file":
return {"ok": True, "data": {"message": f"{arguments['path']} を削除しました"}}

return {"ok": False, "error": "tool_not_implemented"}


calls = [
("search_docs", {"keyword": "返金"}, "guest"),
("update_profile", {"user_id": 7, "city": "台北"}, "operator"),
("delete_file", {"path": "/tmp/a.txt"}, "operator"),
]

for call in calls:
print(call, "->", run_tool(*call))

期待される出力:

('search_docs', {'keyword': '返金'}, 'guest') -> {'ok': True, 'data': {'result': '返金 に関連するドキュメントが見つかりました'}}
('update_profile', {'user_id': 7, 'city': '台北'}, 'operator') -> {'ok': True, 'data': {'message': 'ユーザー 7 の都市を 台北 に更新しました'}}
('delete_file', {'path': '/tmp/a.txt'}, 'operator') -> {'ok': False, 'error': 'permission_denied'}

このコードが「ホワイトリストにあるかどうかだけ」を見るよりずっと強いのはなぜ?

それは、単なるオン・オフの判定ではなく、
ツール安全性の実際の多層構造を表しているからです。

  1. まずツールが存在するか確認する
  2. 次にパラメータがそろっているか確認する
  3. さらに権限が足りるか確認する
  4. 最後に実行する

これが、実際のツール実行器がやるべきことです。

なぜ権限を「Agent を使えるかどうか」だけで分けてはいけないのか?

リスクは一律ではないからです。

  • ドキュメント検索はリスクが低い
  • 情報更新は中程度のリスク
  • ファイル削除は高リスク

だから権限はツールのリスクと結びつける必要があります。
単純な総合スイッチだけでは不十分です。

なぜ高リスクツールには手動確認がよく必要なのか?

モデルがほとんどの場面で正しく選べても、
高リスクな操作は完全自動化に向いていないからです。

典型的なやり方は:

  • まず実行計画を作る
  • その後、ユーザーまたは管理者の確認を求める

ツールの安全性、権限、サンドボックス、監査の図

図の読み方

この図を読むときは、「ツール呼び出し」を実際の操作だと思ってください。低リスクならすぐ記録できますが、高リスクなら権限、サンドボックス、手動確認、そして audit log を通す必要があります。Agent が行動できるほど、システムのガードは省略できません。


なぜエラー処理は try/except だけでは足りないのか?

失敗には種類があるから

代表的な失敗は少なくとも次のとおりです。

  • パラメータエラー
  • 権限エラー
  • ツールのタイムアウト
  • 外部サービスの障害
  • 結果が空

もしすべての失敗を次のようにしか返さないなら、

  • something went wrong

後でデバッグしたり復旧したりするのがほぼ不可能になります。

よりよい方法:エラーを構造化する

def normalize_error(code, detail):
return {
"ok": False,
"error": {
"code": code,
"detail": detail,
"retryable": code in {"timeout", "temporary_unavailable"},
},
}


print(normalize_error("missing_arg", "keyword が不足しています"))
print(normalize_error("timeout", "上流 API が 3 秒以内に返答しませんでした"))

期待される出力:

{'ok': False, 'error': {'code': 'missing_arg', 'detail': 'keyword が不足しています', 'retryable': False}}
{'ok': False, 'error': {'code': 'timeout', 'detail': '上流 API が 3 秒以内に返答しませんでした', 'retryable': True}}

構造化エラーの利点は次のとおりです。

  • スケジューラがリトライすべきか判断できる
  • ログシステムで集計しやすい
  • フロントエンドでも、よりわかりやすく表示できる

どんなエラーがリトライ向きか?

一般に、リトライに向いているのは次のようなものです。

  • timeout
  • temporary unavailable
  • transient network error

リトライに向いていないのは次のようなものです。

  • パラメータ不足
  • 権限不足
  • ロジック検証の失敗

タイムアウト、リトライ、冪等性はそれぞれ何を防いでいるのか?

タイムアウト:システムがずっと止まるのを防ぐ

ツールがいつまでも返さないと、
Agent の処理全体が止まってしまいます。

だからタイムアウトは本質的に、次を守るためのものです。

  • 遅延
  • リソースの占有

リトライ:一時的な失敗を、そのまま最終失敗にしない

上流がたまに不安定になるだけなら、
適切なリトライで安定性をかなり上げられます。

ただしリトライは次と組み合わせる必要があります。

  • それが一時的なエラーかどうか
  • リトライ回数に上限があるかどうか

冪等性:同じ処理を二重に実行しないため

たとえば:

  • 二重課金
  • 二重送信
  • 二重でチケット作成

そのため、書き込み系のツールでは特に次を気にする必要があります。

  • 同じリクエストが来たとき、重複した副作用が起きないか

監査ログはなぜ「あとで追加すればいい」ではないのか?

監査がないと、事故後に流れをたどれない

少なくとも次のことに答えられる必要があります。

  • 誰がどのツールを呼んだか
  • そのときのパラメータは何だったか
  • なぜシステムはそれを実行してよいと判断したか
  • 最終結果は何だったか

最小の監査記録の例

def audit_log(user_id, tool_name, arguments, result):
return {
"user_id": user_id,
"tool_name": tool_name,
"arguments": arguments,
"ok": result["ok"],
"error": result.get("error"),
}


result = run_tool("search_docs", {"keyword": "返金"}, "guest")
print(audit_log("u_001", "search_docs", {"keyword": "返金"}, result))

期待される出力:

{'user_id': 'u_001', 'tool_name': 'search_docs', 'arguments': {'keyword': '返金'}, 'ok': True, 'error': None}

これはシンプルですが、監査の核心はすでに入っています。

  • 動作を記録する
  • 文脈を記録する
  • 結果を記録する

よくある誤解

誤解 1:ツールの安全性はリリース前に付け足せばいい

違います。
ツールの安全性は、設計段階から入っているべきです。

誤解 2:すべての失敗はとりあえずリトライすればいい

パラメータエラーや権限エラーは、
リトライしてもリソースの無駄になるだけです。

誤解 3:読み取り操作には完全にリスクがない

読み取り操作でも、次のような問題が起きることがあります。

  • プライバシー
  • 権限越えの問い合わせ
  • 機微情報の漏えい

まとめ

この節で一番大事なのは、いくつかのエラーコードを暗記することではなく、
ツール層に対する基本的な安全意識を持つことです。

Agent が行動できるようになったら、ツール実行器はバックエンドのコアサービスと同じように、権限、検証、タイムアウト、冪等性、監査をきちんと扱う必要があります。単に「モデルの後ろに関数をつないだだけ」と考えてはいけません。

この意識を早く身につけるほど、
後のコード Agent、多ツール連携、そして本番システムは安定します。


練習

  1. サンプルに send_email ツールを追加して、そのリスクレベルをどう決めるか考えてみましょう。
  2. なぜ「リトライ可能かどうか」はエラー構造の一部であるべきなのでしょうか?
  3. データベースを読むだけのツールでも、なぜ権限制御が必要になることがあるのでしょうか?
  4. 高リスクツールに手動確認を追加するなら、確認は呼び出し前と呼び出し後のどちらに置きますか? その理由も考えてみましょう。