【生成AI】チャットWeb化について

オリジナルRAGのWebアプリ化

今回は、前回「オリジナルRAG開発」にて生成したデータベースを利用して、Webアプリ化を実装していきます。

アプリ化する為に一連のコードをmain処理として下記のコード化を行い、app.pyとして保存します。

from langchain.chains import RetrievalQA
from langchain.schema import (SystemMessage, HumanMessage, AIMessage)
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
import os
import streamlit as st
from langchain.prompts import ChatPromptTemplate
from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)
from langchain.prompts import PromptTemplate

def load_db(embeddings):
    return FAISS.load_local('faiss_store', embeddings, allow_dangerous_deserialization=True)


def init_page():
    st.set_page_config(
        page_title='オリジナルチャットボット',
        page_icon='🧑‍💻',
    )


def main():
    embeddings = GoogleGenerativeAIEmbeddings(
        model="models/embedding-001"
    )
    db = load_db(embeddings)
    init_page()

    llm = ChatGoogleGenerativeAI(
        model="gemini-1.5-flash",
        temperature=0.0,
        max_retries=2,
    )

    # オリジナルのSystem Instructionを定義する
    prompt_template = """
    あなたは、「神風」サイトの岡田茂吉論文のチャットボットです。
    背景情報を参考に、質問に対して岡田茂吉になりきって、質問に回答してくだい。
    
    岡田茂吉論文に全く関係のない質問と思われる質問に関しては、「岡田茂吉論文に関係することについて聞いてください」と答えてください。
    
    以下の背景情報を参照してください。情報がなければ、その内容については言及しないでください。
    # 背景情報
    {context}
    
    # 質問
    {question}"""
    
    PROMPT = PromptTemplate(
        template=prompt_template, input_variables=["context", "question"]
    )
    qa = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=db.as_retriever(search_kwargs={"k": 2}),
        return_source_documents=True,
        chain_type_kwargs={"prompt": PROMPT}# システムプロンプトを追加
    )

    if "messages" not in st.session_state:
      st.session_state.messages = []
    if user_input := st.chat_input('質問しよう!'):
        # 以前のチャットログを表示
        for message in st.session_state.messages:
            with st.chat_message(message["role"]):
                st.markdown(message["content"])
        print(user_input)
        with st.chat_message('user'):
            st.markdown(user_input)
        st.session_state.messages.append({"role": "user", "content": user_input})
        with st.chat_message('assistant'):
            with st.spinner('Gemini is typing ...'):
                response = qa.invoke(user_input)
            st.markdown(response['result'])
            #参考元を表示
            doc_urls = []
            for doc in response["source_documents"]:
                #既に出力したのは、出力しない
                if doc.metadata["source_url"] not in doc_urls:
                    doc_urls.append(doc.metadata["source_url"])
                    st.markdown(f"参考元:{doc.metadata['source_url']}")
        st.session_state.messages.append({"role": "assistant", "content": response["result"]})


if __name__ == "__main__":
  main()

Webアプリをデプロイする

次にWebアプリをデプロイする方法を解説します。作成したチャットボットを実際にWebアプリとして公開するには、Streamlit Cloudを使用して簡単にデプロイが可能です。

①GitHubにコードとデータをアップロードする

作成したアプリケーションと前記事で生成されたデータベースをGitHubのリポジトリへアップロードします。

※GoogleColabで実行した場合は、データベース(faiss_store)とapp.pyアプリケーションファイルをダウンロードしリポジトリへアップロードします。

requirements.txtというファイルを作ってリポジトリ内に配置します。

streamlit
langchain
langchain_google_genai
faiss-cpu
langchain-community
├── app.py                     # Streamlitアプリケーションのメインファイル
├── faiss_store/               # FAISSのベクトルストアデータ
│   ├── index.faiss            # ベクトルデータを格納したFAISSインデックスファイル
│   ├── index.pkl             # メタデータを格納したピクルファイル(ドキュメント情報)
├── requirements.txt           # 必要なライブラリ一覧
└── README.md                  # プロジェクトの概要説明(任意)

※構造は上記のようになります。

②Streamlit Cloudへ接続する

次に、Streamlit Cloudにアクセスして、GitHubのアカウントでサインインします。※プランはFreeプランを選択します

GitHubで続行を選択し、Streamlit CloudへGitHub内へのアクセス権限を付与します

③Streamlit Cloudへアプリ登録する

接続完了後、GitHub内のapp.pyアプリケーションをアプリとして登録します

「Deploy a public app from GitHub」を選択し、アプリ設定を行います

  • Repository:app.pyのあるリポジトリ
  • Branch:対象のブランチ
  • Main file path:対象のアプリケーションファイル
  • App URL:Webアプリの公開URL ※適時修正可能

