🤖

LangChain で社内チャットボット作ってみた

2024/02/08に公開

こんにちは、クラウドエース SRE ディビジョン所属の茜です。

今回は、現在最も普及している対話型 AI サービスである ChatGPT で使用されているモデルと、LLM を使ったアプリケーション開発に特化したライブラリである LangChain を用いて社内向けのチャットボットを作成します。

ターゲット

  • 任意のデータを元に回答を行うチャットボットを作成したい方
  • 任意のデータを元に回答させる仕組みを知りたい方

ChatGPT とは

ChatGPT ロゴ
ChatGPT とは、ユーザーが入力した質問に対して、まるで人間のように自然な対話形式でAIが答えるチャットサービスです。2022 年 11 月に公開されて以来、回答精度の高さが話題となり、利用者が急増しています。

人工知能の研究開発機関「OpenAI」により開発されました。

執筆時点では、GPT-3.5、GPT-4 という大規模言語モデル (LLM) が使用されています。

LangChain とは

LangChain ロゴ
LangChain とは、LLM を活用してサービスを開発する際に役立つライブラリです。

LangChainには大きく分けて 2 つの側面があります。1 つは、LLM を用いたアプリケーション開発に必要なコンポーネントを、抽象化されたモジュールとして提供していることです。もう 1 つは、特定のユースケースに特化した機能を備えたモジュールを提供していることです。

これらのモジュールを使用することで、少ない実装量でのプロトタイプの作成が可能となります。また、特定のユースケースに特化した機能を持つモジュールを活用することにより、独自性の高いアプリケーションの実装が実現できます。

主要モジュール

執筆時点では、モジュールは大きく以下の 6 つに分類されています。
これらのカテゴリに対して、機能に応じてさらに細かく分類されています。

  • Model I/O
    • Prompts
    • LLMs
    • Chat Models
    • Output Parsers
  • Retrieval
    • Document loaders
    • Text Splitting
    • Text embedding models
    • Vector stores
    • Retrievers
    • Indexing
  • Agents
    • Agent
    • AgentExecutor
    • Tools
    • Toolkits
  • Chains
  • Memory
  • Callbacks

RAG について

社内チャットボットでは、企業が持つ社内データを参考に回答を生成する必要があります。
それを実現するために使用されるのが、RAG (Retrieval Augmented Generation) と呼ばれる仕組みです。

RAG では、以下の流れで任意のデータを元にした回答が行われます。

  1. ユーザーの質問を元に関連性のあるデータをベクターストアから検索し取得
  2. 質問と取得したデータを埋め込んだプロンプトを作成
  3. LLM に問い合わせ

RAG の流れ
RAG の流れ

Retrieval で RAG を実装

RAG を実装するために使用するモジュールが、Retrieval です。
以下の機能を使用します。

  • Document loaders:データソースからドキュメントを読み込む
  • Text Splitting:ドキュメントをチャンクという単位に分割
  • Text embedding models:テキストをベクトル化
  • Vector stores:ベクトル化したテキストの保存先
  • Retrievers:ユーザー入力と関連するドキュメントを検索し取得

Retrieval の流れ
Retrieval の流れ (LangChain 公式より引用)

チャットボット作成

使用モジュール

今回のチャットボットでは、以下のモジュールを使用します。

Model I/O

LLMs」または「Chat Models」というモジュールを使用することにより、さまざまな言語モデルを共通のインターフェースで使用することができます。

OpenAI の文章生成 API には、「Completions API」と「Chat Completions API」の2つがあり、前者を LangChain で使用するには LLMs モジュール、後者を使用するには Chat Models モジュールを使用します。

今回は、以下の理由から Chat Models モジュール (Chat Completions API) を使用します。

  • 最新モデル (gpt-4、gpt-3.5-turbo) は、Chat Completions API のみで使用可能
  • Completions API はレガシーな機能となっており、2023 年 7 月以降アップデートが行われていない

Retrieval

前述のように、「Document loaders」「Text Splitting」「Text embedding models」「Vector stores」「Retrievers」を使用し RAG を実装します。

Chains

