LangGraphのTool callingでOpenAI APIのFunction callingを試してみよう #openai #azure #langgraph #langchain #llm #ai
はじめに
LangGraphのチュートリアル Enhancing the Chatbot with Tools では、チャットボットがLLM (Large Language Model; 大規模言語モデル)から答えを得られない場合にウェブ検索を行う仕組みを導入しています。
素朴に考えれば「○○について教えてください。わからない場合はSEARCHとのみ答えてください」というプロンプトを与えて返り値を調べて、SEARCHであれば「○○」でウェブ検索し、そうでなければ返り値を答えとして返す、というような形になると思います。しかし実際のサンプルコードを見てみると、そのような判断を行うプロンプトも条件分岐もありません。
これはLangGraphのTool callingとOpenAI APIの概念であるFunction callingによって、判断プロンプトや条件分岐を作ることなく実現できています。
本項ではLangGraphのTool callingとOpenAI APIのFunction callingがどのようになっているか見てみます。
OpenAI APIのFunction callingとは
OpenAIは2023年6月13日に Function calling and other API updates として発表しました。
チャットに用いる Chat Completions API では通常は次のように、質問や指示の会話文(に加えてロールなど)を与えると思います。
question="今日は何年何月何日で、東京の天気はなんですか?" import os from dotenv import load_dotenv load_dotenv() from openai import AzureOpenAI client = AzureOpenAI( api_key = os.getenv("AZURE_OPENAI_API_KEY"), api_version = os.getenv("AZURE_OPENAI_API_VERSION"), azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT"), ) completion = client.chat.completions.create( model = os.getenv("AZURE_OPENAI_MODEL_NAME"), messages = [{ "role": "user", "content": question }], ) import pprint pprint.pprint((completion.model_dump())["choices"][0]["message"])
結果は次の通りです。
{'audio': None, 'content': '申し訳ありませんが、現在の正確な日時や天気情報を提供することはできません。最新の天気情報を知りたい場合は、天気予報サービスやニュースサイトをご確認ください。', 'function_call': None, 'refusal': None, 'role': 'assistant', 'tool_calls': None}
LLMだけではこのようにつれない結果となってしまいました。
Function calling では、会話文に加えて、関数を呼び出すための指示を与えることができます。「関数を呼び出すための指示」というのがちょっとピンとこないと思いますので、まずは次のコードを見てください。
question="今日は何年何月何日で、東京の天気はなんですか?" import os from dotenv import load_dotenv load_dotenv() from openai import AzureOpenAI client = AzureOpenAI( api_key = os.getenv("AZURE_OPENAI_API_KEY"), api_version = os.getenv("AZURE_OPENAI_API_VERSION"), azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT"), ) completion = client.chat.completions.create( model = os.getenv("AZURE_OPENAI_MODEL_NAME"), messages = [{ "role": "user", "content": question }], tools=[{ "type": "function", "function": { "name": "japanese_search", "description": "包括的、正確、信頼できる結果を得るために最適化された検索エンジン。現在の出来事に関する質問に答える必要がある場合に便利です。入力は検索クエリである必要があります。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "検索クエリ文字列", }, }, }, "required": ["query"], }, }], ) import pprint pprint.pprint((completion.model_dump())["choices"][0]["message"])
ここではChatCompletions APIに tools
という入力を追加していることに注目してください。これが「関数を呼び出すための指示」です。おおまかに、
name
: 関数の名前description
: 関数の動作parameters
: 関数の引数required
: 関数に必須の引数
といった形です。これにより、LLMのみで答えが出せない場合、「こういった形で関数を呼び出して」という答えが返ってくるようになります。なお description
は tavily_search で使われているものを和訳しています。このファイルについては後述します。
実際に動かした結果は次のようになります。
{'audio': None, 'content': None, 'function_call': None, 'refusal': None, 'role': 'assistant', 'tool_calls': [{'function': {'arguments': '{"query": "今日の日付"}', 'name': 'japanese_search'}, 'id': 'call_QSfXkwvwXiVkeJehpLmLI78V', 'type': 'function'}, {'function': {'arguments': '{"query": "東京の天気"}', 'name': 'japanese_search'}, 'id': 'call_ZF0Ijs2wEnqrnsNpyoANbSXU', 'type': 'function'}]}
tool_calls
の内容を見てください。「今日は何年何月何日で、東京の天気はなんですか?」という問いにLLMからは答えが得られないので、 {"query": "今日の日付"}
と {"query": "東京の天気"}
を返すのでこれらを使ってウェブ検索を行う関数を呼び出してください、ということになります。
OpenAI は実際のウェブ検索までは行ってくれないので、その関数を自作しなければいけませんが、ウェブ検索が必要かどうかの判定や必要なキーワードの選定を、それ用のプロンプトを作らなくても行ってくれます。判定・選定は descrption
をはじめとした「関数を呼び出すための指示」に従っているようですが、詳細がよくわからないことが少し気になるところです。しかしながら、それを差し引いてもかなり強力な機能だと思います。
LangGraphのTool callingとは
LangGraphの中で、OpenAI APIにおけるFunction callingを行い、その結果によって関数を実行する、といった一連の流れを行ってくれる仕組みがTool callingです。
まず、Tool callingを使わない、LangGraph のみの単純なコードを見てください。
question="今日は何年何月何日で、東京の天気はなんですか?" import os from dotenv import load_dotenv load_dotenv() from langchain_openai import AzureChatOpenAI llm = AzureChatOpenAI( azure_deployment = os.getenv("AZURE_OPENAI_MODEL_NAME"), api_version = os.getenv("AZURE_OPENAI_API_VERSION"), ) from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph from langgraph.graph.message import add_messages from langchain_core.messages import AIMessage class State(TypedDict): messages: Annotated[list, add_messages] def chatbot(state: State): res = llm.invoke(state["messages"]) return {"messages": res} graph_builder = StateGraph(State) graph_builder.add_node("chatbot", chatbot) graph_builder.set_entry_point("chatbot") graph_builder.set_finish_point("chatbot") graph = graph_builder.compile() events = graph.stream( {"messages": [("user", question)]}, stream_mode="values" ) for event in events: message = event["messages"][-1] if type(message) == AIMessage: print(message.content)
これを実行すると、次のようになります。
今日は2023年10月23日です。しかし、リアルタイムの天気情報を提供することはできません。最新の天気情報は、天気予報のウェブサイトやアプリでご確認ください。
やはり、つれない返事です。しかもLLMの知識の中での「現在」の日付を返してきたようです。
これに Tavily 検索を組み込んでみましょう。これには langchain-community ライブラリの tavily_search ツールを使います。
question="今日は何年何月何日で、東京の天気はなんですか?" import os from dotenv import load_dotenv load_dotenv() from langchain_community.tools.tavily_search import TavilySearchResults tavily_search_tool = TavilySearchResults(max_results=2) from langchain_openai import AzureChatOpenAI llm = AzureChatOpenAI( azure_deployment = os.getenv("AZURE_OPENAI_MODEL_NAME"), api_version = os.getenv("AZURE_OPENAI_API_VERSION"), ) tools = [tavily_search_tool] llm_with_tools = llm.bind_tools(tools) from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph from langgraph.graph.message import add_messages from langchain_core.messages import AIMessage from langgraph.prebuilt import ToolNode, tools_condition class State(TypedDict): messages: Annotated[list, add_messages] def chatbot(state: State): res = llm_with_tools.invoke(state["messages"]) return {"messages": res} graph_builder = StateGraph(State) graph_builder.add_node("chatbot", chatbot) tool_node = ToolNode(tools=tools) graph_builder.add_node("tools", tool_node) graph_builder.set_entry_point("chatbot") graph_builder.add_conditional_edges( "chatbot", tools_condition, ) graph_builder.add_edge("tools", "chatbot") graph_builder.set_finish_point("chatbot") graph = graph_builder.compile() events = graph.stream( {"messages": [("user", question)]}, stream_mode="values" ) for event in events: message = event["messages"][-1] if type(message) == AIMessage: print(message.content)
このようになります。差分は次の通りです。
--- 57_langgraph_function_calling_no_used.py 2025-01-08 18:30:15.662495364 +0900 +++ 58_langgraph_function_calling_tavily.py 2025-01-08 18:48:09.746694169 +0900 @@ -6,28 +6,42 @@ from dotenv import load_dotenv load_dotenv() +from langchain_community.tools.tavily_search import TavilySearchResults +tavily_search_tool = TavilySearchResults(max_results=2) + from langchain_openai import AzureChatOpenAI llm = AzureChatOpenAI( azure_deployment = os.getenv("AZURE_OPENAI_MODEL_NAME"), api_version = os.getenv("AZURE_OPENAI_API_VERSION"), ) +tools = [tavily_search_tool] +llm_with_tools = llm.bind_tools(tools) from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph from langgraph.graph.message import add_messages from langchain_core.messages import AIMessage +from langgraph.prebuilt import ToolNode, tools_condition class State(TypedDict): messages: Annotated[list, add_messages] def chatbot(state: State): - res = llm.invoke(state["messages"]) + res = llm_with_tools.invoke(state["messages"]) return {"messages": res} graph_builder = StateGraph(State) graph_builder.add_node("chatbot", chatbot) +tool_node = ToolNode(tools=tools) +graph_builder.add_node("tools", tool_node) + graph_builder.set_entry_point("chatbot") +graph_builder.add_conditional_edges( + "chatbot", + tools_condition, +) +graph_builder.add_edge("tools", "chatbot") graph_builder.set_finish_point("chatbot") graph = graph_builder.compile()
注目すべき点を見てみましょう。
+from langchain_community.tools.tavily_search import TavilySearchResults +tavily_search_tool = TavilySearchResults(max_results=2)
先程も触れたtavily_search ツールを組み込んでいます。これにより簡単に言うと、Function calling で呼び出す、ウェブ検索を行うための関数を自作せずに済みます。
+tools = [tavily_search_tool] +llm_with_tools = llm.bind_tools(tools)
LLMにツールの関連付けを行っています。
- res = llm.invoke(state["messages"]) + res = llm_with_tools.invoke(state["messages"])
LLM単体ではなく、ツールを関連付けたLLMを使うように変更しています。
graph_builder.add_node("chatbot", chatbot) +tool_node = ToolNode(tools=tools) +graph_builder.add_node("tools", tool_node) + graph_builder.set_entry_point("chatbot") +graph_builder.add_conditional_edges( + "chatbot", + tools_condition, +) +graph_builder.add_edge("tools", "chatbot") graph_builder.set_finish_point("chatbot")
ツールを使うToolNodeを追加し、LLMとやり取りを行う chatbot
ノードと接続しています。 tools_condition
では、ツールを実行するかどうかを判断しています。
結果は次の通りです。
今日は2025年1月8日です。東京の天気は「少し曇り」で、気温は約8.2°Cです。風速は約20.5 km/hで、湿度は34%です。
気象庁のデータを見てみたところ、おおむね正しそうです。
何か特別なプロンプトを書かずに、LLMが知らない知識を検索してくれるようにできました。
ところで、実際にはどのようなやりとりがなされているのでしょうか? 次のように pretty_print
を使うように変更を加えて実行してみましょう。
@@ -51,6 +51,4 @@ ) for event in events: - message = event["messages"][-1] - if type(message) == AIMessage: - print(message.content) + event["messages"][-1].pretty_print()
次のような結果になりました。Tool Message の部分は見やすいように手でJSONを整形しています。
====================== Human Message ======================= 今日は何年何月何日で、東京の天気はなんですか? ======================== Ai Message ======================== Tool Calls: tavily_search_results_json (call_JrIL9nydrduuxZYHIo898iuD) Call ID: call_JrIL9nydrduuxZYHIo898iuD Args: query: Tokyo weather today tavily_search_results_json (call_UZZLIbgIMOT9Ja9PCQetxXCi) Call ID: call_UZZLIbgIMOT9Ja9PCQetxXCi Args: query: current date ======================= Tool Message ======================= Name: tavily_search_results_json [{ "url": "https://datetimetoday.com/", "content": "Check the current date and time for any location in the world. Find date, time, timezone and more for over 200 countries. ... Check the current date and time for any location in the world. Find date, time, timezone and more for over 200 countries. DateTimeToday.Com Dark Mode. Los Angeles. Tuesday, January 7, 2025. 11:28:16 AM . New York. Tue" }, { "url": "https://www.calendardate.com/todays.htm", "content": "Monday November 18, 2024 November 2024 Calendar Holidays 2024 Holidays 2025 Holidays November 2024 Holidays Todays Date Today's Moon Phases November Moon Phases Today's Date Today's Date is Monday November 18, 2024 Time zone: California/Mountain View Change Time zone November 2024 Day Number of Year: 323 Month Number of Year: 11 Sun Today Moon Today Zodiac Signs and Birthday Symbols for Today's Date Nov 28 - Thurs Thanksgiving Day 2024 Federal Holiday Dec 2 - Mon Cyber Monday 2024 Observance Dec 6 - Fri St Nicholas Day 2024 Christian Dec 7 - Sat Pearl Harbor Remebrance Day 2024 Observance Dec 17 - Tues Wright Brothers Day 2024 Observance Site Map By using our site you consent to our Privacy Policy." }] ======================== Ai Message ======================== 今日は2025年1月9日です。 東京の現在の天気は「曇り時々晴れ」で、気温は10°Cです。風は南南西から19.1 km/hで吹いており、湿度は29%です。
人間の入力「今日は何年何月何日で、東京の天気はなんですか?」がLLMの知識からは答えられないので、関数を呼び出すように判断し、そして関数(ツール)の結果を受け取り、それを再度LLMに与えて最終結果を出している、といった形になっています。最終結果は気象庁のデータによれば正しそうです。
まとめ
本項では、LangGraphのTool callingとOpenAI APIのFunction callingによって、判断プロンプトや条件分岐を作ることなく、LLMの知識から答えられない内容はウェブ検索して答えを得られることを見てみました。
LangGraphのツールは今回取り上げたTavily検索以外にも多数のツールがあるため、ウェブ検索以外を作業させられることが期待できます。またFunction callingはOpenAI APIの概念ですが、他のLLMサービスにも同様の実装が行われています。LangGraphのTool callingを使えば、サービスごとの違いを隠蔽して利用することもできそうです。
なお、Function callingも結局のところLLMの知識から呼び出し関数の引数などを決定していること、文中でも挙げた通り、その決定プロセスがブラックボックスであることが気になるところです。また、ツールの使用が必要になった場合はその結果を再びLLMに入力して最終結果を得るという仕組みのため、入出力トークンの大幅増加、つまり人間が入力した会話文の見た目以上にコストがかかることに注意が必要です。
このような留意点があるものの、Tool calling/Function callingは極めて強力です。筆者の経験としては、単にLLMのみの場合だと「知らないことばかりで使えないな…」といった残念な感想になってしまうのですが、ウェブ検索を組み込んでみると「そこそこ答えてくれる、これは使える」と一気に評価が上がるほどでした。組み込み自体もかなり手軽に行えるので、是非お試しください。