④詳細設定にてGoogle API Keyを設定する

llmへの接続用としてAdvanced settingにてGoogle API Keyを設定します。作成してしまった後からでも登録可能です。

※作成後に設定する場合には、My Appsの一覧右メニュー「Settings」より設定可能です。設定したURLからだれでもそのチャットボットを使うことができるようになります。

※変更もGitHubにあげている内容を変更して、以下の画面でRebootを押すだけで変更できます。

⑤アプリURLへ接続する

API Keyが設定できた段階で公開アプリは完成です

まとめ

今回の記事では、Streamlitを使ってWebアプリとしてデプロイするまでの一連の流れを解説しました。前記事よりAI作成~公開まで、全体のプロセスを体験することができたと思います。

RAGは、シンプルなFAQシステムを超えて、ユーザーの質問に対してより自然で具体的な応答を提供する強力なツールです。特に、特定のドメインに関連する情報を提供する場合に非常に有効です。

改善していくためには、データの追加やより高度なプロンプト設計の調整を試みてください。他のAIモデルや検索エンジンと組み合わせることで、より多機能なアプリケーションの構築も視野に入れられるでしょう。

ぜひ今後も、さまざまなユースケースに応用して、新たな挑戦に取り組んでみてください。

参考サイト

Qiita:完全無料で、オリジナルRAGのWebアプリケーションのチャットボットを開発、デプロイまで

【生成AI】オリジナルRAG開発について

はじめに

本記事では、RAG(Retrieval-Augmented Generation)という技術を使って、生成AI系オリジナルチャットボットを作ります。本記事によりオリジナルのRAGチャットボットの作成基礎を学んで頂けます。

まずRAGってなに?という方もいるかもしれません。説明すると、RAGは「検索」と「生成」を組み合わせた技術です。通常、検索システムの場合、検索したいキーワードに基づいてリスト形式で結果が返されますが、RAGでは検索結果を元に、自然な形で情報を生成して回答を返してくれます。まるでAIが人間のように、質問に対して的確な答えを返してくれます。

①使用ツール・サービス

  • Python: おなじみのプログラミング言語です。今回のアプリケーション全体をPythonで書いています
  • Langchain: RAGのコア部分を担当するライブラリです。LLM(大規模言語モデル)を使って生成する部分をサポートしてくれます
  • FAISS: 検索を高速に処理するためのライブラリ。特に大量のデータから必要な情報を効率的に取り出すときに使います
  • Google Generative AI: AIによる生成部分を担当するAPIです。質問に対して、適切な応答を生成します。今回は無料枠内で使える範囲でこのサービスを利用しています

②APIキーを発行する

RAGでは、ChatGPTやGeminiなどのLLMのAPIをもちいて、実装しています。
今回は、Google Generative AIのAPIを利用して、問合せに対し自然な応答を生成します。まずGoogle Cloud Platform(GCP)からAPIキーを発行し、それをアプリケーション内で使用する必要があります。

まずGoogle Generative AI APIキーの取得方法とその使い方を説明します。※既に取得したことがある方はスキップしてください。

※APIキーを生成すると画面下にプロジェクト名とキーの一部が表示されます。

Google AI Studioにサインインした後、右上の「APIキーを作成」ボタンをクリックします。

※取得後画面下にAPIキーが表示されます。

開発準備:APIキーを環境変数に追加

本記事ではGoogleColabを使用した方法で開設します。

①画面左にある鍵マークを押します。

②新しいシークレットを追加を押す

※値の部分に取得したAPIキーを格納します。

③設定の確認

※コード部分に貼り付けて▶をクリックし、格納したキーが表示されればOKです。

データベースへ文章を格納する

今回はウェブサイトからデータを収集していきます。

※岡田茂吉氏論文サイト「神風(sinpu.net)」サイトより特定著述データで実装します

①ライブラリのインストール

#必要なライブラリをインストール
%pip install --upgrade --quiet langchain langchain-google-genai langchain-community faiss-cpu streamlit langchain-text-splitters beautifulsoup4

②必要なライブラリのインポート

# 必要なライブラリのインポート
import requests
from bs4 import BeautifulSoup
from langchain.schema import Document
from langchain.text_splitter import CharacterTextSplitter
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS

