langchianlogo

LangChain入門(6) – Tool/Agent – 外部世界への橋渡し

ToolとAgentは生成AIが外部に働きかけるときの窓口のような働きをします。モデルの可能性を大きく広げてくれます。あらかじめ用意されているツールもありますが、自分で拡張することも可能です。今回はそんなツールとツールを活用するAgentについてみてゆきましょう。

本記事はFuture Coders独自教材からの抜粋です。変化の早い分野なので記事の内容が古くなっている可能性もあります。ご注意ください。

ToolとAgent

生成AIは単体でもいろいろなことができますが、外部の世界とやり取りをすることでより威力を発揮します。RAGで外部から情報を取得することで、特定の分野・領域に特化した対応ができるようになったのがその一例です。

RAGの場合は、「関連する情報を取得する」というように用途が予め決まっていました。しかしながら、もっと柔軟な対応が必要とされることもあるでしょう。

  • 今日の天気や株価、はやりの映画などリアルタイムな情報を質問されるこの場合、質問内容に応じて適切なサイトから情報を取得することが大切になってきます。
    • 天気に特化した検索エンジン
    • 株価に特化した検索エンジン
    • Wikipediaの情報
  • プログラミングや数学など特別な分野の質問をされる
  • 画像や映像の作成を依頼される

どんな質問かによって行うべき処理内容は異なります。

例えば工具箱にはいろいろな道具が入っています。金槌、のこぎり、ドライバー、スパナ、接着剤、、、などあるでしょう。あなたは作業する内容によって道具を使い分けるはずです。

この個々の道具がToolで、Toolを選んで実行するあなたがAgentに相当します。ToolとAgentは混乱しやすい概念なので、最初に整理しておきます。

  • Tool = ツール(道具)です。生成AI単体では完結できない作業を行います。外部のAPIの呼び出し、関数の実行、データベースの検索、ネット検索など、外部世界への橋渡しをするのがツールです。
  • Agent = エージェント(代理人)です。課題に応じて適切なツール(道具)を選択し、ツールを使って、満足ゆく結果が得られるまで処理を行います。あなたの代わりに処理を行ってくれる代理人のようなイメージです。

このToolを使用する方法には主に以下の2通りあります。

  • 直接ツールを呼び出す方法(自分で予め決められたツールを直接つかう)生成AI(LLM)から、Tool単体を呼び出すことが可能です。「この問題を理解してPythonのプログラムを作成し、それを実行して」という処理の場合、Pythonのプログラムを実行する部分をToolに担当させることができます。Chainは「あらかじめどのような処理を、どのような順序で行うか」を明示的に指定します。以下の図はLLMの出力をParserで解釈し、その内容をToolに入力して、最終的なOutputを得るイメージです。LLMが目的に応じたツールを選択します。以下は公式サイトにあった説明イメージです。
tool chain
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 16
  • ツールの実行は一回だけです。納得行く結果が得られなかったとしてもOutputとして出力が行われます。
  • Agentを使用する方法(代理人に任せて適切なツールを繰り返し使ってもらう)代理人という意味の通り、あなたの代わりに処理を行ってくれます。例えば、検索エンジンと組み合わせて何か分析を行う場合、納得がゆく結果が得られるまで繰り返し検索を行ってくれます。ツールを実行しても、良い結果が得られない時は、処理順序を動的に変更したり、繰り返したりする必要があるかもしれません。このようなときはAgentが適しています。以下の図は、Parserの出力をToolに入力し、その結果を再度LLMで判断して、納得行く結果が得られるまで繰り返す様子を示しています。以下は公式サイトにあった説明イメージです。
tool agent
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 17
  • 例えば、検索結果を処理する場合、検索した内容が意図したものでない場合は繰り返し処理が必要になりことがあります。

一般的には、ツールはエージェントと組み合わせて利用することになるでしょう。厳密にいうと、エージェントは「使用するツールを決定する」までが担当範囲で、そのツールを実行するにはAgentExecutorを使用します。以降のサンプルでは、それらを活用しながら作業を進めてゆきます。

ここまでの内容を整理して以下にまとめます。

  • ツール=生成AIの外部から情報を取得する道具
  • Agent=ツールを選択
  • AgentExecutor=Agentを実行

登録済ツールを実行する

LangChainにはすぐに使えるツールが用意されています。以下はツール一覧を列挙する関数です。

from langchain.agents import get_all_tool_names

tool_names = get_all_tool_names()
tool_names

以下のように出力されました。数多くのツールが事前に用意されていることがわかります。

'sleep,wolfram-alpha,google-search,google-search-results-json,searx-search-results-json,bing-search,metaphor-search,ddg-search,google-lens,google-serper,google-scholar,google-finance,google-trends,google-jobs,google-serper-results-json,searchapi,searchapi-results-json,serpapi,dalle-image-generator,twilio,searx-search,merriam-webster,wikipedia,arxiv,golden-query,pubmed,human,awslambda,stackexchange,sceneXplain,graphql,openweathermap-api,dataforseo-api-search,dataforseo-api-search-json,eleven_labs_text2speech,google_cloud_texttospeech,read_file,reddit_search,news-api,tmdb-api,podcast-api,memorize,llm-math,open-meteo-api,requests,requests_get,requests_post,requests_patch,requests_put,requests_delete,terminal'

これらのツールはload_tools関数で取り出して、すぐに実行することができます。いくつかツールを実行してみましょう。

以下はbing-searchというツールを実行した例です。BING_SUBSCRIPTION_KEY、BING_SEARCH_URLという環境変数を設定しておく必要があります。キーはAzureポータル(porta.azure.com)にサインインしてリソースを作成することで生成します。Azureアカウントがない方はアカウントを作成するか、もしくは読むだけでも構いません。

