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?

ゼロから構築!LLMに外部ツールを使わせる自律エージェント開発

0
Posted at

「LLMに外部ツールを連携させたいけど、Function Callingの具体的な実装方法がわからない」「エージェントを構築しても、エラーハンドリングや頑健性に不安がある」──そんな悩みを抱えていませんか?

LLMを単なる対話エンジンではなく、自律的な問題解決エンジンとして活用するためには、外部ツールとの連携が不可欠です。この記事では、LLMのFunction Calling(Tool Use)機能を活用し、外部APIやツールと連携して複雑なタスクを自動実行する自律エージェントを、具体的な実装コードと共に一から構築する手順と設計パターンを解説します。本記事を読めば、あなたのLLMアプリケーションが、より賢く、よりパワフルに進化するための設計指針と実践的なコードが手に入ります。

LLMエージェントとFunction Calling(Tool Use)の基本

このセクションでは、LLMエージェントがどのように外部ツールと連携し、自律的な問題解決を実現するのか、その基本的な仕組みと主要な概念であるFunction Calling/Tool Useについて解説します。

LLMエージェントとは何か?

LLMエージェントとは、大規模言語モデル(LLM)を核として、外部ツールと連携し、与えられた目標を達成するために自律的に思考し、行動するシステムです。従来のLLMが単一のプロンプトに基づいて応答を生成するのに対し、エージェントは以下のようなサイクルを繰り返します。

  1. 目標の理解: ユーザーからの指示や目標を解釈する。
  2. 思考(Reason): 目標達成のためにどのようなステップが必要か、どのツールを使うべきかを推論する。
  3. 行動(Act): 推論に基づき、外部ツールを呼び出して情報を取得したり、操作を実行したりする。
  4. 観察(Observe): ツールの実行結果を評価し、次の行動を決定する。

この「思考→行動→観察」のサイクルを繰り返すことで、LLMエージェントは複雑なタスクや、LLMの学習データにはない最新情報や動的な情報が必要なタスクを解決できるようになります。

Function Calling / Tool Useの仕組み

LLMエージェントが外部ツールと連携するための鍵となるのが、Function Calling(OpenAIでは「Tools」、Anthropicでは「Tool Use」、Googleでは「Function Calling」)という機能です。これは、LLMが直接プログラムを実行するのではなく、「どの関数を、どんな引数で呼ぶべきか」をJSON形式で提案し、実際の処理はアプリケーション側が行う仕組みです。

具体的な実行プロセスは以下の通りです。

  1. ユーザーリクエスト: ユーザーが自然言語でタスクを要求します。
  2. モデルによるツール呼び出しの決定: LLMがユーザーの意図を分析し、タスク達成のために利用可能なツールの中から最適なものを選択します。そして、そのツールを呼び出すために必要な引数をJSON形式で出力します。
  3. アプリケーション層でのツール実行: アプリケーション層(開発者が構築するコード)がLLMから受け取ったJSONをパースし、対応するツール関数を実際の引数で実行します。例えば、外部APIへのリクエストやデータベースへの問い合わせなどです。
  4. 結果のモデルへの返却: ツール実行によって得られた結果(APIレスポンス、データベースのデータなど)を、tool_resultメッセージとしてLLMに返却します。
  5. モデルによる最終応答または追加ツール呼び出し: LLMはツール結果を考慮し、ユーザーへの最終的な自然言語応答を生成するか、必要であればさらに別のツール呼び出しを決定します。

このプロセスは、通常2回以上のLLM呼び出しを伴うマルチターン会話プロトコルで進行します。この仕組みにより、LLMの推論能力と外部システムの実行能力を組み合わせて、より高度なタスク解決が可能になります。

LLMエージェント構築の前提と環境

本記事では、LLMエージェントの構築にPythonとLangChainを使用します。LangChainは、LLMアプリケーション開発のための強力なフレームワークであり、Function Calling(Tool Use)を利用したエージェントの実装を簡素化します。

必要なライブラリと環境設定

