LangChain v0.3　その11 ~toolsを呼び出すtool colling~

# 1. モチベーション
toolsについてドキュメントを読んでたんですよ。
いや、ちょっとわかりづらい。



![スクリーンショット 2025-02-24 12.28.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3655320/b597fdd7-3243-4f34-b4a0-89de342c56c7.png)

Conceptual guideを見ていたらこの並びなので、toolsを先に読み始めたんだけど、ちょっと飛ばしてtool callingを読んだらこっちの方がわかりやすかったので紹介したいな・・・というのがモチベーション。（偉そうに・・・w）
ドキュメントを読みつつ、進めていきます。
ドキュメントはこちら

https://python.langchain.com/docs/how_to/tool_calling/

っと読み進める前に、大事なことがこのページの冒頭に書かれています。

> 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** = **引数の生成**
ってことですね。これを頭に入れて進めていきましょう。

<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. とにかく試してみよう
**「試してみる」**
ドキュメントを呼んでもいまいちわからん。結果的に手を動かしてみることが一番良さげでした。
ドキュメントを読むだけでわかる人はきっと「えらい！」と思う。

<br>


## 3-1. ライブラリ
```python:library import
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
```

<br>

## 3-2. 環境設定
```python:env variables
load_dotenv(dotenv_path='.env')

END_POINT = os.getenv("END_POINT")
API_KEY = os.getenv("API_KEY")
```

<br>

## 3-3. modelの用意
```python: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を使います。

<br>



## 3-4. シンプルな関数を使う方法
### 3-4-1. toolsの用意
```python: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じゃないかな。

<br>


### 3-4-2. LLMモデルにバインド
```python:Tool binding
tools = [add, multiply, get_name_age]
llm_with_tools = llm.bind_tools(tools)
```
GitHubをみても何をしているかはわからなかったけど、モデルにバインドする必要があります。

<br>


### 3-4-3. 実行（テキストを入力）
```python:input text
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関数`を使うことができます。

<br>


```python:tool_calls
result.tool_calls

# [{'name': 'multiply',
#   'args': {'a': 3, 'b': 12},
#   'id': 'call_******************',
#   'type': 'tool_call'}]
```
inputが3×12の答えを求めていますので、使う関数名の「multiply」と引数となるべき「３」と「１２」を抽出することに成功しました。

<br>


### 3-4-4. 実行（HumanMessageを入力する方法）
実はテキストを直接入力すると、HumanMessageに自動変換されています。
そこで、messagesにして入力しても同じになります。

```python:with HumanMessage
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。前にやりましたね。

<br>


```python:tool calls
result.additional_kwargs

# {'tool_calls': [{'id': 'call_******************',
#    'function': {'arguments': '{"a":3,"b":12}', 'name': 'multiply'},
#    'type': 'function'}],
#  'refusal': None}
```
出力の`tool_calls`を見てみると、必要な情報がここに抽出されてます。

<br><br>


### 3-4-5. 実行（ChatPromptTemplateを使う方法入力）
ChatPromptTemplateも当然使えます。

```python:with 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を見た懐かしい思い出があります。笑
あ、ドク・ブラウンじゃ無くて、「エメット・ブラウン」でしたね！汗

<br>

```python
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'}]
```
同じように名前と年齢を抽出することに成功しました。


<br>


## 3-5. tool callingの出力の型定義をする方法
以前やった、Pydanticの使い方を復習しておきましょう。

https://qiita.com/ikedachin/items/7ae52f1c497860bb3624

思い出したところで、まずは型定義を行います。

<br>

### 3-5-1. 出力の型定義
```python:Define Output Type with Pydantic
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`でも定義可能です。

<br><br>

### 3-5-2. 型定義を用いたtoolの設定
```python:create tools with Pydantic
@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`の引数に渡しておきます。

では実行してみましょう。

<br>

### 3-5-3. 型定義されたtoolsの実行
実行方法は型定義していない場合と同じなので、説明は省きます。

```Python:型定義された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}})
```

```python:additional_kwargs
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`は呼ばれていません。

ちなみに、「ドク・ブラウン」は間違いで「エメット・ブラウン」でした。汗
だって、マーティーがドクって呼んでるんだもん。

<br>


## 4. よくわからないこと
実はよくわからな記述がドキュメントにはあります。型定義だけで引数を作る方法もあるようなのです。しかしながらメリットがよくわかりません。さらっと最後に紹介だけしておきます。
ちなみに実行してみたら簡単な計算だけなので余裕で正しい結果を返してきます。難しい計算などを行って検証すべきかと思います。

<br><br>

## 4-1. BasedModelとclassを使ったtoolsの作成
```python: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")
```
<br><br>


## 4-2. Annotatedとclassを使ったtoolsの作成

```python: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名が大文字で始まっていないんだろう・・・。

<br><br>

# 5. 終わりに
今回、toolsの役割と作り方、入出力の方法がわかりました。

<br>

肝は
**tool_callingの機能はどの関数を呼び出すべきか、その関数の引数を作ること**だということ。
これがわかれば半分進んだようなものだと思います。

<br>

長くなりすぎるので一旦ここで終わります。次はtoolsの実行をやってみます。

https://python.langchain.com/docs/how_to/tool_results_pass_to_model/

<br><br>