LLM を使用したアプリケーションでは、単に LLM に入力して出力を得て終わりではなく、処理を連鎖的につなぎたいことが多いです。
今回の場合だと、以下の連鎖的処理が発生します。

  1. ユーザー入力を Embedding
  2. 関連性の高いテキストを検索し取得
  3. 取得した情報をプロンプトに埋め込む
  4. 文章生成 API を呼び出し、回答を取得

このような連鎖した処理を実現するのが、Chains モジュールであり、上記の一連の処理のために 「RetrievalQA」 という Chain (処理のまとまり) が提供されています。

RetrievalQA の基本動作
RetrievalQA の基本動作

Callbacks

Callbacksとは、アプリケーションのロギング、モニタリング、ストリーミングなどを効率的に管理する機能です。
これにより、あるイベントが発生したときや特定の条件が成立した場合に、自動的に関数やメソッドを呼び出すことができます。

今回は「StreamlitCallbackHandler」というモジュールを使用し、ストリーミングで応答を取得します。

事前準備

Open AI API キーの取得

Chat Completion API を使用するために、Open AI の API キーを準備します。

  1. Open AI の Web サイトにアクセスし、ログインします。

  2. API をクリックし、開発者向けの画面に遷移します。
    Open AI 画面1

  3. API キーの一覧画面に遷移し、「Create new secret key」をクリックします。
    Open AI 画面2

  4. 任意の名前をつけ、API キーを発行します。
    Open AI 画面3

ベクターデータベースのセットアップ

社内ドキュメントから取得したベクトルデータを格納するベクターデータベースとして、Pinecone を使用します。

  1. Pinecone の Web サイトにアクセスし、ログインします。

  2. 「Create Index」をクリックし、Index を作成します。Index とは、ベクトルデータをまとめて扱う単位のことです。
    任意の名前を入力し、「Dimensions」の項目には、Open AI Embeddings API の次元数である「1536」を入力します。
    Pinecone 画面1

  3. 「Create Index」をクリックすると Index が作成され、Index の詳細画面に遷移します。
    Pinecone 画面2

  4. API キー一覧画面から、発行されたキーを確認できます。
    Pinecone 画面3

パッケージとライブラリのインストール

今回のアプリケーションでは、Web フレームワークとして streamlit を使用します。

pip install streamlit

LangChain モジュールと Open AI の API を使用するため「langchain」「langchain-community」「langchain-openai」パッケージをインストールします。
また、.env ファイルの内容を環境変数に設定するため「python-dotenv」パッケージをインストールします。

pip install langchain langchain-community langchain-openai python-dotenv

Pinecone を使用するため、クライアントライブラリ「pinecone-client」をインストールします。
また、OpenAIEmbeddings が必要とする「tiktoken」をインストールします。

pip install pinecone-client tiktoken

PDF などの生のドキュメントをテキストとして読み込むため「unstructured」パッケージをインストールします。

pip install "unstructured[all-docs]"

今回使用したバージョンは以下の通りです。

パッケージ(ライブラリ)名 バージョン
streamlit 1.30.0
langchain 0.1.4
langchain-community 0.0.16
langchain-openai 0.0.5
python-dotenv 1.0.1
pinecone-client 2.2.4
tiktoken 0.5.2
unstructured 0.12.2

データの保存

ドキュメントをベクトル化し、Pinecone に保存する処理を実装していきます。
今回は社内ドキュメントとして、日本ディープラーニング協会が提供している生成AIの利用ガイドラインを PDF に変換し使用します。

Python プログラムから Pinecone に接続するため、環境変数として .env ファイルに Pinecone の API キーと Index 名、環境名を記載します。
また、Open AI モジュールを使用するため、Open AI API キーを記載します。使用するモデル、temperature も記載します。

.env
PINECONE_API_KEY=<API キー>
PINECONE_INDEX=company-data
PINECONE_ENV=gcp-starter
OPENAI_API_KEY=<API キー>
OPENAI_API_MODEL=gpt-3.5-turbo
OPENAI_API_TEMPERATURE=0.5

add_document.py ファイルに処理を記述していきます。
import を記述し、load_dotenv で環境変数を設定します。

