【LangChain】LLMの文脈消失問題を解決するセッション管理法

冒頭:どんな問題が発生したか

LangChainを使用して長い対話型アプリケーションを開発していたところ、会話の始めに指示した内容を後半で忘れるという問題が発生しました。具体的には、ユーザーが「以降は常にJSON形式で回答してください」と指示したにもかかわらず、5〜6ターン後にLLMが通常のテキストで回答を開始してしまうのです。

使用した環境はPython 3.11、LangChain 0.2.x、OpenAI GPT-4oです。エラーメッセージは表示されませんでしたが、出力形式の不合という形で問題が広がりました。

結論:解決策を端的に

LangChainのChatMessageHistoryConversationSummaryMemoryを組み合わせてセッション状態を管理し、各プロンプトに過去の重要なコンテキストを意図的に注入することで、文脈消失を解決できました。

具体的な手順:ステップバイステップ

ステップ1: 必要なライブラリをインストールする

pip install langchain langchain-openai langchain-community

ステップ2: 基本的なセッション管理を実装する

まずはLangChainのChatMessageHistoryを使って会話履歴を保存する基本的な構造を作成します。

from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnableWithMessageHistory

# LLMの初期化
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# セッション履歴を管理する辞書
session_histories = {}

def get_session_history(session_id: str) -> ChatMessageHistory:
    """指定されたセッションIDの履歴を取得または作成"""
    if session_id not in session_histories:
        session_histories[session_id] = ChatMessageHistory()
    return session_histories[session_id]

ステップ3: コンテキスト注入パターンを実装する

単純な履歴保存だけでは不十分な場合があります。重要な指示(システムプロンプト的な要素)を各ターンで意図的に注入する手法が有効です。

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser

# コンテキスト注入のためのプロンプトテンプレート
CONTEXT_INJECTION_PROMPT = """
あなたは一貫した形式で回答するAIアシスタントです。
重要な指示: {important_instructions}

以下の会話履歴を参考にして回答してください。
"""

def build_prompt_with_context(session_id: str, user_input: str, important_instructions: str):
    """セッション履歴と重要な指示を組み合わせてプロンプトを構築"""
    history = get_session_history(session_id)
    
    # システムコンテキストを作成
    context_messages = [
        ("system", CONTEXT_INJECTION_PROMPT.format(important_instructions=important_instructions))
    ]
    
    # 過去の会話履歴を追加
    for message in history.messages:
        context_messages.append((message.type, message.content))
    
    # 現在の入力を追加
    context_messages.append(("human", user_input))
    
    return ChatPromptTemplate.from_messages(context_messages)

ステップ4: 重要な指示をセッションに保存・管理する

ユーザーが指示した重要な事項を別途保存し、毎ターン注入することで、長期記憶をシミュレートします。

# セッションごとの重要な指示を管理する辞書
session_instructions = {}

def update_important_instructions(session_id: str, instruction: str):
    """重要な指示を更新(重複を避ける)"""
    if session_id not in session_instructions:
        session_instructions[session_id] = []
    
    # 同じ指示の重複を追加しない
    if instruction not in session_instructions[session_id]:
        session_instructions[session_id].append(instruction)

def get_important_instructions(session_id: str) -> str:
    """保存された重要な指示を取得"""
    if session_id not in session_instructions:
        return "常に准确かつ简洁に回答してください。"
    
    return " ".join([f"[{i+1}] {inst}" for i, inst in enumerate(session_instructions[session_id])])

ステップ5: 完全なチャットボットを実装する

以上の要素を組み合わせた完全な実装がこちらです。

from langchain_core.runnables import RunnablePassthrough