まず環境変数を設定します。

import os
os.environ["BING_SUBSCRIPTION_KEY"] = "98d1b.............6a641"
os.environ["BING_SEARCH_URL"] = "https://api.bing.microsoft.com/v7.0/search"

bing-searchツールを使うには、Azureポータルで取得したBING_SUBSCRIPTION_KEY と、BING_SEARCH_URLを設定する必要があります。

次にツールを作成します。ツールの名前をリストに格納し、load_tools関数を実行するだけです。

from langchain.agents import load_tools, create_react_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from langchain import hub

llm = ChatOpenAI(model="gpt-4o")

tool_names=["bing-search"]
tools = load_tools(tool_names, llm=llm)

今回tool_namesには名前が1つしかないので、生成されたツールも1つだけです。複数のツールを同時に利用するには、このリストに名前を追加するだけです。

ツールのdescriptionをみるとツールの説明を見ることができます。

print(tools[0].description)

以下のように出力されました。

'A wrapper around Bing Search. Useful for when you need to answer questions about current events. Input should be a search query.'

要約すると「BingSearchを使って必要な質問に答えます。クエリ(質問)の入力を受け取ります」という意味になります。生成AIはこの説明を把握して、「検索する必要がある場合にはBingSearchというツールを使えばいいんだな」ということが理解できるようになります。

生成AIに問い合わせるにはプロンプトが必要です。自分で記述してもよいのですが、今回はpromptハブ(いろいろなプロンプトを格納しておく場所)から取得しました。

今回使用したプロンプトは”hwchase17/react”です。以下のプログラムでプロンプトの内容を確認できます。内容は英語です。

from langchain import hub
prompt = hub.pull("hwchase17/react")
print(prompt)

以下のように出力されました。

Answer the following questions as best you can. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}

ざっくり要約すると、「以下のツールを活用して質問に答えてください。必要に応じて{tool_names}を使ってください。最後に結果がわかったらFinal Answerとして答えます」のような内容です。

作成したツールを実行するにはAgentが必要です。ここまできたらあとは、create_react_agent関数を使ってAgentを作成して実行します。

agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)

result = agent_executor.invoke({"input":"今人気のある日本のテレビドラマは?日本語で教えてください。知らないときは正直にわからないと答えてください"})
print(result)

出力は以下のようになりました。

{'input': '今人気のある日本のテレビドラマは?日本語で教えてください。知らないときは正直にわからないと答えてください', 'output': '今人気のある日本のテレビドラマは以下の通りです:\n1. ブラッシュアップライフ(日本テレビ系)\n2. VIVANT(TBS系)\n3. だが、情熱はある(日本テレビ系)\n4. 真夏のシンデレラ(フジテレビ系)\n5. 王様に捧ぐ薬指(TBS系)\n6. どうする家康(NHK)'}

この回答結果からでは実際にBingSearchが使用されているのか微妙なところです。

AgentExecutorを作成するときにverbose=Trueオプションを指定すると、途中の処理経過を出力することができます。

prompt = hub.pull("hwchase17/react")
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = agent_executor.invoke({"input":"今人気のある日本のテレビドラマは?日本語で教えてください。知らないときは正直にわからないと答えてください"})
print(result)
tool7
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 18

ちゃんとBingSearchを呼び出して実行していることが分かります。「日本のテレビドラマの人気は時期によって変わるため、現在の人気ドラマを正確に知るためには最新の情報を検索する必要があります。」と認識したうえで検索エンジンを呼び出しているようです。

この実行途中の様子はLangSmithというツールをセットアップすることで確認することもできます。 以下はLangSmithの出力画面です。BingSearchが呼び出されていること、またその検索エンジンへの入力が「今人気のある日本のテレビドラマ 2023」であったことなどが分かります。

tool8
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 19

このプログラムは以下のように記述しても同じ挙動となります。

from langchain.agents import load_tools, initialize_agent
from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o")
tool_names=["bing-search"]

tools = load_tools(tool_names, llm=llm)
agent = initialize_agent(tools, llm, verbose=True)

result = agent.invoke("今人気のある日本のテレビドラマは?日本語で教えてください。知らないときは正直にわからないと答えてください")
print(result)

initialize_agent関数を使って、エージェントとAgentExecutorをまとめて作成しています。ただし、このinitialize_agent関数はdeprecatedと宣言されており、将来削除される予定のようです。よって、create_react_agent関数を使っておいた方がよさそうです。

wolfram-alpha

Wolframは各種数学の問題を解いてくれるサービスです。利用するにはAppIDを取得する必要があります。以下のリンクから作成してください。

https://developer.wolframalpha.com

tool5
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 20

また、PIPでモジュールを追加します。

py -m pip install wolframalpha

APIキーは環境変数に登録します。

import os
os.environ["WOLFRAM_ALPHA_APPID"] = "4G.......H"

以下がプログラムです。

from langchain.agents import load_tools, create_react_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from langchain import hub

llm = ChatOpenAI(model="gpt-4o")

tool_names=["wolfram-alpha"]
tools = load_tools(tool_names, llm=llm)

prompt = hub.pull("hwchase17/react")

agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = agent_executor.invoke({"input":"5色のボールを一列に並べます。何通りありますか?"})
print(result)

ToolやAgentの作成方法、Promptも前のBingSearchと同じです。

出力は以下のようになりました。

> Entering new AgentExecutor chain...
I should use wolfram_alpha to calculate the number of ways to arrange 5 colored balls in a line.
Action: wolfram_alpha
Action Input: number of permutations of 5 items
Observation: Assumption: permutations | length | 5 
Answer: 120
Thought:I now know the final answer
Final Answer: 120