まずは必要なライブラリをインストールします。

pip install langchain langchain-community langchain-openai pydantic tavily-python
  • langchain: LLMアプリケーション開発のコアライブラリ
  • langchain-community: コミュニティが提供する様々なコンポーネント
  • langchain-openai: OpenAIモデルとの連携
  • pydantic: データ検証と設定管理
  • tavily-python: Web検索ツールとして利用するTavily APIのクライアント

次に、APIキーを設定します。OpenAI APIとTavily APIのキーが必要です。

import os

# OpenAI APIキーの設定
os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"
# Tavily APIキーの設定 (Web検索ツール用)
os.environ["TAVILY_API_KEY"] = "YOUR_TAVILY_API_KEY"

これらのAPIキーは、それぞれのサービスでアカウントを作成し、取得してください。

最新の推奨アーキテクチャ: LangGraph

LangChainはかつてinitialize_agentAgentExecutorといったクラスを提供していましたが、これらは現在、LangGraphを用いたより柔軟で堅牢なアプローチに移行することが推奨されています。LangGraphは、エージェントの思考プロセスをグラフとして定義し、状態管理や条件分岐を容易にするためのライブラリです。本記事では、Function Callingの仕組みに焦点を当てるため、LangChainの@toolデコレータを用いたツール定義と、その実行ロジックを中心に解説します。

具体的なTool定義と実装例

ここからは、実際にLLMエージェントに利用させるツールを定義し、その実装例を見ていきます。堅牢なFunction Callingを実現するためのベストプラクティスも紹介します。

Tool定義の鉄則

LLMがツールを適切に選択し、正確な引数を生成するためには、ツールの定義が非常に重要です。以下の鉄則を守ってツールを定義しましょう。

  • ツール名: 動詞_対象_条件のように、ツールの機能を明確かつ一意に表す名前を付けます。processdoのような曖昧な動詞は避けます。
  • description (docstring): ツールの説明はLLMがツールを選択する際の重要な情報源です。以下の項目を必ず含めます。
    • Use when: どのような状況でこのツールを使うべきか。
    • Don't use when: 誤用しがちなケースや、他のツールを使うべき状況。
    • Args: 必須・任意の引数、そのデータ型や形式。
    • Examples: 具体的なツール呼び出しの例を2〜3個含めます。
  • 引数の型とバリデーション: 自由入力のstrをできるだけ減らし、EnumやPydanticによる厳密な型定義、正規表現による形式強制を優先します。これにより、LLMが生成する引数の正確性を高めます。

lookup_customer_by_emailツールの実装例

顧客のメールアドレスから顧客情報を検索するツールを例に、上記の鉄則に基づいた実装を見ていきましょう。

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="退会済み顧客も含めるかどうかのフラグ。デフォルトはFalse。")

