RAG機能付きチャットボットを作ろう-6_VectorDBの設定
TL;DR
前回の記事では、チャット履歴をMarkdown形式で表示しました。本稿では
- VectorDBの設定
- PDFのベクトル化
を行います。
準備
インストール
VectorStoreとしてChromaDBを使います。以下でインストールします。
pip install chromadb
pip install langchain-chroma
PDFの格納フォルダ作成
文書情報はPDFで格納している方が多いと思いますので、本稿ではPDFの内容をベクトル化して、検索できるようにします。
まずは、PDFを格納するフォルダを作成します。
フォルダ名は自由ですが、本稿ではpdf_docs
とします。
また、その中にサンプルのPDFファイルを格納します。ファイル名はgpt_review.pdf
とします。
gpt_review.pdf
にはChatGPTとは!?ChatGPTはOpenAIの開発した生成AIです。
という文章を記載しています。
.
└── streamlit/
├── main.py
└── .env
└── pdf_docs/
└── gpt_review.pdf
実装
主な変更点
主な変更点は以下です。
- ベクトルDBの設定
- PDFのベクトル化の関数の作成
- ベクトルDBへの検索のためのretrieverの設定
コード
import streamlit as st
from openai import OpenAI
from dotenv import load_dotenv
import os
import chromadb
from langchain_chroma import Chroma
import openai
from pydantic import BaseModel
from typing import List
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import PyMuPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
import glob
# .envファイルから環境変数を読み込む
load_dotenv(".env")
# OpenAIのAPIクライアントを初期化
client = OpenAI(
api_key=os.environ['OPENAI_API_KEY']
)
# text splitterの定義
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=100,
length_function=len,
separators=["\n", " ", ".", ",", ";", ":", "(", ")", "[", "]", "{", "}", "<", ">", '"', "'", "、", "。", ",", ";", ":", "(", ")", "【", "】", "「", "」", "『", "』", "〈", "〉", "《", "》", "“", "”"],
is_separator_regex=False,
)
# ベクトルDBのためのEmbeddingsの定義
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# Chromaの初期化
vector_store = Chroma(
collection_name="example_collection",
embedding_function=embeddings,
persist_directory="./chroma_langchain_db", # ベクトルDBの保存先
)
def pdf_to_vector():
# 特定フォルダのpdfのリスト化
path = "./pdf_docs"
# pdf_list_stored.txt を読み込んで、pdf_list に格納
pdf_list = []
if os.path.exists("pdf_list_stored.txt"):
with open("pdf_list_stored.txt", "r") as f:
for line in f:
pdf_list.append(line.strip())
else:
# pdf_list_stored.txt が存在しない場合は、ファイルを作成
with open("pdf_list_stored.txt", "w") as f:
pass
# globを使ってファイル名のリストを取得
# サブフォルダも含める場合は、"**/*.pdf"とする
pdf_list_current = glob.glob(path + "/**.pdf")
pdf_list_new = list(set(pdf_list_current) - set(pdf_list))
docs = []
for pdf in pdf_list_new:
loader = PyMuPDFLoader(pdf)
text = loader.load_and_split(text_splitter)
docs.append(text)
# Vector storeに保存
vector_store.add_documents(text)
pdf_list.append(pdf)
# pdf_list を pdf_list_stored.txt に保存
with open("pdf_list_stored.txt", "w") as f:
for pdf in pdf_list:
f.write(pdf + "\n")
## ベクトル検索のretireverの定義
retriever = vector_store.as_retriever(
search_type="mmr", search_kwargs={"k": 1, "fetch_k": 10}
)
# プロンプトを入力すると、チャットボットが返答を返す関数を定義
# 入力はOpenAIのAPIクライアントとプロンプト
def default_chat(client, prompt):
response = client.chat.completions.create(
model="gpt-4o-mini", # 好きなモデルを選択
messages=[
{"role": "system", "content": "You are AI assistant."},
{"role": "user", "content": prompt}
]
)
return response.choices[0].message.content
# streamlitのsession_stateにチャット履歴を保存する
# もしチャット履歴がなければ、空のリストを作成
if 'chat_history' not in st.session_state:
st.session_state.chat_history = []
# チャット履歴を表示する関数
def display_chat_history():
for chat in st.session_state.chat_history:
if chat["role"] == "user":
st.markdown(
# 背景をグレーにして、角を丸くする
f'<div style="background-color: #f0f0f0; border-radius: 10px; padding: 10px;">'
f"ユーザー: {chat['content']}"
'</div>', unsafe_allow_html=True)
else:
st.markdown(
# 背景を青にして、角を丸くする
f'<div style="background-color: #cfe2ff; border-radius: 10px; padding: 10px;">'
f"チャットボット: {chat['content']}"
'</div>', unsafe_allow_html=True)
st.title('RAG機能付きチャットボットを作ろう')
st.write('streamlitを使ったUIの作成')
# チャット履歴を表示
display_chat_history()
prompt = st.text_area('プロンプト入力欄', )
button1, button2, button3 = st.columns(3)
if button1.button('チャット'):
chat_response = default_chat(client, prompt)
# チャット履歴に追加
# ユーザーの入力を追加、roleはuser
st.session_state.chat_history.append({"role": "user", "content": prompt})
# チャットボットの返答を追加、roleはsystem
st.session_state.chat_history.append({"role": "system", "content": chat_response})
st.rerun()
if button2.button('RAG'):
retriever_response = retriever.invoke(prompt)
st.session_state.chat_history.append({"role": "user", "content": prompt})
st.session_state.chat_history.append({"role": "system",
"content": retriever_response[0].page_content})
st.write(retriever_response)
st.rerun()
if button3.button('PDFをベクトル化'):
pdf_to_vector()
st.rerun()
かなり長くなりました。一つずつ確認していきましょう。
Text Splitterの定義
ここではlangchainのRecursiveCharacterTextSplitter
を使って、テキストを分割します。
今回のPDFの中身は少ないので正直不要ですが、通常のPDFは複数ページにまたがることが多いので、分割しておくと便利です。
# text splitterの定義
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=100,
length_function=len,
separators=["\n", " ", ".", ",", ";", ":", "(", ")", "[", "]", "{", "}", "<", ">", '"', "'", "、", "。", ",", ";", ":", "(", ")", "【", "】", "「", "」", "『", "』", "〈", "〉", "《", "》", "“", "”"],
is_separator_regex=False,
)
Embeddingsの定義
Embeddingとは文章をベクトル化することで、ここではOpenAIのEmbeddingsを使います。
複数のモデルが出ているので、好きなモデルを選択してください。
# ベクトルDBのためのEmbeddingsの定義
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
Chromaの初期化
すこしややこしいのですが、本稿ではchromadb
とlangchainのChroma
を使っています。
ここではChroma
の方を使っています。
./chroma_langchain_db
のフォルダを作成して、ベクトルDBを保存します。
バージョンによっては、persist_directory
が別の表記になっているかもしれませんので、公式ドキュメントを参照してください。執筆時点で使用しているバージョンは langchain-Chroma 0.1.2 です。
# Chromaの初期化
vector_store = Chroma(
collection_name="example_collection",
embedding_function=embeddings,
persist_directory="./chroma_langchain_db",
)
PDFのベクトル化
streamlitでは起動のたびにすべての処理が実行されるので、
PDFのベクトル化は「PDFをベクトル化」ボタンを押すと実行されるようにしています。
PDFの重複を避けるために、pdf_list_stored.txt
に処理したPDFのリストを保存しています。
def pdf_to_vector():
# 特定フォルダのpdfのリスト化
path = "./pdf_docs"
# pdf_list_stored.txt を読み込んで、pdf_list に格納
pdf_list = []
if os.path.exists("pdf_list_stored.txt"):
with open("pdf_list_stored.txt", "r") as f:
for line in f:
pdf_list.append(line.strip())
else:
# pdf_list_stored.txt が存在しない場合は、ファイルを作成
with open("pdf_list_stored.txt", "w") as f:
pass
# globを使ってファイル名のリストを取得
# サブフォルダも含める場合は、"**/*.pdf"とする
pdf_list_current = glob.glob(path + "/**.pdf")
pdf_list_new = list(set(pdf_list_current) - set(pdf_list))
docs = []
for pdf in pdf_list_new:
loader = PyMuPDFLoader(pdf)
text = loader.load_and_split(text_splitter)
docs.append(text)
# Vector storeに保存
vector_store.add_documents(text)
pdf_list.append(pdf)
# pdf_list を pdf_list_stored.txt に保存
with open("pdf_list_stored.txt", "w") as f:
for pdf in pdf_list:
f.write(pdf + "\n")
この関数を以下のボタンを押すと実行されるようにしています。
if button3.button('PDFをベクトル化'):
pdf_to_vector()
st.rerun()
ベクトル検索のretireverの定義
ベクトルDBへの検索のためのretrieverを定義します。
kは返す文書の数、fetch_kは取得する文書の数です。
search_typeは検索の方法を指定します。ここではmmrを使っています。mmrを使うと、クエリに類似していて、かつ多様性のある文書を返します。
(この例ではpdfが一つなので意味はありませんが。)
retriever = vector_store.as_retriever(
search_type="mmr", search_kwargs={"k": 1, "fetch_k": 10}
)
検索ボタンの処理
RAGの検索ボタンを押すと、retrieverを使って検索を行います。
なお、この例では類似文書の中身を取得しているだけなので、実際にはRAGにはなっていません。
if button2.button('RAG'):
retriever_response = retriever.invoke(prompt)
st.session_state.chat_history.append({"role": "user", "content": prompt})
st.session_state.chat_history.append({"role": "system",
"content": retriever_response[0].page_content})
st.write(retriever_response)
st.rerun()
では、
streamlit run main.py
を実行して、動作を確認してみましょう。
まずはこのような画面が表示されます。PDFをベクトル化ボタンを押して、ベクトル化を行います。(先にPDFを格納しておいてください。)
次に、「ChatGPTとは?」と入力して、RAGボタンを押すと、以下のような画面が表示されたら成功です。
なお、チャットボタンを押したときとの違いを確認してみてください。
リンク
- RAG機能付きチャットボットを作ろう-1:
https://zenn.dev/bluetang/articles/chatbot_with_lc_st_chromadb_01 - RAG機能付きチャットボットを作ろう-2:
https://zenn.dev/bluetang/articles/chatbot_with_lc_st_chromadb_02 - RAG機能付きチャットボットを作ろう-3:
https://zenn.dev/bluetang/articles/chatbot_with_lc_st_chromadb_03 - RAG機能付きチャットボットを作ろう-4:
https://zenn.dev/bluetang/articles/chatbot_with_lc_st_chromadb_04 - RAG機能付きチャットボットを作ろう-5:
https://zenn.dev/bluetang/articles/chatbot_with_lc_st_chromadb_05 - RAG機能付きチャットボットを作ろう-6:
https://zenn.dev/bluetang/articles/chatbot_with_lc_st_chromadb_06 - RAG機能付きチャットボットを作ろう-7: (完成版)
https://zenn.dev/bluetang/articles/chatbot_with_lc_st_chromadb_07 - RAG機能付きチャットボットを作ろう-8: (Structured Outputを追加)
https://zenn.dev/bluetang/articles/chatbot_with_lc_st_chromadb_08
Discussion