下記の役割になるライブラリをインポートします。

  • requests: Webページにアクセスして、HTMLデータを取得するためのライブラリ。
  • BeautifulSoup: 取得したHTMLデータを解析し、テキストを抽出するためのライブラリ。
  • Document: テキストデータを扱うためのオブジェクト形式
  • CharacterTextSplitter: 長いテキストを複数のチャンク(小さなブロック)に分割するためのクラス
  • GoogleGenerativeAIEmbeddings: Google Generative AIを使ってテキストデータをベクトル化(数値表現)するためのクラス
  • FAISS: ベクトル化したデータを効率的に検索するためのライブラリ

③スクレイピング対象のURLリストを定義

# スクレイピング対象のURLリスト
urls = [
    "https://example.com/page1", 
    "https://example.com/page2"
]

※urlsには、スクレイピングを行いたいWebページのURLをリスト形式で指定しています

④WebページのHTMLデータを取得して保存

# ファイル名とURLのペアを保存するリスト
fnames = []

# WebページにアクセスしてHTMLデータをローカルに保存
for url in urls:
    fname = f"{url[url.rfind('/', 0, -1) + 1:-1]}.html"
    fnames.append((fname, url))  # ファイル名とURLのタプルを保存
    response = requests.get(url)
    with open(fname, mode='w', encoding='utf-8') as fout:
        fout.write(response.text)

コードでは、指定されたURLからWebページのHTMLデータを取得して、ローカルファイルに保存しています

  • requests.get(url): 指定したURLのWebページの内容を取得します
  • fname: URLからファイル名を作成して、保存時に使います
  • response.text: 取得したHTMLデータです
  • with open(): 取得したHTMLデータをファイルとして保存します


⑤テキストをチャンクに分割するための設定

# チャンクを分割するための設定
text_splitter = CharacterTextSplitter(
    separator='\n\n',  # 改行で分割
    chunk_size=500,  # 各チャンクのサイズ
    chunk_overlap=0,  # チャンク間の重複なし
    length_function=len  # 文字数でチャンクを計測
)

Webページから抽出した長いテキストを「チャンク」と呼ばれる小さなブロックに分割するための設定をしています

  • separator=’\n\n’: 2つの改行を基準にしてテキストを分割します。文章の段落が分かれる場所の基準です
  • chunk_size=500: 各チャンクの最大文字数は500文字です
  • chunk_overlap=0: チャンク同士が重複しない設定です
  • length_function=len: チャンクの長さを計測する関数です ※文字数を計測

※分け方で、検索による精度が変わりますので適時変更してください。

⑥Webページのテキストを取得してチャンクに分割

# 全てのチャンクを保持するリスト
all_chunks = []

# テキストを取得してチャンクに分割し、メタデータにURLを含める
for fname, url in fnames:
    with open(fname, 'r', encoding='utf-8') as file:
        html_content = file.read()

    soup = BeautifulSoup(html_content, 'html.parser')
    text = soup.get_text(separator="\n")

    # 抽出したテキストをチャンクに分割
    chunks = text_splitter.split_text(text)

    # 各チャンクをDocumentオブジェクトに変換してリストに追加
    for i, chunk in enumerate(chunks):
        # メタデータにURLを保存
        doc = Document(page_content=chunk, metadata={"source_url": url, "chunk_index": i})
        all_chunks.append(doc)

    print(f"Processed {fname}, {len(chunks)} chunks extracted.")

保存したHTMLファイルを開き、その中からテキストを抽出して、さらにチャンクに分割しています

  • BeautifulSoup(html_content, ‘html.parser’): HTMLデータを解析し、テキストデータを取り出します
  • text_splitter.split_text(text): 先ほど設定した条件に基づいてテキストをチャンクに分割します
  • Document: 各チャンクをDocumentオブジェクトに変換し、メタデータとしてsource_url(元のURL)を追加します
  • all_chunks.append(doc): すべてのチャンクをリストに保存します

⑦ベクトル化とベクトルストアへの保存

# Geminiに対応したベクトル変換を実施
embeddings = GoogleGenerativeAIEmbeddings(
    google_api_key=userdata.get('GEMINI_API_KEY'),# APIキーを指定
    model="models/embedding-001"
)

# ベクトルストアにチャンクを保存
db = FAISS.from_documents(all_chunks, embeddings)
db.save_local('faiss_store')

最後に、Google Generative AIを使ってテキストをベクトル化し、FAISSに保存しています

  • GoogleGenerativeAIEmbeddings: テキストをベクトルに変換するためのモデルです。google_api_keyにはGoogleのAPIキーを設定しています
  • FAISS.from_documents(all_chunks, embeddings): ベクトル化されたチャンクをFAISSに保存し、効率的に検索できるようにします
  • db.save_local(‘faiss_store’): ベクトルストアをローカルに保存します。この保存されたデータベースは、後で検索や質問応答の処理に利用されます

