LCEL形式になったLangChainにおけるReActの処理の流れ

## 前回
https://qiita.com/kash203/items/91e1cd45f5b0ef41b3d0

# 概要
前回のLangChainを用いてReActフレームワークの理解からバージョンの更新が進み、
LangChain Expression Languageという記法が導入されました。
記法の導入に伴い前回（`v0.0.198`）からLangChainの中身が大きく変わっていましたので、中身について改めて確認したものとなります。
Agentを改造する際にどの部分を見ればよいかの手助けとすることが目的です。



## 確認した今回のLangChainのバージョン
- langchain                 0.1.4
- langchain-community       0.0.16
- langchain-core            0.1.17
- langchain-openai          0.0.5
- langchainhub              0.1.14

# LCELによるReActの実装の説明
LCELを用いたReActの実装については以下のページをベースとして考えます。
https://python.langchain.com/docs/modules/agents/agent_types/react
TavilyはAPI Keyの取得が必要なため、今回はWikipediaを検索用のツールとしました。
利用したモデルはGPT-4 Turboとなります。

:::note warn
ReActに渡すツールがWikipediaの一つのみの場合、プロンプト内の利用可能なツールの説明が[Wikipedia]となってしまうため、ReActのAction選択にて正しくはWikipediaのところを[Wikipedia]としてしまうことにより正しく処理されない事象がありました。
:::

```python
from langchain import hub
from langchain.agents import AgentExecutor, create_react_agent
from langchain.tools import WikipediaQueryRun
from langchain.callbacks import StdOutCallbackHandler
from langchain_community.utilities import WikipediaAPIWrapper
from langchain_core.runnables.config import RunnableConfig
from langchain_openai import AzureChatOpenAI


llm = AzureChatOpenAI(
        azure_endpoint="base_url",  # 適宜変更
        openai_api_key="api_key",  # 適宜変更
        azure_deployment="deployment_name",  # 適宜変更
        openai_api_version="2023-05-15",  # 適宜変更
        openai_api_type="azure",
        model_name="gpt-4",
        temperature=0,
        # streaming=True,  # 適宜変更
    )
    
# ツールを定義
wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
tools = [wikipedia]

# ReAct用のプロンプトを用意（0-shotのReAct用のプロンプトです。）
prompt = hub.pull("hwchase17/react")

# Agentを作成
agent = create_react_agent(llm, tools, prompt)

# ログ取得などのためのcallbackハンドラの設定（後述）
handlers = [StdOutCallbackHandler()]

# AgentExecutorを作成
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    callbacks=handlers,  # 任意設定：後述します。
    return_intermediate_steps=True,  # 任意設定：invoke時の結果に、Agentの処理の中間結果も追加されます。
)

# 処理開始
agent_executor.invoke({"input": "what is LangChain? Please consider using the tool called Wikipedia as Action Phase, not [Wikipedia]."})
```
`hwchase17/react`の中身は素朴な0-shotのReActのプロンプトです。詳細はLangChainHubからダウンロードすることができます。
https://smith.langchain.com/hub/hwchase17/react


## Callbackのメモ
- AgentExecutorに対する`verbose`の設定と、callbackとして`StdOutCallbackHandler`を設定した場合のフェーズごとの情報出力の挙動の変化は以下の通りです。ログ出力などの検討の際に活用できるかと思います（`return_intermediate_steps`を有効にするだけで十分かもしれません。）。

| verbose | callback | Thought~Action Input | Observation | Final Answer |
|:---:|:---:|:---:|:---:|:---:|
| `False` | 設定なし | × | × | × |
| `False` | 設定あり | 〇 | × | 〇 |
| `True` | 設定あり | 〇 | 〇 | 〇 |
| `True` | 設定なし | 〇 | 〇 | 〇 |



- なお、agent_executorのinvoke実行時にcallbackを渡すと、ほぼすべてのRunnable系の処理に対してそのcallbackが継承されてしまうため、基本的にはAgentExecutorの宣言時にcallbackを設定すると良いと考えます。

## 実行結果

