LangChain Tool/Function Callingで「Agentが適切な関数を確実に呼ぶ」ための実装パターン集(Router + スキーマ + 検証/リトライ)
LangChain の Tool Calling(旧Function Calling)は便利ですが、運用でハマるのはだいたいここです。
- 似たツールが多いと 誤ツール選択する
- ツールは合ってるのに 引数が微妙に壊れる
- 0件/エラー後に そのまま薄い回答を返す
- モデルが「ツール使わずに答えちゃう」ことがある
本記事では、モデルに丸投げせずに、設計とガードレールで「適切なツール呼び出し率」を上げる実装を紹介します。
1. まず前提:Tool Callingの“精度”はツール定義でほぼ決まる
LangChain公式Docsでも、ツールは「関数 + 入力スキーマ」であり、モデルはそのスキーマに沿って呼び出します。つまり、スキーマと説明が雑だと、選択も引数も雑になります。 ([LangChain Docs][1])
ツール定義の鉄則(最重要)
-
ツール名は
動詞_対象_条件で一意に(曖昧動詞process/do/handleは避ける) -
description(docstring)に必ず書く
- Use when(いつ使う?)
- Don’t use when(誤用しがちなケース)
- Args(必須/任意、形式)
- Examples(2〜3個)
-
自由入力
strを増やさない(Enumや形式強制を優先)
2. サンプル:誤選択を減らす “良いTool” の作り方(@tool + Pydantic)
LangChain公式は @tool が最も簡単な定義方法で、型ヒントが入力スキーマになります。 ([LangChain Docs][1])
2-1. ツールが少ない構成(まずはここから)
from typing import Literal, Optional
from pydantic import BaseModel, Field
from langchain.tools import tool
class CustomerLookupArgs(BaseModel):
email: str = Field(..., description="顧客メール。例: alice@example.com")
include_inactive: bool = Field(False, description="退会済みも含めるか")
@tool("lookup_customer_by_email", args_schema=CustomerLookupArgs)
def lookup_customer_by_email(email: str, include_inactive: bool = False) -> dict:
"""
Use when:
- メールから顧客ID/契約状態/プランを特定したい
Don't use when:
- 顧客名の曖昧検索(その場合は search_customer_by_name を作る)
- 最新ニュース検索(web_search を使う)
Returns:
{"found": bool, "customer": {...} | None}
"""
# 例: DBアクセス(ダミー)
if email.lower() == "alice@example.com":
return {"found": True, "customer": {"id": "cus_123", "plan": "pro", "active": True}}
return {"found": False, "customer": None}
ポイント:
- 引数を Pydantic で固める(メール形式のバリデーション等も足せる)
- “使う/使わない”を明記して、モデルの迷いを消す
3. 「モデルに選ばせる前」に絞る:Router(候補ツール集合の縮小)
ツールが増えた瞬間から、誤選択が発生します。
対策はシンプルで、状況に応じて “見せるツール” を減らすのが最強です。
LangChainの Agent は create_agent(LangGraphベースの実行基盤)を提供しており、ツールとモデルを組み合わせてループ実行できます。 ([LangChain Docs][2])
3-1. Routerの考え方(超実務)
- intent分類(例:
db_lookup/web_search/calc/no_tool)を先に決める - intentに応じて toolsリスト自体を差し替える
- これだけで誤選択率がガクっと下がります
3-2. 簡易Router(LLM分類→tools差し替え)
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
llm = ChatOpenAI(model="gpt-5", temperature=0)
class Route(BaseModel):
intent: Literal["db", "web", "no_tool"] = Field(..., description="どの系統のツールが必要か")
reason: str
router_llm = llm.with_structured_output(Route)
def route(user_text: str) -> Route:
return router_llm.invoke(
f"""次のユーザー要求を分類してください:
- db: 社内DB/顧客情報/チケットなど内部データが必要
- web: 外部の最新情報が必要
- no_tool: 一般知識で回答できる
ユーザー要求: {user_text}
"""
)
# ツール群(例)
tools_db = [lookup_customer_by_email] # 本当は他にも入れる
tools_web = [] # 例: web_search tool
tools_none = []
def build_agent_for_intent(intent: str):
tools = {"db": tools_db, "web": tools_web, "no_tool": tools_none}[intent]
return create_agent(
llm,
tools=tools,
system_prompt=(
"必要な場合のみツールを呼び出す。"
"内部データが必要なら必ずDB系ツールを使う。"
),
)
def run(user_text: str):
r = route(user_text)
agent = build_agent_for_intent(r.intent)
return agent.invoke({"messages": [{"role": "user", "content": user_text}]})
create_agent は LangGraphベースの実行基盤でエージェントを構築します(公式Docs)。 ([LangChain Docs][2])
4. 「選んだ後」に壊れない:引数検証・エラー整形・リトライ
Tool Calling運用の事故は、実は **「ツール選択」より「引数の微破損」**が多いです。
4-1. StructuredToolの “validation error handling” を使う
StructuredTool は Runnable で、with_retry なども利用できます。さらに handle_validation_error / handle_tool_error があります。 ([reference.langchain.com][3])
つまり「バリデーションで落ちたら、モデルに修正させる」設計がやりやすい。
(概念例)
- args_schemaで落ちたら、“何が足りないか” をテキストとして返す
- Agentはそれを見て 同じツールを正しい引数で再実行しやすくなる
4-2. エラーは例外で落とさず “型付きで返す” が強い
おすすめの返却フォーマット:
{"ok": false, "error_code": "MISSING_EMAIL", "message": "...", "required_fields": ["email"]}
これにすると、LLMは次の手を打ちやすくなります(再質問/再実行)。
5. 0件・曖昧・失敗のときの「次アクション」を固定する
ツールが0件を返した時に、
- そのまま薄い回答で終わる
- “たぶんこう”で捏造する
…が起きがちです。
対策は ツールの返り値に next_action_hint を入れるのが実務で効きます。
@tool("lookup_customer_by_email", args_schema=CustomerLookupArgs)
def lookup_customer_by_email(email: str, include_inactive: bool = False) -> dict:
...
if not found:
return {
"found": False,
"customer": None,
"next_action_hint": "メールが違う可能性。ユーザーに正しいメールを確認するか、氏名検索ツールを提案して。"
}
6. LangSmithで「誤ツール選択」を観測して潰す(これがないと改善できない)
LangChain公式Docsでは、create_agent で作ったエージェントは LangSmith tracing をサポートし、tool callや意思決定を含むトレースが取れる、と説明されています。 ([LangChain Docs][4])
6-1. 有効化(環境変数)
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=...
export LANGSMITH_PROJECT=my-agent-project
6-2. tags/metadata を付けて原因特定しやすくする
公式Docsにも tags/metadata の例があります。 ([LangChain Docs][4])
response = agent.invoke(
{"messages": [{"role": "user", "content": "alice@example.com の契約プランは?"}]},
config={"tags": ["prod", "tool-routing"], "metadata": {"tenant": "acme", "version": "v1"}}
)
7. “回帰テスト”で確実にする:境界ケースだけでいい
ツール呼び出しの品質は、ユニットテストというより 回帰テストが効きます。
例(やること)
- ありがちな入力20〜50件だけ用意
- 期待する tool 名・最低限の args を検査
- description やルーティングを変えたら必ず回す
ここまでやると「確実に適切なfunctionを呼ぶ」にかなり近づきます。
まとめ:最短で効く順チェックリスト
- ツール description に Use when / Don’t use when / Examples を入れる(最重要) ([LangChain Docs][1])
- 似たツールを統合して 候補数を減らす
- Pydantic等でスキーマを固めて 自由入力を減らす
- Routerで 見せるツールを絞る
- バリデーション失敗/0件時の 次アクションを返り値で誘導
- LangSmithで観測して改善(タグ/メタデータ必須) ([LangChain Docs][4])