🦜

【連載】LangChainの公式チュートリアルを1個ずつ地味に、地道にコツコツと【Basic編#2】

2024/09/02に公開

こんにちは!新シリーズ「LangChainの公式チュートリアルを1個ずつ地味に、地道にコツコツと」の第二回のBasic編#2です。前回の記事では、シンプルなLLMアプリケーションの作成方法を学びましたが、今回は、その知識を基に、より高度なチャットボットの構築に挑戦します。

↓前回の記事
https://zenn.dev/chips0711/articles/f4ed8ac37eb3a8

さて、本記事は、LangChain公式チュートリアルの「Build a Chatbot」セクションをベースにしています。このチュートリアルは、LangChainの基本的な機能を学ぶ上で重要なステップとなります。次回は「Build vector stores and retrievers」に進み、外部データソースと連携するチャットボットの作成方法について学んでいく予定です。

LangChainを使ってチャットボットを作る方法について、公式チュートリアルを元に具体的な手順を解説していきますので、興味のある方はぜひ最後まで読んでみてください!

公式チュートリアル 対応する連載記事 記事の有無
Build a Simple LLM Application with LCEL Basic編#1 公開中
Build a Chatbot Basic編#2(本記事) 公開中
Build vector stores and retrievers Basic編#3 公開中
Build an Agent Basic編#4 予定
Build a Retrieval Augmented Generation (RAG) Application Working with external knowledge編#1 予定
Build a Conversational RAG Application Working with external knowledge編#2 予定
Build a Question/Answering system over SQL data Working with external knowledge編#3 予定
Build a Query Analysis System Working with external knowledge編#4 予定
Build a local RAG application Working with external knowledge編#5 予定
Build a Question Answering application over a Graph Database Working with external knowledge編#6 予定
Build a PDF ingestion and Question/Answering system Working with external knowledge編#7 予定
Build an Extraction Chain Specialized tasks編#1 予定
Build a PDF ingestion and Question/Answering system Specialized tasks編#2 予定
Classify text into labels Specialized tasks編#3 予定
Summarize text Specialized tasks編#4 予定

チャットボットとは?

チャットボットは、ユーザーと自然言語で会話をすることができるプログラムです。最近では、AIを活用したチャットボットが多くの企業で導入されており、顧客サポートや問い合わせ対応など、さまざまな用途で使われています。LangChainを使えば、簡単に高機能なチャットボットを構築することができます。LangChainは、言語モデルの操作、会話履歴の管理、プロンプトの作成など、チャットボット開発に必要な多くの機能を提供しています。

チャットボット構築の基本

今回構築するチャットボットは、会話の履歴を保持し、前回のやり取りを記憶して応答する機能を持つシンプルなものです。この基本的なチャットボットを作ることで、今後さらに高度な機能を追加したチャットボット(例えば、外部データソースと連携するConversational RAGやアクションを実行するエージェント型チャットボットなど)へのステップアップがしやすくなります。

セットアップ

必要な準備

まずは、必要なパッケージをインストールしましょう。今回も、Jupyter Notebookを使用してインタラクティブにコードを実行しながら学んでいきます。

pip install langchain==0.2.14

以下は.envファイルのサンプルです。適宜、値は各自変更してください。

.env例
LANGCHAIN_TRACING_V2=true #ハンズオンでは任意
LANGCHAIN_API_KEY=<LangSmithで作成したAPIキー> #ハンズオンでは任意

AZURE_OPENAI_API_KEY=<あなたが所有するモデルのAPIキー>
AZURE_OPENAI_ENDPOINT=<あなたが所有するモデルのエンドポイント>
OPENAI_API_VERSION="2024-07-01-preview" 
AZURE_OPENAI_DEPLOYMENT="gpt-4o-mini" #Azure OpenAI上のモデルデプロイ名

インストールや.envファイルの準備が完了したら、次のステップに進みましょう!

チャットモデルの使用方法

まずは、基本的なチャットモデルを使用してみましょう。LangChainでは、さまざまな言語モデルを簡単に使うことができます。ここでは、Azure OpenAIのGPTモデル(本記事ではgpt-4o mini)を使って、簡単なチャットボットの操作方法を学びます。

