LangChain v0.3　その13 ~自動的にtoolsを実行する~

# 1. はじめに
前回と前々回でtool callingとtoolの実行を行いました。

https://qiita.com/ikedachin/items/37fe239651dba6336e37

https://qiita.com/ikedachin/items/9a16e3350ecb7b6ac845

なんか、嫌な感じしませんでした？
**これ、人がtool callingしたかどうかを判別しないかんやん**

という事でこれを解決したいと思います。

<br><br>

参考にしたドキュメントはこちら。写経をしつつ進めていきます。

https://langchain-ai.github.io/langgraph/tutorials/introduction/

このページのタイトルにはLangChainとありますが、LangGraphのtutorialです。はい
え？チュートリアル？？？
いきなりチュートリアルでこれって、LangGraphがとっつきにくいと感じてしまうじゃん。だって、今回は**その13**だよ？笑

<br><br>

# 2. バージョン情報
:::note
バージョン情報
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
:::

はいはい、コピペです。手抜きです。

<br><br>


# 3. 課題の確認
じゃ、前回のどこが問題だったかを確認していきましょう。

https://qiita.com/ikedachin/items/9a16e3350ecb7b6ac845#3-9-tool-calling

まぁ、この辺りですよね。
toolsをバインドしたモデルの出力に **ツールを実行しろ！** と出力された後に呼び出されたツールを一つずつ実行する必要があるのでした。

![スクリーンショット 2025-02-26 23.23.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655320/4ddcf167-4e0b-4327-bde5-630c2075dcb4.png)

ね！ここ、人の判断が入っちゃってる。これを自動化しようって話です。

<br><br>

# 4. 実装
チュートリアルはantholopicを使っていたり、toolが一つだったりするので、いきなり改造していきます。

<br>

## 4-1. 実装イメージ
1. 質問文がインプットされる
1. モデルが推論
1. モデルの出力を元に会話を終了するか、toolsを実行するか判断（終了の時は終わり）
1. toolsの実行
1. ３に戻る（ループ）

プログラムで言えば、while文みたいなもんだな。

<br>

## 4-2. ライブラリインポート
```python:ライブラリインポート
# 環境関連
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
```
さすがにインポートするライブラリが増えてきたな・・・。

<br><br>

## 4-3. 環境
```python:環境準備
load_dotenv('.env')

END_POINT = os.getenv('END_POINT')
API_VERSION = os.getenv('API_VERSION')
API_KEY = os.getenv('API_KEY')
```
<br><br>

## 4-4. モデルの用意
```python:モデルの用意

model = AzureChatOpenAI(
    azure_deployment='gpt-4o-mini',
    azure_endpoint=END_POINT,
    api_version=API_VERSION,
    api_key=API_KEY
)
```
いつも通り4o-mini

<br><br>


## 4-5. toolsの作成
```python: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の名前は重要です。未検証ですが、一意な名前にした方が良いと思います。

<br><br>


## 4-6. toolsバインドモデルの用意
```python:toolsをバインドしたモデルを用意
tools = [
    add,
    multiply,
    get_name_age_from_birthday,
    date_difference,
]
model_with_tools = model.bind_tools(tools)
```
toolsではおなじみのtoolsバインド。

<br><br>


## 4-6. nodeとなる関数の用意
ここからLangGraphのノードとなる関数を作っていきます。
引数となるstateはStateクラスを継承します。これで型定義します。
引数のstateと継承クラスのStateは先頭の文字が小文字か、大文字かで区別してますのでご注意を。（初心者の方はよく引っかかると思う）

<br>


### 4-6-1. Chatbotノード用
```python:チャットノード用関数
def chatbot(state: State):
    return {"messages": [model_with_tools.invoke(state["messages"])]}
```

これはシンプル。
stateを受け取って、state（辞書型）のmessagesのバリューをバインドされたモデルに渡した出力を辞書形式でまたもmessagesキーのバリューに入れて出力します。
ChatPromptTemplateを受け取って、その出力をそのまま返すといった方がわかりやすいかも。

<br>

### 4-6-2. toolsを実行するノード用
```python: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ノードを実行するためのクラスを作ります。（コメントアウトを除いて、ドキュメントからのコピペ）
このクラスは後で使います。（↓）

https://qiita.com/ikedachin/items/fa72476398cf34505147#4-7-1-1-%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AE%E5%AE%9A%E7%BE%A9

