1. はじめに
前回と前々回でtool callingとtoolの実行を行いました。
なんか、嫌な感じしませんでした?
これ、人がtool callingしたかどうかを判別しないかんやん
という事でこれを解決したいと思います。
参考にしたドキュメントはこちら。写経をしつつ進めていきます。
このページのタイトルにはLangChainとありますが、LangGraphのtutorialです。はい
え?チュートリアル???
いきなりチュートリアルでこれって、LangGraphがとっつきにくいと感じてしまうじゃん。だって、今回はその13だよ?笑
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. 課題の確認
じゃ、前回のどこが問題だったかを確認していきましょう。
まぁ、この辺りですよね。
toolsをバインドしたモデルの出力に ツールを実行しろ! と出力された後に呼び出されたツールを一つずつ実行する必要があるのでした。
ね!ここ、人の判断が入っちゃってる。これを自動化しようって話です。
4. 実装
チュートリアルはantholopicを使っていたり、toolが一つだったりするので、いきなり改造していきます。
4-1. 実装イメージ
- 質問文がインプットされる
- モデルが推論
- モデルの出力を元に会話を終了するか、toolsを実行するか判断(終了の時は終わり)
- toolsの実行
- 3に戻る(ループ)
プログラムで言えば、while文みたいなもんだな。
4-2. ライブラリインポート
# 環境関連
import os
import json
import datetime
from dateutil.relativedelta import relativedelta
from dotenv import load_dotenv
# モデル
from langchain_openai import AzureChatOpenAI
# Chat関連
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import ToolMessage
# tools関連
from langchain_core.tools import tool
# graph関連
from pydantic import BaseModel, Field
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import START, END, StateGraph
from langgraph.graph.message import add_messages
さすがにインポートするライブラリが増えてきたな・・・。
4-3. 環境
load_dotenv('.env')
END_POINT = os.getenv('END_POINT')
API_VERSION = os.getenv('API_VERSION')
API_KEY = os.getenv('API_KEY')
4-4. モデルの用意
model = AzureChatOpenAI(
    azure_deployment='gpt-4o-mini',
    azure_endpoint=END_POINT,
    api_version=API_VERSION,
    api_key=API_KEY
)
いつも通り4o-mini
4-5. toolsの作成
# 型定義
class CalculatorInput(BaseModel):
    a: int = Field(..., description="first number")
    b: int = Field(..., description="second number")
class GetNameAgeFromBirthdayInput(BaseModel):
    name: str = Field(..., description="name")
    birthday: str = Field(..., description="birthday")
class GetDiffernceDateInput(BaseModel):
    start_date: str = Field(..., description="first date")
    end_date: str = Field(..., description="second date")
# toolsの作成
@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-from-birthday-tool", args_schema=GetNameAgeFromBirthdayInput, return_direct=True)
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("get-defference-date-tool", args_schema=GetDiffernceDateInput, return_direct=True)
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デコレータを使ってtoolsにします。
ここで、デコレータに引数を渡します。toolの名前と、Pydanticで型定義クラス名を渡します。
今回の方法では特にこのtoolの名前は重要です。未検証ですが、一意な名前にした方が良いと思います。
4-6. toolsバインドモデルの用意
tools = [
    add,
    multiply,
    get_name_age_from_birthday,
    date_difference,
]
model_with_tools = model.bind_tools(tools)
toolsではおなじみのtoolsバインド。
4-6. nodeとなる関数の用意
ここからLangGraphのノードとなる関数を作っていきます。
引数となるstateはStateクラスを継承します。これで型定義します。
引数のstateと継承クラスのStateは先頭の文字が小文字か、大文字かで区別してますのでご注意を。(初心者の方はよく引っかかると思う)
4-6-1. Chatbotノード用
def chatbot(state: State):
    return {"messages": [model_with_tools.invoke(state["messages"])]}