import os

from dotenv import find_dotenv, load_dotenv
from langchain_openai import AzureChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# 環境変数を設定します
load_dotenv(find_dotenv())

# AzureChatOpenAIインスタンスを作成します
model = AzureChatOpenAI(
    model=os.environ["AZURE_OPENAI_MODEL_NAME"],
    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
    azure_deployment=os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"],
    api_version=os.environ["AZURE_OPENAI_API_VERSION"],
    streaming=True
)

# シンプルなメッセージをモデルに渡してみましょう
from langchain_core.messages import HumanMessage

response = model.invoke([HumanMessage(content="こんにちわ!わたしはボブです!")])
print(response.content)
実行結果
こんにちは、ボブさん!お元気ですか?何かお話ししたいことがありますか?

このコードでは、AzureのGPTモデルを使って「こんにちは!わたしはボブです!」というメッセージを送信し、その応答を取得しています。model.invoke()メソッドは、メッセージのリストを受け取り、AIからの応答を返します。ここでの応答は、ユーザーの挨拶に対して適切な返事をしていることがわかります。
ただし、この段階では、モデルは会話の状態を持たず、独立したメッセージに対してのみ応答を返します。次のセクションでは、この制限を克服し、会話の文脈を維持する方法を学びます。

会話履歴の管理

チャットボットが有効に機能するためには、過去の会話の履歴を保持し、それに基づいて応答する必要があります。RunnableWithMessageHistoryクラスを使用することで、会話履歴を管理し、チャットボットを「状態保持型」にすることができます。

RunnableWithMessageHistory クラスの使用方法

RunnableWithMessageHistoryクラスは、別のRunnableオブジェクト(この場合は言語モデル)をラップし、そのチャットメッセージの履歴を管理します。具体的な動作は以下の通りです:

  1. 会話の前に、過去のメッセージをRunnableに渡す前にロードします。
  2. 実行後に生成された応答をメッセージとして保存します。
  3. session_idを使って複数の会話を管理し、呼び出し時にconfigsession_idを指定して該当する会話履歴を読み込みます。

以下のコードでは、SQLiteを使用してメッセージ履歴を管理する例を示しています:

from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import SQLChatMessageHistory

# セッションIDに基づいて履歴を取得する関数を定義します
def get_session_history(session_id):
    return SQLChatMessageHistory(session_id, "sqlite:///memory.db")

# メッセージ履歴を持つチャットボットを作成します
with_message_history = RunnableWithMessageHistory(
    model,
    get_session_history,
)

# コンフィグを設定し、チャットボットにメッセージを送信します
response = with_message_history.invoke(
    [HumanMessage(content="こんにちは!私はボブです。")],
    config={"configurable": {"session_id": "1"}},
)

print(response.content)
実行結果
こんにちは、ボブさん!今日はどのようなことをお話ししましょうか?

このコードでは、セッションID「1」に基づいてメッセージ履歴を管理しています。「こんにちは!私はボブです。」というメッセージを送信し、その履歴が保存されます。

次に、同じセッションで別の質問をしてみましょう:

response = with_message_history.invoke(
    [HumanMessage(content="私の名前は何ですか?")],
    config={"configurable": {"session_id": "1"}},
)

print(response.content)
実行結果
あなたの名前はボブさんです!他にお話ししたいことはありますか?

この例では、チャットボットが以前の会話履歴を参照して、ユーザーの名前(ボブ)を正確に覚えていることがわかります。session_idを変更することで、新しい会話を始めることができ、過去の会話履歴が別々に保存される仕組みになっています。
この機能により、チャットボットは複数のユーザーとの会話を個別に管理し、それぞれのコンテキストを維持することができます。これは、実際のアプリケーションで複数のユーザーと同時に対話する際に非常に重要です。

プロンプトテンプレートの追加

