0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LangChain Tool/Function Callingで「Agentが適切な関数を確実に呼ぶ」ための実装パターン集

0
Posted at

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を呼ぶ」にかなり近づきます。


まとめ:最短で効く順チェックリスト

  1. ツール description に Use when / Don’t use when / Examples を入れる(最重要) ([LangChain Docs][1])
  2. 似たツールを統合して 候補数を減らす
  3. Pydantic等でスキーマを固めて 自由入力を減らす
  4. Routerで 見せるツールを絞る
  5. バリデーション失敗/0件時の 次アクションを返り値で誘導
  6. LangSmithで観測して改善(タグ/メタデータ必須) ([LangChain Docs][4])
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?