langchianlogo

LangChain入門(4) – RAG – 情報を与えて精度の良い解答を得る

RAGとはRetrieval Augmented Generationの頭文字で、生成AIに問い合わせをするときに関連する情報を効率よく渡すことで、より精度の高い応答を得る手法です。ここではRAGについてみてゆきましょう。

本記事はFuture Coders独自教材からの抜粋です。変化の早い分野なので記事の内容が古くなっている可能性もあります。ご注意ください。

RAG (Retrieval Augmented Generation)

RAGとは、生成AIで応答を生成するときに、質問の内容に関連しそうな情報を同時に生成AIに入力することで、より期待に近い応答を得る技術のことです。

生成AIの性能に驚かされることもあると思いますが、生成AIも万能ではありません。以下のような質問をしても、生成AIが適切な答えを返すことは困難です。

  • あなたの会社の製品の詳しい仕様に関する質問
  • 最近施行された法律に関する質問
  • あなたの今までの治療歴を踏まえた助言

このような問い合わせに対して適切な応答を得るには、プロンプト以外に背景情報(コンテキスト・文脈)を適切に生成AIに渡す必要があります。かといって、常にすべての情報をインプットするのも非効率的です。

そこで以下のような処理を行います。

  1. 文脈・背景情報となる情報(ドキュメント、データベース、表計算)を一定のサイズ(文字数)に分割しておく
  2. プロンプトを入力
  3. プロンプトが与えられたら、データベースから、プロンプトに関連しそうな情報を検索
  4. 2と3の情報をまとめて一緒に生成AIに入力
rag1
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 12

このようにすれば生成AIは必要な情報から応答を生成できるようになります。これがRAGの大まかな仕組みです。

RAGを動かしてみる

まずはどのようなものか動かしてみましょう。

以下のコマンドを実行して使用するモジュールを最新の状態にしてください。

py -m pip install --upgrade --quiet langchain langchain-community langchainhub gpt4all langchain-chroma 

今回は生成AIでも詳しく把握していなさそうなサンプルとしてJIS工業規格の仕様書をつかってみました。以下のURLから参照できます。

https://kikakurui.com/c60/C61558-2-7-2012-01.html

まずは何も背景情報(コンテキスト)を与えずに、生成AIに質問してみましょう。

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
r = llm.invoke("電源装置の定格出力電圧はいくつ?交流と直流の電圧を教えてください")
print(r.content)

以下のような返答が返ってきました。

電源装置の定格出力電圧は、使用するデバイスやシステムによって異なります。以下は一般的な定格出力電圧の例です。

### 直流電源装置(DC power supply)の例
- **5V DC**: 一般的に小型電子機器やUSBデバイスで使用。
- **12V DC**: 自動車の電装品や一部の家庭用電子機器で使用。
- **24V DC**: 工業用機器や一部の通信機器で使用。
- **48V DC**: 電話交換機やデータセンターの機器で使用。

### 交流電源装置(AC power supply)の例
- **100V AC**: 日本などで一般的な家庭用電圧。
- **120V AC**: 北米などで一般的な家庭用電圧。
- **230V AC**: ヨーロッパなどで一般的な家庭用電圧。
- **240V AC**: 一部の工業用機器や大容量の家庭用機器に使用。

これらは一般的な例であり、特定のアプリケーションや地域によって異なる場合があります。使用する機器の仕様書やメーカーの指示に従ってください。

それっぽい内容のように見えますが、「これらは一般的な例であり、…」と但し書きがあります。上記のWebを参照すると「6.101 変圧器及び変圧器を組み込んだ電源装置の定格出力電圧は交流24 V以下,又は電源装置の場合,リプルフリーの直流33 V以下でなければならない。 」と記載されています。この内容とはだいぶ異なっていることがわかります。

それでは、RAGを使ってどのように修正できるか見てゆきましょう。

まずは、Webからの情報をWebBaseLoaderを使って読み込みます。

from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
    web_paths=("https://kikakurui.com/c60/C61558-2-7-2012-01.html",),
)
docs = loader.load()
docs

docsはDocumentオブジェクトのリストです。中をみてみると、上記のWebページの内容がそのまま保存されていることがわかります。

今回はWebBasedLoaderを使用してみましたが、ほかにもいろいろなLoaderが用意されています。以下のURLを参照してください。

https://api.python.langchain.com/en/latest/community_api_reference.html#module-langchain_community.document_loaders

