LangchainでAgentExecutorを実行した際のOpenAICallbackHandlerによるトークン数が記録されない問題の解決方法


## はじめに

Langchainを用いてエージェントを実行する際、トークン数を記録するために`OpenAICallbackHandler`及び`get_openai_callback`を使用することが一般的です。しかし、特定のバージョン（例：langchain==0.1.11以前）では、トークン数が正しく記録されない問題が発生していました。本記事では、その問題の原因と解決方法について解説します。

## 問題の背景

特定の状況下で、`AgentExecutor`を使用してエージェントを実行すると、トークン数が記録されない問題が発生していました。これは特に`RunnableAgent.plan`で`runnable.stream`が実行される場合に顕著でした。


<details><summary>参考：問題の詳細</summary>

- `AzureChatOpenAI`は`langchain_core.language_models.chat_model.BaseChatModel`を継承しています。
- `BaseChatModel`は`Runnable`を継承しており、`stream`メソッド内で`runmanager.on_llm_end`が呼び出されますが、このとき`llm_output`が引数として渡されないため、`OpenAICallbackHandler`が正しく機能しません。
- `OpenAICallbackHandler`はcallback処理において、`on_llm_end`が呼び出された際に、引数にLLMResult.llm_outputを読み取ることでトークン数の利用状況を記録します。llm_outputはstream実行時に情報が取得できないため、設定されません。
- 一方、`BaseChatModel`の`generate`メソッドでは`llm_output`が設定されています。通常のAzureChatOpenAIでinvokeからLLMの処理を実行した際はトークン消費状況が記録されるということです。
このため、ストリーミングの場合にはトークン利用量が記録されないという問題が発生していました。
</details>

## 解決方法

Langchainのバージョン0.1.12以降では、この問題に対する解決策が提供されています。具体的には、`AgentExecutor`が継承する`RunnableAgent`に`stream_runnable`というメンバ変数が追加され、`plan`実行時にストリーミングを使用するかバッチ処理を使用するかを選択できるようになりました。

### 実装例

以下のコード例では、`stream_runnable`を`False`に設定することで、ストリーミングを使用せずにバッチ処理を行い、トークン数の情報を正しく取得できるようにしています。

```python
from langchain import AgentExecutor
from langchain.callbacks.openai_info import OpenAICallbackHandler
from langchain_community.callbacks import get_openai_callback
from langchain_core.runnables.config import RunnableConfig

# stream_runnableをFalseに設定
# 細かい設定はこちら：https://qiita.com/kash203/items/e264ea57d43fa9a43a3d
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,
    stream_runnable=False,
)

# エージェントを実行（パターン１：ハンドラをagent_executorに設定する方法）
openai_callback_handler = OpenAICallbackHandler()
config = RunnableConfig(callbacks=[openai_callback_handler])
pattern1 = agent_executor.invoke({"input": "what is LangChain?."}, config=config)
print(openai_callback_handler)  # トークン数を表示

# エージェントを実行（パターン２：get_openai_callbackを用いる方法）
with get_openai_callback() as cb:
    pattern2 = agent_executor.invoke({"input": "what is LangChain?."})
print(cb)  # トークン数を表示

# 表示例
"""
Tokens Used: 1000
	Prompt Tokens: 800
	Completion Tokens: 200
Successful Requests: 2
Total Cost (USD): $0.0075
"""
```

このようにすることで、エージェント実行時にトークン数が正しく記録されるようになります。
ただし、トークンの料金については最新モデルのものがLangchainに設定されていないケースがありますので、必要に応じて自身で計算する必要がある可能性がある点に注意です。
詳細は以下の一覧にモデルの情報があるかどうかをチェックしてください。
<https://github.com/langchain-ai/langchain/blob/873d06c009409560d52cde3f1a2b39f268319112/libs/community/langchain_community/callbacks/openai_info.py#L8-L100>

## おわりに

Langchainを用いたエージェント実行時に発生していたトークン数記録の問題は、バージョン0.1.12以降で提供された`stream_runnable`オプションを利用することで解決できます。エージェントのストリーミングが必要な場合は要注意ですが、基本的にはこの設定を使用することでトークン数の正確な記録が可能となります。

この情報が、Langchainを利用する開発者の皆様の問題解決の一助となれば幸いです。今後も最新のアップデート情報をチェックし、効率的な開発を行ってください。

# 謝辞
本記事の作成にあたり、ChatGPTの協力を得ました。高度な自然言語処理能力を持つChatGPTのおかげで、複雑な問題の解決策を明確に示すことができました。心より感謝申し上げます。

----
謝辞の通り、自身のメモ書きから、おおよその内容をChatGPTに記事化してもらいました。謝辞自体もChatGPTが考えています。

# 参考
- https://github.com/langchain-ai/langchain/issues/16798
- https://python.langchain.com/v0.1/docs/modules/callbacks/token_counting/