ジェネラティブエージェンツの大嶋です。
この記事では、LangChainの@tool
をPythonのクラスにつける例を紹介します。
はじめに
LangChainの@tool
デコレーターは、自作の関数をLangChainのツールにするのに便利です。
たとえば以下のように使用します。
from langchain_core.tools import tool @tool def get_current_weather(location: str) -> str: """Get the current weather in a given location""" return f"It is currently 70 degrees and sunny in {location}."
実はこの@tool
デコレーターはPythonのクラスにもつけることができます。
@tool class FinalAnswer(BaseModel): """Final answer to the user's question.""" final_answer: str reference_urls: list[str]
LangChain Interruptのワークショップ でこのような例を見かけたので、この記事では@tool
をPythonのクラスにつける例を紹介し、それがなぜ動作するのかを解説します。
@tool
をPythonのクラスにつける場面は、エージェントのツールとして特定の構造で出力させたいが、その出力を関数の引数としては使わないときです。
たとえばエージェントの最終出力として、以下のFinalAnswerクラスの形式で出力させたいとします。
from langchain_core.tools import tool from pydantic import BaseModel @tool class FinalAnswer(BaseModel): """Final answer to the user's question.""" final_answer: str reference_urls: list[str]
Tavilyなどの検索ツールと一緒に使い、最終出力では回答と参考URLリストを出力させたいという例です。
このとき、上記のコードのようにクラスに@tool
をつけることで、エージェントにツールの一種として指定した形式で出力させることができます。
ここから、上記のFinalAnswerツールを使う例を見ていきます。
ツールをLLMにバインドする
まずは@tool
をつけたFinalAnswerを、bind_toolsメソッドで通常のツールと同じくLLMにバインドします。
from langchain_openai import ChatOpenAI from langchain_tavily import TavilySearch tavily_search = TavilySearch() tools = [tavily_search, FinalAnswer] llm = ChatOpenAI(model="gpt-4.1", temperature=0) llm_with_tools = llm.bind_tools(tools, tool_choice="required")
エージェントの実装
上記のllm_with_toolsを使ったエージェントを実装します。
from typing import Any from langchain_core.messages import BaseMessage, ToolMessage from langsmith import traceable @traceable def agent(messages: list[BaseMessage]) -> FinalAnswer: while True: ai_message = llm_with_tools.invoke(messages) for tool_call in ai_message.tool_calls: tool_name = tool_call["name"] tool_args = tool_call["args"] if tool_name == tavily_search.name: tool_to_call = next(t for t in tools if t.name == tool_name) tool_result = tool_to_call.invoke(tool_args) tool_message = ToolMessage( content=tool_result, tool_call_id=tool_call["id"], ) messages.extend([ai_message, tool_message]) elif tool_name == FinalAnswer.name: return FinalAnswer(tool_args) else: raise ValueError(f"Unknown tool name: {tool_name}")
ポイントは、LLMが選択したツール名が「FinalAnswer」であれば、その引数をエージェントの実行結果として返している点です。
※上記のコードではエージェントを「agent」という関数として実装していますが、もちろんLangGraphで実装することもできます。
エージェントの実行
上記のagent関数は以下のように実行できます。
initial_messages = [("user", "ChatGPTのニュースを教えて")] final_answer = agent(initial_messages) print(final_answer.final_answer) print("n参考") for url in final_answer.reference_urls: print(f"- {url}")
実行結果は以下のような出力になります。
最新のChatGPT関連ニュースとしては、以下の話題があります: : : 参考 - - -
LangSmithでトレースを見ると、最終的にFinalAnswerの形式で出力していることが分かります。
このように、エージェントのツールとして特定の構造で出力させたいが、その出力を関数の引数としては使わないときに、@tool
をPythonのクラスにつける手法が活用できます。
FinalAnswer以外の例
FinalAnswer以外の例としては、たとえばユーザーに質問したいときに選択するツールを@tool
をつけたクラスで実装することができます。
@tool class Question(BaseModel): """Question to ask user.""" content: str
LangGraphのinterrupt機能と組み合わせて、このツールが選択されたらユーザーに質問を求める、という挙動を実装できます。
ここまで解説したように、LangChainの@tool
デコレーターはPythonのクラスにもつけることができます。
しかし、その例は公式ドキュメントには記載されていません。
ここから、なぜ@tool
をPythonのクラスにつけることができるのかを簡単に解説します。
@tool
でツール化できるもの
LangChainの@tool
デコレーターは、LangChainのソースコードの以下の箇所で実装されています。
ここを参照すると、@tool
デコレーターはCallableにつけることができると分かります。
def tool( name_or_callable: Optional[Union[str, Callable]] = None, :
Callableというのは、関数その他呼び出し可能なオブジェクトを指します。
PythonのクラスはCallable
実は、PythonのクラスはCallableです。
たとえばFinalAnswerクラスは、FinalAnswer({"final_answer": "...", "reference_urls": ["...", "..."]})
のように呼び出すことができます。
@tool class FinalAnswer(BaseModel): """Final answer to the user's question.""" final_answer: str reference_urls: list[str]
実際、以下のコードでMyClassがCallableであることを確認できます。
class MyClass(BaseModel): pass print(callable(MyClass))
PythonではクラスはCallableであるため、LangChainの@tool
はクラスにもつけることができるということです。
まとめ
以上、LangChainの@tool
デコレーターをPythonのクラスにもつける例を紹介し、それがなぜ動作するのかを解説しました。
例に出したFinalAnswerやQuestionのように、エージェントのツールとして特定の構造で出力させたいが、その出力を関数の引数としては使わないときに、@tool
をPythonのクラスにつける手法が活用できます。
Pythonのクラスに@tool
をつけることができるのは、PythonのクラスがCallableであるためです。
動作確認したコード
この記事で使ったコードの全体像は以下のようになります。
from typing import Any from langchain_core.messages import BaseMessage, ToolMessage from langchain_core.tools import tool from langchain_openai import ChatOpenAI from langchain_tavily import TavilySearch from langsmith import traceable from pydantic import BaseModel @tool class FinalAnswer(BaseModel): """Final answer to the user's question.""" final_answer: str reference_urls: list[str] tavily_search = TavilySearch() tools = [tavily_search, FinalAnswer] llm = ChatOpenAI(model="gpt-4.1", temperature=0) llm_with_tools = llm.bind_tools(tools, tool_choice="required") @traceable def agent(messages: list[BaseMessage]) -> FinalAnswer: while True: ai_message = llm_with_tools.invoke(messages) for tool_call in ai_message.tool_calls: tool_name = tool_call["name"] tool_args = tool_call["args"] if tool_name == tavily_search.name: tool_to_call = next(t for t in tools if t.name == tool_name) tool_result = tool_to_call.invoke(tool_args) tool_message = ToolMessage( content=tool_result, tool_call_id=tool_call["id"], ) messages.extend([ai_message, tool_message]) elif tool_name == FinalAnswer.name: return FinalAnswer(tool_args) else: raise ValueError(f"Unknown tool name: {tool_name}") initial_messages = [("user", "ChatGPTのニュースを教えて")] final_answer = agent(initial_messages) print(final_answer.final_answer) print("n参考") for url in final_answer.reference_urls: print(f"- {url}")
ちなみに、上記のコードと同じような挙動は、LangGraphのcreate_react_agentでresponse_formatを使うことでも実装できます。
from langgraph.prebuilt import create_react_agent agent = create_react_agent(model=llm, tools=[tavily_search], response_format=FinalAnswer) result = agent.invoke({"messages": [("user", "ChatGPTのニュースを教えて")]}) final_answer = FinalAnswer(result["structured_response"]) print(final_answer)
create_react_agentで単に最終的な出力を指定した構造にしたいだけであれば、create_react_agentのresponse_formatを使うだけで問題ありません。
より複雑な例になると、create_react_agentではなく自身でエージェントを実装することになります。
この記事のコードは、LangChainなどの以下のバージョンで動作確認しました。
langchain==0.3.25 langchain-core==0.3.59 langchain-openai==0.3.16 langchain-tavily==0.1.6 langchain-text-splitters==0.3.8 langgraph==0.4.3 langgraph-checkpoint==2.0.25 langgraph-prebuilt==0.1.8 langgraph-sdk==0.1.69 langsmith==0.3.42
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = “//connect.facebook.net/ja_JP/sdk.js#xfbml=1&appId=719729204785177&version=v17.0”;
fjs.parentNode.insertBefore(js, fjs);
}(document, ‘script’, ‘facebook-jssdk’));
続きを読む
Views: 0