🦆

[LangChain] Tool Calling 入門

2024/10/16に公開

はじめに

こんにちは。PharmaXでエンジニアをしている諸岡(@hakoten)です。

この記事では、LangChainの「Tool Calling」の基本的な使い方と仕組みについてご紹介しています。LangChainをこれから始める方や、Tool Callingをまだあまり使ったことがない方に、ぜひ最後まで読んでいただけると嬉しいです。

環境

この記事執筆時点では、以下のバージョンで実施しています。
LangChain周りは非常に開発速度が早いため、現在の最新バージョンを合わせてご確認ください

  • langchain: 0.3.1
  • langchain-openai: 0.2.1
  • langgraph: 0.2.28
  • langsmith: 0.1.129

Tool Calling とはなにか?

LangChainでは、LLM(大規模言語モデル)から外部機能(関数など)を呼び出す機能を「Tool Calling」と呼びます。

Tool Callingは、LLMが出力する非構造的なデータを、プログラムの「関数」と結びつけるための非常に便利な仕組みです。

これは、OpenAIの Function calling や、Anthropicの Tool use などの機能に対応しています。LangChainのTool Callingは、これらの機能を抽象化し、同じインターフェースとして扱えるようにしているのが特徴です。

どのモデルがどの機能に対応しているかについては、次のような表があります。2024年10月時点では、主要なLLMはすべてTool Callingに対応しているようです。


https://python.langchain.com/docs/integrations/chat/#featured-providers

Tool Callingの基本的な使い方

LangChainのTool Callingを利用するには、次の3つのステップがあります。

  1. Tool Callingの関数を定義
  2. 定義した関数をLLMの実行に紐づける
  3. LLMから呼び出された関数を実際に実行する

1.Tool Callingの関数を定義

Tool Callingの関数を定義する方法はいくつかありますが、ここでは @tool デコレーターを使用します。以下のように、関数に @tool を付けるだけで、Tool CallingのToolとして変換することができます。

from langchain_core.tools import tool

# Toolデコレーターを関数に付ける
@tool
def add(a: int, b: int) -> int:
    """2つの値を足し算して返す"""
    return a + b

@tool デコレーターを付けた関数は、BaseTool クラスを継承したインスタンスに変換されます。このインスタンスは Runnable を継承しているため、invoke メソッドで呼び出すことが可能です。

print(add.invoke({'a': 3, 'b': 4}))
# => 7

2.定義した関数をLLMの実行に紐づける

作成したToolをLLMの実行に紐づけます。LLMとツールを紐づけるには、bind_tools というメソッドを使用します。

1回のLLM実行に対して、複数のツールを紐づけることも可能です。

prompt = ChatPromptTemplate.from_messages(
    [
        (
            'system',
            """
            与えられたメッセージに従って計算処理を呼び出してください

            """,
        ),
        ('placeholder', '{messages}'),
    ]
)
# bind_toolsで、toolを紐づける
chain = prompt | ChatOpenAI().bind_tools([add])
print(chain.invoke({'messages': ['3 + 4']}))

この呼び出し結果として、LLMからは次のような結果が出力されます。

invokeの結果
{
    "content": "",
    "additional_kwargs": {
        "tool_calls": [
            {
                "id": "call_5KVgiKrfgV5BnoT3vnZG3ncx",
                "function": {
                    "arguments": "{\"a\":3,\"b\":4}",
                    "name": "add"
                },
                "type": "function"
            }
        ],
        "refusal": null
    },
    "response_metadata": {
        "token_usage": {
            "completion_tokens": 17,
            "prompt_tokens": 92,
            "total_tokens": 109,
            "completion_tokens_details": {
                "reasoning_tokens": 0
            },
            "prompt_tokens_details": {
                "cached_tokens": 0
            }
        },
        "model_name": "gpt-3.5-turbo-0125",
        "system_fingerprint": null,
        "finish_reason": "tool_calls",
        "logprobs": null
    },
    "id": "run-e18d045a-f4d5-4e00-af5a-be879db5ea26-0",
    "tool_calls": [
        {
            "name": "add",
            "args": {
                "a": 3,
                "b": 4
            },
            "id": "call_5KVgiKrfgV5BnoT3vnZG3ncx",
            "type": "tool_call"
        }
    ],
    "usage_metadata": {
        "input_tokens": 92,
        "output_tokens": 17,
        "total_tokens": 109
    }
}

