[LangChain] Tool Calling 入門
はじめに
こんにちは。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つのステップがあります。
- Tool Callingの関数を定義
- 定義した関数をLLMの実行に紐づける
- 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'
以上が、@tool
と bind_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
というクラスがあります。BaseTool
は Runnable
を継承したクラスであり、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のフローを再現したグラフで、次のような流れで動作します。
- LLMが与えられたプロンプトに基づき、Toolを使用すべきか判断する
- Toolを使用する場合は、連携しているToolを呼び出し、その結果を追加する
- Toolの結果をもとに、再度回答を導き出せるか判断する(2~3を繰り返し、結果が得られれば終了)
こちらは、公式ドキュメントから引用した 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を作成することが可能です。
- 関数から変換
- Runnableから変換
- 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']})
その他(公式ドキュメント)
その他、Toolの実装については、公式のドキュメントも参照ください。
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
が呼び出されています。
- LLMのTool Callingの結果を構造化した結果に変換するのが「Structured Output」
- LLMのTool Callingの結果を関数の呼び出し情報に変換するのが「Tool Calling」
という違いがありますが、Structured Output
も内部では、Tool Callingを活用した機能となります。
Structured Output
については、次の記事もご覧いただけると幸いです。
終わりに
今回は、LangChainの「Tool Calling」について、その基本的な使い方や仕組みをご紹介しました。Tool Callingを利用することで、LLMとプログラミングの接続がよりシームレスになりアプリケーション開発が楽になります。LangChainを活用したい方にとって、この記事が参考になれば幸いです。
PharmaXでは、AIやLLMに関連する技術の活用を積極的に進めています。もし、この記事が興味を引いた方や、LangGraphの活用に関心がある方は、ぜひ私のXアカウント(@hakoten)やコメントで気軽にお声がけください。PharmaXのエンジニアチームで一緒に働けることを楽しみにしています。
まずはカジュアルにお話できることを楽しみにしています!
PharmaXエンジニアチームのテックブログです。エンジニアメンバーが、PharmaXの事業を通じて得た技術的な知見や、チームマネジメントについての知見を共有します。 PharmaXエンジニアチームやメンバーの雰囲気が分かるような記事は、note(note.com/pharmax)もご覧ください。
Discussion