これはシンプル。
stateを受け取って、state(辞書型)のmessagesのバリューをバインドされたモデルに渡した出力を辞書形式でまたもmessagesキーのバリューに入れて出力します。
ChatPromptTemplateを受け取って、その出力をそのまま返すといった方がわかりやすいかも。
4-6-2. toolsを実行するノード用
class BasicToolNode:
    """A node that runs the tools requested in the last AIMessage."""
    def __init__(self, tools: list) -> None:
        # デコレータ名をkeyとして入力するとvalueとしてtoolを呼び出せる辞書を作る(少し下に説明を書いた)
        self.tools_by_name = {tool.name: tool for tool in tools}
    def __call__(self, inputs: dict):
        # inputのmessages(List)の最後の要素を取得する。
        if messages := inputs.get("messages", []):
            message = messages[-1]
        else:
            raise ValueError("No message found in input")
        # 出力用のListを用意する
        outputs = []
        # messagesの中のtools_calls(一つだったり、複数だったり)を順に取得する
        for tool_call in message.tool_calls:
            # __init__で作った辞書を使ってtoolを実行する
            tool_result = self.tools_by_name[tool_call["name"]].invoke(
                tool_call["args"]
            )
            # 用意した出力用のListに付け加える
            outputs.append(
                ToolMessage(
                    content=json.dumps(tool_result),
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )
        # 返り値として出力用のListをvalueにして出力する
        return {"messages": outputs}
ここではtoolsノードを実行するためのクラスを作ります。(コメントアウトを除いて、ドキュメントからのコピペ)
このクラスは後で使います。(↓)
ここでこのクラスのインスタンスを立てることになります。そしてこのインスタンスをGraphのノードとして登録しています。
classの復習をすると、__init__関数はこのクラスが受け取った引数が実行され、__call__関数はこのインスタンスが呼ばれたときに実行されます。
では__init__関数の{tool.name: tool for tool in tools}の理解をしましょう。この出力は以下の通りになります。
{
    'addition-tool': StructuredTool(name='addition-tool', description='Add two integers.\n\n    Args:\n        a: First integer\n        b: Second integer', args_schema=<class '__main__.CalculatorInput'>, return_direct=True, func=<function add at 0x00000290E44A9990>),
    'multiplication-tool': StructuredTool(name='multiplication-tool', description='Multiply two integers.\n\n    Args:\n        a: First integer\n        b: Second integer', args_schema=<class '__main__.CalculatorInput'>, return_direct=True, func=<function multiply at 0x00000290E44AB520>),
    'get-name-age-from-birthday-tool': StructuredTool(name='get-name-age-from-birthday-tool', description='Get name and Birthday.\n\n    Args:\n        name: Name\n        agbirthdaye: Birthday like "YYYY-MM-DD"', args_schema=<class '__main__.GetNameAgeFromBirthdayInput'>, return_direct=True, func=<function get_name_age_from_birthday at 0x00000290E44AB640>),
    'get-defference-date-tool': StructuredTool(name='get-defference-date-tool', description='二つの日付の差を「〇年〇か月〇日」として計算する関数\n\n    Args:\n        start_date: start_date\n        end_date: end_date', args_schema=<class '__main__.GetDiffernceDateInput'>, return_direct=True, func=<function date_difference at 0x00000290E44AB6D0>)
 }
つまり、@toolsで定義された名前で、StructuredToolが呼び出される辞書を作ることになります。
そして、このクラスのインスタンスが呼ばれたときにはinput引数の中にtool_callsがあればtoolsを実行するっていう関数です。
4-6-3. Condition関数
def conditional_edge(state: State):
    """
    stateがリストの場合、最後の要素をai_messageとします。
    stateが辞書で、"messages"キーがある場合、その最後の要素をai_messageとします。
    どちらでもない場合はエラーを発生させます。
    ai_messageにtool_calls属性があり、その長さが0より大きい場合、"tools"を返します。
    """
    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "to_tools"
    return "to_the_end"
目的はtoolsをするか否かを決めるための関数です。
受け取った引数がtool_callsの要素を持っている場合、返り値として、"to_tools"を、要素がない場合は"to_the_end"という返り値を返します。
これで、tool_callingされたときはtools実行ノードへ、そうでない場合は会話の修了に向かうためのフラグとして用います。
このノード(今は関数だけど)の返り値はあとで使われることになりますのでしばしお待ちを。
4-7. Graphの構築
ではグラフを作っていきます。
4-7-1. グラフの作成
graph_builder = StateGraph(State)
定番
4-7-1-1. ノードの定義
graph_builder.add_node("chatbot", chatbot)
チャットボットノードを作ります。
tool_node = BasicToolNode(tools=tools)
graph_builder.add_node("tools", tool_node)
BasicToolNodeを実行するためのノードを作ります。
クラスのインスタンスを立てて、そのインスタンスを登録します。
4-7-1-2. edgeの作成
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("tools", "chatbot")
スタートノードからchatbotノードまでのエッヂと、toolsからchatbotに戻すためのノードを作ります。
graph_builder.add_conditional_edges(
    "chatbot",
    conditional_edge,
    {"to_tools": "tools", "to_the_end": END},
)
4-6-3で定義したconditionノード用関数の出力がここで使われます。
add_conditonal_edgesで、元ノードの名前"chatbot"を渡し、第二引数で先に作ったtoolsを実行するかどうかを決める関数を渡します。
"chatbot"の出力が"to_tools"であれば"tools"ノードへ、"to_the_end"であればENDノードへ向かわせるエッヂを作ります。
4-7-1-3. グラフの構築
graph = graph_builder.compile()
コンパイルして、グラフを完成させましょう。
4−7−2. グラフの確認
可視化してグラフがきちんとできているか、確認しましょう。
from IPython.display import Image, display
try:
    display(Image(graph.get_graph().draw_mermaid_png()))
except Exception:
    # This requires some extra dependencies and is optional
    pass
"chatbot"からconditional_edgeのconditional_edge関数によって、to_toolsの時はtoolsの実行を、to_the_endであれば会話を終了しなさいという流れになってます。
5. 実行
わくわくしかない!実行しますよ!
5-1. 実行するための関数
その前にこのままだと実行する関数がごちゃごちゃめんどくさいので、簡素化するための関数をちょろっと書いておきます。
prompt_template = ChatPromptTemplate([
    ('system', 'あなたは優秀なAIアシスタントです。様々な回答を正確に行います。'),
    ('user', '{query}'),
])
def prompt(input):
    return prompt_template.invoke(input)
def graph_updates(user_input: str):
    return graph.invoke(State(prompt(user_input)))
こんどこそ、実行です!
5-2. 実行1
user_input = 'エメット・ブラウンは1938年10月22日生まれです。マーティー・マクフライは1961年6月9日生まれです。二人の年齢と、年齢の差を教えてください。'
実はこれ、エメット・ブラウン役のクリストファー・ロイドさんの誕生日とマーティー役のマイケル・J・フォックスさんの誕生日です。w
graph.invoke(State(prompt(user_input)))
# {'messages': [SystemMessage(content='あなたは優秀なAIアシスタントです。様々な回答を正確に行います。', additional_kwargs={}, response_metadata={}, id='52a041b4-1926-4493-9742-e957f2acf127'),
#   HumanMessage(content='エメット・ブラウンは1938年10月22日生まれです。マーティー・マクフライは1961年6月9日生まれです。二人の年齢と、年齢の差を教えてください。', additional_kwargs={}, response_metadata={}, id='24afe22e-484e-4b15-83b5-fc20beaaf4b0'),
#   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1938-10-22"}', 'name': 'get-name-age-from-birthday-tool'}, 'type': 'function'}, {'id': 'call_**********', 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1961-06-09"}', 'name': 'get-name-age-from-birthday-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 84, 'prompt_tokens': 307, 'total_tokens': 391, '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-**********0', tool_calls=[{'name': 'get-name-age-from-birthday-tool', 'args': {'name': 'エメット・ブラウン', 'birthday': '1938-10-22'}, 'id': 'call_**********', 'type': 'tool_call'}, {'name': 'get-name-age-from-birthday-tool', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1961-06-09'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 307, 'output_tokens': 84, 'total_tokens': 391, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#   ToolMessage(content='["\\u30a8\\u30e1\\u30c3\\u30c8\\u30fb\\u30d6\\u30e9\\u30a6\\u30f3", 86]', name='get-name-age-from-birthday-tool', id='605a7f12-fb47-453f-8dcf-218f290943c1', tool_call_id='call_**********'),
#   ToolMessage(content='["\\u30de\\u30fc\\u30c6\\u30a3\\u30fc\\u30fb\\u30de\\u30af\\u30d5\\u30e9\\u30a4", 63]', name='get-name-age-from-birthday-tool', id='d6c394d4-9e5e-4f93-a680-86a428c6d35a', tool_call_id='call_**********'),
#   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"start_date":"1938-10-22","end_date":"1961-06-09"}', 'name': 'get-defference-date-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 487, 'total_tokens': 521, '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-defference-date-tool', 'args': {'start_date': '1938-10-22', 'end_date': '1961-06-09'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 487, 'output_tokens': 34, 'total_tokens': 521, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#   ToolMessage(content='"22\\u5e747\\u304b\\u670818\\u65e5"', name='get-defference-date-tool', id='bea8db1f-2d13-48d6-82dc-1671f5a32682', tool_call_id='call_**********'),
#   AIMessage(content='エメット・ブラウンの年齢は86歳、マーティー・マクフライの年齢は63歳です。二人の年齢差は22年7か月18日です。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 46, 'prompt_tokens': 550, 'total_tokens': 596, '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': 'stop', 'logprobs': None, 'content_filter_results': {}}, id='run-**********', usage_metadata={'input_tokens': 550, 'output_tokens': 46, 'total_tokens': 596, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}
きちんとモデルがtool_callingして、toolsを実行、最終出力を得てますね。
一つ目と二つ目のAIMessageがtool_callingしてます。
ToolMessageは実行された関数の出力、最後のAIMessageが最終出力です。
5-2. 実行1
user_input_2 = "エメット・ブラウンの年齢は86歳、マーティー・マクフライの年齢は63歳です。二人の年齢の差は22年7か月18日です。エメット・ブラウンの誕生日が1938年10月22日生まれの時、マーティー・マクフライの誕生日を教えてください。"
ちょっと質問を変えてみましょう。
二人の年齢と、年齢差を日付単位まで与えた状態で、エメット・ブラウンの誕生日からマーティー・マクフライの誕生日を推論してもらいましょう。
超むずいやん。w
graph.invoke(State(prompt(user_input_2)))
# {'messages': [SystemMessage(content='あなたは優秀なAIアシスタントです。様々な回答を正確に行います。', additional_kwargs={}, response_metadata={}, id='958d4b31-22c7-47b6-9fb9-5540a017e5b3'),
#   HumanMessage(content='エメット・ブラウンの年齢は86歳、マーティー・マクフライの年齢は63歳です。二人の年齢の差は22年7か月18日です。エメット・ブラウンの誕生日が1938年10月22日生まれの時、マーティー・マクフライの誕生日を教えてください。', additional_kwargs={}, response_metadata={}, id='76c71f92-f852-4ccb-b0ff-dfa8ae2d86af'),
# 
# tool_callig #1
#   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"name":"エメット・ブラウン","birthday":"1938-10-22"}', 'name': 'get-name-age-from-birthday-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 33, 'prompt_tokens': 338, 'total_tokens': 371, '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-from-birthday-tool', 'args': {'name': 'エメット・ブラウン', 'birthday': '1938-10-22'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 338, 'output_tokens': 33, 'total_tokens': 371, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#   ToolMessage(content='["\\u30a8\\u30e1\\u30c3\\u30c8\\u30fb\\u30d6\\u30e9\\u30a6\\u30f3", 86]', name='get-name-age-from-birthday-tool', id='e2fed87c-c20c-4ff5-8a93-461a5a088434', tool_call_id='call_**********'),
# 
# tool_callig #2
#   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"start_date":"1938-10-22","end_date":"2023-10-22"}', 'name': 'get-defference-date-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 425, 'total_tokens': 459, '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-defference-date-tool', 'args': {'start_date': '1938-10-22', 'end_date': '2023-10-22'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 425, 'output_tokens': 34, 'total_tokens': 459, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#   ToolMessage(content='"85\\u5e740\\u304b\\u67080\\u65e5"', name='get-defference-date-tool', id='2739c881-dfd8-406d-b2be-16e80f6a9513', tool_call_id='call_**********'),
#   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"start_date":"2023-10-22","end_date":"2023-10-22"}', 'name': 'get-defference-date-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 488, 'total_tokens': 522, '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-defference-date-tool', 'args': {'start_date': '2023-10-22', 'end_date': '2023-10-22'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 488, 'output_tokens': 34, 'total_tokens': 522, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#   ToolMessage(content='"0\\u5e740\\u304b\\u67080\\u65e5"', name='get-defference-date-tool', id='**********', tool_call_id='call_**********'),
# 
# tool_callig #3
#   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"start_date":"2023-10-22","end_date":"2023-01-05"}', 'name': 'get-defference-date-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 551, 'total_tokens': 585, '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-defference-date-tool', 'args': {'start_date': '2023-10-22', 'end_date': '2023-01-05'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 551, 'output_tokens': 34, 'total_tokens': 585, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#   ToolMessage(content='"0\\u5e74-9\\u304b\\u6708-17\\u65e5"', name='get-defference-date-tool', id='4a7eedca-a54a-4e20-80f6-5e19ee710cc6', tool_call_id='call_**********'),
#   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"start_date":"2023-10-22","end_date":"1960-01-05"}', 'name': 'get-defference-date-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 618, 'total_tokens': 652, '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-defference-date-tool', 'args': {'start_date': '2023-10-22', 'end_date': '1960-01-05'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 618, 'output_tokens': 34, 'total_tokens': 652, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#   ToolMessage(content='"-63\\u5e74-9\\u304b\\u6708-17\\u65e5"', name='get-defference-date-tool', id='f53345ef-98c8-412e-ab1c-f03279a7beb1', tool_call_id='call_**********'),
# 
# tool_callig #4
#   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"start_date": "1960-01-05", "end_date": "2023-10-22"}', 'name': 'get-defference-date-tool'}, 'type': 'function'}, {'id': 'call_**********, 'function': {'arguments': '{"start_date": "1960-01-05", "end_date": "1938-10-22"}', 'name': 'get-defference-date-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 84, 'prompt_tokens': 685, 'total_tokens': 769, '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-defference-date-tool', 'args': {'start_date': '1960-01-05', 'end_date': '2023-10-22'}, 'id': 'call_**********', 'type': 'tool_call'}, {'name': 'get-defference-date-tool', 'args': {'start_date': '1960-01-05', 'end_date': '1938-10-22'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 685, 'output_tokens': 84, 'total_tokens': 769, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#   ToolMessage(content='"63\\u5e749\\u304b\\u670817\\u65e5"', name='get-defference-date-tool', id='2c373fcc-598e-488f-b66f-ded02deb46c8', tool_call_id='call_**********'),
#   ToolMessage(content='"-21\\u5e74-2\\u304b\\u6708-14\\u65e5"', name='get-defference-date-tool', id='d5a6f412-f38d-43b2-98c7-77c605f8db59', tool_call_id='call_**********'),
# 
# tool_callig #5
#   AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"name":"マーティー・マクフライ","birthday":"1960-01-05"}', 'name': 'get-name-age-from-birthday-tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 35, 'prompt_tokens': 817, 'total_tokens': 852, '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-from-birthday-tool', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1960-01-05'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 817, 'output_tokens': 35, 'total_tokens': 852, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#   ToolMessage(content='["\\u30de\\u30fc\\u30c6\\u30a3\\u30fc\\u30fb\\u30de\\u30af\\u30d5\\u30e9\\u30a4", 65]', name='get-name-age-from-birthday-tool', id='3b907395-3a1e-477a-a3b6-731a0bb810a2', tool_call_id='call_**********'),
#   AIMessage(content='エメット・ブラウンの誕生日が1938年10月22日であるため、マーティー・マクフライの誕生日は1960年1月5日です。これにより、マーティー・マクフライの年齢は現在65歳になります。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 66, 'prompt_tokens': 909, 'total_tokens': 975, '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': 'stop', 'logprobs': None, 'content_filter_results': {}}, id='run-**********', usage_metadata={'input_tokens': 909, 'output_tokens': 66, 'total_tokens': 975, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}
見やすくするために# tool_callig #*と改行を追加しています。
どうやら、3回目のtool_callingで1960-01-05と言う意味不明な日付を引数として呼び出しています。
どこからこれが出てきたのかわかりませんが、ここから大きくおかしくなっています。
きっとtoolsの中に日付だけを取得する関数がないからではないかと思います。
二回目の出力はうまくいきませんでしたが、このようなtoolsを増やしていくことで簡単に解決できるのではないかと思います。
課題
という事で、適したtoolsがないとなんとか答えを出そうとする真面目なLLM努力が垣間見られました。(結果としてハルシネーション)
ちゃんと、システムプロンプトで「無理なもんは無理と回答してね!」って入れておいた方がいいのかもしれません。
また、ツールを自動で追加してくれるといいのですが、そうはいかんっすね。
終わりに
自動的にツールを実行する方法がわかりました。
モデルの出力がtool_callingされているかをチェックするためのエッヂを用いることでtoolsの実行可否判断をします。
LangChainシリーズも長くなってきました。まだまだ続くのかもしれません。(多分続く)
langmem、面白そう・・・