add_document.py
import os
import sys
import pinecone
from dotenv import load_dotenv
from langchain_community.document_loaders import UnstructuredFileLoader
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.vectorstores import Pinecone

load_dotenv()

Pinecone を LangChain のベクターストアとして使用するための関数を定義します。

add_document.py
def initialize_vectorstore():
    pinecone.init(
        api_key=os.environ["PINECONE_API_KEY"],
        environment=os.environ["PINECONE_ENV"],
    )

    index_name = os.environ["PINECONE_INDEX"]
    embeddings = OpenAIEmbeddings()
    return Pinecone.from_existing_index(index_name, embeddings)

メインの処理を実装します。
引数で与えられたファイルを UnstructuredFileLoader で読み込み、CharacterTextSplitter で分割し Pinecone に保存します。
ドキュメントのベクトル化処理は、ベクターストアにデータを保存する際に内部的に実行されます。

add_document.py
if __name__ == "__main__":
    file_path = sys.argv[1]
    loader = UnstructuredFileLoader(file_path) 
    raw_docs = loader.load()

    text_splitter = CharacterTextSplitter(chunk_size=300, chunk_overlap=30)
    docs = text_splitter.split_documents(raw_docs)

    vectorstore = initialize_vectorstore()
    vectorstore.add_documents(docs)

add_document.py を実行します。

python add_document.py <ドキュメントパス>

Pinecone を確認すると、16 個のベクトルデータが保存されています。
Pinecone 画面4

関連情報を元にした回答

質問に関連した文書を Pinecone から検索し、回答する処理を実装していきます。

internal_qa.py ファイルに処理を記述していきます。
import を記述し、load_dotenv で環境変数を設定します。

internal_qa.py
import os
import streamlit as st
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_community.callbacks import StreamlitCallbackHandler
from add_document import initialize_vectorstore
from langchain.chains import RetrievalQA

load_dotenv()
st.title("社内チャットボット")

ベクターストアからデータを参照し回答を行う処理を定義します。
ChatOpenAI クラスから OpenAI の言語モデルを使用するインスタンスを作成し、RetrievalQA を用いて質問に関するデータを検索し回答を生成する chain を構築します。
Retriever (質問に関するデータを取得するインターフェース) は、ベクターストアのインスタンスから作成できます。

internal_qa.py
def create_qa_chain():
    vectorstore = initialize_vectorstore()
    callback = StreamlitCallbackHandler(st.container())

    llm = ChatOpenAI(
        model_name=os.environ["OPENAI_API_MODEL"],
        temperature=os.environ["OPENAI_API_TEMPERATURE"],
        streaming=True,
        callbacks=[callback],
    )

    qa_chain = RetrievalQA.from_llm(llm=llm, retriever=vectorstore.as_retriever())
    return qa_chain

最後に UI 部分を実装していきます。
ユーザー入力を受け取り、それに対する応答を生成してチャット形式で表示するインターフェースを作成します。
st.session_state を使用することで、会話履歴が一時的に保持されます。

internal_qa.py
if "messages" not in st.session_state:
    st.session_state.messages = []

for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

prompt = st.chat_input("What's up?")

if prompt:
    st.session_state.messages.append({"role": "user", "content": prompt})

    with st.chat_message("user"):
        st.markdown(prompt)

    with st.chat_message("assistant"):
        qa_chain = create_qa_chain()
        response = qa_chain.invoke(prompt)

    st.session_state.messages.append({"role": "assistant", "content": response["result"]})

streamlit コマンドを使用し、アプリケーションを起動します。

streamlit run internal_qa.py --server.port 8080

以下の画面が表示されました。
Streamlit 画面1

質問をします。
ベクターストアに保存した情報を元に回答が生成されました。
Streamlit 画面2

まとめ

今回は、LangChain を使用し、任意のデータを元に回答を行うチャットボットを作成しました。

LangChain を使用することで、さまざまなモデルを統一のインターフェースで扱うことができました。また、LLM を用いたアプリケーションに必要な機能を簡単に実装することができました。

一方、以下の点には注意が必要です。

LangChain を使用して、LLM を活かしたアプリケーションを作ってみてはいかがでしょうか。

Discussion