※データベースへ文章を格納するのが完了です

チャットボットを作る

引き続き、GoogleColabを用いて行う前提ですすめます。自分の環境で行う場合は適宜、環境変数にAPIキーを入れたり、データベースのパスをご自身のものに変えてください。

①必要なライブラリのインポート

from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

3つの重要なライブラリをインポートしています

  • ChatGoogleGenerativeAI: Google Generative AIを使って、自然言語応答を生成するためのライブラリです
  • RetrievalQA: 質問応答の仕組みを構築するためのクラスです。これは、データベースから関連情報を取得し、その情報を元に応答を生成します
  • PromptTemplate: モデルに対してどのような形式でプロンプト(入力)を与えるかを定義するためのクラスです

②Google Generative AIの設定

# Geminiモデルを定義する
llm = ChatGoogleGenerativeAI(
    google_api_key=userdata.get('GEMINI_API_KEY'),
    model="gemini-1.5-flash",
    temperature=0.0,
    max_retries=2,
)

Google Generative AIの「Geminiモデル」を使って、チャットボットがユーザーの質問に答えるように設定しています

  • google_api_key: 事前に発行したGoogle APIキーを設定します。ここでは、環境変数やファイルなどから取得しています
  • model: 使用するGenerative AIのモデルです。ここでは「gemini-1.5-flash」というモデルを使用しています
  • temperature: 応答の多様性をコントロールするパラメータです。0に設定しているので、決定論的な(毎回同じ結果を返す)応答を得ることができます
  • max_retries: 応答が失敗した際に再試行する回数を設定します

③プロンプトの設定

# オリジナルのSystem Instructionを定義する
prompt_template = """
あなたは、「神風」サイトの岡田茂吉論文のチャットボットです。
背景情報を参考に、質問に対して岡田茂吉になりきって、質問に回答してくだい。

岡田茂吉論文に全く関係のない質問と思われる質問に関しては、「岡田茂吉論文に関係することについて聞いてください」と答えてください。

以下の背景情報を参照してください。情報がなければ、その内容については言及しないでください。
# 背景情報
{context}

# 質問
{question}"""

チャットボットに対して、どのように応答を生成するかを指示する「プロンプト」を設定しています ※プロンプトを制御することで、精度がかなり変わってきます

{context}と{question}: プロンプト内にユーザーの質問と関連情報を挿入するためのプレースホルダです

④リトリーバーの設定

# リトリーバーが上位2つのドキュメントを返すように設定
retriever = db.as_retriever(search_kwargs={"k": 5})

リトリーバー(検索機能)が、データベースから関連するドキュメントを取得する設定を行っています

search_kwargs={“k”: 5}: kというパラメータにより、検索結果の上位5つのドキュメントを返すように設定しています。この場合、質問に対して最も関連性の高い5つのドキュメントが選ばれます

⑤質問応答システムの設定

qa = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True,
    chain_type_kwargs={"prompt": PROMPT}
)

実際に質問応答システムを作成しています

  • llm=llm: 先ほど定義したGoogle Generative AIモデルを使用します
  • retriever=retriever: データベースから関連ドキュメントを取得するリトリーバーを設定します
  • return_source_documents=True: 質問に対する応答を生成する際に、使用された元のドキュメントも返すようにしています
  • chain_type_kwargs={“prompt”: PROMPT}: チャットボットが応答を生成する際に使用するプロンプトを指定します

処理を実行する

query = "病気とは?"
generate_answer(query)

応答文: 病気とは、身体の浄化作用の停止です。

参照元のURL:

https://sinpu.net/akuno-hassei-to-yamai/

https://sinpu.net/kekkaku-to-seishinmen/

https://sinpu.net/eiyo/

https://sinpu.net/izunome-sin/

https://sinpu.net/kanzeon-bosatsu/

補足:無料枠が終了した場合の挙動

※上記のようにエラー表示され、retry_delay値により無料枠の数値内の待ち時間が指定されます。

ダッシュボードでの確認方法

Gemini API Usage項目のUsageタブ内に実際のトークン値とリクエスト値が表示されます。

まとめ

本記事では、RAG(Retrieval-Augmented Generation)という技術を活用したオリジナルチャットボットを作成手順について体験頂けたと思います。

RAGは、シンプルなFAQシステムを超えて、ユーザーの質問に対してより自然で具体的な応答を提供する強力なツールです。特に特定の社内共有情報や記事情報、FAQなどの情報に基づいてユーザーフレンドリーなツールを生成することが可能です。

