この書籍を購入しました。
gihyo.jp
第2章に「Function calling」が出てきますが、さっそくそこで躓きました。そこで勉強することにしました。
「Function calling」について
利用可能な関数の一覧を質問などのテキストとともにLLMに送信する。それに対してLLMが「この関数を使いたい」と応答してくれる。それがFunction callingと自分なりに解釈しました。ポイントは「利用可能な関数」をユーザーが用意してLLMに送ることです。参考にさせて頂いたサイト
非常にわかりやすくまとめてくれているZennの記事を見つけました。zenn.dev
書籍ではOpenAIを使っていますが、この記事ではOllamaを使っています。OpenAIに課金していない自分でも試せるので大いに参考にさせて頂きました。
Pythonスクリプト
見よう見まねでスクリプトを書いてみました。from ollama import Client from typing import Dict, Callable import copy client = Client(host="http://localhost:11434") model_name = "qwen2.5:latest" prompt = "5に3をかけるといくつ?" print(f'プロンプト: {prompt}') def verify_no_overlap(list1, list2): set1 = set(list1) # setを使用することで検索効率が向上する for item in list2: if item in set1: return False return True def add_two_numbers(a: int, b: int) -> int: result = a + b return result def subtract_two_numbers(a: int, b: int) -> int: result = a - b return result # ツールは手動で定義してchatに渡すことができます add_two_numbers_tool = { "type": "function", "function": { "name": "add_two_numbers", "description": "2つの数値の足し算を行う", "parameters": { "type": "object", "required": ["a", "b"], "properties": { "a": {"type": "integer", "description": "1つ目の数値"}, 'b': {"type": "integer", "description": "2つ目の数値"}, }, }, }, } subtract_two_numbers_tool = { "type": "function", "function": { "name": "subtract_two_numbers", "description": "2つの数値の引き算を行う", "parameters": { "type": "object", "required": ["a", "b"], "properties": { "a": {"type": "integer", "description": "引かれる数"}, "b": {"type": "integer", "description": "引く数"}, }, }, }, } # チャットの初期メッセージを設定 messages_original = [ { "role": "user", "content": prompt } ] # 利用可能な関数を辞書として定義 available_functions: Dict[str, Callable] = { "add_two_numbers": add_two_numbers, "subtract_two_numbers": subtract_two_numbers, } # モデルとチャットを開始 response = client.chat( model_name, messages=messages_original, tools=[ add_two_numbers_tool, subtract_two_numbers_tool ], ) # response.message.tool_calls は list型 で返ってくる # 例: tool_calls=[ToolCall(function=Function(name='multiply_two_numbers', arguments={'a': 1, 'b': 3}))] if response.message.tool_calls: print("Function calling...") print([x.function.name for x in response.message.tool_calls]) if verify_no_overlap(list(available_functions.keys()), [x.function.name for x in response.message.tool_calls]): # 通常の返答を返す response = client.chat( model_name, messages=messages_original ) print("利用したい関数が定義されていません。") print("通常の回答を返します。") print(response.message.content) else: # Function calling for tool in response.message.tool_calls: if function_to_call := available_functions.get(tool.function.name): print(f"関数の呼び出し: {tool.function.name}") print(f"引数: {tool.function.arguments}") output = function_to_call(**tool.function.arguments) print(f'関数の出力: {output}') # ツール呼び出しの結果を使ってモデルとチャットを続ける場合 # モデルが使用するメッセージにツールの応答を追加 messages = copy.deepcopy(messages_original) messages.append(response.message) messages.append( { "role": "tool", "content": str(output), "name": tool.function.name } ) # 関数の出力を含めた最終的な応答を取得 final_response = client.chat( model_name, messages=messages ) print(f"最終応答: {final_response.message.content}") else: print("利用できる関数が見つかりませんでした。") print("通常の回答を返します。") print(response.message.content)
結果
プロンプト: 5に3をかけるといくつ? Function calling... ['multiply_two_numbers'] 利用できる関数が見つかりませんでした。 通常の回答を返します。 15です。
プロンプト: 5に3をたすといくつ?
Function calling...
['add_two_numbers']
関数の呼び出し: add_two_numbers
引数: {'a': 5, 'b': 3}
関数の出力: 8
最終応答: 答えは8です。
疑問点
書籍によるとOpenAIライブラリでは「tool_choice」というパラメーターを指定できるようです。「tool_choice="auto"」を指定すると、関数を使うべきでないと判断した場合に通常の回答を返してくれるようです。Ollamaライブラリで同様のパラメーターが指定できるかどうかわかりませんでした。LangChainを使ってみる
LangChainを使ったFunction callingのプログラムも見よう見まねで書いてみました。Pythonスクリプト
from langchain_ollama import ChatOllama from langchain.tools import tool from langchain_core.prompts import ChatPromptTemplate from langchain.agents import AgentExecutor, create_tool_calling_agent prompt = ChatPromptTemplate( [ ("system", "あなたは優秀なAIアシスタントです。日本語で回答して下さい。"), ("human", "{input}"), # Placeholders fill up a **list** of messages ("placeholder", "{agent_scratchpad}"), ] ) # Chatモデルの初期化 chat_model = ChatOllama(model="qwen2.5:14b", temperature=0) # ツールとしての関数を定義 @tool def add_two_numbers(a: int, b: int) -> int: """2つの数値を足し算する関数。 Args: a (int): 1つ目の数値 b (int): 2つ目の数値 Returns: int: 足し算の結果 """ result = a + b return result @tool def subtract_two_numbers(a: int, b: int) -> int: """2つの数値を引き算する関数。 Args: a (int): 引かれる数 b (int): 引く数 Returns: int: 引き算の結果 """ result = a - b return result # ツールリストを作成 tools = [add_two_numbers, subtract_two_numbers] # エージェントの初期化 agent = create_tool_calling_agent(chat_model, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools) query = "9に3を加えるといくつになりますか?" for step in agent_executor.stream({"input": query}): print(step) print()
結果
{'actions': [ToolAgentAction(tool='add_two_numbers', tool_input={'a': 9, 'b': 3}, log="\nInvoking: `add_two_numbers` with `{'a': 9, 'b': 3}`\n\n\n", message_log=[AIMessageChunk(content='', additional_kwargs={}, response_metadata={'model': 'qwen2.5:14b', 'created_at': '2025-01-04T11:32:23.7166181Z', 'done': True, 'done_reason': 'stop', 'total_duration': 10193175200, 'load_duration': 8126786800, 'prompt_eval_count': 340, 'prompt_eval_duration': 624000000, 'eval_count': 29, 'eval_duration': 1055000000, 'message': Message(role='assistant', content='', images=None, tool_calls=None)}, id='run-b915e2b4-86d0-4be5-a57d-d99d431ae604', tool_calls=[{'name': 'add_two_numbers', 'args': {'a': 9, 'b': 3}, 'id': '769feb1c-2902-4d72-9113-63aa291639cf', 'type': 'tool_call'}], usage_metadata={'input_tokens': 340, 'output_tokens': 29, 'total_tokens': 369}, tool_call_chunks=[{'name': 'add_two_numbers', 'args': '{"a": 9, "b": 3}', 'id': '769feb1c-2902-4d72-9113-63aa291639cf', 'index': None, 'type': 'tool_call_chunk'}])], tool_call_id='769feb1c-2902-4d72-9113-63aa291639cf')], 'messages': [AIMessageChunk(content='', additional_kwargs={}, response_metadata={'model': 'qwen2.5:14b', 'created_at': '2025-01-04T11:32:23.7166181Z', 'done': True, 'done_reason': 'stop', 'total_duration': 10193175200, 'load_duration': 8126786800, 'prompt_eval_count': 340, 'prompt_eval_duration': 624000000, 'eval_count': 29, 'eval_duration': 1055000000, 'message': Message(role='assistant', content='', images=None, tool_calls=None)}, id='run-b915e2b4-86d0-4be5-a57d-d99d431ae604', tool_calls=[{'name': 'add_two_numbers', 'args': {'a': 9, 'b': 3}, 'id': '769feb1c-2902-4d72-9113-63aa291639cf', 'type': 'tool_call'}], usage_metadata={'input_tokens': 340, 'output_tokens': 29, 'total_tokens': 369}, tool_call_chunks=[{'name': 'add_two_numbers', 'args': '{"a": 9, "b": 3}', 'id': '769feb1c-2902-4d72-9113-63aa291639cf', 'index': None, 'type': 'tool_call_chunk'}])]}
{'steps': [AgentStep(action=ToolAgentAction(tool='add_two_numbers', tool_input={'a': 9, 'b': 3}, log="\nInvoking: `add_two_numbers` with `{'a': 9, 'b': 3}`\n\n\n", message_log=[AIMessageChunk(content='', additional_kwargs={}, response_metadata={'model': 'qwen2.5:14b', 'created_at': '2025-01-04T11:32:23.7166181Z', 'done': True, 'done_reason': 'stop', 'total_duration': 10193175200, 'load_duration': 8126786800, 'prompt_eval_count': 340, 'prompt_eval_duration': 624000000, 'eval_count': 29, 'eval_duration': 1055000000, 'message': Message(role='assistant', content='', images=None, tool_calls=None)}, id='run-b915e2b4-86d0-4be5-a57d-d99d431ae604', tool_calls=[{'name': 'add_two_numbers', 'args': {'a': 9, 'b': 3}, 'id': '769feb1c-2902-4d72-9113-63aa291639cf', 'type': 'tool_call'}], usage_metadata={'input_tokens': 340, 'output_tokens': 29, 'total_tokens': 369}, tool_call_chunks=[{'name': 'add_two_numbers', 'args': '{"a": 9, "b": 3}', 'id': '769feb1c-2902-4d72-9113-63aa291639cf', 'index': None, 'type': 'tool_call_chunk'}])], tool_call_id='769feb1c-2902-4d72-9113-63aa291639cf'), observation=12)], 'messages': [FunctionMessage(content='12', additional_kwargs={}, response_metadata={}, name='add_two_numbers')]}
{'output': '9に3を加えた結果は12になります。', 'messages': [AIMessage(content='9に3を加えた結果は12になります。', additional_kwargs={}, response_metadata={})]}
出力を簡略にする
最後のstreamをinvokeに変えると出力が簡略化されます。response = agent_executor.invoke({"input": query})
print(response)
{'input': '9に3を加えるといくつになりますか?', 'output': '9に3を加えた結果は12になります。'}
一般的でない関数
ここまで指定した関数は足し算、引き算の一般的な関数でした。関数を呼び出さないでも正確に答えてくれる可能性が高く、Function callingがうまく機能しているかよくわかりません。そこで物の価格に消費税を加える関数を作ってみました。消費税率は20%と現実と異なる数字を設定しています。from langchain_ollama import ChatOllama from langchain.tools import tool from langchain_core.prompts import ChatPromptTemplate from langchain.agents import AgentExecutor, create_tool_calling_agent prompt = ChatPromptTemplate( [ ("system", "あなたは優秀なAIアシスタントです。日本語で回答して下さい。"), ("human", "{input}"), # Placeholders fill up a **list** of messages ("placeholder", "{agent_scratchpad}"), ] ) # Chatモデルの初期化 chat_model = ChatOllama(model="qwen2.5:14b", temperature=0) # ツールとしての関数を定義 @tool def add_tax(price: int) -> int: """商品の価格に消費税を加える関数。 Args: price (int): 商品の価格(消費税抜き) Returns: int: 商品の価格に消費税を加えた価格 """ tax_rate = 0.2 result = int(price * (1 + tax_rate)) return result # ツールリストを作成 tools = [add_tax] # エージェントの初期化 agent = create_tool_calling_agent(chat_model, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools) query = "500円の商品に消費税を加えるといくらになりますか?" for step in agent_executor.stream({"input": query}): print(step) print()
結果
{'actions': [ToolAgentAction(tool='add_tax', tool_input={'price': 500}, log="\nInvoking: `add_tax` with `{'price': 500}`\n\n\n", message_log=[AIMessageChunk(content='', additional_kwargs={}, response_metadata={'model': 'qwen2.5:14b', 'created_at': '2025-01-07T01:37:16.5321764Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1213907000, 'load_duration': 23134800, 'prompt_eval_count': 223, 'prompt_eval_duration': 338000000, 'eval_count': 22, 'eval_duration': 848000000, 'message': Message(role='assistant', content='', images=None, tool_calls=None)}, id='run-9e1ea839-2d9a-4c39-bd7d-af745368e4f2', tool_calls=[{'name': 'add_tax', 'args': {'price': 500}, 'id': '30807374-c0cb-4227-8b52-c7e5f20eb29c', 'type': 'tool_call'}], usage_metadata={'input_tokens': 223, 'output_tokens': 22, 'total_tokens': 245}, tool_call_chunks=[{'name': 'add_tax', 'args': '{"price": 500}', 'id': '30807374-c0cb-4227-8b52-c7e5f20eb29c', 'index': None, 'type': 'tool_call_chunk'}])], tool_call_id='30807374-c0cb-4227-8b52-c7e5f20eb29c')], 'messages': [AIMessageChunk(content='', additional_kwargs={}, response_metadata={'model': 'qwen2.5:14b', 'created_at': '2025-01-07T01:37:16.5321764Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1213907000, 'load_duration': 23134800, 'prompt_eval_count': 223, 'prompt_eval_duration': 338000000, 'eval_count': 22, 'eval_duration': 848000000, 'message': Message(role='assistant', content='', images=None, tool_calls=None)}, id='run-9e1ea839-2d9a-4c39-bd7d-af745368e4f2', tool_calls=[{'name': 'add_tax', 'args': {'price': 500}, 'id': '30807374-c0cb-4227-8b52-c7e5f20eb29c', 'type': 'tool_call'}], usage_metadata={'input_tokens': 223, 'output_tokens': 22, 'total_tokens': 245}, tool_call_chunks=[{'name': 'add_tax', 'args': '{"price": 500}', 'id': '30807374-c0cb-4227-8b52-c7e5f20eb29c', 'index': None, 'type': 'tool_call_chunk'}])]}
{'steps': [AgentStep(action=ToolAgentAction(tool='add_tax', tool_input={'price': 500}, log="\nInvoking: `add_tax` with `{'price': 500}`\n\n\n", message_log=[AIMessageChunk(content='', additional_kwargs={}, response_metadata={'model': 'qwen2.5:14b', 'created_at': '2025-01-07T01:37:16.5321764Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1213907000, 'load_duration': 23134800, 'prompt_eval_count': 223, 'prompt_eval_duration': 338000000, 'eval_count': 22, 'eval_duration': 848000000, 'message': Message(role='assistant', content='', images=None, tool_calls=None)}, id='run-9e1ea839-2d9a-4c39-bd7d-af745368e4f2', tool_calls=[{'name': 'add_tax', 'args': {'price': 500}, 'id': '30807374-c0cb-4227-8b52-c7e5f20eb29c', 'type': 'tool_call'}], usage_metadata={'input_tokens': 223, 'output_tokens': 22, 'total_tokens': 245}, tool_call_chunks=[{'name': 'add_tax', 'args': '{"price": 500}', 'id': '30807374-c0cb-4227-8b52-c7e5f20eb29c', 'index': None, 'type': 'tool_call_chunk'}])], tool_call_id='30807374-c0cb-4227-8b52-c7e5f20eb29c'), observation=600)], 'messages': [FunctionMessage(content='600', additional_kwargs={}, response_metadata={}, name='add_tax')]}
{'output': '500円の商品に消費税を加えた価格は600円になります。', 'messages': [AIMessage(content='500円の商品に消費税を加えた価格は600円になります。', additional_kwargs={}, response_metadata={})]}
正確な回答が返って来ました。これでFunction Callingがうまくいっていることが証明できます。
まったく関係ない質問を投げかけても正確に回答してくれます。
query = "山梨県の県庁所在地は"
{'output': '山梨県の県庁所在地は甲府市です。', 'messages': [AIMessage(content='山梨県の県庁所在地は甲府市です。', additional_kwargs={}, response_metadata={})]}
「bind_tools」を使って書く
「bind_tools」を使って書く方法もあるようです。Pythonスクリプト
from langchain_core.messages import HumanMessage, SystemMessage from langchain_ollama import ChatOllama from langchain.tools import tool model = ChatOllama(model="qwen2.5:14b", temperature=0) # ツールとしての関数を定義 @tool def add_tax(price: int) -> int: """商品の価格に消費税を加える関数。 Args: price (int): 商品の価格(消費税抜き) Returns: int: 商品の価格に消費税を加えた価格 """ tax_rate = 0.2 result = int(price * (1 + tax_rate)) return result tools_list = [add_tax] model_with_tools = model.bind_tools(tools_list) messages = [ SystemMessage(content="あなたは優秀なAIアシスタントです。日本語で回答して下さい。") ] #query = "山梨県の県庁所在地は?" query = "500円の商品に消費税を加えるといくらになりますか?" messages.append(HumanMessage(content=query)) ai_message = model_with_tools.invoke(messages) if len(ai_message.tool_calls) == 0: print(f"result: {ai_message.content}") else: print(ai_message.tool_calls) for tool_call in ai_message.tool_calls: selected_tool = {"add_tax": add_tax}[tool_call["name"].lower()] tool_msg = selected_tool.invoke(tool_call) messages.append(tool_msg) result = model_with_tools.invoke(messages) print(f"result: {result.content}")
結果
query = "500円の商品に消費税を加えるといくらになりますか?"
[{'name': 'add_tax', 'args': {'price': 500}, 'id': '784495f1-38c8-4187-8178-1c058fcf7fba', 'type': 'tool_call'}]
result: 500円の商品に消費税を加えた価格は600円になります。
query = "山梨県の県庁所在地は?"
result: 山梨県の県庁所在地は甲府市です。
使用したライブラリのバージョン
langchain==0.3.14 langchain-core==0.3.29 langchain-ollama==0.2.2 ollama==0.4.5