1. モチベーション
toolsについてドキュメントを読んでたんですよ。
いや、ちょっとわかりづらい。
Conceptual guideを見ていたらこの並びなので、toolsを先に読み始めたんだけど、ちょっと飛ばしてtool callingを読んだらこっちの方がわかりやすかったので紹介したいな・・・というのがモチベーション。(偉そうに・・・w)
ドキュメントを読みつつ、進めていきます。
ドキュメントはこちら
っと読み進める前に、大事なことがこのページの冒頭に書かれています。
Remember, while the name "tool calling" implies that the model is directly performing some action, this is actually not the case! The model only generates the arguments to a tool, and actually running the tool (or not) is up to the user.
はい、翻訳
「ツール呼び出し」という名前は、モデルが直接何らかのアクションを実行していることを意味しますが、実際にはそうではないことを忘れないでください。モデルはツールに引数を生成するだけであり、実際にツールを実行するかどうかはユーザー次第です。
つまりは
tool calling = 引数の生成
ってことですね。これを頭に入れて進めていきましょう。
2. バージョン情報
バージョン情報
Python 3.10.8
langchain==0.3.7
python-dotenv
langchain-openai==0.2.5
langgraph>0.2.27
langchain-core
langchain-community==0.3.5
っちゅう事でレッツラゴー
3. とにかく試してみよう
「試してみる」
ドキュメントを呼んでもいまいちわからん。結果的に手を動かしてみることが一番良さげでした。
ドキュメントを読むだけでわかる人はきっと「えらい!」と思う。
3-1. ライブラリ
import os
from langchain_openai import AzureChatOpenAI
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from langchain_core.output_parsers import PydanticToolsParser
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate
3-2. 環境設定
load_dotenv(dotenv_path='.env')
END_POINT = os.getenv("END_POINT")
API_KEY = os.getenv("API_KEY")
3-3. modelの用意
llm = AzureChatOpenAI(
model='gpt-4o-mini',
azure_endpoint=os.getenv("END_POINT"),
api_version=os.getenv("API_VERSION"),
api_key=os.getenv("API_KEY")
)
いつも通り、gpt-4o-miniを使います。
3-4. シンプルな関数を使う方法
3-4-1. toolsの用意
@tool
def add(a: int, b: int) -> int:
"""Add two integers.
Args:
a: First integer
b: Second integer
"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers.
Args:
a: First integer
b: Second integer
"""
return a * b
@tool
def get_name_age(name: str, age: int) -> int:
"""Get name and age.
Args:
name: Name
age: Age
"""
return name, age
For a model to be able to call tools, we need to pass in tool schemas that describe what the tool does and what it's arguments are.
モデルがツールを呼び出せるようにするには、ツールの機能とその引数の説明をDocstringsにしっかりと記述する必要があります。これが大事です。
多分、日本語でもOKじゃないかな。
3-4-2. LLMモデルにバインド
tools = [add, multiply, get_name_age]
llm_with_tools = llm.bind_tools(tools)
GitHubをみても何をしているかはわからなかったけど、モデルにバインドする必要があります。
3-4-3. 実行(テキストを入力)
query = "What is 3 * 12?"
result = llm_with_tools.invoke(query)
print(result)
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_******************', 'function': {'arguments': '{"a":3,"b":12}', 'name': 'multiply'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 134, 'total_tokens': 151, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_******************', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='******************', tool_calls=[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_******************', 'type': 'tool_call'}], usage_metadata={'input_tokens': 134, 'output_tokens': 17, 'total_tokens': 151, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
toolsをバインドしたllm_with_tools
もRunnable
なんですね!
invoke関数
を使うことができます。
result.tool_calls
# [{'name': 'multiply',
# 'args': {'a': 3, 'b': 12},
# 'id': 'call_******************',
# 'type': 'tool_call'}]
inputが3×12の答えを求めていますので、使う関数名の「multiply」と引数となるべき「3」と「12」を抽出することに成功しました。
3-4-4. 実行(HumanMessageを入力する方法)
実はテキストを直接入力すると、HumanMessageに自動変換されています。
そこで、messagesにして入力しても同じになります。
messages = [HumanMessage(query)]
result = llm_with_tools.invoke(messages)
result
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_******************', 'function': {'arguments': '{"a":3,"b":12}', 'name': 'multiply'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 134, 'total_tokens': 151, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': '******************', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-******************', tool_calls=[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_******************', 'type': 'tool_call'}], usage_metadata={'input_tokens': 134, 'output_tokens': 17, 'total_tokens': 151, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
チャット履歴を残すならこのresult
をmessages
にアペンドしてあげればOK。前にやりましたね。
result.additional_kwargs
# {'tool_calls': [{'id': 'call_******************',
# 'function': {'arguments': '{"a":3,"b":12}', 'name': 'multiply'},
# 'type': 'function'}],
# 'refusal': None}
出力のtool_calls
を見てみると、必要な情報がここに抽出されてます。
3-4-5. 実行(ChatPromptTemplateを使う方法入力)
ChatPromptTemplateも当然使えます。
query = "ドク・ブラウンはバックトゥーザ・フューチャーの登場人物の一人で、65歳です。マーティー・マクフライは17歳の高校生です。二人の年齢を足し合わせてください。"
prompt = ChatPromptTemplate([
("system", "あなたは優秀なAIで、様々な要求に回答ができます。"),
("user", '{query}')
])
prompt_value = prompt.invoke({'query': query})
prompt_value
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_******************', 'function': {'arguments': '{"name": "ドク・ブラウン", "age": 65}', 'name': 'get_name_age'}, 'type': 'function'}, {'id': 'call_******************', 'function': {'arguments': '{"name": "マーティー・マクフライ", "age": 17}', 'name': 'get_name_age'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 65, 'prompt_tokens': 202, 'total_tokens': 267, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_******************', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-******************', tool_calls=[{'name': 'get_name_age', 'args': {'name': 'ドク・ブラウン', 'age': 65}, 'id': 'call_******************', 'type': 'tool_call'}, {'name': 'get_name_age', 'args': {'name': 'マーティー・マクフライ', 'age': 17}, 'id': 'call_******************', 'type': 'tool_call'}], usage_metadata={'input_tokens': 202, 'output_tokens': 65, 'total_tokens': 267, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
Back to the Future、大学受験が終わった翌日に友達の家にみんな集まって、レンタルしたVHSを見た懐かしい思い出があります。笑
あ、ドク・ブラウンじゃ無くて、「エメット・ブラウン」でしたね!汗
result.tool_calls
# [{'name': 'get_name_age',
# 'args': {'name': 'ドク・ブラウン', 'age': 65},
# 'id': 'call_qxaLzQBz3XZwnch7tULX3K4b',
# 'type': 'tool_call'},
# {'name': 'get_name_age',
# 'args': {'name': 'マーティー・マクフライ', 'age': 17},
# 'id': 'call_1h7AnhfBPCzaPkjTFXjUqfbw',
# 'type': 'tool_call'}]
同じように名前と年齢を抽出することに成功しました。
3-5. tool callingの出力の型定義をする方法
以前やった、Pydanticの使い方を復習しておきましょう。
思い出したところで、まずは型定義を行います。
3-5-1. 出力の型定義
class CalculatorInput(BaseModel):
a: int = Field(..., description="first number")
b: int = Field(..., description="second number")
class GetNameAgeInput(BaseModel):
name: str = Field(..., description="name")
age: int = Field(..., description="age")
CalculatorInput
とGetNameAgeInput
classを作って、出力の型定義を行います。
ここではPydanticを使いますが、Annotated
でも定義可能です。
3-5-2. 型定義を用いたtoolの設定
@tool("addition-tool", args_schema=CalculatorInput, return_direct=True)
def add(a: int, b: int) -> int:
"""Add two integers.
Args:
a: First integer
b: Second integer
"""
return a + b
@tool("multiplication-tool", args_schema=CalculatorInput, return_direct=True)
def multiply(a: int, b: int) -> int:
"""Multiply two integers.
Args:
a: First integer
b: Second integer
"""
return a * b
@tool("get-name-age-tool", args_schema=GetNameAgeInput, return_direct=True)
def get_name_age(name: str, age: int) -> int:
"""Get name and age.
Args:
name: Name
age: Age
"""
return name, age
@tool
デコレータに引数を渡しておきます。ここで型定義したclass名をargs_schema
の引数に渡しておきます。
では実行してみましょう。
3-5-3. 型定義されたtoolsの実行
実行方法は型定義していない場合と同じなので、説明は省きます。
tools = [add, multiply, get_name_age]
llm_with_tools = llm.bind_tools(tools)
query = """以下の文章から名前と年齢の一覧を作りたいです。
ドク・ブラウンはバックトゥーザ・フューチャーの登場人物の一人で、65歳です。
マーティー・マクフライは17歳の高校生です。二人の年齢を足し合わせてください。"""
messages = [HumanMessage(query)]
result = llm_with_tools.invoke(messages)
result
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_******************', 'function': {'arguments': '{"name": "ドク・ブラウン", "age": 65}', 'name': 'get-name-age-tool'}, 'type': 'function'}, {'id': 'call_******************', 'function': {'arguments': '{"name": "マーティー・マクフライ", "age": 17}', 'name': 'get-name-age-tool'}, 'type': 'function'}, {'id': 'call_******************', 'function': {'arguments': '{"a": 65, "b": 17}', 'name': 'addition-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 86, 'prompt_tokens': 240, 'total_tokens': 326, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_b705f0c291', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-******************', tool_calls=[{'name': 'get-name-age-tool', 'args': {'name': 'ドク・ブラウン', 'age': 65}, 'id': 'call_******************', 'type': 'tool_call'}, {'name': 'get-name-age-tool', 'args': {'name': 'マーティー・マクフライ', 'age': 17}, 'id': 'call_******************', 'type': 'tool_call'}, {'name': 'addition-tool', 'args': {'a': 65, 'b': 17}, 'id': 'call_******************', 'type': 'tool_call'}], usage_metadata={'input_tokens': 240, 'output_tokens': 86, 'total_tokens': 326, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
result.additional_kwargs
# {'tool_calls': [{'id': 'call_******************',
# 'function': {'arguments': '{"name": "ドク・ブラウン", "age": 65}',
# 'name': 'get-name-age-tool'},
# 'type': 'function'},
# {'id': 'call_******************',
# 'function': {'arguments': '{"name": "マーティー・マクフライ", "age": 17}',
# 'name': 'get-name-age-tool'},
# 'type': 'function'},
# {'id': 'call_******************',
# 'function': {'arguments': '{"a": 65, "b": 17}', 'name': 'addition-tool'},
# 'type': 'function'}],
# 'refusal': None}
しっかりと名前と年齢を抜き出して、さらに、年齢のみを抽出しています。
tool_callingは引数の作成だけなので、ここまでです。
実は、型定義しなかった時と出力が変わってしまっています。
型定義した方は年齢を足すための関数と、引数が抽出されています。不必要なmultiply
は呼ばれていません。
ちなみに、「ドク・ブラウン」は間違いで「エメット・ブラウン」でした。汗
だって、マーティーがドクって呼んでるんだもん。
4. よくわからないこと
実はよくわからな記述がドキュメントにはあります。型定義だけで引数を作る方法もあるようなのです。しかしながらメリットがよくわかりません。さらっと最後に紹介だけしておきます。
ちなみに実行してみたら簡単な計算だけなので余裕で正しい結果を返してきます。難しい計算などを行って検証すべきかと思います。
4-1. BasedModelとclassを使ったtoolsの作成
class add(BaseModel):
"""Add two integers."""
a: int = Field(..., description="First integer")
b: int = Field(..., description="Second integer")
class multiply(BaseModel):
"""Multiply two integers."""
a: int = Field(..., description="First integer")
b: int = Field(..., description="Second integer")
class get_name_age(BaseModel):
"""Get name and age."""
name: str = Field(..., description="Name")
age: int = Field(..., description="Age")
4-2. Annotatedとclassを使ったtoolsの作成
from typing_extensions import Annotated, TypedDict
class add(TypedDict):
"""Add two integers."""
# Annotations must have the type and can optionally include a default value and description (in that order).
a: Annotated[int, ..., "First integer"]
b: Annotated[int, ..., "Second integer"]
class multiply(TypedDict):
"""Multiply two integers."""
a: Annotated[int, ..., "First integer"]
b: Annotated[int, ..., "Second integer"]
class get_name_age(TypedDict):
"""Get name and age."""
name: Annotated[str, ..., "name"]
age: Annotated[int, ..., "age"]
ドキュメントを読みながらほぼ写経して作りましたが、classなのに何でclass名が大文字で始まっていないんだろう・・・。
5. 終わりに
今回、toolsの役割と作り方、入出力の方法がわかりました。
肝は
tool_callingの機能はどの関数を呼び出すべきか、その関数の引数を作ることだということ。
これがわかれば半分進んだようなものだと思います。
長くなりすぎるので一旦ここで終わります。次はtoolsの実行をやってみます。