次に、プロンプトテンプレートを使って、チャットボットの応答をより柔軟にする方法を見ていきましょう。プロンプトテンプレートは、ユーザーからの入力を特定のフォーマットに変換することで、LLMが処理しやすい形式に整えるためのものです。

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# プロンプトテンプレートを作成します
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "あなたは役に立つアシスタントです。全ての質問に対してできる限りの答えをしてください。",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

# チェーンを作成し、メッセージを送信します
chain = prompt | model | StrOutputParser()

response = chain.invoke({"messages": [HumanMessage(content="こんにちは!私はボブです。")]})
print(response)
実行結果
こんにちは、ボブさん!お話しできてうれしいです。今日はどんなことをお手伝いできますか?

このコードでは、システムメッセージを使って、チャットボットに「役立つアシスタント」としての役割を指示しています。ChatPromptTemplateを使用することで、システムメッセージとユーザーメッセージを組み合わせた柔軟なプロンプトを作成できます。
MessagesPlaceholderは、会話履歴を挿入する場所を指定するために使用されています。これにより、過去の会話コンテキストを保持しながら、新しい入力に対応することができます。

多言語対応のチャットボット

さらに進んで、プロンプトテンプレートの柔軟性を活かして、多言語対応のチャットボットを作成してみましょう。以下のコードでは、ユーザーが選択した言語で応答するようにチャットボットを設定しています。

# 多言語対応のプロンプトテンプレートを作成します
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "あなたは役に立つアシスタントです。全ての質問に対して{language}で答えてください。",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

# チェーンを作成し、メッセージを送信します
chain = prompt | model

response = chain.invoke(
    {"messages": [HumanMessage(content="こんにちは!私はボブです。")], "language": "スペイン語"}
)
print(response.content)
実行結果
¡Hola, Bob! ¿Cómo estás?

このコードでは、languageパラメータを追加することで、チャットボットの応答言語を動的に変更できるようになりました。ここでは「スペイン語」を指定したため、チャットボットがスペイン語で応答しています。
この機能により、単一のチャットボットで複数の言語をサポートすることが可能になり、グローバルなユーザーベースに対応できるようになります。

カスタマイズオプション

RunnableWithMessageHistoryクラスを使用すると、メッセージ履歴の管理方法をさらにカスタマイズすることができます。以下の例では、ConfigurableFieldSpecを使用して、user_idconversation_idの2つのパラメータで会話履歴を管理する方法を示しています。

from langchain_core.runnables import ConfigurableFieldSpec

# プロンプトテンプレートを作成します
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "あなたは役に立つアシスタントです。全ての質問に対して{language}でできる限りの答えをしてください。",
        ),
        MessagesPlaceholder(variable_name="messages"),
    ]
)

# プロンプトテンプレートとモデルを組み合わせたランナブルを作成します
runnable_with_prompt = prompt | model | StrOutputParser()

# セッションIDに基づいて履歴を取得する関数を定義します
def get_session_history(user_id: str, conversation_id: str):
    return SQLChatMessageHistory(f"{user_id}--{conversation_id}", "sqlite:///memory.db")

# メッセージ履歴を持つチャットボットを作成します
with_message_history = RunnableWithMessageHistory(
    runnable_with_prompt,
    get_session_history,
    input_messages_key="messages",  
    history_messages_key="history",
    history_factory_config=[
        ConfigurableFieldSpec(
            id="user_id",
            annotation=str,
            name="ユーザーID",
            description="ユーザーのユニークな識別子。",
            default="",
            is_shared=True,
        ),
        ConfigurableFieldSpec(
            id="conversation_id",
            annotation=str,
            name="会話ID",
            description="会話のユニークな識別子。",
            default="",
            is_shared=True,
        ),
    ],
)

# 正しい形式の入力メッセージと設定を行います
input_message = [HumanMessage(content="こんにちは!私はボブです。")]

# メッセージを送信して応答を取得します
response = with_message_history.invoke(
    {"language": "イタリア語", "messages": input_message},  
    config={"configurable": {"user_id": "123", "conversation_id": "1"}},
)

print(response)
実行結果
Ciao Bob! Come posso aiutarti oggi?

