木曜日, 5月 15, 2025
ホームニュースChatGPTLangChainの`@tool`をPythonのクラスにつける例の紹介 #LangChain - Generative Agents Tech Blog

LangChainの`@tool`をPythonのクラスにつける例の紹介 #LangChain – Generative Agents Tech Blog


ジェネラティブエージェンツの大嶋です。

この記事では、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のクラスにもつけることができます。

しかし、その例は公式ドキュメントには記載されていません。

python.langchain.com

ここから、なぜ@toolをPythonのクラスにつけることができるのかを簡単に解説します。

@toolでツール化できるもの

LangChainの@toolデコレーターは、LangChainのソースコードの以下の箇所で実装されています。

github.com

ここを参照すると、@toolデコレーターはCallableにつけることができると分かります。

def tool(
    name_or_callable: Optional[Union[str, Callable]] = None,
        :

Callableというのは、関数その他呼び出し可能なオブジェクトを指します。

docs.python.org

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

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -

インモビ転職