> Finished chain.
{'input': '5色のボールを一列に並べます。何通りありますか?', 'output': '120'}

質問を変えてみました。

result = agent_executor.invoke({"input":"鶴と亀がいます。足の合計は10です。頭の合計は4です。それぞれ何匹?"})
print(result)

以下のように正しい答えが得られました。

{'input': '鶴と亀がいます。足の合計は10です。頭の合計は4です。それぞれ何匹?', 'output': '鶴は3匹、亀は1匹です。'}

途中経過を見ると連立2次方程式を組み立てて回答にたどり着いていることがわかります。

dalle-image-generator

DalleはOpenAIが提供する画像生成モデルです。 ハロウィーンのお化け屋敷の絵を描いてもらいました。

from langchain.agents import load_tools, create_react_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from langchain import hub

llm = ChatOpenAI(model="gpt-4o")

tool_names=["dalle-image-generator"]
tools = load_tools(tool_names, llm=llm)

prompt = hub.pull("hwchase17/react")

agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = agent_executor.invoke({"input":"Create an image of a halloween night at a haunted museum"})
print(result)

プログラムの内容はほぼ同じです。ツールの名前と生成AIへのinputを変更しただけです。

生成途中に表示されるURLをクリックすると、画像を参照できます。

tool6
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 21

ツールを自作してみる1

ここまで既存のツールを利用してきました。生成AIが利用するツールを自作することも可能です。自作したツールのことをカスタムツールと呼んだりもします。

カスタムツールの作り方にはいくつかの方法があります。

  • Toolクラスを使用する
  • BaseToolクラスを継承する
  • @toolデコレータを使用する

生成AIは処理内容によってツールを選択します。生成AIの立場になって考えてみましょう。どんなツールがあってどんな用途に使えるのか、どんな情報を渡して、どんな結果を得るのか、事前に理解しておく必要があります。

板を切る場合、道具箱からのこぎりを選択するでしょう。釘を打つ場合は金槌を選ぶはずです。釘を打つのに鋸を選んでしまったら結果は悲惨なものになるでしょう。良い結果を得るために、正しい道具を選択することはとても大切です。

つまり、ツールを作る場合には以下のような情報を規定することが大切となります。

  • どんな処理を行うツールなのかという説明
  • どのような情報を入力として受け取るのか
  • 何を結果として返すのか

そのことを踏まえながら読み進めてください。

まず、@toolデコレーターを使って、簡単なカスタムツールを作成してみましょう。

以下は2つの引数を掛け算して結果を返すツールです。

from langchain_core.tools import tool

@tool
def multiply(first_int: int, second_int: int) -> int:
    """Multiply two integers together."""
    return first_int * second_int

関数の前に@toolと修飾されています。これがToolデコレーターです。関数の定義の最初には”””Multiply two integers together.””” とコメントがあります。 Multiplyとは掛け算のことです。two integers(2つの整数)を掛け算する、という意味です。コメントは実行時には無視されますが、ツールをつくるときには重要なヒントとして扱われます。LangChainでツールを自作するときには、生成AIにとって、わかりやすいコメントを記述するのが重要となります。

関数の引数(first_int, second_int)の後ろにある”:int”は型を指定しています。これらの引数は整数を受け取るという意思表示になります。”-> int”は関数が整数を返すという意味になります。

この型に関する情報はLangChainに限った書き方ではありません。Pythonは型を指定しなくても動作しますが、型を明示的に指定することでより堅牢なプログラムになります。この型に関する記述は現在オプション的な位置づけですが、将来はより普及してゆく可能性があります。

要は、基本的には普通の関数定義と同じ書き方で、”@tool”で修飾するとLangchainで使用できるツールとなります。

単なる関数定義と異なることは、以下のように確認することができます。

print(multiply.name)
print(multiply.description)
print(multiply.args)

以下のように出力されます。

multiply
multiply(first_int: int, second_int: int) -> int - Multiply two integers together.
{'first_int': {'title': 'First Int', 'type': 'integer'}, 'second_int': {'title': 'Second Int', 'type': 'integer'}}

Pythonで関数定義はオブジェクトとして扱われます。そのnameプロパティには関数名が、descriptionにはコメントとして記述した関数の説明が、argsには引数名と型が保存されていることが確認できます。

この関数はLangChainで実行されるツールです。以下のように実行することができます。”multiply(4,5)”のように呼び出すことはできません。

multiply.invoke({"first_int": 4, "second_int": 5})

ツールを自作してみる2

以下はStructuredToolクラスを使ってカスタムツールを作成するサンプルです。二つの数値を足し算するだけのツールです。

from langchain.tools import BaseTool, StructuredTool, tool
from langchain.pydantic_v1 import BaseModel, Field

def add_numbers(a: int, b: int) -> int:
    return a + b

class AddNumbersInput(BaseModel):
    a: int= Field()
    b: int= Field()
    

add_numbers_tool = StructuredTool.from_function(
    name="add_numbers",
    func=add_numbers,
    description="Add two numbers: a and b.",
    args_schema=AddNumbersInput
)

add_numbers_tool.invoke({"a":3, "b":5})

関数を定義しておき、StructuredTool.from_function関数に名前や、関数への参照、関数の説明、引数の型などの情報を渡すことで、LangChain用のツールを作成します。

ツールを直接呼び出す

では、このmultiplyツールやadd_numbersツールをLangChainから呼び出してみましょう。まず生成AIを作成します。

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4")