Tool Callingとして重要なのは以下の箇所です。 @tool で定義した関数の呼び出し情報が返却されていることがわかります。LLMが関数を呼び出しているかどうかは、この 「tool_calls に値が入っているか」で確認することが可能です。

...
"tool_calls": [
        {
            "name": "add",
            "args": {
                "a": 3,
                "b": 4
            },
            "id": "call_5KVgiKrfgV5BnoT3vnZG3ncx",
            "type": "tool_call"
        }
    ]
...

3. LLMから呼び出された関数を実際に実行する

LLM実行の結果として tool_calls というプロパティで関数の呼び出しが返ってきたとしても、定義した add 関数は自動的に実行されません。LLMの結果から、手動で関数を呼び出す必要があります。

前述のように、toolとして定義した関数は invoke によって実行できます。次のようにLLMから受け取った引数をinvokeに渡すことで、関数を実行することが可能です。

...
chain = prompt | ChatOpenAI().bind_tools([add])
result = chain.invoke({'messages': ['3 + 4']})
# LLMから戻ってきた引数({"a": 3,"b": 4}) をadd関数に渡す
print(add.invoke(result.tool_calls[0]['args'])))
# => 7

もう一つの方法として、tool_calls プロパティの中身をそのまま渡すことも可能です。この場合、戻り値として ToolMessage クラスのインスタンスが返されます。

...
chain = prompt | ChatOpenAI().bind_tools([add])
result = chain.invoke({'messages': ['3 + 4']})
# LLMからtool_callの値をそのままadd関数に渡す
print(add.invoke(result.tool_calls[0])))
# ToolMessageが結果として返る
# => content='7' name='add' tool_call_id='call_4B6CQ7J3ENR64LOeLimPe3oj'

以上が、@toolbind_tools を使った基本的な Tool Callingの使い方になります。

これまでのコードは次の通りです。

コード全体
from langchain_core.tools import tool

# toolの定義
@tool
def add(a: int, b: int) -> int:
    """2つの値を足し算して返す"""
    return a + b

# プロンプトの定義
prompt = ChatPromptTemplate.from_messages(
    [
        (
            'system',
            """
        与えられたメッセージに従って計算処理を呼び出してください

        """,
        ),
        ('placeholder', '{messages}'),
    ]
)

# bind_toolsでLLMモデルとtoolを紐づける
chain = prompt | ChatOpenAI().bind_tools([add])
# LLMから関数の呼び出し結果を取得
result = chain.invoke({'messages': ['3 + 4']})

# 引数のみで呼び出す
print(add.invoke(result.tool_calls[0]['args']))
# LLMからtool_callの値をそのままadd関数に渡して関数を実行
print(add.invoke(result.tool_calls[0]))

Tool Callingの仕組み

ここでは、「Tool Calling」がどのように実行されるか、その仕組みについて簡単に説明します。今回は、OpenAIモデルのFunction Callingをベースに話を進めます。

前述の通り、「Tool Calling」は各LLMモデルが提供する関数の外部呼び出し機能を抽象化しています。@tool などで作成した関数は、LangChainの内部で、それぞれのLLMの関数呼び出し機能へ変換されて実行されます。

例えば、OpenAIの Function Calling を実行する際には、以下のようなパラメータをLLMに渡す必要があります。

  • name: 関数名
  • description: 関数の説明
  • parameters: 関数の引数とその説明

以下は、OpenAIの公式ページにあるサンプルになります。

{
    "name": "get_delivery_date",
    "description": "Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'",
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "The customer's order ID.",
            },
        },
        "required": ["order_id"],
        "additionalProperties": false,
    }
}

OpenAIのモデルを使う場合、Tool Callingのために定義した関数(Tool)は、最終的にこれらのパラメータに変換されます。

BaseTool