# ツール関数の定義
@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' ツールを使用してください)。
    - 顧客の注文履歴など、顧客情報以外の詳細情報を取得したい場合。

    Args:
    - email (str, required): 検索対象の顧客メールアドレス。
    - include_inactive (bool, optional): 退会済みの顧客も含めて検索するかどうか。デフォルトは False。

    Examples:
    1. 顧客 'alice@example.com' の情報を検索する:
       lookup_customer_by_email(email="alice@example.com")
    2. 退会済みも含めて 'bob@example.com' の情報を検索する:
       lookup_customer_by_email(email="bob@example.com", include_inactive=True)
    """
    # 実際の顧客検索ロジック(ここではモックデータを使用)
    print(f"DEBUG: lookup_customer_by_email called with email={email}, include_inactive={include_inactive}")
    if email == "alice@example.com":
        return {"id": "123", "status": "active", "plan": "premium", "email": email}
    elif include_inactive and email == "bob@example.com":
        return {"id": "456", "status": "inactive", "plan": "basic", "email": email}
    return {}

# ツールリストの作成
tools = [lookup_customer_by_email]

Tavily Web検索ツールの追加

LLMエージェントが最新の情報を取得できるように、Web検索ツールを追加します。LangChainはTavily APIとの連携を簡単に実現できます。

from langchain_community.tools.tavily_research import TavilySearchResults

# Web検索ツールの定義
tavily_tool = TavilySearchResults(max_results=3) # 上位3件の結果を取得

# ツールリストに追加
tools.append(tavily_tool)

これで、lookup_customer_by_emailtavily_toolの2つのツールが利用可能になりました。

LLMとツールの連携(LangChainによるAgentExecutorの構築)

Function Callingを実際に動かすために、LLMと定義したツールを組み合わせてAgentExecutorを構築します。ここではOpenAIのChatモデルを使用します。

from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# LLMの初期化
llm = ChatOpenAI(model="gpt-4o", temperature=0) # gpt-4oはFunction Callingに最適

# プロンプトの定義
# Function Callingでは、`tools`と`agent_scratchpad`のPlaceholderが重要
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "あなたは強力なアシスタントです。ユーザーの質問に答えるために、必要に応じてツールを使用してください。"),
        MessagesPlaceholder("chat_history", optional=True), # 履歴を考慮する場合
        ("human", "{input}"),
        MessagesPlaceholder("agent_scratchpad"), # エージェントの思考過程とツール呼び出しがここに入る
    ]
)

# エージェントの作成
# create_openai_tools_agent は、OpenAIのTools機能を利用したエージェントを簡単に作成する
agent = create_openai_tools_agent(llm, tools, prompt)

# AgentExecutorの作成
# これがエージェントの実行エンジン
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

create_openai_tools_agentは、OpenAIのTools機能に特化したエージェントを作成するヘルパー関数です。agent_scratchpadは、エージェントの思考プロセスやツール呼び出しの履歴を内部的に保持するために使われます。verbose=Trueに設定すると、エージェントの思考プロセス(どのツールを呼び出し、どんな引数を生成したかなど)がコンソールに出力され、デバッグに役立ちます。

ツール呼び出しの実行例

構築したagent_executorを使って、実際にツールを呼び出すエージェントの動作を確認します。

# 実行例1: 顧客情報を検索する
print("--- 実行例1: 顧客情報を検索する ---")
result1 = agent_executor.invoke({"input": "alice@example.comの顧客情報を教えてください。"})
print(f"最終応答: {result1['output']}\n")

# 実行例2: Web検索が必要な質問
print("--- 実行例2: Web検索が必要な質問 ---")
result2 = agent_executor.invoke({"input": "今日の日本の首相の名前は何ですか?"})
print(f"最終応答: {result2['output']}\n")

# 実行例3: ツールを使わずに回答できる質問
print("--- 実行例3: ツールを使わずに回答できる質問 ---")
result3 = agent_executor.invoke({"input": "2 + 2 はいくつですか?"})
print(f"最終応答: {result3['output']}\n")

実行結果の観察:
verbose=Trueに設定しているため、LLMがどのように思考し、どのツールを選択し、どのような引数を渡して実行したかが詳細に出力されます。

  • 実行例1では、LLMがlookup_customer_by_emailツールを選択し、email="alice@example.com"という引数を生成して実行する様子が確認できます。
  • 実行例2では、LLMがtavily_tool(Web検索)を選択し、関連するキーワードで検索を実行し、その結果に基づいて回答を生成するでしょう。
  • 実行例3では、LLMがツールを使わずに自身の知識で直接回答することが確認できます。

このように、LLMはユーザーの質問内容に応じて、自律的に最適なツールを選択し、その結果を基に回答を生成します。

LLMエージェント開発で遭遇するつまずきポイントと回避策

LLMエージェント、特にFunction Callingを利用した開発では、いくつかの共通の課題に直面します。ここでは、よくあるエラーやハマりどころとその回避策を具体的に解説します。

1. 誤ツール選択

原因:
似たような機能を持つツールが複数ある場合や、ツールのdescriptionが不明瞭な場合に、LLMが適切なツールを選択できないことがあります。引数のスキーマが曖昧だと、LLMがユーザーの意図を正確に解釈しづらくなります。

回避策:

  • ツールスキーマの丁寧な設計: 各ツールの引数について、型、必須/任意、詳細な説明をPydanticなどを活用して厳密に定義します。LLMが正確な引数を提案できるかはスキーマの質に大きく依存します。
  • 明確なdescription: ツールのdescription(docstring)にUse when(いつ使うか)、Don't use when(誤用しがちなケース)、Args(引数)、Examples(使用例)を具体的に記述します。特にDon't use whenは、LLMが不適切なツールを選択するのを防ぐ上で非常に有効です。
  • 簡易Routerの導入: 多数のツールがある場合、まずLLMに「どのカテゴリのツールが必要か」を判断させ、そのカテゴリに属するツールセットのみをLLMに提示するRouterを導入することで、LLMが選択する選択肢を絞り込み、誤選択のリスクを減らすことができます。

2. 引数の破損・スキーマ不一致

原因:
LLMは正しいツールを選択したものの、生成した引数の値がツールの期待するデータ型や形式に合致しない(例: 数値が文字列になっている、必須引数が欠けている)ことがあります。また、LLMがハルシネーションを起こし、存在しない引数や不適切な値を生成することもあります。

回避策:

  • Pydanticによる厳密なスキーマ検証: ツール呼び出しの引数定義にはPydanticモデルを積極的に使用します。これにより、LLMが生成したJSONをアプリケーション側でパースする際に、型チェックや値のバリデーションを強制できます。スキーマに合わないデータはエラーとして扱われ、エージェントの回復ロジックに繋げられます。
  • Enumや正規表現による形式強制: 自由入力のstr引数を可能な限り減らし、Enum(列挙型)で選択肢を限定したり、正規表現を使って入力形式を強制したりすることで、LLMが生成する引数のバリエーションを制限し、正確性を高めます。
  • 回復ループとフォールバック: 引数破損が連続する場合や、一定回数のリトライでも成功しない場合は、処理を中断し、ユーザーに再入力を促す、またはデフォルト値で処理を進めるなどのフォールバックロジックを実装します。

3. 外部サービスエラー・ネットワーク問題

原因:
ツールが外部APIやデータベースと連携する際、以下のような問題が発生することがあります。

  • 一時的なサービス停止
  • 外部APIの内部エラー
  • レート制限
  • 認証失敗
  • ネットワーク接続問題(タイムアウト、DNS解決失敗など)

回避策:

  • リトライメカニズム: オーケストレーション層でエラーハンドリングを行い、一時的な問題(例: ネットワークエラー、レート制限)に対しては、Exponential BackoffとJitterを伴うリトライメカニズムを実装します。これにより、システムの可用性を高めます。
  • サーキットブレーカーパターン: 連続する失敗時にツール呼び出しを一時的に停止し、システム全体の負荷を軽減し、外部サービスへの無駄なリクエストを防ぎます。一定時間経過後に再度呼び出しを試みるようにします。
  • 構造化されたエラーメッセージ: ツールがエラーを検出した場合、そのエラー情報を構造化されたメッセージとしてLLMに返します。例えば、「API呼び出しが失敗しました。理由:レート制限。しばらくしてから再試行してください。」のような具体的なメッセージを返すことで、LLMが失敗を理解し、次の行動(例: リトライ、ユーザーへの説明)を決定できるようにします。

4. LLMがツールを使わずに答えてしまう

原因:
モデルがツールを使うべき状況で、自身の学習データ内の知識で回答を生成してしまうことがあります。特に、LLMの学習データ内に類似の情報がある場合や、プロンプトでの指示が曖昧な場合に発生しやすいです。

回避策:

  • プロンプトエンジニアリングによる指示の明確化: プロンプト内でツール使用の優先度を明確に指示します。
    • 「この質問に答えるためには、必ず提供されたツールを使用してください。」
    • 「もしツールを使わずに回答できる場合でも、まずはツールで情報を確認してください。」
    • 「ツールの結果が不明瞭な場合は、追加のツール呼び出しを検討してください。」
      このような具体的な指示をプロンプトに含めることで、LLMの行動を誘導します。
  • RAGとの連携: 特定の知識ベースや最新情報が必要な場合は、RAG(Retrieval-Augmented Generation)と組み合わせ、事前に情報を検索してLLMに提示することで、LLMがツールを使わずに回答してしまうリスクを減らし、より正確な情報に基づいた回答を促すことができます。

これらの回避策を講じることで、より堅牢で信頼性の高いLLMエージェントを構築することが可能になります。

LLMエージェントの設計パターンとトレードオフ

LLMエージェントを実用的なシステムとして設計する際には、さまざまな設計パターンと、それに伴うトレードオフを理解しておく必要があります。

安全性と拡張性の両立

Function Callingの大きな利点の一つは、安全性と拡張性を両立できる点です。LLMが直接プログラムを実行するのではなく、アプリ側でバリデーションを挟めるため、開発者はエラーハンドリング、権限チェック、ログ記録といったセキュリティ・運用上の重要なコントロールを自分のコードで実現できます。これにより、LLMの能力を最大限に活用しつつ、システム全体の信頼性と安全性を確保できます。

複雑なワークフローの自動化

複数のツールを連続的に、あるいは条件分岐を伴って呼び出すことで、LLMエージェントは複雑な業務ワークフローを自動化できます。例えば、「会議の議事録を要約 → 特定のキーワードに基づいてタスクを抽出し、タスク管理ツールに登録 → 関係者へメールで通知 → 次回の会議予定をカレンダーに登録」といったマルチステップの処理を、LLMの推論能力とツール連携によって実現可能です。

エラーハンドリングの重要性

前述の通り、Function Callingを用いたエージェント開発において、エラーハンドリングは非常に重要です。LLMにツール呼び出しのエラーハンドリングを完全に任せるのは危険であり、非現実的です。
オーケストレーション層とLLMで回復責任を分担することがベストプラクティスです。

  • オーケストレーション層: ネットワークエラー、レート制限、一時的なサービス停止など、技術的な問題や一時的な問題に対しては、リトライ、サーキットブレーカー、タイムアウトといったメカニズムで対処します。
  • LLM: オーケストレーション層で解決できない、あるいはビジネスロジックに関わるエラー(例: 「指定された顧客が見つかりません」)に対しては、構造化されたエラーメッセージを受け取り、それを基にユーザーへの説明、代替案の提案、または追加のツール呼び出しによる問題解決を試みます。

ReActとCodeAct、ReWoOの選択

エージェントの推論戦略にはいくつかのパターンがあり、それぞれにトレードオフが存在します。

  • ReAct (Reason + Act):

    • 特徴: LLMが「思考(Reason)」と「行動(Act)」を交互に繰り返すことでタスクを遂行します。明示的な推論ステップを通じて、透明性の高い「行動→観察」サイクルに従います。
    • 利点: ネイティブなFunction Calling機能を持たないモデルでも実装可能。推論過程がトレースしやすく、デバッグが容易。モデルが依存関係を処理してくれる。
    • 欠点: 複数ステップのタスクでは、モデル呼び出し回数が増え、遅延が大きくなる可能性がある。
    • 用途: 透明性やデバッグのしやすさが重要な場合、複雑すぎないタスク。
  • CodeAct:

    • 特徴: LLMがFunction Callingの代わりに、PythonコードやBashコマンドといった実行可能なコードを生成し、サンドボックス環境で実行します。
    • 利点: Function Callingのスキーマ管理の複雑性や表現力の制限を回避できる。複数ステップのタスクにおいて、より柔軟かつ効率的な解決が可能になる場合がある。
    • 欠点: コードパースエラー、モデルのコーディング能力への依存、セキュリティ確保のためのサンドボックス環境の構築が必須。コード実行によるリスクが高い。
    • 用途: 複雑なデータ処理やプログラミング的なタスク、高い表現力が求められる場合。安全性確保が最優先。
  • ReWoO (Reasoning with Working Memory and Observations):

    • 特徴: 複数のツール呼び出しを1回のモデル呼び出しで決定できるなど、遅延の面でReActより優れています。作業メモリと観察結果を組み合わせて推論します。
    • 利点: パフォーマンス(特に遅延)がReActよりも優れる場合がある。計画に従うのが得意。
    • 欠点: ツールの実行結果のパースや依存関係の管理を自前で行う必要があり、より多くのコードと脆弱性が生まれる可能性がある。
    • 用途: パフォーマンスが重要な場合、開発コストを許容できる場合。

Function CallingとCodeActは、表現力と安全性のトレードオフの関係にあります。Function Callingは安全性が高い反面、表現力に限界があり、CodeActは表現力が高い反面、セキュリティリスクや実装コストが高まります。

ハイブリッドな設計とレイヤードアクションスペース

Function Callingの利点とCodeActの表現力を組み合わせるために、ハイブリッドな設計も存在します。例えば、Bash実行、ファイル操作、コード実行といったアトミックな低レベル操作に限定してFunction Callingを利用し、実際の複雑なタスク処理はサンドボックス層でのコード生成に委ねるという「レイヤードアクションスペース」の設計パターンです。これにより、Function Callingの安全性とCodeActの柔軟性を両立させることができます。

ファイルシステムを外部メモリとして活用

LLMのコンテキストウィンドウには限界があります。長期的な情報を保持し、複雑なタスクを遂行するためには、ファイルシステムを外部メモリとして活用する設計パターンが有効です。LLMが生成した中間結果や、外部ツールから取得した重要な情報をファイルに書き込み、必要に応じてそれを読み込むことで、コンテキストウィンドウの制約を超えた長期記憶を実現できます。

人間による介入 (Human-in-the-Loop)

特に、重要な意思決定や外部システムへの書き込み操作を伴うエージェントでは、ヒューマン・イン・ザ・ループが不可欠です。モデルの自律性に対するコントロールポイントとして、顧客に見せる前に人間のオペレーターが表示内容をチェックする仕組みや、重要な操作の前に人間の承認を求めるフローを導入することが推奨されます。これにより、不適切な行動や意図しない結果を防ぎ、システムの信頼性を向上させます。

まとめと次の一歩

この記事では、LLMに外部ツールを使わせる自律エージェントをゼロから構築する方法を、Function Calling(Tool Use)の仕組みから具体的な実装コード、そして堅牢なシステムを構築するための設計パターンとトレードオフまで、幅広く解説しました。

主要なポイント:

  • Function Calling(Tool Use): LLMが外部の関数呼び出しを提案し、アプリケーション側で実行することで、LLMの能力を外部システムと連携させます。
  • Tool定義のベストプラクティス: 明確なツール名、詳細なdescription、Pydanticによる厳密な引数スキーマ定義が重要です。
  • エラーハンドリング: 誤ツール選択、引数破損、外部サービスエラー、LLMの誤動作といった課題に対し、スキーマ検証、リトライ、サーキットブレーカー、プロンプトエンジニアリングなどの多層的な回避策を講じることが不可欠です。
  • 設計パターンとトレードオフ: ReAct、CodeAct、ReWoOといったエージェント戦略の選択や、安全性と拡張性の両立、人間による介入の重要性を理解し、タスクの要件に応じた最適な設計を行うことが求められます。

LLMエージェントの開発は、LLMを単なるチャットボットから、現実世界の問題を解決する強力なツールへと進化させるための鍵となります。本記事で紹介した知識とコードを参考に、ぜひあなたのLLMアプリケーションをさらに高度なものにしてください。

次の一歩:
より深くエージェントの設計を学びたい場合は、LangChainやLangGraphの公式ドキュメントで、エージェントグラフの構築や、より複雑な状態管理、メモリの扱いついて学習することをお勧めします。

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?