🔖

RAGを使った自前Chatbot構築入門:社内ドキュメントを賢く活用する方法

2024/12/13に公開

導入

昨今、大規模言語モデル(LLM)を用いたチャットボットが注目される中で、ChatGPTやBing、Bardなどの汎用モデルに加え、組織特有のナレッジを反映した「自社用チャットボット」を構築したい場面が増えています。ここで最近話題の実装方法としてRAG(Retrieval-Augmented Generation)が注目されています。

RAGは、LLMによる回答生成の前に、外部データを検索・参照するステップを組み込むことで、モデルが与えられた独自データを最新情報として活用し、根拠を明示した回答を可能にします。これにより、例えば社内ドキュメント、製品マニュアル、社内ナレッジベースなど、ChatGPTなどのLLMではカバーできない固有情報を直接参照しながら質問に回答する“自社用チャットボット”を実現できます。

本記事では、RAGの基本的な実装例として、Google Drive上のドキュメントを検索して参照しつつ回答する簡易なChatbotを紹介します。StreamlitでUIを作成した、OpenAIのLLM API、Embeddingsによる類似度検索を組み合わせることで、最小限のコードで独自ドキュメントを活用したQ&A環境を構築します。

実際に動かしてみた

全体像

フロー

  1. Google Drive APIを使って特定フォルダ内のGoogle Docsを取得し、テキストを抽出。
  2. 抽出したテキストをOpenAIのEmbeddings APIでベクトル化して保持。
  3. ユーザーが入力した質問もEmbeddingし、事前に用意したドキュメント群のEmbeddingと類似度比較。
  4. 質問に最も関連度の高いドキュメント(上位N件)を特定。
  5. 「この関連ドキュメントのみを参照せよ」という指示をLLMに与え、その範囲内で回答を生成。
  6. 回答と参照ドキュメントへのリンクをユーザーに提示。

フローを図示したシーケンス図

こうしたRAGフローにより、ユーザーは「どの文書を根拠に回答したのか」も確認可能となり、回答の透明性と信頼性が向上します。

使用技術

  • LLM
    • OpenAI API(今回はgpt-4o-miniを使用)
  • 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リポジトリとしても公開しておりますので、興味のある方はぜひご覧ください!

GitHubリポジトリはこちら

Discussion