LangChainを使って任意データから自然言語で情報を取得してみる

AI

ChatGPTは2021年9月までの情報しか持っていないということが一つのネックとして挙げられていました。

しかし最近ではプラグインによってブラウジングして最新情報を取得したり、任意のデータをソース元として回答を出してくれたりといろいろな使い方が出てきています。

その中でも注目されている使い方としてカスタマーサポートやFAQなど今まで人力でやっていた業務の置き換えです。

例えば携帯キャリアのページなどで、よくある質問などがまとめられていたりしますが、ユーザーにとっては本当に聞きたいことがなかったりします。

そういったときにAIを使って自社データを元に回答させるようなことができれば、質問があるたびにFAQページに追加したりせず何でも回答するようなチャットを用意しておけばだいたい解決すると思います。

この記事では任意データを元にChatGPTで回答させるような仕組みをLangChainを使って紹介します。

LangChainとは

GPT-3のような大規模言語モデル(Large Language Model: LLM)を利用してサービスの開発をしたいときに便利に使えるライブラリです。

LangChainを使用することで、独自データの読み込み、Google検索の実行、LLMが苦手とする計算問題の解決など、様々な機能が利用可能となります。

PythonとTypeScript(JavaScript)のライブラリが公開されていますが、TypeScriptではPythonにはある機能が一部使えなかったりするのでそのあたりの技術選定は調査してから使うことをおすすめします。

またドキュメントは結構充実しているのでどういうことができるのかを調べるのもおもしろいです。

Get started | 🦜️🔗 Langchain
Get started with LangChain

今回やることの流れ

・テキストデータを分割する
・分割したデータをベクトル化しベクトルデータベースに保存
・ChatGPTに分割したテキストデータを元に質問に回答してもらう

LangChainの設定とインストール

前提としてOpenAIのAPIキーが必要となります。

APIキーについてはOpenAIのマイページから発行することができます。

LangChainは裏でOpenAIのAPIを叩くことになるので事前にAPIキーを環境変数に設定しておきます。

export OPENAI_API_KEY={OpenAI APIキー}

事前準備はこれだけでLangChainのインストール自体も以下を実行するだけです。

pip install openai

GitHub - hwchase17/langchain: ⚡ Building applications with LLMs through composability ⚡
⚡ Building applications with LLMs through composability ⚡ - GitHub - hwchase17/langchain: ⚡ Building applications with LLMs through composability ⚡

LangChainの使い方

今回はある動画をOpenAIのWhisperという文字起こしAPIを使って事前に文字起こししたテキストファイルを使うという想定で進めます。

Whisperについては別記事にまとめます。

テキストデータを分割する

どうしてテキストを分割するかというと、ChatGPTでは一度に扱えるデータ量に限界があるため大きなテキストデータを複数に分割します。

詳しくは後述しますが、分割したテキスト間の関連性についてのベクトルデータをベクトルデータベースに格納して回答するときに使います。

6/13のアップデートでトークン数の上限が16kまでになったモデルが発表されました。
一度に渡せるデータ量が大幅に増えたのでこのあたりの使い方は変わっていきそうです。
https://openai.com/blog/function-calling-and-other-api-updates

from langchain.text_splitter import CharacterTextSplitter
from langchain.document_loaders import TextLoader

loader = TextLoader("テキストデータ.txt")
documents = loader.load()
text_splitter = CharacterTextSplitter(separator=' ', chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)

※Whisperで文字起こしした文章は句読点がなく、会話の途切れなどが半角スペースとして出力されるのでseparator=' 'として半角スペースごとにテキストを分割しています。

分割したデータをベクトル化しベクトルデータベースに保存する

分割したテキストをベクトル化してベクトルデータベースに保存します。

これはLLMに送るテキストのうち、最も関連性の高いものを見つけるために使用します。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma

embeddings = OpenAIEmbeddings(model='text-embedding-ada-002')
vector_db = Chroma.from_documents(texts, embedding=embeddings, persist_directory='db')
vector_db.persist()