CSV, WORD, PDF,などの各種フォーマットを読み込むParserはもちろん、OneDrive, Azure, GCPなどのクラウドサービスから情報を取得するクラスなど様々なLoaderが用意されています。

それでは取得したドキュメントのリスト(docs)をより細かく(サイズ1000)に分割して、ベクトル形式で保存します。

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())

RecursiveCharacterTextSplitterはドキュメントをさらに分割するクラスです。今回はチャンク(塊)のサイズを1000として、重複部分200のサイズに分割します。分割した結果はsplits変数に格納されます。今回の例では28個のチャンクに分割されました。興味のある人は変数splitsの中身をみてください。

最後にChroma.from_documentsを使って、分割したチャンクからベクトルストアのオブジェクトを作成します。チャンクからベクトルデータを作成するのは文書の長さにもよりますが、それなりに負荷の大きな作業となります。よって、このベクトル化したデータはファイルに保存しておき、利用する度にファイルから読み込むことも可能です。

ベクトルストアから検索してみましょう。similarity_searchメソッドは指定された内容に近いものをk個返してくれます。

vectorstore.similarity_search("電源装置の定格出力電圧はいくつ?", k=2)

以下のような応答が得られました。

[Document(page_content='部回路及びそれらの部品には適..<中略>.は,交流24\xa0V以下\xa0\n玩具用変圧器を組み込んだ電源装置は,リプルフリーの直流33\xa0V以下', metadata={'description': 'この規格は,玩具用変圧器及び玩具用変圧器を組み込んだ電源装置の電気的安全性,熱的安全性,機械的安全性などの安全側面について規定する。', 'language': 'ja', 'source': 'https://kikakurui.com/c60/C61558-2-7-2012-01.html', 'title': 'JISC61558-2-7:2012 変圧器,電源装置,リアクトル及びこれに類する装置の安全性-第2-7部:玩具用変圧器及び玩具用電源装置の個別要求事項及び試験'}),

 Document(page_content='変圧器は,定格入力電圧..<中略>.について規定する。', 'language': 'ja', 'source': 'https://kikakurui.com/c60/C61558-2-7-2012-01.html', 'title': 'JISC61558-2-7:2012 変圧器,電源装置,リアクトル及びこれに類する装置の安全性-第2-7部:玩具用変圧器及び玩具用電源装置の個別要求事項及び試験'})]

Documentオブジェクトを2つ含むリスト形式です。それぞれのドキュメントには質問内容に関連しそうな情報が格納されていることがわかります。

次にプロンプトです。RAGを活用したプロンプトは自分で作成してもよいのですが、すでに用意されているものがあるので、それを流用してみましょう。

from langchain import hub
prompt = hub.pull("rlm/rag-prompt")
prompt.pretty_print()

以下のように出力されました。

================================ Human Message =================================

You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
Question: {question} 
Context: {context} 
Answer:

英語ではありますが、「以下の{context}を踏まえて、{question}にある質問に答えてください。わからない時はわからないと答えてください。文章は3つまで、わかりやすく答えます。」といったニュアンスです。

準備はここまでです。RAGを使って生成AIに問い合わせてみましょう。

from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

retriever = vectorstore.as_retriever()

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

rag_chain.invoke("電源装置の定格出力電圧はいくつ?交流と直流の電圧を教えてください")

以下のような答えが得られました。ちゃんとWEBサイトの内容を踏まえて、正しい答えが得られていることがわかります。

'定格出力電圧は、交流が24V以下、直流がリプルフリーの33V以下です。'

Chainの様子は以下の通りです。

rag2
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 13

Inputは入力です。”電源装置の定格出力電圧はいくつ?交流と直流の電圧を教えてください”がその値です。入力値はPassThrough(素通り)とVectorStoreとい2つの処理に流れてゆきます。PassThroughは文字通り、後段のChatPromptTemplateにそのまま文字列を渡します。VectorStoreではInputの入力をもとにベクトルストアから検索して、その結果をContextとしてChatPromptTemplateに渡します。

つまりChatPromptTemplateは以下の2つの情報をうけとることになります。

  • question = 最初の入力
  • context = ベクトルストアで検索された情報

これらがプロンプトとして生成AI(ChatOpenAI)に渡されて処理が行われます。

このChainがつながっている様子は以下の命令を実行することでも確認できます。

rag_chain.get_graph().print_ascii()

Streamlitでのアプリ化

ここまでの内容をまとめてStreamlitのアプリにしてみます。

import streamlit as st
from langchain_openai import ChatOpenAI
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain import hub
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

