LangGraph やその他のエージェントフレームワークを使って、LLM(大規模言語モデル)にツール呼び出しをさせる仕組みを組んでいるフロントエンド・バックエンドエンジニアに向けた内容です。デモでは動くのに本番で事故る、というパターンの原因と対策を整理しました。
チャット UI に「エージェント機能」を組み込むプロジェクトは増えています。DB 検索、チケット更新、API 呼び出しまで LLM に任せる構成です。ここには見落とされがちな設計上の落とし穴があります。
何が起きるか
典型的なエージェント構成は、ユーザーの入力を LLM が受け取り、ツールを選び、パラメータを生成し、そのまま実行する、という一直線の流れです。
User → LLM → Tool → Action という構造です。デモ動画やハッカソンではこれで十分に見えます。
問題は、LLM が「何をすべきか判断する」役割と「それを実行してよいか許可する」役割を同時に担ってしまう点です。
テキスト生成だけなら、誤った出力は単に「間違った回答」で済みます。しかしツール実行権限を持つ LLM が間違えると、それは本番データベースの誤更新や意図しない API 呼び出しといった「悪い状態変化」に直結します。プロンプトで「重要な変更の前には確認を取ってください」と指示しても、それは単なる指示文であってセキュリティ境界にはなりません。LLM は指示に従わないことがあるからです。
なぜ起きるか
原因は段階的に分解できます。
1つ目は、モデルが使えるからという理由だけで、決定的(deterministic、入力が同じなら必ず同じ結果になる処理)に書けるはずのルーティング判断まで LLM に任せてしまうことです。既知の障害パターンや、明確な条件分岐で処理できるタスクまで LLM の推論に委ねると、必要のない不確実性を持ち込むことになります。
2つ目は、LLM の出力が「自然文の説明」のまま後続処理に渡っていることです。「根本原因はおそらく〜だと思われます」という自然文は、後続のコードが安全に解釈できません。構造化されていない出力を受け取った実行系は、パースの失敗や誤読のリスクを抱えたまま動くことになります。
3つ目は、認可(authorization、その操作を実行してよいかの許可判断)の主体が明示的に設計されていないことです。「確認してください」というプロンプト内の指示だけに頼ると、モデルの気まぐれな挙動がそのまま本番環境への操作になってしまいます。人間によるレビューや、コード側のゲート(許可条件をチェックする仕組み)が介在しないアーキテクチャでは、事故の発生確率がモデルの精度にそのまま依存してしまいます。
自分のプロジェクトが該当するか確認する方法
以下の観点で、手元のエージェント実装を点検してみてください。
- LLM の出力から直接ツール呼び出しの引数を生成し、そのまま
tool.execute()のような関数に渡していないか、エージェントのコード上でトレースする - ツール定義(OpenAI の function calling や LangChain の Tool クラスなど)に、書き込み系操作(DB の UPDATE、外部 API への POST/DELETE、本番リソースの変更)が含まれていないか一覧化する
- 「確認してください」「慎重に判断してください」といった文言がシステムプロンプト内にあり、それが唯一の安全対策になっていないか確認する
- ログや監視で、LLM の判断から実行までの間に人間のレビューやコード上の検証ステップが存在するか確認する(存在しなければ即実行構成)
- リトライやエラーハンドリングの経路で、失敗時にモデルが再度自由な判断を下せる状態になっていないか確認する
該当する項目が2つ以上あれば、実行権限の設計を見直す価値があります。
対策の手順
対策は「LLM に推論させ、実行権限は与えない」という原則に沿って組み立てます。
1. ルーティングは可能な限りコードで判断する
タスクの分類を、LLM ではなく明示的な条件分岐で行える部分は分岐にします。
def classify(task):
if task.has_known_failure_signal:
return "deterministic"
if task.needs_investigation:
return "ai_investigation"
return "human_review"ここで重要なのはデフォルト値です。「わからなければ human_review」であって、「わからなければモデルに推測させる」ではありません。これによりレイテンシ、コスト、デバッグのしやすさ、回帰テストの再現性が改善します。
2. モデルの出力を構造化する
自然文の説明ではなく、後続コードが検証可能な JSON 形式で出力させます。
{
"root_cause": "...",
"severity": "medium",
"missing_information": [],
"recommended_actions": [],
"citations": []
}この形式なら、severity が high の場合は自動実行を止めて人間レビューに回す、といったコード側のゲートを機械的に実装できます。
3. 実行前にコードによるゲートを挟む
書き込み系のツール呼び出しには、モデルの判断とは独立したチェックを設けます。たとえば「対象レコード数が一定以上なら承認待ちにする」「本番環境フラグが立っている場合は必ず人間承認を挟む」といった条件は、プロンプトではなくコードで強制します。LangGraph であれば、実行前に人間の承認ステップを挟む human-in-the-loop(人間の判断を介在させる仕組み)のノードを使う設計が該当します。
4. ツールの権限をスコープで分離する
読み取り専用のツールと、書き込み・削除を伴うツールを別のセットとして管理し、後者だけ追加の認可レイヤーを通す設計にします。API キーやサービスアカウントの権限も、エージェント用に最小権限(least privilege)で分けておくと、モデルの誤判断が及ぶ範囲を物理的に絞れます。
まとめ
エージェント設計で確認しておきたい点を整理します。
- LLM の役割は「推論」であり「実行の許可」ではない、という切り分けをコード上で明示できているか
- ルーティングやゲートの判断を、プロンプトの指示文だけに頼っていないか
- モデルの出力を構造化し、severity などの値で後続処理を機械的に制御できる形にしているか
まずは自分のエージェント実装の中で、書き込み系ツールが LLM の出力から直接呼ばれている箇所を洗い出すところから始めてみてください。そこに承認ステップやコードのゲートを1つ挟むだけでも、事故の起きにくい構成に近づきます。