Toolとして定義した関数を各LLMの関数呼び出し機能へ変換する際に重要なクラスとして、BaseTool というクラスがあります。BaseToolRunnable を継承したクラスであり、invoke メソッドを使って実行できるToolの基盤となるクラスです。

前述の @tool デコレーターを使用した場合も、この BaseTool のインスタンスに変換されます。

BaseToolのコード

BaseToolは、次のようなプロパティを持っています。

プロパティ名 説明
name ツールの目的を明確に伝えるための一意の名前。
description モデルにツールの使い方や目的、使用用途を伝えるための詳細情報。いくつかの使用例を説明に含めることもできる。
args_schema ツールの入力引数を検証および解析するためのPydanticモデルクラス。

LangChainでは、@tool デコレーターなどで定義した関数を一度 BaseTool に変換し、その後、OpenAIやAnthropicなど各LLMモデル用のパラメータへと変換しています。(@toolデコレーター => BaseTool => モデルのパラメタ)

  • 関数名 -> name
  • 関数の説明 -> description
  • 各引数の型や説明 -> args_schema

このように、LangChainでは、特定のクラスや関数を BaseTool という抽象クラスに一度変換することで、各LLMモデルのTool Calling機能への接続を柔軟に行っています。

@tool デコレーターの、StructuredToolへの変換コードはこのあたり
bind_tool メソッドの、OpenAIへのパラメタへの変換コードはこのあたり

エージェントを使ったToolの呼び出し

前述のとおり、Toolとして定義した関数は bind_tool でモデルに紐づけただけでは自動的に実行されません。ここでは、LLMモデルから呼び出されたToolを実際に実行するための方法として、エージェントを使った呼び出し方法について説明します。

LangChainでは、ある特定の処理の制御をLLMに判断させるシステムを「エージェント」と呼んでいます。エージェントを活用することで、Tool Callingをアプリケーションの実行フローに組み込むことが容易になります。

LangGraph(create_react_agent)

まずは、LangGraphを使ったエージェントの例を紹介します。LangGraphでは、LLMに特定の処理判断を行わせ、Toolの実行と連携させるcreate_react_agent というテンプレートの関数が提供されています。

関数名の通り、これはReActのフローを再現したグラフで、次のような流れで動作します。

  1. LLMが与えられたプロンプトに基づき、Toolを使用すべきか判断する
  2. Toolを使用する場合は、連携しているToolを呼び出し、その結果を追加する
  3. Toolの結果をもとに、再度回答を導き出せるか判断する(2~3を繰り返し、結果が得られれば終了)

こちらは、公式ドキュメントから引用した create_react_agent フローのグラフです。

create_react_agent リファレンス

以下はシーケンス図です。

create_react_agent リファレンス

コード例として、add 関数を create_react_agent で動作させてみます。

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

# toolの定義
@tool
def add(
    a: Annotated[int, '一つ目の値'],
    b: Annotated[int, '二つ目の値'],
) -> int:
    """2つの値を足し算して返す"""
    return a + b

# プロンプトの定義
prompt = ChatPromptTemplate.from_messages(
    [
        (
            'system',
            '与えられたinputに従って計算処理を呼び出してください',
        ),
        ('placeholder', '{messages}'),
    ]
)

# エージェントの作成
agent = create_react_agent(
  model=ChatOpenAI(model='gpt-3.5-turbo'),
  tools=[add],
  state_modifier=prompt
)

# エージェントの実行
result = agent.invoke({'messages': ['3 + 4の計算結果は?']})
print(result['messages'][-1].content)
# => 3 + 4の計算結果は7です。

create_react_agent では、LLMのモデルとToolを指定します。また、プロンプトなど特定のパラメタを上書きすることも可能です。

動作したグラフを LangSmithで見ると以下のようになります。

LLMの実行後にToolの関数が実際に実行されていることがわかります。

AgentExecutor(create_tool_calling_agent)

LangGraphを使わずにLangChainでエージェントを実行する方法としては、AgentExecutor を使用します。AgentExecutor を利用するには、まず create_tool_calling_agent という関数を使って、ToolとLLMを連携させたRunnableを作成します。