このコードでは、ユーザーIDと会話IDを使ってメッセージ履歴を管理しています。ConfigurableFieldSpecを使用することで、これらのパラメータをより柔軟に設定できるようになります。同じユーザーIDであれば過去の会話履歴を保持し、新しいユーザーIDであれば新しい会話履歴が始まります。
この例では、イタリア語での応答を要求しており、チャットボットは適切にイタリア語で挨拶を返しています。この方法により、複数のユーザーとの会話を個別に管理しつつ、言語設定も柔軟に変更できる高度なチャットボットシステムを構築することができます。

ここまでのまとめ

ここまでの内容で、LangChainを使用してチャットボットを構築する基本的な方法を学びました。具体的には以下の点を押さえています:

  1. 基本的なチャットモデルの使用方法
  2. RunnableWithMessageHistoryを用いた会話履歴の管理
  3. プロンプトテンプレートによる柔軟な応答生成
  4. 多言語対応の実装
  5. ユーザーIDと会話IDを用いたカスタマイズされた履歴管理

これらの機能を組み合わせることで、実用的でカスタマイズ可能なチャットボットシステムを構築することができます。

会話履歴の管理と最適化

チャットボットを長期間運用する場合、会話履歴が無限に増え続けると、LLMのコンテキストウィンドウを超えてしまい、効率的な応答ができなくなる可能性があります。LangChainには、会話履歴のサイズを適切に制限するためのツールが用意されています。

メッセージ履歴のトリミング

trim_messages ユーティリティを使用すると、メッセージのリストを指定されたトークン数に合わせてトリミングすることができます。これにより、モデルのコンテキストウィンドウ内に収まるようにメッセージの長さを調整できます。

以下に、trim_messagesを使って送信するメッセージの数を制限し、チャットボットの応答を最適化する方法を示します:

from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, trim_messages
from langchain_openai import AzureChatOpenAI

# トリミング設定を作成します
trimmer = trim_messages(
    max_tokens=65,
    strategy="last",
    token_counter=AzureChatOpenAI(model="gpt-4o"),
    include_system=True,
    allow_partial=False,
    start_on="human",
)

# メッセージリストを定義します
messages = [
    SystemMessage(content="あなたは優秀なアシスタントです。"),
    HumanMessage(content="こんにちは!私はボブです。"),
    AIMessage(content="こんにちは!"),
    HumanMessage(content="私はバニラアイスクリームが好きです。"),
    AIMessage(content="いいですね。"),
    HumanMessage(content="2 + 2は何ですか?"),
    AIMessage(content="4です。"),
    HumanMessage(content="ありがとう。"),
    AIMessage(content="どういたしまして!"),
    HumanMessage(content="楽しんでますか?"),
    AIMessage(content="はい!"),
]

# メッセージ履歴をトリミングします
trimmed_messages = trimmer.invoke(messages)
print(trimmed_messages)
実行結果
[SystemMessage(content='あなたは優秀なアシスタントです。'), HumanMessage(content='ありがとう。'), AIMessage(content='どういたしまして!'), HumanMessage(content='楽しんでますか?'), AIMessage(content='はい!')]

このコードでは、trim_messages関数を使用して、履歴を最大トークン数65に制限しています。結果を見ると、システムメッセージが保持され、最新の数個のメッセージだけが残っていることがわかります。これにより、最新のコンテキストを保持しつつ、トークン数を制限することができます。

また、trim_messagesをチェーン内で利用することで、プロンプトに渡すメッセージ数を動的に調整し、モデルの応答を最適化することも可能です:

from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage, SystemMessage

# プロンプトテンプレートを定義します
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "あなたは役に立つアシスタントです。"),
        ("human", "{question}"),
    ]
)

# トリミング結果を表示する関数
def print_trimmed(messages):
    print("Trimmed messages:")
    for msg in messages:
        print(f"{msg.type}: {msg.content}")
    return messages

# メッセージ履歴をトリミングしてからプロンプトに渡すチェーンを作成します
chain = (
    RunnablePassthrough.assign(messages=itemgetter("messages") | trimmer | print_trimmed)
    | prompt
    | model
)