model = ChatOpenAI(model="gpt-4o")
prompt = hub.pull("rlm/rag-prompt")

@st.cache_resource
def get_chain():
    loader = WebBaseLoader(
        web_paths=("https://kikakurui.com/c60/C61558-2-7-2012-01.html",),
    )
    docs = loader.load()

    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
    splits = text_splitter.split_documents(docs)
    vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
    retriever = vectorstore.as_retriever()

    rag_chain = (
        {"context": retriever | format_docs, "question": RunnablePassthrough()}
        | prompt
        | model
        | StrOutputParser()
    )
    return rag_chain

st.title("変圧器,電源装置の安全性")
q = st.text_input("質問")
if st.button("質問"):
    chain = get_chain()
    answer = chain.invoke(q)
    st.write(answer)

実行時はコマンドプロンプトから以下のように実行してください。

streamlit run ファイル名
rag3
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 14

GCPで動かす

ローカルで動いたアプリをクラウドGCPで動かしてみましょう。rag-jisというフォルダの下で作業しているものとします。上記のPythonのプログラムをmain.pyという名前で保存します。

プログラムで使用するモジュールをrequirements.txtに列挙します。requirements.txt

bs4
streamlit
langchain
langchainhub
langchain_core
langchain_openai
langchain_community
langchain_chroma
langchain_text_splitters

Procfileというファイル名で以下の内容のファイルを作成します。

web: streamlit run main.py --server.port ${PORT:-8080}

以下のようなフォルダ構成となっていることを確認してください。

├─rag-jis
│      main.py
│      Procfile
│      requirements.txt

GCPではプロジェクトという単位で管理します。例えば今回のアプリをGCP上で動作させるには、新規にプロジェクトを作成するか、もしくは既存プロジェクトから対象を選択して、その中で動かすことになります。

GCPでは、ダッシュボードというWebページ経由でいろいろな操作を行えますが、gcloudというコマンドをつかってさまざまな操作を行うことも可能です。

以下のコマンドでプロジェクトにログインすることができます。

gcloud auth login           ... GCPにログインする

以下のコマンドでプロジェクトからログアウトします。

gcloud auth revoke           ... GCPからログアウトする

複数のアカウントを使用している場合、どのアカウントでログインしているか確認するには以下のコマンドを実行します。

gcloud auth list            ... どのアカウントでログインしているか確認する

以下のコマンドでプロジェクトを列挙することが可能です。

gcloud projects list

GCPでデプロイするときにはAPIを有効にする必要があります。以下のコマンドを実行することで現在選択済のプロジェクトに対してAPIを有効化することができます。

gcloud services enable compute.googleapis.com run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com

ローカルPCにあるプロジェクトどGCPで動作させるようにするコマンドはgcloud run deployです。実行するといろいろと質問されます。都度答えてゆくことも可能ですが、コマンドの引数として指定することも可能です。

  • アプリの名前 = deployの直後に指定
  • ソースコードの場所 = –source .
  • クラウドの地域 = –region “asia-northeast1”
  • 匿名の実行の可否 = –allow-unauthenticated
  • 環境変数の設定 = –update-env-vars

gcloud run deployと実行して答えていってもよいし、以下のようにまとめて入力しても構いません。

gcloud run deploy my-app --region "asia-northeast1" --source . --allow-unauthenticated --update-env-vars OPENAI_API_KEY=APIキーの値

認証のエラーがでることがあるかもしれません。その場合は以下のコマンドも試してみてください。

gcloud auth application-default login   ... デフォルトの認証設定を行う

デプロイが終わると、URLが表示されます。そのURLからサービスにアクセスすることができます。

URLはGCPのコンソールから確認することも可能です。プロジェクトを選択して、CloudRunを選択すると、該当するURLを見つけることができます。

rag5
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 15

もし動作しない場合は、GCPのコンソールからCloudRunのログを参照してください。

また、カレントディレクトリをOneDrive上のフォルダでデプロイを試みるとサービスの起動に失敗するというエラーに悩まされました。ログをみたところ、Procfileが適切に検出されないためか、streamlitの実行が行えなかったようです。フォルダ一色をCドライブなどのローカルドライブにコピーしてから実行すると正常に動作しました。WindowsでOneDriveを使っている場合には注意してください。

ベクトル化と類似検索

RAGでは、「プロンプトに関連しそうな内容を検索してくる」ということが重要になります。言葉でいうと簡単ですが、「どの部分がプロンプトに近い内容なのか?」これは、自分で実装しようとすると難しいはずです。