ここでこのクラスのインスタンスを立てることになります。そしてこのインスタンスをGraphのノードとして登録しています。
classの復習をすると、`__init__`関数はこのクラスが受け取った引数が実行され、`__call__`関数はこのインスタンスが呼ばれたときに実行されます。

では`__init__`関数の`{tool.name: tool for tool in tools}`の理解をしましょう。この出力は以下の通りになります。


```text:{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を実行するっていう関数です。


<br>


### 4-6-3. Condition関数
```python:toolsを呼び出すか否かを決める関数
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実行ノードへ、そうでない場合は会話の修了に向かうためのフラグとして用います。
このノード（今は関数だけど）の返り値はあとで使われることになりますのでしばしお待ちを。

<br>


## 4-7. Graphの構築
ではグラフを作っていきます。

### 4-7-1. グラフの作成
```python:グラフを作るためのインスタンスを立てる
graph_builder = StateGraph(State)
```
定番

<br>


#### 4-7-1-1. ノードの定義
```python:チャットボットノードを作る
graph_builder.add_node("chatbot", chatbot)
```
チャットボットノードを作ります。

<br>


```python:toolsを実行するノードを作る
tool_node = BasicToolNode(tools=tools)
graph_builder.add_node("tools", tool_node)
```
BasicToolNodeを実行するためのノードを作ります。
クラスのインスタンスを立てて、そのインスタンスを登録します。

<br>

#### 4-7-1-2. edgeの作成
```python:エッヂの作成
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("tools", "chatbot")
```
スタートノードからchatbotノードまでのエッヂと、toolsからchatbotに戻すためのノードを作ります。

<br>

```python:toolsを呼ぶかどうかを決めるエッヂを作る
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`ノードへ向かわせるエッヂを作ります。

<br>


#### 4-7-1-3. グラフの構築
```python
graph = graph_builder.compile()
```
コンパイルして、グラフを完成させましょう。

<br>



### 4−7−2. グラフの確認
可視化してグラフがきちんとできているか、確認しましょう。

```python
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
```

![graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655320/aec14e98-11d9-4c7b-b958-365f471f0d94.png)

`"chatbot"`から`conditional_edge`の`conditional_edge`関数によって、`to_tools`の時は`tools`の実行を、`to_the_end`であれば会話を終了しなさいという流れになってます。


<br><br>


# 5. 実行
わくわくしかない！実行しますよ！

## 5-1. 実行するための関数
その前にこのままだと実行する関数がごちゃごちゃめんどくさいので、簡素化するための関数をちょろっと書いておきます。


```python:実行を簡素化するための関数
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)))
```

こんどこそ、実行です！

<br>


## 5-2. 実行１
```python:
user_input = 'エメット・ブラウンは1938年10月22日生まれです。マーティー・マクフライは1961年6月9日生まれです。二人の年齢と、年齢の差を教えてください。'
```
実はこれ、エメット・ブラウン役のクリストファー・ロイドさんの誕生日とマーティー役のマイケル・J・フォックスさんの誕生日です。ｗ

<br>

```python
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が最終出力です。

<br><br>



## 5-2. 実行１
```python
user_input_2 = "エメット・ブラウンの年齢は86歳、マーティー・マクフライの年齢は63歳です。二人の年齢の差は22年7か月18日です。エメット・ブラウンの誕生日が1938年10月22日生まれの時、マーティー・マクフライの誕生日を教えてください。"
```
ちょっと質問を変えてみましょう。
二人の年齢と、年齢差を日付単位まで与えた状態で、エメット・ブラウンの誕生日からマーティー・マクフライの誕生日を推論してもらいましょう。
超むずいやん。ｗ

<br>



```python
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を増やしていくことで簡単に解決できるのではないかと思います。


<br>
<br>



# 課題
という事で、適したtoolsがないとなんとか答えを出そうとする真面目なLLM努力が垣間見られました。（結果としてハルシネーション）
ちゃんと、システムプロンプトで「無理なもんは無理と回答してね！」って入れておいた方がいいのかもしれません。
また、ツールを自動で追加してくれるといいのですが、そうはいかんっすね。

<br><br>


# 終わりに
自動的にツールを実行する方法がわかりました。
モデルの出力がtool_callingされているかをチェックするためのエッヂを用いることでtoolsの実行可否判断をします。

LangChainシリーズも長くなってきました。まだまだ続くのかもしれません。（多分続く）

langmem、面白そう・・・

<br><br>