class PersistentContextChatbot:
    def __init__(self, llm):
        self.llm = llm
        self.session_histories = {}
        self.session_instructions = {}
    
    def get_history(self, session_id: str):
        if session_id not in self.session_histories:
            self.session_histories[session_id] = ChatMessageHistory()
        return self.session_histories[session_id]
    
    def update_instructions(self, session_id: str, instruction: str):
        if session_id not in self.session_instructions:
            self.session_instructions[session_id] = []
        if instruction not in self.session_instructions[session_id]:
            self.session_instructions[session_id].append(instruction)
    
    def get_instructions(self, session_id: str) -> str:
        if session_id not in self.session_instructions:
            return "常に准确かつ简洁に回答してください。"
        return " ".join([f"[{i+1}] {inst}" 
                        for i, inst in enumerate(self.session_instructions[session_id])])
    
    def build_prompt(self, session_id: str, user_input: str):
        history = self.get_history(session_id)
        instructions = self.get_instructions(session_id)
        
        context_system = f"""
你是可靠的AI助手。
重要な指示: {instructions}

会話履歴を考慮して回答してください。
"""
        messages = [("system", context_system)]
        
        for msg in history.messages:
            messages.append((msg.type, msg.content))
        
        messages.append(("human", user_input))
        
        return ChatPromptTemplate.from_messages(messages)
    
    def chat(self, session_id: str, user_input: str) -> str:
        # 指示の検出と保存
        if "JSON" in user_input.upper() or "形式" in user_input:
            self.update_instructions(session_id, user_input)
        
        # プロンプトを構築
        prompt = self.build_prompt(session_id, user_input)
        
        # LLMを呼び出し
        chain = prompt | self.llm | StrOutputParser()
        response = chain.invoke({})
        
        # 履歴に保存
        history = self.get_history(session_id)
        history.add_user_message(user_input)
        history.add_ai_message(response)
        
        return response

# 使用例
chatbot = PersistentContextChatbot(llm)
session_id = "user_123"

# 重要な指示を与える
print(chatbot.chat(session_id, "今後は常にJSON形式で回答してください"))
# 通常の会話
print(chatbot.chat(session_id, "私の名前は田中です"))
# 数ターン後
print(chatbot.chat(session_id, "私の名前は何ですか?"))  # JSON形式で回答される

ステップ6: ConversationSummaryMemoryの活用(長文対応)

会話が非常に長くなった場合は、ConversationSummaryMemoryを使って履歴を要約し、コンテキストウィンドウを有効に活用します。

from langchain.memory import ConversationSummaryMemory

class SummarizingChatbot:
    def __init__(self, llm):
        self.llm = llm
        self.sessions = {}
    
    def get_memory(self, session_id: str):
        if session_id not in self.sessions:
            self.sessions[session_id] = ConversationSummaryMemory(
                llm=self.llm,
                memory_key="chat_history",
                return_messages=True
            )
        return self.sessions[session_id]
    
    def chat(self, session_id: str, user_input: str) -> str:
        memory = self.get_memory(session_id)
        
        prompt = ChatPromptTemplate.from_messages([
            ("system", "あなたは一貫した помощник です。重要な指示: {instructions}"),
            MessagesPlaceholder(variable_name="chat_history"),
            ("human", "{input}")
        ])
        
        # 重要な指示を取得(別途管理が必要)
        instructions = get_important_instructions(session_id)
        
        chain = RunnablePassthrough.assign(
            chat_history=memory.load_memory_variables({})["chat_history"],
            instructions=lambda x: instructions
        ) | prompt | self.llm | StrOutputParser()
        
        response = chain.invoke({"input": user_input})
        
        # メモリに保存
        memory.save_context(
            {"input": user_input},
            {"output": response}
        )
        
        return response

補足・注意点

  • コンテキストウィンドウの制限: GPT-4oでも最大128Kトークンまでです。長文になる前はConversationSummaryMemoryを使うか、不要な履歴を要約・削除してください。
  • 指示の優先順位: プロンプト内で矛盾する指示があ場合は最初の方を優先しやすい傾向があります。重要な指示はプロンプトの冒頭部分に配置してください。
  • コスト管理: 履歴をすべて保存するとAPIコストが増加します。max_token_limitパラメータで履歴の長さを制限できます。
  • JSON出力の保証: プロンプトだけでJSON出力を100%保証するのは困難です。必要に応じてJSONOutputParserの使用を検討してください。
  • セッションIDのセキュリティ: セッションIDが他者に推測されると履歴漏洩の可能性があります。適切な認証機構を実装してください。

参考元

おすすめ環境

🚀 プロンプト技術をさらに磨くなら

プロンプトエンジニアリングの実践には、高性能なAIモデルへのアクセスが不可欠です。

  • ChatGPT Plus — GPT-4o/o1による高度な推論が利用可能
  • Claude Pro — 長文コンテキストと精密な指示理解に強い
この記事は役に立ちましたか?