生成AIの分野では、文章などは全てベクトルという数値の列で表現します。以下は”apple”という単語をベクトルに変換するプログラムです。

from langchain_openai import OpenAIEmbeddings
embeddings=OpenAIEmbeddings()
apple_vec=embeddings.embed_query("apple")
print(apple_vec)

出力は以下のようになりました。”apple”という単語が1536個の数値に変換されています。

[0.007754413411021233, -0.02315402403473854, -0.007501848973333836, -0.027768446132540703, ....

今回は”apple”という短い単語で試しましたが、どのような入力を与えても1536個の数値になります。Embeddingsとは「埋め込み」と訳されたりしますが、任意のテキストを数値の羅列に変換することができます。

ちなみに、使用するEmbeddingsによってベクトルの次元数(数値の個数)は異なります。今回使用したのはOpenAIが提供するEmbeddingsです。1536個の数値に変換されました。

以下は別のEmbeddingsを使った場合の例です。

pipコマンドで必要なモジュールをインストールします。

py -m pip install -U langchain-huggingface

以下は、”all-MiniLM-L6-v2″というモデルを使った場合のサンプルです。

from langchain_huggingface import HuggingFaceEmbeddings
mini_embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
apple_vec=mini_embeddings.embed_query("apple")
print(apple_vec)
print(len(apple_vec))

“apple”をベクトル化した結果、その個数は384個となりました。一般的には、この次元数が多い方が一般的にはより精度が向上する代わりに、計算処理の負荷もおおきくなります。

「単語や文章をベクトルにして何がうれしいの?」と思われるかもしれませんが、ベクトルに変換すると、類似検索が容易になるという利点があります。つまり、似たような意味をもつ単語や文章の検索がしやすくなります。

似たような意味をもつ言葉であれば、似たような数値を持っているのですが、1536個の値を比較するのも大変です。

どのくらい類似度があるかという数値で表現するための関数cosine_similarityが用意されています。

cosine_similarityは2つのベクトルを比較して、それらが1に近いほど類似度が高いということを意味します。

以下はざっくりとしたイメージでの説明です。

rag6
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 16

cosineとは三角関数のCOS(コサイン)です。左の円の中心から矢印を引いた時に、矢印と円の交差したところからX軸に線を引いたところの値がCOSの値になります。

オレンジ色の太い線です。2つのベクトルでこのCOSの値を求めます。似たような値の時はCOSの値は大きくなりますが、向きや大きさが違うときは小さくなります。このようなCOSの特徴をつかって、2つの値の類似度を計算して似た値を検索することができます

from langchain_openai import OpenAIEmbeddings
from langchain_community.utils.math import cosine_similarity

embeddings=OpenAIEmbeddings()
apple_vec=[embeddings.embed_query("apple はおいしいですね")]
words = ["apple", "orange", "bag", "リンゴ", "コンピューター"]
vecs = [[embeddings.embed_query(w)] for w in words]
scores = [cosine_similarity(apple_vec, v) for v in vecs]
for w, s in zip(words, scores):
    print(f"{w}\t:{s}")

出力は以下のようになりました。

apple	:[[0.87618095]]
orange	:[[0.7854982]]
bag	:[[0.76191926]]
リンゴ	:[[0.7365006]]
コンピューター	:[[0.7310821]]

“apple taste good!”という文字列のベクトルをapple_vecに格納しています。そのあと5つの単語のベクトルを求め、cosine_similarity関数を使って、それぞれとapple_vecの類似度を求めています。appleが一番近い値になっています。日本語の”リンゴ”は”bag”よりも低い類似度となりました。

最初の文章を「”apple はおいしいですね”」のように日本語を含めると、”リンゴ”の類似度は0.82とかなり大きくなりました。また、「”appleはiPadが人気です”」とすると、”コンピューター”の類似度は0.80と向上しました。文章や単語を変えながら類似度をみてみると興味深い結果になるかもしれません。

ともあれ、ここでは以下の内容を押さえておいてください。

  • 全ての単語や文章をベクトルという数値のリストとして処理している
  • それらの類似度をベクトルの計算で求めることで、関連するテキストを検索できる

ファイルへの保存

先ほどの例では、以下のように文書データからベクトルストアを作成しました。

vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())

このように都度from_documentsをつかってベクトルストアを構築する方法もありますが、文書をベクトル化する処理はそれなりの負担があります。なんども検索を利用するのであれば、ベクトル化した内容をファイルに保存しておき、ベクトルストアを利用するときには、ファイルから再構築する方が効率的です。