このLLMにツールを紐づけます(bindする)。これにより、必要に応じてLLMがツールを実行できるようになります。

llm_with_tools = llm.bind_tools([multiply, add_numbers_tool])

生成AIがツールを実行するときの様子(どんな引数が渡されたかなど)はtool_callsで確認することができます。

msg = llm_with_tools.invoke("2の3倍はいくつですか?")
msg.tool_calls

以下のように出力されます。multiplyツールを選択し、引数として2と3が渡されていることが確認できます。

[{'name': 'multiply',
  'args': {'first_int': 2, 'second_int': 3},
  'id': 'call_kmKkju4dEEkLejp0dPBrVtxz'}]

足算も実行してみましょう。

msg = llm_with_tools.invoke("2に3を加えるといくつですか?")
msg.tool_calls

出力は以下のようになりました。

[{'name': 'add_numbers',
  'args': {'a': 2, 'b': 3},
  'id': 'call_MWN25G2MLBuZxwyQu9i6SwoD'}]

生成AIがツールに渡す情報を作成できたことが確認できました。では、その値を実際にツールに渡して実行してみます。msg.tool_callsはオブジェクト1つを含むリストです。よって、tool_calls[0][“args”]とアクセスすることで引数を抽出できます。

msg = llm_with_tools.invoke("whats 5 times forty two")
msg.tool_calls[0]["args"]

以下のように出力されます。これをmultiplyツールへの引き渡します。

{'first_int': 5, 'second_int': 42}

以下のように実行すると、結果として92(=23×4)が得られました。

chain = llm_with_tools | (lambda x: x.tool_calls[0]["args"]) | multiply
chain.invoke("What's four times 23")

足算と掛算というシンプルな例ですが、生成AIが外部ツールを実行していることが確認できました。

郵便番号検索ツール

もうすこし実用的なツールにしてみましょう。7桁の番号から住所を検索するツールです。

from langchain_core.tools import tool
import requests

@tool
def zip2addr(zipcode: int) -> str:
    """7桁の郵便番号から住所を検索します"""
    r = requests.get(f"https://zipcloud.ibsnet.co.jp/api/search?zipcode={zipcode}")
    s = [v for k,v in r.json()["results"][0].items() if k.startswith("address")]
    return "".join(s)

llm_with_tools = llm.bind_tools([zip2addr])
chain = llm_with_tools | (lambda x: x.tool_calls[0]["args"]) | zip2addr
chain.invoke("郵便番号2220012の住所を教えてください")

@toolデコレーターを使ってzip2addr関数をLangChainのツールに変換しています。あとは、先ほどの例のようにbind_toolsメソッドで生成AIに紐づけています。

実行すると以下のように正しく住所が表示されました。

'神奈川県横浜市港北区富士塚'

「生成AIが自分で検索して調べたのでは?」と思われるかもしれません。LangSmithというツールを使うと、実際に内部でどのような処理が行われたか確認することができます。

tool2
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 22

zip2addrというツールがよびだされ、その引数に2220012という値が渡されていることが確認できました。LangSmithに関しては別途機会があれば詳しく説明してゆきたいと思います。

Agentからツールを使用する

このようにToolを作って直接Chainから呼び出すこともできますが、希望する結果が得られるまで繰り返したり、いろんなツールを組み合わせたり、という要望にも対応するにはAgentからツールを利用します。

Agentを作成するには、create_react_agent や create_tool_calling_agent (ツールを呼び出すエージェントを作成)などの関数を使用します。そのAgentを実行するときにAgentExecutorを使用します。まず、必要なモジュールをimportします。

from langchain import hub
from langchain.agents import AgentExecutor, create_tool_calling_agent

hubはいろいろなプロンプト取得するためのモジュールです。以下のサイトから登録されているプロンプトを検索することができます。

https://smith.langchain.com/hub

hub.pull(プロンプトの名前)と実行すると、そのプロンプトを取得できます。都度プロンプトを自分で書くのが面倒な場合、このHubからプロンプトを取得して利用することが可能です。

今回は、”hwchase17/openai-tools-agent”というプロンプトを使用します。

prompt = hub.pull("hwchase17/openai-tools-agent")
prompt.pretty_print()

出力は以下の通りです。

================================ System Message ================================

You are a helpful assistant

============================= Messages Placeholder =============================

{chat_history}

================================ Human Message =================================

{input}

============================= Messages Placeholder =============================

{agent_scratchpad}

プロンプトは自分で作成しても構いませんが、Agentを使用する場合には、{agent_scratchpad}という記述(Agentがメモとして使用する場所)が必要になることに注意してください。

ちなみに、Agentを作成するときに、いくつか関数が用意されていますが、それぞれプロンプトの書き方の前提が決まっているようです。

  • create_tool_calling_agent = “hwchase17/openai-tools-agent”のプロンプト
  • create_react_agent = “hwchase17/react”のプロンプト

ReActとは https://react-lm.github.io/ に論文があり、その内容(問題を段階的考え、推測して確認してゆく)を実装したもののようです。

今回は足し算(add)とべき乗(exponentiate)の2つのツールを自分で定義します。

@tool
def add(first_int: int, second_int: int) -> int:
    "Add two integers."
    return first_int + second_int


@tool
def exponentiate(base: int, exponent: int) -> int:
    "Exponentiate the base to the exponent power."
    return base**exponent

tools = [multiply, add, exponentiate]

Agentと、それを実行するExecutorを作成します。

agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

invokeメソッドで実行します。

agent_executor.invoke({"input": "3の2乗と5を足し算して、その結果全体を2乗する"})

以下のように出力されました。

> Entering new AgentExecutor chain...

