1. はじめに
前回、tool callingをやりましたね。
大事なことが記載されていました。
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
import datetime
from dotenv import load_dotenv
from dateutil.relativedelta import relativedelta
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import AzureChatOpenAI
まぁ、ほぼいつも通り。
のちに書きますが、日付を扱うことになるのでライブラリが増えてます。
3-2. 環境変数
load_dotenv(dotenv_path='.env')
END_POINT = os.getenv("END_POINT")
API_KEY = os.getenv("API_KEY")
もう言うことはあるまい。(さぼった。w)
3-3. モデルの用意
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. 回答させたい質問
query = "エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。"
この文章の回答をさせましょう。多分、gpt-4o-miniであれば、きちんと回答できちゃうと思いますが、あえてtoolsを使って回答させます。
ということで、この目的に合わせたtoolsを作っていきましょう!
3-5. toolsの用意
@tool
def subtract(a: int, b: int) -> int:
"""absolute difference between two integers.
return abs value.
Args:
a: First integer
b: Second integer
"""
return abs(a - b)
@tool
def get_name_age(name: str, age: int):
"""Get name and age.
Args:
name: Name
age: Age like "YYYY-MM-DD"
"""
return name, age
@tool
def get_name_age_from_birthday(name: str, birthday: str):
"""Get name and Birthday.
Args:
name: Name
agbirthdaye: Birthday like "YYYY-MM-DD"
"""
b_day = datetime.datetime.strptime(birthday, "%Y-%m-%d")
age = datetime.datetime.now() - b_day
return name, age.days // 365
@tool
def date_difference(start_date: str, end_date: str) -> str:
"""二つの日付の差を「〇年〇か月〇日」として計算する関数
Args:
start_date: start_date
end_date: end_date
"""
start = datetime.datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.datetime.strptime(end_date, "%Y-%m-%d")
delta = relativedelta(end, start)
return f"{delta.years}年{delta.months}か月{delta.days}日"
四つのtoolsを用意してみました。型定義はせず、シンプルにデコレータで定義しました。
-
subtract
二つの整数の差を計算する関数 -
get_name_age
名前と、年齢を取得する関数 -
get_name_age_from_birthday
名前と誕生日から取得した年齢を計算する関数 -
date_difference
日付けと日付を比較して、何年何か月何日を計算する関数
さて、どのtoolが使われるんでしょうね。
3-6. messagesの用意
messages = [HumanMessage(query)]
messages
# [HumanMessage(content='エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。', additional_kwargs={}, response_metadata={})]
過去の会話履歴をどんどんと積み重ねていく必要があるのでmessagesを用意します。ここに入力やモデルからのレスポンスを付け加えていきます。
最初の入力は質問文なので、HumanMessage
として入れておきます。
3-7. toolsをモデルにバインド
tools = [
get_name_age,
date_difference,
get_name_age_from_birthday,
subtract,
]
llm_with_tools = llm.bind_tools(tools)
用意したtoolsをモデルにバインドします。前回やったやつですね。
3-8. toolsの呼び出し辞書の作成
tools_dict = {
"get_name_age": get_name_age,
"get_name_age_from_birthday": get_name_age_from_birthday,
"date_difference": date_difference,
"subtract": subtract
}
後で、モデルが選択した実行すべき関数を選択するために、valueに関数が入った辞書を用意します。
なぜかって?理由はtool_collingの出力をよく見るとわかりますので、ちょっとここはがまん。
3-9. tool calling
result = llm_with_tools.invoke(messages)
result
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_***********', 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}, {'id': 'call_***********', 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 258, 'total_tokens': 340, '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_from_birthday', 'args': {'name': 'エメット・ブラウン', 'birthday': '1950-11-04'}, 'id': 'call_***********', 'type': 'tool_call'}, {'name': 'get_name_age_from_birthday', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1971-05-03'}, 'id': 'call_***********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 258, 'output_tokens': 82, 'total_tokens': 340, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
tool callingの実行結果です。この出力のadditional_kwargs
を確認しましょう。
result.additional_kwargs
# {'tool_calls': [{'id': 'call_***********',
# 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}',
# 'name': 'get_name_age_from_birthday'},
# 'type': 'function'},
# {'id': 'call_***********',
# 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}',
# 'name': 'get_name_age_from_birthday'},
# 'type': 'function'}],
# 'refusal': None}
result.name
に呼ぶべき関数が、result.arguments
には関数に渡す引数が取得できてます。
どうやら、get_name_age_from_birthday
関数が疲れそうです。
3-10. tool calling後の処理1
messages.append(result)
messages
# [HumanMessage(content='エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。', additional_kwargs={}, response_metadata={}),
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_***********', 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}, {'id': 'call_***********', 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 258, 'total_tokens': 340, '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_from_birthday', 'args': {'name': 'エメット・ブラウン', 'birthday': '1950-11-04'}, 'id': 'call_***********', 'type': 'tool_call'}, {'name': 'get_name_age_from_birthday', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1971-05-03'}, 'id': 'call_***********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 258, 'output_tokens': 82, 'total_tokens': 340, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]
ひとまず、tool_callingの結果をmessagesに追加しましょう。
3-11. tool実行&実行結果のmessagesへの追加
for tool_call in result.tool_calls:
choiced_tool = tools_dict[tool_call['name']] # `tool_call['name']`が呼ぶべき関数名なので、`tools_dict`から関数を選択
messages.append(choiced_tool.invoke(tool_call)) # 選択した関数を実行&messagesに追加
messages
# [HumanMessage(content='エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。', additional_kwargs={}, response_metadata={}),
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_***********', 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}, {'id': 'call_***********', 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 258, 'total_tokens': 340, '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_from_birthday', 'args': {'name': 'エメット・ブラウン', 'birthday': '1950-11-04'}, 'id': 'call_***********', 'type': 'tool_call'}, {'name': 'get_name_age_from_birthday', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1971-05-03'}, 'id': 'call_***********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 258, 'output_tokens': 82, 'total_tokens': 340, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
# ToolMessage(content='["エメット・ブラウン", 74]', name='get_name_age_from_birthday', tool_call_id='call_***********'),
# ToolMessage(content='["マーティー・マクフライ", 53]', name='get_name_age_from_birthday', tool_call_id='call_***********')]
tool_call['name']
が呼ぶべき関数名なので、tools_dict
から関数を選択します。
そして、tools_callをこの関数に渡して、関数を実行します。
実行結果をmessages
に追加します。
きちんと、ドクが74歳(@2025年)、マーティーが53歳(@2025年)と抽出できています。
使われた関数はget_name_age_from_birthday
です。
3-12. さらにmessagesをバインドされたモデルに入力
result = llm_with_tools.invoke(messages)
result
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"a":74,"b":53}', 'name': 'subtract'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 436, 'total_tokens': 453, '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': 'subtract', 'args': {'a': 74, 'b': 53}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 436, 'output_tokens': 17, 'total_tokens': 453, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
求めている答えはまだ出ていません。
messages
をバインドされたモデルにさらに入力します。
またも、tool calling
されたようですね。今度はsubtract
関数が呼ばれそうです。
3-13. tool calling & toolsの実行結果をmessagesに追加
messages.append(result) # 忘れがち!
for tool_call in result.tool_calls:
choiced_tool = tools_dict[tool_call['name']]
messages.append(choiced_tool.invoke(tool_call))
messages
# [HumanMessage(content='エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。', additional_kwargs={}, response_metadata={}),
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}, {'id': 'call_**********', 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 258, 'total_tokens': 340, '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_from_birthday', 'args': {'name': 'エメット・ブラウン', 'birthday': '1950-11-04'}, 'id': 'call_**********', 'type': 'tool_call'}, {'name': 'get_name_age_from_birthday', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1971-05-03'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 258, 'output_tokens': 82, 'total_tokens': 340, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
# ToolMessage(content='["エメット・ブラウン", 74]', name='get_name_age_from_birthday', tool_call_id='call_**********'),
# ToolMessage(content='["マーティー・マクフライ", 53]', name='get_name_age_from_birthday', tool_call_id='call_**********'),
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"a":74,"b":53}', 'name': 'subtract'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 436, 'total_tokens': 453, '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': 'subtract', 'args': {'a': 74, 'b': 53}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 436, 'output_tokens': 17, 'total_tokens': 453, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
# ToolMessage(content='21', name='subtract', tool_call_id='call_**********')]
またまた同じようにtool calling
の結果をmessages
に追加し、さらにtool calling
で選択された関数を実行&messages
に追加していきます。
result = llm_with_tools.invoke(messages)
result
# AIMessage(content='エメット・ブラウンは74歳、マーティー・マクフライは53歳です。二人の年齢差は21年です。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 35, 'prompt_tokens': 461, 'total_tokens': 496, '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': 'stop', 'logprobs': None, 'content_filter_results': {}}, id='run-**********', usage_metadata={'input_tokens': 461, 'output_tokens': 35, 'total_tokens': 496, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
出力されましたね!
ドクとマーティーの年齢差は21歳。
ん?ドクってそんなに若くないよね?最初の入力文章を作り間違えたみたい。笑
4. 終わりに
もう一つ例を出そうかと思ったけど、それほど変わらないものしか思い浮かばなかったのでやめときます。
今回、tool calling
の出力結果(使用すべき関数名と引数)を実行し、その結果をmessages
に追加して出力、さらにその出力を使ってほしい結果を得る方法がわかりました。
tool calling
の結果をmessages
に追加するとき、入れ忘れそうになるので気を付けましょう。僕は何度もやらかしました。
きちんと型定義した方が間違いのない動作になると思うので、要件等ですね。
ということで、前回、今回でtools
の基礎を理解することができました。
ドキュメントは焦らず、慌てず、じっくりと読めば何とかなるもんですね。💦
ではまた