# 入力メッセージを作成
input_messages = messages + [HumanMessage(content="私の名前は何ですか?")]

response = chain.invoke(
    {
        "messages": input_messages,
        "question": "私の名前は何ですか?",
        "language": "日本語",
    }
)
print("\nResponse:")
print(response.content)
実行結果
Trimmed messages:
system: あなたは優秀なアシスタントです。
human: ありがとう。
ai: どういたしまして!
human: 楽しんでますか?
ai: はい!
human: 私の名前は何ですか?

Response:
申し訳ありませんが、あなたの名前を知りません。この会話の中であなたの名前は言及されていないようです。もし名前を教えていただければ、覚えておきますよ。何かお手伝いできることはありますか?

このコードでは、トリミングされた履歴を使ってプロンプトにメッセージを渡しています。結果を見ると、「私の名前は何ですか?」という質問に対して、チャットボットは名前を知らないと正直に答えています。これは、トリミングによって以前の会話(名前の紹介を含む)が削除されたためです。

この方法により、長い会話履歴を持つチャットボットでも、常に最新かつ関連性の高い情報を用いて応答を生成することができます。

チャット履歴とトリミングの組み合わせ

最後に、RunnableWithMessageHistoryを使って、トリミングされたメッセージ履歴を管理する方法を見ていきましょう。これにより、ユーザーとの会話履歴を保持しつつ、適切にトリミングされたデータのみをプロンプトに渡すことができます。

from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# プロンプトテンプレートの作成
prompt = ChatPromptTemplate.from_messages([
    ("system", "あなたは役に立つアシスタントです。全ての質問に対して{language}で答えてください。"),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}")
])

# チェーンの作成
chain = (
    RunnablePassthrough.assign(history=lambda x: x["history"])
    | prompt
    | model
    | StrOutputParser()
)

# チャット履歴の初期化
messages = [
    SystemMessage(content="あなたは優秀なアシスタントです。"),
    HumanMessage(content="こんにちは!私はボブです。"),
    AIMessage(content="こんにちは、ボブさん!お手伝いできることがありますか?"),
]
chat_history = InMemoryChatMessageHistory(messages=messages)

# セッションIDに基づいて履歴を取得するダミー関数を定義
def dummy_get_session_history(session_id):
    if session_id != "1":
        return InMemoryChatMessageHistory()
    return chat_history

# トリミングされたメッセージ履歴を持つチェーンを作成
chain_with_history = RunnableWithMessageHistory(
    chain,
    dummy_get_session_history,
    input_messages_key="input",
    history_messages_key="history",
)

# メッセージを送信して応答を取得
response = chain_with_history.invoke(
    {
        "input": "喋れないオウムを何と呼びますか?また、私の名前はなんですか?",
        "language": "日本語"
    },
    config={"configurable": {"session_id": "1"}},
)

print(response)
実行結果
喋れないオウムは「無口のオウム」と呼ぶことができます。また、あなたの名前はボブさんです。何か他に知りたいことがありますか?

このコードでは、チャット履歴の管理とトリミングを組み合わせています。結果を見ると、チャットボットは「喋れないオウム」について答えつつ、ユーザーの名前(ボブ)も正確に覚えていることがわかります。

ここで、セッションIDを変更してみると、新しい会話が始まることを確認できます:

response = chain_with_history.invoke(
    {
        "input": "喋れないオウムを何と呼びますか?また、私の名前はなんですか?",
        "language": "日本語"
    },
    config={"configurable": {"session_id": "2"}},
)

print(response)
実行結果
喋れないオウムは「無口なオウム」や「黙っているオウム」と呼ぶことがあります。また、あなたの名前はわかりません。もし教えていただければ、覚えますよ!

この結果から、新しいセッションIDでは、以前の会話履歴(ボブさんの名前を含む)にアクセスできていないことがわかります。これは、会話履歴が適切にセッションごとに分離されていることを示しています。