以下はベクトルストアを作って、ファイルに保存するサンプルです。

from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain.docstore.document import Document

words = ["apple", "elephant", "curry", "jazz"]
docs = [Document(page_content=word) for word in words]
embeddings=OpenAIEmbeddings()

# save to disk
chroma_db = Chroma.from_documents(docs, embeddings, persist_directory="./chroma_db")
r = chroma_db.similarity_search("food", k=2)
print(r)

実行結果です。foodに近い単語が2つ検索されています。

[Document(page_content='apple'), Document(page_content='curry')]

実行時のフォルダに”chroma_db”というフォルダが作成されていることを確認しておいてください。

以下は”chroma_db”というフォルダからベクトルストアを読み込むサンプルです。wordsやdocsという変数はありませんが、上記と同じ結果が得られます。必要な情報はすべてベクトルストアに保存されているためです。

from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

embeddings=OpenAIEmbeddings()

# load from disk
chroma_db = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
r = chroma_db.similarity_search("food", k=2)
print(r)

FAISS

Chromaはオープンソースのベクトルデータベースです。Chromaと同じようによく利用されるベクトルストアに、Meta(Facebook)製のFAISSがあります。

LangChainを使うと、FAISSも同じように利用することができます。

FAISSは以下のコマンドでインストールします。

py -m pip install faiss-cpu

GPUを使ったより高速なバージョンも提供されています。

以下は、4つの単語をFAISSに保存して、検索するサンプルです。先ほどChromaで実行したときとほとんど同じ内容になっている点に注目してください。

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

words = ["apple", "elephant", "curry", "jazz"]
docs = [Document(page_content=word) for word in words]
embeddings=OpenAIEmbeddings()

faiss_db = FAISS.from_documents(docs, embeddings)
r = faiss_db.similarity_search("food", k=2)
print(r)

ChromaとFAISSは別の製品です。直接呼び出す場合には、プログラムがそれぞれ専用の命令を記述する必要があります。LangChainを使うとベクトルストアの違いをほとんど意識することなく、使用することが可能です。これはLangChainが、それらベクトルストアの違いを吸収してくれているためです。

以下はPDFから就業規則を読み込んで、それに応答するプログラムのサンプルです。

PDFを読み込むため以下のモジュールをインストールしてください。

py -m pip install pypdf

PDFファイルは厚生労働省からダウンロードしました。URLは以下の通りです。

https://www.mhlw.go.jp/file/06-Seisakujouhou-11200000-Roudoukijunkyoku/0000118951.pdf

ダウンロードしたPDFはshugyo-kisoku.pdfというファイル名でdataフォルダに格納しました。以下はPDFを読み込んでFAISS_DBに保存するプログラムです。

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("data/shugyo-kisoku.pdf")
pages = loader.load_and_split()

faiss_db = FAISS.from_documents(pages, OpenAIEmbeddings())
faiss_db.save_local("./faiss_db")

実行すると、カレントフォルダにfaiss_dbというフォルダが作成されていることが確認できます。

以下は上記で保存したFAISS_DBから読み込んで質問に答えてみます。

faiss_db = FAISS.load_local("./faiss_db", embeddings, allow_dangerous_deserialization=True)
results = faiss_db.similarity_search("パートタイム勤務で有給はとれますか", k=2)
for r in results:
    print(str(r.metadata["page"]) + ":", r.page_content[:100])

以下のような結果が得られました。質問「”パートタイム勤務で有給はとれますか”」に関連が近い文章が検索されていることがわかります。

4: 5 
 第2項中の「無給/通常の賃金を支払うこと」の部分)には、あらかじめ数字や文言を記
入しているものがありますが、これらは規程例の内容を分かりやすく解説するために便宜
的に記入したものですので、こ
10: - 11 - 
 ます。パートタイム労働者等について、規程の一部を適用除外とする場合や全面的に適
用除外とする場合には、就業規則本体にその旨明記し、パートタイム労働者等に適用さ
れる規定を設けたり、別

この段階では、類似した文章を検索しただけです。生成AIは利用していません。文書が検索できるようになったら、生成AIと組み合わせてRAGを構築することができます。

以下はStreamlitを使ったRAGアプリのサンプルです。就業規則を格納したベクトルストアから関連する情報を取得しながら、生成AIを使ってユーザーからの質問に答えます。