Invoking: `exponentiate` with `{'base': 3, 'exponent': 2}`
9

Invoking: `add` with `{'first_int': 9, 'second_int': 5}`
14

Invoking: `exponentiate` with `{'base': 14, 'exponent': 2}`
196

結果は196です。

> Finished chain.

{'input': '3の2乗と5を足し算して、その結果全体を2乗する', 'output': '結果は196です。'}

agent_executorを作成するときにverbose=Trueと設定しているので途中経過も出力されています。Invokingというのがツールを呼び出しているところです。内容に応じて適切なツールが実行されていることが分かります。

ビルトインツール

LangChainではあらかじめ多くのツールが用意されています。いくつか試してみましょう。

Wikipedia – ウィキ

外部とやりとりするツールはlangchain_communityモジュールに含まれています。以下の行で必要なモジュールをインポートします。

from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper

また、pipコマンドでwikipediaもインストールします。

py -m pip install wikipedia

ツールを作成します。

api_wrapper = WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=100)
wikitool = WikipediaQueryRun(api_wrapper=api_wrapper)

ツールの名前や説明は以下のようにアクセスできます。

print(wikitool.name)
print(wikitool.description)
print(wikitool.args)

このツールの名前、説明、引数などの情報を確認できます。

wikipedia
A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, facts, historical events, or other subjects. Input should be a search query.
{'query': {'title': 'Query', 'type': 'string'}}

ツールを直接実行してみましょう。

tool.run({"query": "山田洋次"})

出力は以下のようになりました。

'Page: Yoji Yamada\nSummary: Yoji Yamada (山田 洋次, Yamada Yōji, born 13 September 1931) is a Japanese fi'

ツールを実行するエージェントと、そのエージェントを実行するExecutorを作成します。Executorはinvokeメソッドで実行します。

agent = create_tool_calling_agent(llm, [wikitool], prompt)
agent_executor = AgentExecutor(agent=agent, tools=[wikitool], verbose=True)
agent_executor.invoke({"input":"日本の映画で男はつらいよというシリーズがあります。その映画監督の年齢は?"})

以下のように出力されました。

> Entering new AgentExecutor chain...

Invoking: `wikipedia` with `男はつらいよ`