このRunnableを AgentExecutor インスタンスを介して invoke メソッドで実行すると、Toolの関数を実行することができます。
create_tool_calling_agent の内部では、bind_tool メソッドによってモデルとToolの紐づけが行われています。

先ほど同様 add 関数を使って動作させた例は以下の通りです。プロンプトの設定や引数の指定方法は若干異なりますが、基本的な使い方としては、create_react_agentに似ています。

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

# toolの定義
@tool
def add(
    a: Annotated[int, '一つ目の値'],
    b: Annotated[int, '二つ目の値'],
) -> int:
    """2つの値を足し算して返す"""
    return a + b

# プロンプトの定義
prompt = ChatPromptTemplate.from_messages(
    [
        (
            'system',
            '与えられたinputに従って計算処理を呼び出してください',
        ),
        ('human', '{input}'),
        # Placeholders fill up a **list** of messages
        ('placeholder', '{agent_scratchpad}'),
    ]
)

# エージェントを作成
agent = create_tool_calling_agent(ChatOpenAI(model='gpt-3.5-turbo'), [add], prompt)

# エージェントを実行
agent_executor = AgentExecutor(agent=agent, tools=[add])
result = agent_executor.invoke({'input': '3 + 4の計算結果は?'})
print(result)
# => {'input': '3 + 4の計算結果は?', 'output': '3 + 4の計算結果は、7です。'}

3 + 4の計算結果は、7です。 のように計算された結果が返ってきます。

実行結果をLangSmithで見ると以下のようになります。
LLMによるToolの利用判定の後に実際にToolの関数が実行されていることがわかります。

Toolの作り方

ここまでは、主に @tool デコレーターを使ったToolの定義方法を紹介してきましたが、Tool Callingに使用するToolの定義には、他にもいくつかの方法があります。

ここでは、自分でToolを定義するための方法について説明します。

大きく分けて、以下の3つの方法でToolを作成することが可能です。

  1. 関数から変換
  2. Runnableから変換
  3. BaseToolを継承してToolを実装

関数から変換

@tool デコレーター

これまでの説明でも使用してきましたが、定義した関数に @tool というデコレーターをつける方法です。

@tool
def add(
    a: Annotated[int, '一つ目の値'],
    b: Annotated[int, '二つ目の値'],
) -> int:
    """2つの値を足し算して返す"""
    return a + b

StructuredTool

関数から変換するもう一つの方法として、StructuredToolクラスの from_function を使う方法があります。

以下は、公式ドキュメントのサンプルコードになります。

from langchain_core.tools import StructuredTool


def multiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b


