LangChain(ReAct)による生成AIエージェントを試す

# 生成AIエージェント
生成AIは様々な質問に答えることができるが、学習したデータから応答を生成するため学習できていない最新の情報を踏まえた回答を行うことはできない。そこで、Web検索やAPIなどを必要に応じて呼び出すことにより適宜最新の情報を取得して、LLMの応答に含めることができる。また、与えたツール（プログラム）を呼び出すことができるため、メール送信など応答出力以外の行動も行うことができる。

# LangChain、ReAct
LangChainはLLMへの入力の前処理や、LLMからの出力後にプログラムをパイプラインで実行することができるフレームワークである。さらに、そこでReActを使うことによりLLMが論理的思考を行い、応答を出力する上で必要な検索やファイル読み込みといったツールを呼び出すことができる。また、ツールはプログラムとして自由に与えられるため、メールを送信するツールを定義して与えればメール送信といった、応答を出力する過程で様々なタスクを行わせることも可能である。

# 実験環境
- 実行環境: Colaboratory (T4: GPU 16GB)
- 言語: python

今回はOpenAIのAPI経由でLLMを使用するので、ColaboratoryのシークレットにOpenAIのトークンを設定する必要がある。
今回作成したColaboratoryのノートは以下となる。
[Colaboratoryノート](https://colab.research.google.com/drive/1lzoLiK9jhsGtVN4fY1XfhUHoHCaYsq7Y?usp=sharing)

# コード
必要なライブラリのインストール。
```python
!pip install langchain
!pip install langchain-community
!pip install langchain-openai
!pip install wikipedia
```

OpenAIのアクセストークンを設定。
```python
import os

from google.colab import userdata
os.environ["OPENAI_API_KEY"]=userdata.get('OPEN_API_KEY')
```

ReActエージェントを設定する。LLMが使えるツールとしてWikipediaを与えている。
```python
from langchain import hub
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import OpenAI
from langchain.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
from langchain.callbacks import StdOutCallbackHandler

wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
tools = [wikipedia]

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

llm = OpenAI()

# Construct the ReAct agent
agent = create_react_agent(llm, tools, prompt)

handlers = [StdOutCallbackHandler()]

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    callbacks=handlers,
    return_intermediate_steps=True,
)
```

エージェントの呼び出し。
```python
agent_executor.invoke({"input": "What is the highlight of Japanese history? Please consider using the tool called Wikipedia as Action Phase, not [Wikipedia]."})
```

出力は以下となり、Wikipediaを調べて必要な知識を得たうえでLLMが回答を生成している。

> Entering new AgentExecutor chain...
 To answer this question, I should do some research on Japanese history.
Action: wikipedia
Action Input: Japanese historyPage: History of Japan
Summary: The first human inhabitants of the Japanese archipelago have been traced to the Paleolithic, around 38–39,000 years ago. The Jōmon period, named after its cord-marked pottery, was followed by the Yayoi period in the first millennium BC when new inventions were introduced from Asia. During this period, the first known written reference to Japan was recorded in the Chinese Book of Han in the first century AD.
Around the 3rd century BC, the Yayoi people from the continent immigrated to the Japanese archipelago and introduced iron technology and agricultural civilization. Because they had an agricultural civilization, the population of the Yayoi began to grow rapidly and ultimately overwhelmed the Jōmon people, natives of the Japanese archipelago who were hunter-gatherers.・・・・

# カスタムツールの実装
既存のツールとして多くのツールが用意されているが、カスタムツールを使えばローカル環境で立ち上げているサーバーのAPIを叩いたり様々な行動をカスタマイズできる。カスタムツールの作り方は、langchain_core.toolsパッケージのBaseToolをベースクラスとした派生クラスを定義することにより作成することができる。
今回は、実験として天気を返すツールを定義した。ただし、ツールが呼び出されていることを確認したかったので実際の天気ではなく"sunny"をすべての場合において返すダミーのツールとなっている。

```python
# Custom Tool
from typing import Optional, Type
from langchain_core.callbacks import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field

class WeatherInput(BaseModel):
    place: str = Field(description="place")

class CustomWeatherTool(BaseTool):
    name: str = "WeatherFetcher"
    description: str = "useful for when you need to answer questions about weather"
    args_schema: Type[BaseModel] = WeatherInput
    return_direct: bool = True

    def _run(
        self, place:str, run_manager: Optional[CallbackManagerForToolRun] = None
    ) -> str:
        """Use the tool."""
        return "sunny"

    async def _arun(
        self,place: str,run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
    ) -> str:
        """Use the tool asynchronously."""
        return self._run(place, run_manager=run_manager.get_sync())

tools = [wikipedia, CustomWeatherTool()]
```

エージェントの呼び出し。
```python
agent_executor.invoke({"input": "What is the weather in Tokyo today? Please consider using the tool called WeatherFetcher as Action Phase, not [WeatherFetcher]."})
```
呼び出し結果。
> Finished chain.
{'input': 'What is the weather in Tokyo today? Please consider using the tool called WeatherFetcher as Action Phase, not [WeatherFetcher].',
 'output': 'sunny',
 'intermediate_steps': [(AgentAction(tool='WeatherFetcher', tool_input='Tokyo', log=' I should use the WeatherFetcher tool to get the current weather in Tokyo.\nAction: WeatherFetcher\nAction Input: Tokyo'),
   'sunny')]}


# まとめ
LLMから自律的にツールを呼び出すAIエージェントを作成することができた。様々なカスタムツールを与えればAIエージェントをより汎用的なタスクに使用できることを認識した。