RAGを使った自前Chatbot構築入門:社内ドキュメントを賢く活用する方法
導入
昨今、大規模言語モデル(LLM)を用いたチャットボットが注目される中で、ChatGPTやBing、Bardなどの汎用モデルに加え、組織特有のナレッジを反映した「自社用チャットボット」を構築したい場面が増えています。ここで最近話題の実装方法としてRAG(Retrieval-Augmented Generation)が注目されています。
RAGは、LLMによる回答生成の前に、外部データを検索・参照するステップを組み込むことで、モデルが与えられた独自データを最新情報として活用し、根拠を明示した回答を可能にします。これにより、例えば社内ドキュメント、製品マニュアル、社内ナレッジベースなど、ChatGPTなどのLLMではカバーできない固有情報を直接参照しながら質問に回答する“自社用チャットボット”を実現できます。
本記事では、RAGの基本的な実装例として、Google Drive上のドキュメントを検索して参照しつつ回答する簡易なChatbotを紹介します。StreamlitでUIを作成した、OpenAIのLLM API、Embeddingsによる類似度検索を組み合わせることで、最小限のコードで独自ドキュメントを活用したQ&A環境を構築します。
実際に動かしてみた
全体像
フロー
- Google Drive APIを使って特定フォルダ内のGoogle Docsを取得し、テキストを抽出。
- 抽出したテキストをOpenAIのEmbeddings APIでベクトル化して保持。
- ユーザーが入力した質問もEmbeddingし、事前に用意したドキュメント群のEmbeddingと類似度比較。
- 質問に最も関連度の高いドキュメント(上位N件)を特定。
- 「この関連ドキュメントのみを参照せよ」という指示をLLMに与え、その範囲内で回答を生成。
- 回答と参照ドキュメントへのリンクをユーザーに提示。
フローを図示したシーケンス図
こうしたRAGフローにより、ユーザーは「どの文書を根拠に回答したのか」も確認可能となり、回答の透明性と信頼性が向上します。
使用技術
-
LLM
- OpenAI API(今回は
gpt-4o-mini
を使用)
- OpenAI API(今回は
-
UI
- Streamlit
-
データ取得
- Google Drive APIを用いて指定フォルダ内のGoogle Documentの一覧を抽出
-
Embeddingと類似検索
- OpenAIのEmbeddings API + cosine類似度計算
ファイル構成
-
app.py
- メイン。StreamlitによるUI、質問受付、回答表示など。
-
utils.py
- ドキュメント取得・Embedding・類似度計算・LLMへの問い合わせ関数。
-
config.yml
- APIキーやサービスアカウントファイル、DriveフォルダIDなどの設定ファイル。
コード全文
以下に本実装例の全コードを掲載します。
app.py
import streamlit as st
import yaml
from utils import vectorize_text, find_most_similar, ask_question, get_docs_list
# 設定ファイルを読み込む
with open('config.yml', 'r') as file:
config = yaml.safe_load(file)
def main():
st.title('Document Search Chatbot')
# ドキュメントとそのベクトルを取得
docs = get_docs_list(config['google_drive']['folder_id'])
contents = [doc['content'] for doc in docs]
vectors = [vectorize_text(content) for content in contents]
# チャット履歴の初期化
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"])
# ユーザーの入力を処理
if user_input := st.chat_input('メッセージを入力してください:'):
st.session_state.messages.append({"role": "user", "content": user_input})
question_vector = vectorize_text(user_input)
similar_documents = find_most_similar(question_vector, vectors, contents)
answer = ask_question(user_input, similar_documents)
response = f"{answer}\n\n参照したドキュメント:\n"
for doc in docs:
if doc['content'] in similar_documents:
response += f"- [{doc['name']}]({doc['url']})\n"
st.session_state.messages.append({"role": "assistant", "content": response})
st.rerun()
if __name__ == "__main__":
main()
utils.py
import yaml
from googleapiclient.discovery import build
from google.oauth2.service_account import Credentials
from openai import OpenAI
from sklearn.metrics.pairwise import cosine_similarity
with open('config.yml', 'r') as file:
config = yaml.safe_load(file)
client = OpenAI(api_key=config['openai']['api_key'])
credentials = Credentials.from_service_account_file(config['google']['service_account_file'])
google_client = build('drive', 'v3', credentials=credentials)
def get_docs_list(folder_id):
files = google_client.files().list(
q=f"'{folder_id}' in parents and mimeType='application/vnd.google-apps.document'",
fields="files(id, name)"
).execute().get('files', [])
docs_list = []
for file in files:
doc_id = file['id']
doc_content = google_client.files().export(fileId=doc_id, mimeType='text/plain').execute()
docs_list.append({
'name': file['name'],
'url': f"https://docs.google.com/document/d/{doc_id}/edit",
'content': doc_content.decode('utf-8')
})
return docs_list
def vectorize_text(text):
response = client.embeddings.create(
input=text,
model=config['openai']['embedding_model']
)
return response.data[0].embedding
def find_most_similar(question_vector, vectors, documents):
similarities = []
for index, vector in enumerate(vectors):
similarity = cosine_similarity([question_vector], [vector])[0][0]
similarities.append([similarity, index])
similarities.sort(reverse=True, key=lambda x: x[0])
top_documents = [documents[index] for similarity, index in similarities[:2]]
return top_documents
def ask_question(question, context):
messages = [
{"role": "system", "content": "以下の情報のみを使用して回答してください。"},
{"role": "user", "content": f"質問: {question}\n\n情報: {context}"}
]
response = client.chat.completions.create(
model=config['openai']['chat_model'],
messages=messages
)
return response.choices[0].message.content
config.yml
# OpenAI設定
openai:
api_key: 'OpenAIのAPIキーを記載'
embedding_model: 'text-embedding-3-large'
chat_model: 'gpt-4o-mini'
# Google設定
google:
service_account_file: 'GCPのサービスアカウントキーを記載'
# Google Drive設定
google_drive:
folder_id: 'ドキュメントを格納したGoogleDriveフォルダIDを記載'
このコードのカスタマイズポイント
-
チャンク分割
- 長いドキュメントは事前に小さめの塊に分割してEmbeddingし、検索精度を高める。
-
ベクトルDB活用
- ベクトルデータベースを導入して大量のドキュメントにも対応可能に。
-
Cloud Storageなどの活用
- 毎回Drive APIを叩くので、送信までの時間がかかってしまっていますが、storageを活用することで送信までの時間を短縮できることが見込めます。
まとめとGitHubリンク
RAGを用いることで、LLMが自前で用意したドキュメントを元に回答を生成し、回答のソースを表示することが可能になります。上記のコードはシンプルなサンプルですが、組織ごとのニーズに合わせてカスタマイズ・拡張することで、社内FAQ、マニュアルガイド、顧客サポート向けAIアシスタントなど、多岐にわたるユースケースに応用できます。
自分の所属する企業では、Slackと組み合わせてSlackChatBotを作成しました。
↓ GitHubリポジトリとしても公開しておりますので、興味のある方はぜひご覧ください!
Discussion