チャットボットの精度や応答の質は、プロンプトの設計・使用するデータの量や質によって大きく変わります。プロンプトの工夫次第で、より正確で自然な応答を引き出せる一方で、不適切なプロンプトや不十分なデータによっては、期待する結果が得られないこともあります。

改善していくためには、データの追加やより高度なプロンプト設計の調整を試みてください。例えば、ベクトル検索だけでなく、従来の単語検索とのハイブリッドなアプローチを導入したり、ハルシネーションを抑えるための検証プロセスを追加したりすることが考えられます。

いろいろなユースケースに応用して挑戦してみて下さい。

STEP1:データベース格納まで

# 必要なライブラリをインストール
%pip install --upgrade --quiet langchain langchain-google-genai langchain-community faiss-cpu streamlit langchain-text-splitters beautifulsoup4
# 必要なライブラリのインポート
import requests
from bs4 import BeautifulSoup
from langchain.schema import Document
from langchain.text_splitter import CharacterTextSplitter
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS

# スクレイピング対象のURLリスト
urls = [
    # 対象のURLリストを配列指定する
]

# ファイル名とURLのペアを保存するリスト
fnames = []

# WebページにアクセスしてHTMLデータをローカルに保存
for url in urls:
    fname = f"{url[url.rfind('/', 0, -1) + 1:-1]}.html"
    fnames.append((fname, url))  # ファイル名とURLのタプルを保存
    response = requests.get(url)
    with open(fname, mode='w', encoding='utf-8') as fout:
        fout.write(response.text)

# チャンクを分割するための設定
text_splitter = CharacterTextSplitter(
    separator='\n\n',  # 改行で分割
    chunk_size=500,  # 各チャンクのサイズ
    chunk_overlap=0,  # チャンク間の重複なし
    length_function=len  # 文字数でチャンクを計測
)

# 全てのチャンクを保持するリスト
all_chunks = []

# テキストを取得してチャンクに分割し、メタデータにURLを含める
for fname, url in fnames:
    with open(fname, 'r', encoding='utf-8') as file:
        html_content = file.read()

    soup = BeautifulSoup(html_content, 'html.parser')
    text = soup.get_text(separator="\n")

    # 抽出したテキストをチャンクに分割
    chunks = text_splitter.split_text(text)

    # 各チャンクをDocumentオブジェクトに変換してリストに追加
    for i, chunk in enumerate(chunks):
        # メタデータにURLを保存
        doc = Document(page_content=chunk, metadata={"source_url": url, "chunk_index": i})
        all_chunks.append(doc)

    print(f"Processed {fname}, {len(chunks)} chunks extracted.")

# Geminiに対応したベクトル変換を実施
embeddings = GoogleGenerativeAIEmbeddings(
    google_api_key=userdata.get('GEMINI_API_KEY'),# APIキーを指定
    model="models/embedding-001"
)

# ベクトルストアにチャンクを保存
db = FAISS.from_documents(all_chunks, embeddings)
db.save_local('faiss_store')

STEP2:チャットボット生成まで

from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# Geminiモデルを定義する
llm = ChatGoogleGenerativeAI(
    google_api_key=userdata.get('GEMINI_API_KEY'),
    model="gemini-1.5-flash",
    temperature=1.0,
    max_retries=2,
)

# オリジナルのSystem Instructionを定義する
prompt_template = """
あなたは、「神風」サイトの岡田茂吉論文のチャットボットです。
背景情報を参考に、質問に対して岡田茂吉になりきって、質問に回答してくだい。

岡田茂吉論文に全く関係のない質問と思われる質問に関しては、「岡田茂吉論文に関係することについて聞いてください」と答えてください。

以下の背景情報を参照してください。情報がなければ、その内容については言及しないでください。
# 背景情報
{context}

# 質問
{question}"""

PROMPT = PromptTemplate(
    template=prompt_template, input_variables=["context", "question"]
)

# リトリーバーが上位2つのドキュメントを返すように設定
retriever = db.as_retriever(search_kwargs={"k": 5})

qa = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True,
    chain_type_kwargs={"prompt": PROMPT}
)

def generate_answer(query):
    content = qa.invoke(query)

    # 応答文の表示
    print("応答文:")
    print(content["result"])

    # 参照されたドキュメントを表示
    print("\n参照元のURL:")
    doc_urls = []
    for doc in content["source_documents"]:
        #既に出力したのは、出力しない
        if doc.metadata["source_url"] not in doc_urls:
            doc_urls.append(doc.metadata["source_url"])
            print(doc.metadata["source_url"])
        

query = "病気とは?"
generate_answer(query)

参考サイト

Qiita:完全無料で、オリジナルRAGのWebアプリケーションのチャットボットを開発、デプロイまで

PAGE TOP