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?

LangChain v0.3 その12 ~tool collingの出力を使ってtoolを実行する~

Posted at

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のバインド
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

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を確認しましょう。


tool callingの結果
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へtool callingの関数名、引数を追加
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への追加

messagesへtool実行結果を追加
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の基礎を理解することができました。
ドキュメントは焦らず、慌てず、じっくりと読めば何とかなるもんですね。💦

ではまた



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?