Page: Otoko wa Tsurai yo
Summary: Otoko wa Tsurai yo (男はつらいよ, It's Tough Being a Man) is a Japanese 
Invoking: `wikipedia` with `Otoko wa Tsurai yo director`


Page: Otoko wa Tsurai yo
Summary: Otoko wa Tsurai yo (男はつらいよ, It's Tough Being a Man) is a Japanese 
Invoking: `wikipedia` with `Yoji Yamada`


Page: Yoji Yamada
Summary: Yoji Yamada (山田 洋次, Yamada Yōji, born 13 September 1931) is a Japanese fi「男はつらいよ」の映画監督である山田洋次さんは、1931年9月13日生まれです。

> Finished chain.

{'input': '日本の映画で男はつらいよというシリーズがあります。その映画監督の年齢は?',
 'output': '「男はつらいよ」の映画監督である山田洋次さんは、1931年9月13日生まれです。'}

「Invoking: wikipedia」という部分がwikipediaを呼び出している部分です。必要な情報が得られるまで繰り返しツールを使っていることが分かります。

上記の例では、Agentの作成にはcreate_tool_calling_agentを使用しましたが、以下のようにOpenAIFunctionsAgent.from_llm_and_toolsを使用することも可能です。

from langchain.agents import OpenAIFunctionsAgent

agent = OpenAIFunctionsAgent.from_llm_and_tools(llm, tools=[wikitool])
agent_executor = AgentExecutor(agent=agent, tools=[wikitool], verbose=True)
agent_executor.invoke({"input":"日本の映画で男はつらいよというシリーズがあります。その映画監督の年齢は?"})

wikipediaに関しては、load_toolsでツールを呼び出すことも可能です。以下のようなプログラムも可能です。

from langchain.agents import load_tools, create_react_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from langchain import hub

llm = ChatOpenAI(model="gpt-4o")

tool_names=["wikipedia"]
tools = load_tools(tool_names, llm=llm)

prompt = hub.pull("hwchase17/react")

agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = agent_executor.invoke({"input":"日本の映画で男はつらいよというシリーズがあります。その映画監督の年齢は?"})
print(result)

Alpha Vantage – 株価検索/為替レート

https://www.alphavantage.co/ は株価に関する情報を提供するAPIを公開しています。利用する場合には無料のAPIKeyを取得する必要があります。以下のサイトからAPIKeyを取得してください。

https://www.alphavantage.co/support/#api-key

tool1
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 23

環境変数ALPHAVANTAGE_API_KEYにキーを設定します。getpass.getpass()はパスワードの入力を促す命令です。以下のコードを実行すると、キー入力用ダイアログが表示されるので、そこにキーを入力してください。

import getpass
import os

os.environ["ALPHAVANTAGE_API_KEY"] = getpass.getpass()

ツール単体で実行してみます。

alpha_vantage = AlphaVantageAPIWrapper()
alpha_vantage._get_exchange_rate("USD", "JPY")

以下のように現在の為替レートが表示されました。

{'Realtime Currency Exchange Rate': {'1. From_Currency Code': 'USD',
  '2. From_Currency Name': 'United States Dollar',
  '3. To_Currency Code': 'JPY',
  '4. To_Currency Name': 'Japanese Yen',
  '5. Exchange Rate': '153.97700000',
  '6. Last Refreshed': '2024-05-16 05:35:32',
  '7. Time Zone': 'UTC',
  '8. Bid Price': '153.97650000',
  '9. Ask Price': '153.98270000'}}

以下のコードを実行すると、マイクロソフト直近の株価が出力されます。

alpha_vantage._get_quote_endpoint("MSFT")
{'Global Quote': {'01. symbol': 'MSFT',
  '02. open': '422.5400',
  '03. high': '422.9200',
  '04. low': '418.0250',
  '05. price': '420.2100',
  '06. volume': '15352239',
  '07. latest trading day': '2024-05-17',
  '08. previous close': '420.9900',
  '09. change': '-0.7800',
  '10. change percent': '-0.1853%'}}

AlphaVantageAPIWrapperを使ってツールを定義します。

from langchain_core.tools import tool

@tool
def exchange_tool(from_currency: str, to_currency: str) -> str:
    """from_currencyからto_currencyへの為替レートに関する情報を返します"""
    return alpha_vantage._get_exchange_rate(from_currency, to_currency)

@tool
def stockprice_tool(code: str) -> str:
    """指定された企業の最新株価を返します"""
    return alpha_vantage._get_quote_endpoint(code)

Toolとエージェントを作成して実行します。

prompt = hub.pull("hwchase17/openai-tools-agent")
tools =[exchange_tool, stockprice_tool]
agent = create_tool_calling_agent(llm,tools , prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
agent_executor.invoke({"input":"昨日のマイクロソフトの株価を円で教えてください"})

以下出力結果です。まずstockprice_toolを使って株価を取得し、exchange_toolを使って円に換算しています。

> Entering new AgentExecutor chain...

Invoking: `stockprice_tool` with `{'code': 'MSFT'}`


{'Global Quote': {'01. symbol': 'MSFT', '02. open': '422.5400', '03. high': '422.9200', '04. low': '418.0250', '05. price': '420.2100', '06. volume': '15352239', '07. latest trading day': '2024-05-17', '08. previous close': '420.9900', '09. change': '-0.7800', '10. change percent': '-0.1853%'}}
Invoking: `exchange_tool` with `{'from_currency': 'USD', 'to_currency': 'JPY'}`


{'Realtime Currency Exchange Rate': {'1. From_Currency Code': 'USD', '2. From_Currency Name': 'United States Dollar', '3. To_Currency Code': 'JPY', '4. To_Currency Name': 'Japanese Yen', '5. Exchange Rate': '155.79300000', '6. Last Refreshed': '2024-05-20 03:20:01', '7. Time Zone': 'UTC', '8. Bid Price': '155.79190000', '9. Ask Price': '155.80020000'}}昨日のマイクロソフトの株価は420.21ドルでした。これを円に換算すると、約65381.79円になります。ただし、為替レートは常に変動するため、この値は参考程度にご覧ください。

> Finished chain.

{'input': '昨日のマイクロソフトの株価を円で教えてください',
 'output': '昨日のマイクロソフトの株価は420.21ドルでした。これを円に換算すると、約65381.79円になります。ただし、為替レートは常に変動するため、この値は参考程度にご覧ください。'}

Bing Search

Azureにアカウントが必要です。以下のリンクからBing検索リソースを作成してAPIキーを取得してください。

https://portal.azure.com/#create/microsoft.bingsearch

tool3
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 24
tool4
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 25

取得したAPIキーとURLを環境変数に登録します。

import os

os.environ["BING_SUBSCRIPTION_KEY"] = "9...................641"
os.environ["BING_SEARCH_URL"] = "https://api.bing.microsoft.com/v7.0/search"

BingSearchAPIWrapperのオブジェクトを作成してrunメソッドを呼び出すと検索結果を取得することができます。

from langchain_community.utilities import BingSearchAPIWrapper
search = BingSearchAPIWrapper()
search.run("python")

このオブジェクトを使ってツールを以下のように定義します。

from langchain.agents import AgentExecutor, create_tool_calling_agent, Tool

def search_tool(query: str):
    return search.run(query)

tools = [
    Tool(
        name="BingSearch",
        func=search_tool,
        description="Use Bing search to find information on the internet",
    )
]

Tool関数でLangChainで使用する関数を定義しています。func引数に実際に呼び出す関数を記述します。あとは、AgentとAgentを実行するExecutorを作成して、invokeメソッドで実行します。

llm = ChatOpenAI(model="gpt-4")
prompt = hub.pull("hwchase17/openai-tools-agent")

agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
agent_executor.invoke({"input":"宮崎駿監督の最新作のタイトルは?"})

出力は以下の通りです。

{'input': '宮崎駿監督の最新作のタイトルは?',
 'output': '宮崎駿監督の最新作は「君たちはどう生きるか」です。これは2023年に公開されたスタジオジブリ制作のアニメーション映画で、宮崎駿の長編監督作としては2013年公開の「風立ちぬ」以来10年ぶりの作品となりました。'}

BingSearchは事前にツールが用意されているので、このように自分でToolを定義する必要はありません。

Googleもいろいろなツールを提供しています。

以下のURLからSerpAPIにサインアップしてAPIキーを取得します。

https://serpapi.com/users/sign_up

py -m pip install --upgrade --quiet  google-search-results

以下のようにツールを作成します。

import os

from langchain_community.tools.google_trends import GoogleTrendsQueryRun
from langchain_community.utilities.google_trends import GoogleTrendsAPIWrapper

os.environ["SERPAPI_API_KEY"] = "e8d...........................3960d"
tool = GoogleTrendsQueryRun(api_wrapper=GoogleTrendsAPIWrapper())

runメソッドでツールを直接実行できます。

r = tool.run("鬼滅の刃")
print(r)

AgentとAgentExecuterを使ってツールを実行してみます。

from langchain.agents import OpenAIFunctionsAgent
from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI()

agent = OpenAIFunctionsAgent.from_llm_and_tools(llm, tools=[tool])
agent_executor = AgentExecutor(agent=agent, tools=[tool], verbose=True)
agent_executor.invoke({"input":"最近人気のテレビドラマは?"})

以下のような出力となりました。

{'input': '最近人気のテレビドラマは?',
 'output': '最近の人気のテレビドラマに関するGoogle Trendsのデータによると、特に人気のあるテレビドラマは特定されていません。しかし、関連する検索クエリには「フジテレビドラマ」「韓国ドラマ人気」などがあります。特定の人気テレビドラマを知りたい場合は、詳細な情報を検索してみることをおすすめします。'}

検索とRetrieverの組合せ

複数のツールを組み合わせる例として、検索(Tavily)とRetrieverを組み合わせてみます。プロンプトの内容によって、適切なツールが選択され、処理が行われる様子を実感してください。

Tavily

この検索サービスを利用する場合、以下のURLからAPIキーを取得してください。

https://tavily.com

agent0
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 26

取得したキーは環境変数 TAVILY_API_KEY に保存しておきます。

まずツールが動作するか確認しておきましょう。

from langchain_community.tools.tavily_search import TavilySearchResults
search = TavilySearchResults(max_results=2)

searchというのが検索のツールです。ツールはinvokeメソッドで直接実行すうことができます。

search.invoke("東京の天気は?")

実行すると以下のようになりました。

[{'url': 'https://tenki.jp/forecast/3/16/',
  'content': '最新の天気履歴. 関東は雲が多めながらも日差しが届いています。. 午後3時までの最高気温は、東京都心で25.3度、横浜で24.5度でした。. きのう ...'},
 {'url': 'https://weather.yahoo.co.jp/weather/jp/13/4410.html',
  'content': '東京(東京)の天気予報。今日・明日の天気と風と波、明日までの6時間ごとの降水確率と最高・最低気温を見られます。 ... 再生する 5/21(火)19時 梅雨入りした沖縄は大雨続くおそれ 本州付近は晴天と25℃以上の ...'}]

このsearchという変数には langchain_community.tools.tavily_search.tool.TavilySearchResults クラスのオブジェクトが格納されています。つまり、searchがLangChainにおけるツールそのものとなります。ツールの内容はdescriptionプロパティで確認できます。

print(search.description)

以下のように出力されました。

'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events. Input should be a search query.'

日本語に訳すと以下のようになりました。

「包括的で正確かつ信頼できる結果を提供するために最適化された検索エンジン。最新の出来事について質問に答える必要があるときに役立ちます。入力は検索クエリであるべきです。」

つまり、生成AIはなにか調べる必要があるときに、このsearchというツールが使えること、その引数としては検索する内容である、ということがわかるようになります。

このTavilySearchResultsはもともとLangChainで用意されているツールです。次に、RAGでも使用したRetrieverを使ってツールを自作してみましょう。

Retriever

RAGでは、事前になんらかのドキュメントやデータベースなどを用意しておき、その内容をベクトル化して保存しておきます。retrieverはベクトルストアから関連しそうな情報を取得するオブジェクトです。 create_retriever_tool関数をつかうと、retrieverオブジェクトからLangChainのツールを作成することができます。そんなサンプルをみてみましょう。

以下は https://www.aozora.gr.jp/cards/000329/files/18376_12100.html というサイトからドキュメントを取得して、1000の長さのテキストに分割して保存するサンプルです。桃太郎のお話です。

情報はFAISSというベクター型データベースに保存しておきます。

from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

loader = WebBaseLoader("https://www.aozora.gr.jp/cards/000329/files/18376_12100.html")
docs = loader.load()
documents = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=200
).split_documents(docs)
vector = FAISS.from_documents(documents, OpenAIEmbeddings())
retriever = vector.as_retriever()

以下のように関連する情報を検索できます。

retriever.invoke("どんな登場人物がいますか")[0]

結果は以下のようになりました。主要登場人物がでてくるシーンが選ばれています。

Document(page_content='犬と、猿と、きじと、これで三にんまで、いい家来ができたので、桃太郎はいよいよ勇み立って、またずんずん進んで行きますと、やがてひろい海ばたに出ました。\r\n\u3000そこには、ちょうどいいぐあいに、船が一そうつないでありました。\r\n\u3000桃太郎と、三にんの家来は、さっそく、この船に<<中略>>
た。いかめしいくろがねの門の前に見はりをしている鬼の兵隊のすがたも見えました。\r\nそのお城のいちばん高い屋根の上に、きじがとまって、こちらを見ていました。\r\nこうして何年も、何年もこいで行かなければならないという鬼が島へ、ほんの目をつぶっている間に来たのです。', metadata={'source': 'https://www.aozora.gr.jp/cards/000329/files/18376_12100.html', 'title': '楠山正雄 桃太郎', 'language': 'No language found.'})

このretrieverからLangChain用のツールをつくります。

from langchain.tools.retriever import create_retriever_tool
retriever_tool = create_retriever_tool(
    retriever,
    "momotaro_search",
    "桃太郎に関する質問があるときには、このツールを使用してください。",
)

create_retriever_tool関数で関連する情報を検索するツールを作成します。 引数は以下の通りです。

  • retrieverオブジェクト
  • ツールの名前 = “momotaro_search”
  • ツールの説明 = ツールの説明が”桃太郎に関する質問があるときには、このツールを使用してください。”と記述しています。生成AIはこの説明をヒントにして「桃太郎について調べたいときにはこのツールを使えばいいんだな」という判断を行います。
print(retriever_tool.description)

と実行すると、ツールの説明で指定した文字列がそのまま取得できることが確認できます。

Using Language Models

ここまで作成したツールを以下のようにリストに格納します。

tools = [search, retriever_tool]

LLMを使用して、ツールを実行してみましょう。

from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4")

生成AIが変数modelに格納されています。直接LLMに問いかけをしてみましょう。LLMへの入力はメッセージのリストです。応答はcontentプロパティで参照できます。

from langchain_core.messages import HumanMessage
response = model.invoke([HumanMessage(content="hi!")])
response.content

以下の応答が得られました。

'Hello! How can I assist you today?'

この生成LLMにツールのことを教えるには以下のようにbind_toolsメソッドを使用します。

model_with_tools = model.bind_tools(tools)

生成AI経由でツールを実行してみましょう。

response = model_with_tools.invoke([HumanMessage(content="大阪の天気は?")])
print(f"ContentString: {response.content}")
print(f"ToolCalls: {response.tool_calls}")

以下のような応答が得られました。

ContentString: 
ToolCalls: [{'name': 'tavily_search_results_json', 'args': {'query': '大阪の天気'}, 'id': 'call_LfVIkoEFc6cauuKSnm8DyPv5'}]

天気を聞いたのでtavily_search_results_jsonが呼び出されていることが分かります。しかしながら応答(ContentString)は空でした。

この段階で生成AIは「どのツールを使うのか」を判断しただけで、実際のツールの実行はなされていないことに注意してください。「大阪の天気を知りたいなら、tavily_search」ルールを実行するにはAgentが必要となります。

Create the agent

ツールとLLMを作成しました。これからAgentを作成します。

langgraphモジュールをインストールします。

py -m pip install langgraph

Agentの作成には chat_agent_executorモジュールに含まれている create_tool_calling_executor という関数を使用します。

from langgraph.prebuilt import chat_agent_executor

agent_executor = chat_agent_executor.create_tool_calling_executor(model, tools)

ここで生成AIを渡す引数にはmodelを指定しています。さきほどbind_toolsでツールを紐づけたmodel_with_toolsではありません。これは create_tool_calling_executor が内部でbind_toolsを実行してくれるからです。

戻り値のagent_executorがエージェントを実行するオブジェクトです。

Run the agent

Agentを実行してみましょう。

response = agent_executor.invoke({"messages": [HumanMessage(content="hi!")]})
response["messages"]

以下のような応答がかえってきました。

[HumanMessage(content='hi!', id='89311906-d540-45c3-b7ff-9f6ac2dd8491'),
 AIMessage(content='Hello! How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 129, 'total_tokens': 139}, 'model_name': 'gpt-4', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-17b34fff-d936-48de-b697-95f1a2f5ba1d-0')]

実際に裏で何が行われているかは、LangSmithを見るとわかります。LangSmithを使用するにはアカウントの設定等が必要になります。

上記の実行では以下のような結果が得られていました。

agent1
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 27

実際にツールを起動するような問い合わせをしてみましょう。

response = agent_executor.invoke(
    {"messages": [HumanMessage(content="桃太郎の仲間は誰?")]}
)
response["messages"]

応答は以下のようになりました。

[HumanMessage(content='桃太郎の仲間は誰?', id='9add4514-8e4f-4d36-a0e3-8ac01a302a27'),
 AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_30QTKV6T1LMlDPLks1J2Iy9C', 'function': {'arguments': '{\n  "query": "
 <<中略>>
 'model_name': 'gpt-4', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-b6739592-a257-4759-898e-1e511318e52c-0')]

agent_executorの戻り値はリスト形式で、HumanMessage, AIMessage, ToolMessageなどが含まれています。リストの最後の要素には[-1]でアクセスできまる。今回ほしい結果は一番最後のAIMessageなので最後の生成AIの応答だけとりだしてみます。

response["messages"][-1].content

以下のように正しく応答が得られています。

'桃太郎の仲間は犬、猿、キジの3匹です。桃太郎が鬼退治に向かう途中で、これらの動物たちは桃太郎からきびだんごをもらい、彼に従って鬼退治に参加しました。'

実際にツールが使われているかはLangSmithから判断できます。

agent2
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 28

momotaro_searchが呼び出されている様子が確認できます。

同じように天気をしらべてみます。

response = agent_executor.invoke(
    {"messages": [HumanMessage(content="大阪の今の天気は?")]}
)
response["messages"][-1].content

以下の応答が得られました。

'大阪の現在の天気は星がはっきり見える状態で、気温は約10-12℃です。明日の最高気温は16℃、最低気温は6℃で、天気は晴れの予想です。ただし、春が本番となり、日差しが強くなると花粉が大量に飛散する可能性があるため、花粉症の方は注意が必要です。[詳細はこちら](https://weathernews.jp/onebox/34.702920/135.496578/q=%E5%A4%A7%E9%98%AA%EF%BC%88%E5%A4%A7%E9%98%AA%E7%9C%8C%EF%BC%89&v=ad8f2ef94.......e1ae24&temp=c&lang=ja)'

LangSmithをみたところ、tavily_searchが呼び出されていることが確認できました。

agent3
LangChain入門(6) – Tool/Agent - 外部世界への橋渡し 29

このように生成AI単体でできないこと、特に外部への働きかけを行う場合は、ツールとエージェントを使用するとよいでしょう。

Future Coders

Future Codersではほかにも多くの独自教材を用意しています。少人数個別指導・リモート対応でレッスンを行っています。レッスン以外にも出張授業やコンサルタントも行っております。興味のある方はお気軽にお問い合わせください。

Categories: LangChain, OpenAI, Python