このように、RunnableWithMessageHistoryクラスとdummy_get_session_history関数を使用したセッション管理により、チャットボットは新しい会話を開始し、以前の履歴にアクセスしなくなります。これにより、複数のユーザーや異なる会話コンテキストを適切に管理できます。

また、この結果は、チャットボットが未知の情報(この場合はユーザーの名前)に対してどのように対応するかも示しています。適切に「わからない」と応答し、さらに情報を求めるプロンプトを提供することで、自然な会話の流れを維持しています。

このような動作は、実際のアプリケーションで重要です。例えば、カスタマーサポートボットが複数のユーザーと同時に対話する場合、各ユーザーの会話を適切に分離し、個別の文脈を維持することが必要です。同時に、ボットが知らない情報に対して適切に対応することで、ユーザーエクスペリエンスを向上させることができます。

以上で、会話履歴の管理と最適化に関する説明を終わります。次は、ストリーミングの導入について解説していきます。

合わせて、会話履歴の管理において、参考になりそうな記事を紹介しておきます。

https://zenn.dev/mizunny/articles/d974720d8acc6f

ストリーミングの導入

チャットボットのユーザーエクスペリエンスを向上させるために、ストリーミング機能の導入は非常に重要です。LLMは応答生成に時間がかかることがありますが、ストリーミングを使用することで、ユーザーはリアルタイムで生成されるトークンを見ることができ、よりスムーズなインタラクションが可能になります。

LangChainは、モデルの出力をリアルタイムで取得するためのストリーミング機能をサポートしています。これにより、ユーザーは応答の完了を待つことなく、途中経過を確認できます。

ストリーミングの方法

LangChainでは、主に2つのストリーミング方法を提供しています:

  1. 同期的なストリーミング(stream メソッド): 通常のPythonコードで使用します。
  2. 非同期ストリーミング(astream メソッド): asyncioなどの非同期プログラミング環境で使用します。

同期的なストリーミングの実装

以下は、同期的なストリーミングの例です。これまでに作成したチャットボットの設定を使用します。

from langchain_core.messages import HumanMessage

config = {
    "configurable": {
        "session_id": "abc15",
        "user_id": "user123"
    }
}

# ストリーミング応答を取得します
for r in chain_with_history.stream(
    {
        "input": "こんにちは!私はトッドです。ジョークを教えてください",
        "language": "日本語",
    },
    config=config,
):
    print(r, end="|", flush=True)
print()  # 最後に改行を入れる
実行結果(注:実際の出力は一文字ずつ表示されます)
|こんにちは|||ッド|さん||ジョ|ーク|||||紹介|します||||||||||||ない||?」

|||って|||||||って|いる|から|!」

|どう|でした|||もっと|||たい|です||||

このコードでは、chain_with_history.streamメソッドを使用して、チャットモデルからのストリーミング応答を逐次取得しています。
実際のアプリケーションでは、上記のコードで end="|"end="" に変更することで、ユーザーにはスムーズな文字の流れとして表示されます。ここでは、ストリーミングの過程を可視化するために | で区切って表示しています。

非同期ストリーミングの実装

次に、非同期ストリーミングの例を示します。この方法は、Webアプリケーションなど、非同期処理が必要な環境で特に有用です。

import asyncio

async def get_streaming_response():
    async for r in chain_with_history.astream(
        {
            "input": "別のジョークを教えてください。今度は猫に関するものをお願いします。",
            "language": "日本語",
        },
        config=config,
    ):
        print(r, end="|", flush=True)
    print()  # 最後に改行を入れる

# 非同期関数を実行
asyncio.run(get_streaming_response())
実行結果(注:実際の出力は一文字ずつ表示されます)
|もちろん|です||こん||ジョ|ーク|はい|||です|||||||コン||使|||||||します||||||||クリック|する|!」||

この例では、asyncioを使用して非同期にストリーミング応答を処理しています。これにより、他の非同期タスクと並行して効率的に処理を行うことができます。

ストリーミングのメリットとエラーハンドリング