```
> Entering new AgentExecutor chain...
To answer the question about what LangChain is, I will use the Wikipedia tool to search for information.

Action: Wikipedia
Action Input: LangChain
Page: LangChain
Summary: LangChain is a framework designed to simplify the creation of applications using large language models (LLMs). As a language model integration framework, LangChain's use-cases largely overlap with those of language models in general, including document analysis and summarization, chatbots, and code analysis.

Page: Prompt engineering
Summary: Prompt engineering is the process of structuring text that can be interpreted and understood by a generative AI model. A prompt is natural language text describing the task that an AI should perform.A prompt for a text-to-text language model can be a query such as "what is Fermat's little theorem?", a command such as "write a poem about leaves falling", a short statement of feedback (for example, "too verbose", "too formal", "rephrase again", "omit this word") or a longer statement including context, instructions, and input data. Prompt engineering may involve phrasing a query, specifying a style, providing relevant context or assigning a role to the AI such as "Act as a native French speaker". A prompt may include a few examples for a model to learn from, such as asking the model to complete "maison → house, chat → cat, chien →" (the expected response being dog), an approach called few-shot learning.When communicating with a text-to-image or a text-to-audio model, a typical prompt is a description of a desired output such as "a high-quality photo of an astronaut riding a horse" or "Lo-fi slow BPM electro chill with organic samples". Prompting a text-to-image model may involve adding, removing, emphasizing and re-ordering words to achieve a desired subject, style, layout, lighting, and aesthetic.

Page: OpenAI
Summary: OpenAI is a U.S. based artificial intelligence (AI) research organization founded in December 2015, researching artificial intelligence with the goal of developing "safe and beneficial" artificial general intelligence, which it defines as "highly autonomous systems that outperform humans at most economically valuable work".
As one of the leading organizations of the AI Spring, it has developed several large language models, advanced image generation models, and previously, released open-source models. Its release of ChatGPT has been credited with starting the artificial intelligence spring.The organization consists of the non-profit OpenAI, Inc. registered in Delaware and its for-profit subsidiary OpenAI Global, LLC. It was founded by Ilya Sutskever, Greg Brockman, Trevor Blackwell, Vicki Cheung, Andrej Karpathy, Durk Kingma, Jessica Livingston, John Schulman, Pamela Vagata, and Wojciech Zaremba, with Sam Altman and Elon Musk serving as the initial board members. Microsoft provided OpenAI Global LLC with a $1 billion investment in 2019 and a $10 billion investment in 2023, with a significant portion of the investment in the form of compute resources on Microsoft's Azure cloud service.On November 17, 2023, the board removed Altman as CEO, while Brockman was removed as chairman and then resigned as president. Four days later, both returned after negotiations with the board, and most of the board members resigned. The new initial board included former Salesforce co-CEO Bret Taylor as chairman. It was also announced that Microsoft will have a non-voting board seat.I now know the final answer

Final Answer: LangChain is a framework designed to simplify the creation of applications using large language models (LLMs). It is used for language model integration and its use-cases include document analysis and summarization, chatbots, and code analysis.

> Finished chain.
```

## LCELの記載について
今回の処理では、`agent = create_react_agent(llm, tools, prompt)`にてLCEL記法で処理内容が宣言されています。
詳しくは以下の通りとなります。
```python
# langchain/agents/react/agent.py L.99より抜粋
    prompt = prompt.partial(
        tools=render_text_description(list(tools)),
        tool_names=", ".join([t.name for t in tools]),
    )
    llm_with_stop = llm.bind(stop=["\nObservation"])
    agent = (
        RunnablePassthrough.assign(
            agent_scratchpad=lambda x: format_log_to_str(x["intermediate_steps"]),
        )
        | prompt
        | llm_with_stop
        | ReActSingleInputOutputParser()
    )
```
- `prompt`に関する処理はツールの名称や説明を付与しています。
- `llm_with_stop`に関する処理はstop_sequenceとしてObservationを追加しています。LLMが改行＋Observationの文字列を出力したら生成を中断させる設定です。
- `agent`に関する処理はLCEL記法で記述されている箇所です。もう少し深掘ると、
    - `RunnablePassthrough.assign`に関する処理：ReActの対話履歴である`intermediate_steps`をもとに元のプロンプトの会話履歴を挿入する`agent_scratchpad`の中身を構築します。`agent_scratchpad`をいじりたい場合はこの`format_log_to_str`関数を独自のものに改変すると良さそうです。
    - `prompt`に関する処理：LLMに送信するプロンプトを構築します。
    - `llm_with_stop`に関する処理：LLMにプロンプトを送信します。
    - `ReActSingleInputParser`に関する処理：LLMから受け取った生成内容についてReAct形式に沿ってパースします。（Actionの内容やFinal Answerの内容をパースします。）

# クラス図のイメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2249065/acf0ee33-7fca-1df9-9ac4-fe1c3d3889bc.png)
- LCEL形式では、先述の通り、agentを構築するにあたって`prompt`や`llm`, `parser`を`Runnable`を継承する要素として扱い、その要素を`|`でつなぎ合わせて順番に処理をステップバイステップで実行する形式に変わったようです。
    - `|`の実装については、`Runnable`や`RunnableSequence`の`__or__`関数に実装が含まれており、順番に要素を実行するように実装されています。

# 処理の流れ（シーケンス図イメージ）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2249065/3c505cb8-3d4b-349f-ee8c-90b0135b6e13.png)

## 主な変更内容
- `AgentExecutor`の実行方法が`run`から`invoke`に変わっているようです。
    - `run`はDeprecatedになっています。
- `AgentExecutor._take_next_step`の中身の大半が`AgentExecutor._iter_next_step`に移動しているようです。

### よく見る箇所のメモ
- デバッグ時に具体的なリクエストの中身を参照可能（httpxを使う直前）
    - `openai._base_client.py SyncAPIClient._request`
        - もう少し手前（post前）
            - `openai.resources.chat.completion.py Completions.create`

LCEL形式になってよく見るとLangChainの奥までクラスを継承して変更する、などの対応はしなくてよくなったかもしれません。
改造する上で重要な部品については見つけ次第追記していければと思います。