未解決なエラー/どうしようかな。LangChainの新しいチャット履歴管理RunnableWithMessageHistory

こんにちはmakokonです。先日LangChainの新しいチャット履歴管理として、今後はRunnableWithMessageHistoryはこれを使おうといったばかりなのですが、よくわからないエラーに悩んでいます。スッキリした解決がないので、記事にしても仕方ないのですが、現在の状況に対するこの問題を忘れないためにメモとして残しています。


遭遇したエラー


この2つ

Error in RootListenersTracer.on_chain_end callback: ValueError()
Error in callback coroutine: ValueError()

githubあたってみましたが、いい感じの解決が見当たりません。
どうしたもんなんでしょうかね。


現在の環境

今回確認した環境です。

$ python --version       
Python 3.11.2
$ pip list |grep langchain                      
langchain                     0.2.12
langchain-community           0.2.11
langchain-core                0.2.29
langchain-openai              0.1.17
$ pip list |grep openai          
langchain-openai              0.1.17
openai                        1.40.3

今回のプログラム


ほとんど公式サンプル通りですが、将来を考えてclass化しています。
class ChatProgram:でライブラリ化しているので、
CjatProgram.chat()で履歴付きチャットを扱うことができます。

import argparse
import json
import datetime
from operator import itemgetter
from typing import List
from langchain_openai.chat_models import ChatOpenAI
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import BaseMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.runnables import RunnableLambda, ConfigurableFieldSpec, RunnablePassthrough
from langchain_core.runnables.history import RunnableWithMessageHistory

class InMemoryHistory(BaseChatMessageHistory, BaseModel):
    """In memory implementation of chat message history."""

    messages: List[BaseMessage] = Field(default_factory=list)

    def add_messages(self, messages: List[BaseMessage]) -> None:
        """Add a list of messages to the store"""
        self.messages.extend(messages)

    def clear(self) -> None:
        self.messages = []

class ChatProgram:
    def __init__(self, chat_session: str = "chat", llm_model: str = "gpt-3.5-turbo"):
        self.chat_session = chat_session
        self.llm_model = llm_model
        self.store = {}
        self.history = self.get_by_session_id(chat_session)
        self.chain_with_history = self.setup_chain(llm_model)

    def get_by_session_id(self, session_id: str) -> BaseChatMessageHistory:
        if session_id not in self.store:
            self.store[session_id] = InMemoryHistory()
        return self.store[session_id]

    def setup_chain(self, llm_model: str):
        prompt = ChatPromptTemplate.from_messages([
            ("system", "あなたは、各分野の専門家として、ユーザーの入力に対し、以下の条件を守って、わかりやすく解説します。条件:\n 1.出力は、平易な日本語の平文、スライド、プログラムコードからなります。\n 2.スライドは、VS CodeのMarp extensionで確認するので、そのまま使えるmarkdown形式で出力してください。\n 3.プログラムコードは、特に指定がなければpythonで出力してください。\n 4.その他、特にプロンプトで指定された場合は、その指示に従ってください。"),
            MessagesPlaceholder(variable_name="history"),
            ("human", "{question}"),
        ])

        chain = prompt | ChatOpenAI(model=llm_model,temperature=0.7,max_tokens=2048)
        

        return RunnableWithMessageHistory(
            chain,
            self.get_by_session_id,
            input_messages_key="question",
            history_messages_key="history",
        )
        
    def my_parser(self, inputs):
        # パーサーロジック
        #print(inputs)
        return inputs

    def chat(self):
        while True:
            user_input = input("あなた: ")
            if user_input.lower() == "exit":
                break
            
            """
            response = self.chain_with_history.invoke(
                {"question": user_input},
                config={"configurable": {"session_id": self.chat_session}}
            )
            """
            
            # パーサーロジックを含む完全なチェーンを使用
            response = (self.chain_with_history | self.my_parser).invoke(
                {"question": user_input},
                config={"configurable": {"session_id": self.chat_session}}
            )

            # Get the latest AI response
            latest_response = self.store[self.chat_session].messages[-1].content
            print("AI: ", latest_response)

    def save_history(self):
        # Save chat history to a JSON file
        
        filename = f"log_{self.chat_session}"+datetime.datetime.now().strftime("%Y%m%d-%H%M%S")+".json"
        
        chat_log = [
            {"human": msg.content} if isinstance(msg, BaseMessage) else {"AI": msg.content} 
            for msg in self.store[self.chat_session].messages
        ]

        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(chat_log, f, ensure_ascii=False, indent=2)

