Tool Calling 基礎
〜 Python から OpenAI API を使用 〜
2026-02-22 作成 福島
TOP > llm > tool-calling
[ TIPS | TOYS | OTAKU | LINK | MOVIE | CGI | AvTitle | ConfuTerm | HIST | AnSt | Asob | Shell | GBC | LLM ]

0. 前置き

0-1. Tool Calling とは
ChatGPT に質疑をするときに操作するのは人間ですが、人間の代わりにプログラムが直接 ChatGPT を使う事もできます。
(正しくは、プログラムが OpenAI API を操作します)

プログラムで OpenAI をアクセスすることにより、定時処理や応用がやりやすくなります。
このインタフェースを OpenAI API (以下、API と呼称) と呼び、機能のひとつに Tool Calling があります。

Tool Calling は仕組みの名称であり、プログラムそのものではありません。
本稿では、Python から Tool Calling を使います。

Tool Calling は有料 API のサブスクリプション契約が必要です。
llm = ChatOpenAI() で環境変数 OPENAI_API_KEY を参照します。
Linux の例:
export OPENAI_API_KEY="sk-proj-…"
0-2. Tool Calling の動作
Tool Calling は以下 Loop-1~6 の動作を想定して Python でプログラミングします。
0-3. LangChain
Tool Calling は LLM とメッセージのやり取りを JSON フォーマットで行いますが、これを通常
• ネイティブ Tool Calling
• Raw Tool Calling
等と呼びます。(正式な名称は存在しない)
本稿では JSON フォーマットの使用より可読性の高い LangChain を採用しています。
ネイティブ Tool Calling の例
messages = [
    {'role': 'user', 'content': 'おなかが空いたらどうする?'}
]

client = OpenAI()
response = client.chat.completions.create(
    model    = 'gpt-4o-mini',
    messages = messages,
    tools    = [],
)
 ⇓
ToolChain 版 Tool Calling の例 : こちらを使う
humanMsg = [
    HumanMessage(content = 'おなかが空いたらどうする?')
]

llm = ChatOpenAI(model = 'gpt-4o-mini')
llm_tools = llm.bind_tools([])
llm_answer = llm_tools.invoke(humanMsg)
この例では確認用関数の登録を省略していますが、登録時はさらに可読性が上がります。


1. 一番簡単な Tool Calling

白米、卵、納豆を使って、どんな料理を作れるかを提案させてみる。

API は「トークン」を単位に課金される*1ので、トークン数を気にする必要がある。
*1トークンとは文章を分割して最小単位の語にしたもの。
正しくは「あらかじめ購入したトークンが消費される」。買ったお菓子が減っていくのと同じ。

1-1. プログラムを作成する。(1)
シンプルにするため、前述の Loop-2, 3 だけを使用する。

< sample_calling_1.py >
#!/usr/bin/python3

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool

MODEL = 'gpt-4o-mini'   # Tool Calling には mini 以上のプラン(有料)が必要
TEMP  = 0.0             # ゆらぎを 0 にする

if __name__ == '__main__':

    # LLM tools の用意
    llm = ChatOpenAI(model=MODEL, temperature=TEMP)
    llm_tools = llm.bind_tools([])

    # 指示履歴の作成
    humanMsg = []
    humanMsg.append(
        HumanMessage(
            content = '白米と卵と納豆で作れる料理を一つ挙げなさい。'
        )
    )

    total_prompt_tokens     = 0 # 入力トークン数の合計を初期化
    total_completion_tokens = 0 # 出力トークン数の合計を初期化


# 指示履歴を実行し、LLM から回答を得る llm_answer = llm_tools.invoke(humanMsg) # 解答から今回の消費トークン数を得る (入出力トークン数) usage = llm_answer.response_metadata.get('token_usage', {}) prompt_tokens = usage.get('prompt_tokens', 0) completion_tokens = usage.get('completion_tokens', 0) # 入出力トークン数をそれぞれ積算 total_prompt_tokens += prompt_tokens total_completion_tokens += completion_tokens print(f'生成 AI の最終回答:\n{llm_answer.content}')
print('-----') print(f'総消費トークン: {total_prompt_tokens + total_completion_tokens}') print(f'(入力: {total_prompt_tokens}, 出力: {total_completion_tokens})')
1-2. プログラムを実行する。(1)
$ ./sample_calling_1.py
生成 AI の最終回答:
白米と卵と納豆を使って作れる料理の一つは「納豆卵かけご飯」です。

### 作り方:
1. 白米を炊きます。
2. 炊き上がった白米を丼に盛ります。
3. 納豆をパックから出し、付属のタレを加えてよく混ぜます。
4. 生卵を白米の上に割り入れます。
5. 混ぜた納豆を卵の上にのせます。
6. お好みで醤油やネギをトッピングして完成です。