import streamlit as st
from langchain import hub
from langchain_openai import OpenAIEmbeddings
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import FAISS

embeddings=OpenAIEmbeddings()

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

@st.cache_resource
def get_chain():
    faiss_db = FAISS.load_local("./faiss_db", embeddings, allow_dangerous_deserialization=True)
    retriever = faiss_db.as_retriever()

    llm = ChatOpenAI(model="gpt-4o")
    prompt = hub.pull("rlm/rag-prompt")
 
    rag_chain = (
        {"context": retriever | format_docs, "question": RunnablePassthrough()}
        | prompt
        | llm
        | StrOutputParser()
    )
    return rag_chain

st.title("就業規則Q&A")

text = st.text_input("質問をどうぞ")
if st.button("質問"):
    chain = get_chain()
    r = chain.invoke(text)
    st.write(r)
rag7
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 17

Pinecone

Chroma、FAISSと紹介してきました。これらはローカルのPC上で動作していました。

Pinecone( https://www.pinecone.io/ )はクラウドのVectorStoreサービスです。データベース1つだけであれば現在は無料で利用できます。サインアップ・ログインをしてください。プロジェクトの選択を促されたら、Starter (Free Tier)を選択します。画面左にあるIndexesメニューからCreate Indexでデータベースの作成に進んでください。

rag8
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 18

データベースを作成するときに必要な情報、名前やインデックスの次元を入力します。今回は”bunko”というインデックス名前で、1536という次元を指定しました。Pod Typeはstarterを指定します。

rag9
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 19

アクセスに必要なAPIキーとEnvironmnetは画面左のAPI Keysメニューから参照します。

rag10
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 20

これでPineconeが使用できるようになりました。今回はLangChain経由でPineconeを使用します。以下はLangChainを使わずにベクトルストアを作成するサンプルです。実行する必要はありません。「LangChainを使わない場合はこんな感じなんだ」ということを見ていただければ十分です。

from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(
    api_key="003560c1-e644-4319-a746-d8739809747b"
)

# Now do stuff
if 'bunko' not in pc.list_indexes().names():
    pc.create_index(
        name='bunko', 
        dimension=1536, 
        metric='cosine',
        spec=ServerlessSpec(
            cloud='aws',
            region='us-east-1'
        )
    )

LangChainからpineconeを使用するために以下のモジュールをインストールします。

py -m pip install -U langchain-pinecone

以下はLangChainからPineConeのベクトルストアを作成するコードです。PineConeのサイトから取得したAPIキーと、インデクス名を指定します。ベクトル化するためのEmbeddingにはいつものOpenAIEmbeddingsを使用しています。

from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore

embedding=OpenAIEmbeddings()
vectorstore = PineconeVectorStore(embedding=embeddings, 
    pinecone_api_key="003560c1-e644-4319-a746-d8739809747b",
    index_name="bunko")

以下はクラウド上のPineconeにドキュメントを登録するコードです。今回はdataフォルダのしたに、青空文庫からダウンロードした「人間失格」のテキストをおいておきました。

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

docs = TextLoader('./data/shikkaku.txt', encoding="utf-8").load()
text_splitter = CharacterTextSplitter(separator=" ")
chunks = text_splitter.split_documents(docs)
vectorstore.add_documents(chunks)

実行が終わって、Pineconeのサイトを見ると、ローカルにあったテキスト(人間失格:shikkaku.txt)が分割されてアップロードされている様子を確認できます。

rag11
LangChain入門(4) – RAG - 情報を与えて精度の良い解答を得る 21

Pineconeからデータを検索してみましょう。

from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore
embedding=OpenAIEmbeddings()
vectorstore = PineconeVectorStore(embedding =embeddings, 
    pinecone_api_key="003560c1-e644-4319-a746-d8739809747b",
    index_name="bunko")

vectorstore.similarity_search("電車通りの薬屋で購入したものは?", k=1)

今回以下のような応答がえられました。

[Document(page_content='これは、造血剤。\n\u3000これは、ヴィタミンの注射液。注射器は、これ。\n\u3000これは、カルシウムの錠剤。胃腸をこわさないように、ジアスターゼ。\n\u3000これは...

質問と関連しそうな箇所の文章が得られていることが確認できます。

Future Coders

Future Codersではほかにも多くの独自教材を用意しています。少人数個別指導・リモート対応でレッスンを行っています。レッスン以外にも出張授業やコンサルタントも行っております。興味のある方はお気軽にお問い合わせください。