def main():
    parser = argparse.ArgumentParser(description="Basic Chat Program")
    parser.add_argument('-c', '--chat_session', type=str, default="chat", help="Chat session ID")
    parser.add_argument('-m', '--llm_model', type=str, default="gpt-4o", help="LLM model to use")
    args = parser.parse_args()

    chat_program = ChatProgram(chat_session=args.chat_session, llm_model=args.llm_model)
    chat_program.chat()
    chat_program.save_history()

if __name__ == "__main__":
    main()

実行結果

あなた: 明日の天気の調べ方を簡潔に教えて
Error in RootListenersTracer.on_chain_end callback: ValueError()
Error in callback coroutine: ValueError()
AI: 申し訳ありませんが、私はリアルタイムの情報にアクセスすることができないため、具体的な天気予報を提供することはできません。最新の天気情報を知りたい場合は、天気予報のウェブサイトやアプリをご利用ください。
あなた: おすすめの方法はなんですか
Error in RootListenersTracer.on_chain_end callback: ValueError()
Error in callback coroutine: ValueError()
AI: 天気予報を確認するためのおすすめの方法をいくつか紹介します。

以下略

実行結果

2回目におすすめの方法を尋ねると、天気予報のおすすめの確認方法を教えてくれますので、履歴は正常に機能しているようですが、
毎回、2つのエラーが挟まります。
Error in RootListenersTracer.on_chain_end callback: ValueError()
Error in callback coroutine: ValueError()

実行結果にこのエラーは影響を与えていないのですが、(メタデータの確認もしましたが、怪しい結果は残っていないようです)これでは安心して使えませんね。

解決手段(になっていなかった)


また、上のエラーはgithub#20511 によると
「RunnableWithMessageHistoryは会話履歴を管理するための設計であり、入力と出力はメッセージとして扱うべきです。そのため、パース(解析)ロジックを混ぜる場合は、その後に行う必要があります。」ということらしいです。
具体的には、以下のように変更するということです:

  1. パースロジックを含む完全なチェーンを定義するのではなく、まずメッセージチェーンを定義する。

  2. その後にパースロジックを追加する。

例として、以下のようにコードを変更します:

# 以前の不適切な例
full_chain = prompt | llm | parser
with_message_history = RunnableWithMessageHistory(
    full_chain,
    ...
)

# 正しい例
underlying_chain = prompt | llm
with_message_history = RunnableWithMessageHistory(
    underlying_chain,
    ...
) | parser

ということらしいです。(解決した人もいるらしい)
しかし、今回の実行プログラムは、そもそも後段にパースを含んでいなかったのですが、念のわざわざ何もしないパースを入れてみたのですが、解決しませんでした。

下のエラーについては、今のところ見当がついていません。

まとめ

前回に続き、LangChainの新しいチャット履歴管理のRunnableWithMessageHistory を試してみましたが、
メモリー機能そのものに問題はないものの、謎のエラーが出て来ました。
将来のために、解決するか、解決の情報を調べたかったのですが、今のところ原因がはっきりしません。
対策をするよりバージョンアップでの解決を待つほうがいいのかもしれませんね。
もし解決をご存じの方がおられましたらぜひ教えてほしいところです。
makokonもしばらく、探索するつもりなので、解決したらお知らせします。
なんか単純な見落としだったらいいんですけどね。関連ライブラリとか

この記事が気に入ったらサポートをしてみませんか?