シンプルで栄養満点の一品です!
-----
総消費トークン: 200 (入力: 44, 出力: 156)

ChatGPT (Web ブラウザの無料版) とは回答が異なる。
このプログラムでは LLM のモデルとして安価な gpt-4o-mini を指定しているが、
ChatGPT は可能な限り最新のモデルを使用するのが回答が異なる最大の理由。


2. 条件をつける

料理を考えるときに、条件として食材の確認を追加する。

条件を付けるとプログラムが急に複雑になり、前述の Loop-1~6 すべてを使用する必要がある。
これは「食材」というキーワードに、白米・卵・納豆 がそれぞれマッチするため。

2-1. プログラムを作成する。(2)
ここでは MAX_LOOP = 5 として、連続問い合わせを最大 5 回に制限している。
もしも無限に問い合わせが生じると請求金額が多額になる恐れがあるため、これを防止する。

< sample_calling_2.py >
#!/usr/bin/python3

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool

MODEL = 'gpt-4o-mini'   # Tool Calling には mini 以上のプラン(有料)が必要
TEMP  = 0.0             # ゆらぎを 0 にする


@tool def check_ingredients(item_name: str) -> str: """食材があるか確認する""" print(f'--- 食材確認: {item_name} -') stocks = { '白米': 'ある', #'卵': 'ある', #'納豆': 'ある', } return stocks.get(item_name, 'ない') # チェック関数をリストにする tools = [ check_ingredients, # 在庫確認の関数を設定 ]
if __name__ == '__main__': # LLM tools の用意 llm = ChatOpenAI(model=MODEL, temperature=TEMP) llm_tools = llm.bind_tools(tools) # 関数を登録 # 指示履歴の作成 humanMsg = [] humanMsg.append( HumanMessage( content = '白米と卵と納豆で作れる料理を一つ挙げなさい。' '食材の有無を確認すること。' # <-- 条件を追加 ) ) total_prompt_tokens = 0 # 入力トークン数の合計を初期化 total_completion_tokens = 0 # 出力トークン数の合計を初期化
MAX_LOOP = 5 # 最大 5 回の連続問い合わせ for loopCnt in range(MAX_LOOP): print(f'\n--- ループ回数: {loopCnt + 1} -') # 指示履歴を実行し、LLM から回答を得る llm_answer = llm_tools.invoke(humanMsg) humanMsg.append(llm_answer) # 解答から今回の消費トークン数を得る (入出力トークン数) usage = llm_answer.response_metadata.get('token_usage', {}) prompt_tokens = usage.get('prompt_tokens', 0) completion_tokens = usage.get('completion_tokens', 0) # 入出力トークン数をそれぞれ積算 total_prompt_tokens += prompt_tokens total_completion_tokens += completion_tokens print(f'[消費トークン] 入力={prompt_tokens}, 出力={completion_tokens}') tool_calls = llm_answer.tool_calls if not tool_calls: # Tool Calling が選択されなかった print(f'生成 AI の最終回答:\n{llm_answer.content}') break for tool_call in tool_calls: # 選択された関数名と引数を得る tool_name = tool_call['name'] args = tool_call['args'] # 選択された関数を実行する tool_obj = {t.name: t for t in tools}[tool_name] result = tool_obj.invoke(args) print(f'--- [DEBUG] {tool_name} の返戻値: {result} ---') # 再指示のために関数の返戻値を追加する humanMsg.append( ToolMessage( content = result, tool_call_id = tool_call['id'], ) )
print('-----') print( f'総消費トークン: {total_prompt_tokens + total_completion_tokens}' f' (入力: {total_prompt_tokens}, 出力: {total_completion_tokens})' )
2-2. プログラムを実行する。(2)
$ ./sample_calling_2.py

--- ループ回数: 1 -
[消費トークン] 入力=78, 出力=67
--- 食材確認: 白米 -
--- [DEBUG] check_ingredients の返戻値: ある ---
--- 食材確認: 卵 -
--- [DEBUG] check_ingredients の返戻値: ない ---
--- 食材確認: 納豆 -
--- [DEBUG] check_ingredients の返戻値: ない ---

--- ループ回数: 2 -
[消費トークン] 入力=180, 出力=63
生成 AI の最終回答:
白米はありますが、卵と納豆はありません。
これらの食材が必要な料理として「納豆ご飯」が考えられますが、
卵と納豆がないため、作ることはできません。
別の料理を考える必要があります。
-----
総消費トークン: 388 (入力: 258, 出力: 130)

現在の生成 AI システムはステートレスに作られており、連続する会話の継続性はクライアント側で担保する。
これは、前の状態を履歴としてすべて蓄積・投入する構造のため、ループが多くなるほど 1 回あたりの送信トークンが増加していく。
(サーバ側でステートフルにすると膨大なリソースが必要になるため、仕方がないのだろう)


3. さらに条件を付ける

料理を考えるときに、条件として予算を追加する。

3-1. プログラムを作成する。(3)
< sample_calling_3.py >
#!/usr/bin/python3

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool

MODEL = 'gpt-4o-mini'   # Tool Calling には mini 以上のプラン(有料)が必要
TEMP  = 0.0             # ゆらぎを 0 にする


@tool def check_ingredients(item_name: str) -> str: """食材があるか確認する""" print(f'--- 食材確認: {item_name} -') stocks = { '白米': 'ある', '卵': 'ある', # 変更 'ない' --> 'ある' '納豆': 'ある', # 変更 'ない' --> 'ある' } return stocks.get(item_name, 'ない')
@tool def check_price(item_name: str) -> str: """食材の価格を確認する""" print(f'--- 価格確認: {item_name} -') price = { '白米': '130円', '卵' : '35円', '納豆': '45円', } return price.get(item_name, 'ない') # チェック関数をリストにする tools = [ check_ingredients, check_price, # <-- 価格確認の関数を追加 ]
if __name__ == '__main__': # LLM tools の用意 llm = ChatOpenAI(model=MODEL, temperature=TEMP) llm_tools = llm.bind_tools(tools) # 指示履歴の作成 humanMsg = [] humanMsg.append( HumanMessage( content = '白米と卵と納豆で作れる料理を一つ挙げなさい。' '食材の有無を確認すること。' '予算は 200 円。' # <-- さらに条件を追加 ) ) total_prompt_tokens = 0 # 入力トークン数の合計を初期化 total_completion_tokens = 0 # 出力トークン数の合計を初期化
MAX_LOOP = 5 # 最大 5 回の連続問い合わせ for loopCnt in range(MAX_LOOP): print(f'\n--- ループ回数: {loopCnt + 1} -') # 指示履歴を実行し、LLM から回答を得る llm_answer = llm_tools.invoke(humanMsg) humanMsg.append(llm_answer) # 解答から今回の消費トークン数を得る (入出力トークン数) usage = llm_answer.response_metadata.get('token_usage', {}) prompt_tokens = usage.get('prompt_tokens', 0) completion_tokens = usage.get('completion_tokens', 0) # 入出力トークン数をそれぞれ積算 total_prompt_tokens += prompt_tokens total_completion_tokens += completion_tokens print(f'[消費トークン] 入力={prompt_tokens}, 出力={completion_tokens}') tool_calls = llm_answer.tool_calls if not tool_calls: # Tool Calling が選択されなかった print(f'生成 AI の最終回答:\n{llm_answer.content}') break for tool_call in tool_calls: # 選択された関数名と引数を得る tool_name = tool_call['name'] args = tool_call['args'] # 選択された関数を実行する tool_obj = {t.name: t for t in tools}[tool_name] result = tool_obj.invoke(args) print(f'--- [DEBUG] {tool_name} の返戻値: {result} ---') humanMsg.append( ToolMessage( content = result, tool_call_id = tool_call['id'], ) )
print('-----') print(f'総消費トークン: {total_prompt_tokens + total_completion_tokens}' f'(入力: {total_prompt_tokens}, 出力: {total_completion_tokens})' )
3-2. プログラムを実行する。(3)
$ ./sample_calling_3.py

--- ループ回数: 1 -
[消費トークン] 入力=108, 出力=67
--- 食材確認: 白米 -
--- [DEBUG] check_ingredients の返戻値: ある ---
--- 食材確認: 卵 -
--- [DEBUG] check_ingredients の返戻値: ある ---
--- 食材確認: 納豆 -
--- [DEBUG] check_ingredients の返戻値: ある ---

--- ループ回数: 2 -
[消費トークン] 入力=210, 出力=64
--- 価格確認: 白米 -
--- [DEBUG] check_price の返戻値: 130円 ---
--- 価格確認: 卵 -
--- [DEBUG] check_price の返戻値: 35円 ---
--- 価格確認: 納豆 -
--- [DEBUG] check_price の返戻値: 45円 ---

--- ループ回数: 3 -
[消費トークン] 入力=302, 出力=158
生成 AI の最終回答:
白米、卵、納豆はすべて揃っています。これらの食材を使って「納豆ご飯」を作ることができます。

それぞれの価格は以下の通りです:
- 白米: 130円
- 卵: 35円
- 納豆: 45円

合計は 130 + 35 + 45 = 210円 となり、予算の 200円を超えています。
したがって、予算内で作るためには 、卵を省いて「納豆ご飯」を作ることができます。
納豆ご飯は、白米と納豆を混ぜるだけのシンプルな料理です。
-----
総消費トークン: 909 (入力: 620, 出力: 289)