ストリーミングには以下のようなメリットがあります:

  1. 応答速度の向上: ユーザーは完全な応答を待つ必要がなく、生成されたトークンをリアルタイムで見ることができます。
  2. リソースの効率的な使用: 特に非同期ストリーミングを使用する場合、システムリソースをより効率的に利用できます。
  3. ユーザー体験の向上: インタラクティブな応答により、ユーザーはより自然な会話を体験できます。

エラーハンドリング

ストリーミング処理中にエラーが発生する可能性もあるため、適切なエラーハンドリングが重要です。以下に例を示します:

async def handle_streaming_response():
    try:
        async for r in chain_with_history.astream(
            {
                "input": "プログラミングに関する面白い事実を教えてください",
                "language": "日本語",
            },
            config=config,
        ):
            print(r, end="", flush=True)
        print()  # 最後に改行を入れる
    except Exception as e:
        print(f"エラーが発生しました: {e}")

asyncio.run(handle_streaming_response())

このコードでは、try-exceptブロックを使用してエラーをキャッチし、適切に処理しています。これにより、ストリーミング中にエラーが発生しても、プログラムが予期せず停止することを防ぎます。

実践的なタスク: チャットボットの機能拡張

ここまでの内容を踏まえて、以下のような実践的なタスクに挑戦してみましょう:

  1. 感情分析機能の追加:
    ユーザーの入力に対して感情分析を行い、その結果に基づいてチャットボットの応答を調整する機能を実装してみましょう。これには、外部の感情分析APIやLLMを使用した簡単な感情分類を利用できます。

  2. 多言語翻訳機能:
    ユーザーが指定した言語で入力し、別の言語で応答を返す翻訳チャットボットを作成してみましょう。これには、LLMの多言語能力や外部の翻訳APIを活用できます。

  3. 知識ベースとの連携:
    特定のトピックに関する情報をCSVファイルやデータベースに保存し、ユーザーの質問に対してその知識ベースを参照して回答する機能を追加してみましょう。LangChainのドキュメント検索機能を活用できます。

これらのタスクに取り組むことで、LangChainの様々な機能を組み合わせて使う実践的な経験が得られます。

トラブルシューティング

チャットボット開発中に遭遇する可能性のある一般的な問題とその解決方法を紹介します:

  1. APIキーの設定エラー:

    • 問題: InvalidRequestError: No API key provided
    • 解決策: 環境変数に正しいAPIキーが設定されているか確認してください。
  2. メモリ使用量の増大:

    • 問題: 長時間の使用でメモリ使用量が急増する。
    • 解決策: trim_messagesを使用して定期的に会話履歴をトリミングしてください。
  3. 応答の一貫性がない:

    • 問題: 同じ質問に対して毎回異なる回答が返ってくる。
    • 解決策: システムメッセージやプロンプトテンプレートを調整し、より具体的な指示を与えてください。
  4. ストリーミングが機能しない:

    • 問題: ストリーミングを有効にしても一括で応答が返ってくる。
    • 解決策: 使用しているモデルがストリーミングをサポートしているか確認し、streaming=Trueオプションが正しく設定されているか確認してください。

これらの問題に遭遇した場合は、LangChainの公式ドキュメントやコミュニティフォーラムも参考になるでしょう。

まとめ

今回の【Basic編#2】では、LangChainを使用してチャットボットを構築する方法を学びました。主に以下の点に焦点を当てました:

  1. 基本的なチャットモデルの使用方法
  2. 会話履歴の効果的な管理
  3. プロンプトテンプレートを使用した柔軟な応答生成
  4. 多言語対応の実装
  5. メッセージ履歴のトリミングによる最適化
  6. ストリーミング機能の導入とそのメリット

これらの技術を組み合わせることで、より高度で柔軟なチャットボットを構築することができます。ストリーミング機能を活用することで、ユーザー体験を大幅に向上させることができました。

LangChain公式チュートリアルは、基礎から応用まで幅広いトピックをカバーしています。
本シリーズでは、これらのチュートリアルを順に解説していきます。次回の「Build vector
stores and retrievers
」では、チャットボットの知識ベースを外部データソースと連携させる方法を学び、より高度な質問応答システムの構築に挑戦します

参考文献

Discussion