async def amultiply(a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b

# 引数に関数を渡し、StructuredToolのインスタンスを生成する
calculator = StructuredTool.from_function(func=multiply, coroutine=amultiply)

print(calculator.invoke({"a": 2, "b": 3}))
print(await calculator.ainvoke({"a": 2, "b": 5}))

通常の関数は、funcパラメタ、async関数は、coroutineパラメタで指定します。

Runnableからの変換(as_tool)

Tool(BaseTool)は、Runnableのため、RunnableからもToolへの変換が可能です。
変換には as_toolというメソッドを使います。as_toolを使うと、特定のChainをそのままToolとして実行することができます。

以下に例を示します。

# 計算を行うLLMのchainを作成する
prompt = ChatPromptTemplate.from_messages(
    [('system', '渡された内容をもとに計算してください'), ('placeholder', '{messages}')]
)
tool_chain = prompt | ChatOpenAI() | StrOutputParser()

# as_toolでchainをtoolに変換する
tool = tool_chain.as_tool(name='add_tool', description='2つの値を足し算して返す')

# エージェントにツールとして渡す
agent = create_react_agent(
    ChatOpenAI(model='gpt-4o'),
    tools=[tool],
)

result = agent.invoke({'messages': ['3 + 4の計算結果は?']})
print(result['messages'][-1].content)
# => 3 + 4の計算結果は7です。

実行結果をLangSmithで見ると以下のようになります。

BaseToolを継承したサブクラスを作る

変換を行わずにBaseToolを直接継承して、Toolを作成することができます。

@tool
def add(a: int, b: int) -> int:
    """2つの値を足し算して返す"""
    return a + b

例えば @toolデコレーターを使って変換していたToolをBaseToolに置き換えると次のようになります。

class AddInput(BaseModel):
    a: int = Field(description='一つ目の値')
    b: int = Field(description='二つ目の値')


class AddTool(BaseTool):
    name: str = 'Add'
    description: str = '2つの値を足し算して返す'
    args_schema: type[BaseModel] = AddInput
    return_direct: bool = True

    def _run(self, a: int, b: int, run_manager: CallbackManagerForToolRun | None = None) -> str:
        """2つの値を足し算して返す"""
        return a + b

    async def _arun(
        self,
        a: int,
        b: int,
        run_manager: AsyncCallbackManagerForToolRun | None = None,
    ) -> str:
        """2つの値を足し算して返す。"""
        return self._run(a, b, run_manager=run_manager.get_sync())

使い方は、他のToolと同様に bind_tools で生成したインスタンスを引数に渡します。

chain = prompt | ChatOpenAI().bind_tools([AddTool()])
result = chain.invoke({'messages': ['3 + 4']})

その他(公式ドキュメント)

https://python.langchain.com/docs/how_to/custom_tools/

その他、Toolの実装については、公式のドキュメントも参照ください。

Structured Output

https://python.langchain.com/docs/how_to/structured_output/

Tool Callingと類似の機能として、Structured Output を紹介します。この機能は、LLMの戻り値を構造化されたクラス(Pydanticなど)に変換して返すものです。

以下は、そのコード例です。

# 構造化されたクラスを用意する。
class OutputModel(BaseModel):
    """
    食材の構成要素
    """

    name: str = Field(..., description='食材の名前')
    weight: str = Field(..., description='食材の重さ')
    protein: str = Field(..., description='食材の蛋白質')
    fat: str = Field(..., description='食材の脂肪')
    carbohydrate: str = Field(..., description='食材の炭水化物')


prompt = ChatPromptTemplate.from_messages(
    [
        (
            'system',
            """
            与えられた質問に対して、構成要素を返してください
            """,
        ),
        ('placeholder', '{messages}'),
    ]
)
# LLM実行時に、クラスを指定することで、結果を指定したクラスで受け取れる
chain = prompt | ChatOpenAI(model='gpt-4o', temperature=0).with_structured_output(OutputModel)

Structured Output にはいくつかの変換方法がありますが、その中でも「function_calling」という方式で変換する場合、これは内部では、Tool Callingを利用して変換しています。

以下は、with_structured_output 関数のコードの一部です。内部では bind_tool と同じく convert_to_openai_tool という関数でOpenAI形式のパラメータ変換が行われ、bind_tool が呼び出されています。

https://github.com/langchain-ai/langchain/blob/3f74dfc3d8efb7c58065a336b12d99853d275e15/libs/partners/openai/langchain_openai/chat_models/base.py#L1414-L1425

  • LLMのTool Callingの結果を構造化した結果に変換するのが「Structured Output」
  • LLMのTool Callingの結果を関数の呼び出し情報に変換するのが「Tool Calling」

という違いがありますが、Structured Output も内部では、Tool Callingを活用した機能となります。

Structured Output については、次の記事もご覧いただけると幸いです。

https://zenn.dev/pharmax/articles/8ed156e9ec9a68

終わりに

今回は、LangChainの「Tool Calling」について、その基本的な使い方や仕組みをご紹介しました。Tool Callingを利用することで、LLMとプログラミングの接続がよりシームレスになりアプリケーション開発が楽になります。LangChainを活用したい方にとって、この記事が参考になれば幸いです。

PharmaXでは、AIやLLMに関連する技術の活用を積極的に進めています。もし、この記事が興味を引いた方や、LangGraphの活用に関心がある方は、ぜひ私のXアカウント(@hakoten)やコメントで気軽にお声がけください。PharmaXのエンジニアチームで一緒に働けることを楽しみにしています。

まずはカジュアルにお話できることを楽しみにしています!

PharmaXテックブログ

Discussion