EmbeddingsというOpenAIのベクトル化する機能を使ってChromaというベクトルデータベースに格納します。

persist_directoryを指定することでディスクにデータを保存します。

Chromaはインメモリでも使用できますが、persist()を実行することで永続化できます。

永続化したときに出力されるファイルは以下のようなものです。

|-index
 |-index_{uuid}.bin
 |-id_to_uuid_{uuid}.pkl
 |-index_metadata_{uuid}.pkl
 |-uuid_to_id_{uuid}.pkl
|-chroma-collections.parquet
|-chroma-embeddings.parquet

ChromaはAI 用のオープンソースのベクトルデータベースです。
https://docs.trychroma.com

自然言語で任意データから情報を取得する

任意データに対して回答を求める準備が整ったのであとは自然言語で問い合わせて適切なデータを返してもらうようにします。

from langchain.chains import ConversationalRetrievalChain
from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI(temperature=0, model_name='gpt-3.5-turbo')
qa = ConversationalRetrievalChain.from_llm(llm, vector_db.as_retriever(), return_source_documents=True)

question = '質問内容'
response = qa({'question': question, 'chat_history': []})

まず、使用するLLMのモデルを指定します。

そしてConversationalRetrievalChainでどのような振る舞いをするかを指定していきます。

return_source_documentsは回答結果に対して参照した情報をレスポンスに含めるかの設定です。

qaに渡した引数を元に自然言語で問い合わせた結果をresponseに格納します。

以上が任意データに対して自然言語で問い合わせて情報を取得する例になります。

LangChainを使えば複雑なコードを書くことなく実現できました。

おまけ:ChatGPTのようにストリーミングデータを返す

上記で説明した方法だと同期処理になるので回答内容が全て出力されたタイミングでレスポンスが返ってきます。

ChatGPTのUIのようにストリーリングデータで返すことも可能です。

from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler

handler = StreamingStdOutCallbackHandler()
llm = ChatOpenAI(temperature=0, model_name='gpt-3.5-turbo', streaming=True, callbacks=[handler])

streaming=Trueと指定することでストリーミングとしてレスポンスを返します。

しかしこのままではそのストリーミングデータをどのように処理するかが不明なため、callbacksにどのような処理をするか設定します。

BaseCallbackHandlerというベースクラスが存在し、このクラスを継承したクラスをいくつかLangChainが用意しています。

例えばStreamingStdOutCallbackHandlerクラスは以下のように実装されています。

def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
    """Run on new LLM token. Only available when streaming is enabled."""
    sys.stdout.write(token)
    sys.stdout.flush()

on_llm_new_tokenメソッドがトークン(回答の文字)が生成される度に実行されます。

ストリーミングデータを標準出力するような処理ですね。

また、自分でBaseCallbackHandlerクラスを継承したクラスを作成することも可能です。

例えばAPI GatewayのWebSocket APIを使ってレスポンスを返したい場合は、以下のようにすることで実現可能です。

def on_llm_new_token(self, token: str, **kwargs) -> None:
    apigw_management = boto3.client('apigatewaymanagementapi', endpoint_url='{WebSocketエンドポイント}')
    apigw_management.post_to_connection(ConnectionId=self.connection_id, Data=token)

Lambdaで動かすような場合はChromaが重たく、Zipでは無理だったのでコンテナイメージ使って対応しました。

最後に

ChatGPTなどOpenAIのAPIを使う上でLangChainはとても便利だということが実感できました。

今回のようにOpenAI社が持っていない情報に対して回答を求めるということはいろいろなところで求められそうです。

私もChatGPTが出てきてからLLMについて興味を持ち調べるようになりましたが、まだまだLLMについては知識不足です。

LangChainについてもドキュメントに書いてあるコードを呼び出しているだけで裏でどのようなことをしているのかまで追いきれていません。

実際にサービスとして運用する場合には問題が起きたときに対応するためにもこのあたりの知識もキャッチアップが必要そうです。

参考

コメント

タイトルとURLをコピーしました