Zenn
🤖

回答を分岐させる簡易RAGスクリプトを組んでみた

2025/02/14に公開

はじめに

以前保険の窓口に行った際、細かい要望をたくさん出してしまったため、自分に合う保険を探すのに担当の人がすごく時間を使っていて、少し申し訳なく思ったことがありました。

そこで膨大な量の保険商品の中から最適な保険商品を選ぶAIを作ってみようと考えました。

具体的には商品pdfをアップロードしておく事で、通常の生成AIではできない専門的な応答ができるシステムを考えました。

また、保険の質問中に保険以外のことも気になるかもしれないので通常のgeminiとしても動作するようにしてみました。

仕組み

  1. 質問判定   : 保険の質問かを判定します。保険に関する質問でないなら手順4に進みます。

  2. 顧客情報取得 : 顧客情報が必要な場合はデータベースに接続し顧客情報を取得します。

  3. PDFの取得   : 入力内容と顧客情報をもとにPDFを取得します。

  4. 回答     : 入力内容、顧客情報、PDFを元に回答します。

構築したコードの動作結果

コード解説の前に動作イメージをお見せします。

まず、名前と年齢、保険に関する質問、顧客データベースの参照指示をプロンプトに入力します。

システムが保険に関する質問であると判断しました。
データベースから顧客情報を取得しています。
個人情報の取得も出来ています。

マニュアルの取得もできているようです。

結果として、通常の生成AIとは異なり専門的な回答が提供されています。

保健と関係のない適当な質問をしてみましたが、通常のgeminiとしての応答ができています。

使用した環境やデータ

環境はGoogle Colabで構築しました。
https://colab.google/

ベクトル化と文章生成にはgemini 2.0を用いました。
https://ai.google.dev/gemini-api/docs?hl=ja

ベクトルデータベースとしてはコードだけで簡単に構築ができるchromaDBを、pdfを取得する際のフレームワークとしてはLangChainを用いました

5社の保険会社様のPDFを使用しました。
(病気に関する保険以外も入れました)

使用したパンフレット一覧

事前準備

ライブラリ等のimport

geminiのAPI KEYが必要になります。
gemini2.0は2025年2月時点では無料で使うことができました。
https://ai.google.dev/gemini-api/docs?hl=ja

api_key=""
!pip install -U --quiet langchain-google-genai
!pip install langchain openai langchain_community
!pip install chromadb tiktoken
!pip install Spire.PDF
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.chains import RetrievalQA
from google import genai
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain.vectorstores import Chroma
from spire.pdf import PdfDocument
from spire.pdf import PdfTextExtractOptions
from spire.pdf import PdfTextExtractor
import sqlite3
import pandas as pd
import os
os.environ['GOOGLE_API_KEY'] = api_key

データベース作成

pdfをベクトル化しchromeDBに保存する

Google Colabのフォルダに、参照させたいPDFをアップロードし、そのファイル名を file_name= の欄に記入してください。
コードを複数回実行することで、複数のPDFをベクトルデータベースに保存できます。

file_name="file.pdf"
pdf = PdfDocument()
#ここにファイル名を入力
pdf.LoadFromFile(file_name)

pdfから文章を抽出します。

#pdfから文章を抽出
for i in range(pdf.Pages.Count):
    # ページを取得
    page = pdf.Pages.get_Item(i)
    # ページをパラメータとして渡してPdfTextExtractorのオブジェクトを作成
    text_extractor = PdfTextExtractor(page)
    # ページからテキストを抽出
    texts = text_extractor.ExtractText(PdfTextExtractOptions())
    # 抽出されたテキストを文字列オブジェクトに追加
    docs = texts
pdf.Close()

テキストから空白、\n、\rを削除するコードと、長すぎるpdfを分割するコードです。なくても動きますが、必要なら使ってください。

コードを表示
docs=docs[71:].replace(" ", "")
docs=docs.replace("\n", "")
docs=docs.replace("\r", "")


from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(separator=".",
    chunk_size=6000,
    chunk_overlap=15)
docs = text_splitter.split_text(docs)
#分割時についてしまう文字列を削除します。
cleaned_docs = []
for doc in docs:
    cleaned_doc = doc.replace("PDFforPython.", "")
    cleaned_doc = cleaned_doc.replace("EvaluationWarning:ThedocumentwascreatedwithSpire.PDFforPython.", "")
    cleaned_docs.append(cleaned_doc)
docs=cleaned_docs

ベクトルデータベースを作成します。データベースがある場合はデータベースに追加で記述されます。

#ベクトル化方法を設定
embeddings =  GoogleGenerativeAIEmbeddings(model="models/text-embedding-004")

chroma_store = Chroma.from_texts(
    docs,
    embedding=embeddings,
    collection_name="chromedb"
)
print(len(docs))
for doc in docs:
    print(doc)

顧客情報データベースの作成

まずはデータフレームを作り、それをsqlに変換します。

# データベース接続を作成(存在しない場合は作成されます)
conn = sqlite3.connect('data.db')

# データフレームを作成
data = {
    'last_name': ["田中", "山田", "田中", "一ノ瀬"],
    'first_name': ["太郎", "太郎", "次郎", "一郎"],
    'age': [45, 45, 12, 111],
    'information': ['15年前に歯牙腫を手術。既婚。子供2人', '健康。既存契約あり', '保険未加入', '要介護状態']
}
df = pd.DataFrame(data)

# データをSQLに変換し、データベースに挿入
df.to_sql('persons', conn, if_exists='replace', index=False)

# データベース接続を閉じる
conn.close()

geminiに質問をする

geminiによるデータベース接続判断

データベースへの接続手順は以下の通りです。

  1. まず保険の質問かどうかをgeminiに判断させます。

  2. 保険の質問の場合はデータベースから顧客情報を取得するべきか判断させます。

  3. 保険の質問で顧客情報の取得が必要な場合、顧客の名前や年齢を抽出します

  4. 顧客の名前や年齢を元にデータベースに接続します。

gemini2.0に質問を書きます。

名前と年齢でデータベースに検索を掛けるので、テキストに名前と年齢を含めておきます。
今回は実装していませんが、同姓同名かつ同年齢の人がいた場合、顧客を手動で選べるシステムをつけると良いかもしれません。

text="田中太郎さん45歳に個人データを参照しておすすめの保険を教えて。"

保健に関する質問か、顧客の詳細データが必要か、の2点を判断させるコードです。

customer_information=" "
#保険の質問かどうか
response = client.models.generate_content(model='gemini-2.0-flash-exp', contents=text+'これは保険に関する質問か、はいかいいえの一言だけで答えて')
if response.text.strip().lower()  == "はい" :
  print("保険に関する質問であると判断したため、関連する保険マニュアルを元に回答します。")
  #詳細が必要な質問かどうか
  response = client.models.generate_content(model='gemini-2.0-flash-exp', contents=text+'これは個人データが追加で必要な質問か、はいかいいえの一言だけで答えて')
  if response.text.strip().lower()  == "はい" :
    #文章から個人情報取得
    print("データベースから顧客の詳細情報を取得します")

もし顧客情報が必要だとgeminiが判断した場合に、文章から名前と年齢を抜き取るコードです。

初めにjson形式を指定していますが、稀にjson形式が崩れるうえ、初めから名前だけを抽出することも難しいので、動作を分割させています。

生成する文章が短いため処理はすぐ終わります。

    response_json = client.models.generate_content(model='gemini-2.0-flash-exp', contents='''以下の文章の中から顧客1人の名前と年齢を抽出してjson形式のみで返して。例{first_name='一郎',last_name='順規',age=22}'''+text)
    response = client.models.generate_content(model='gemini-2.0-flash-exp', contents='以下の文章からlast_nameを取得して一言で答えて'+str(response_json))
    print("性 :",response.text.strip().lower())
    last_name='"'+response.text.strip().lower()+'"'
    response = client.models.generate_content(model='gemini-2.0-flash-exp', contents='以下の文章から1人目のfirst_nameを取得して一言で答えて'+str(response_json))
    print("名 :",response.text.strip().lower())
    first_name='"'+response.text.strip().lower()+'"'
    response = client.models.generate_content(model='gemini-2.0-flash-exp', contents='以下の文章からageを取得してint型で答えて、取得できないならnullで'+str(response_json))
    print("年齢 :",response.text.strip().lower())
    age='"'+response.text.strip().lower()+'"'

取得した名前と年齢を元にデータベースから情報を取得するコードです。

    # データベース接続
    conn = sqlite3.connect('data.db')
    cursor = conn.cursor()
    #情報取得
    try:
      query = f"SELECT information FROM persons WHERE last_name = {last_name}  AND first_name = {first_name} AND age = {age}"
      cursor.execute(query)
      customer_information = cursor.fetchone()
    except:
      print("顧客情報を取得できませんでした。")
    # データベース接続を閉じる
    conn.close()
  text= text + str(customer_information)+" この情報のみの時に最適な答えを出して"
  else:
  else:
    print("顧客の詳細情報取得は必要ないと判断しました。")

PDFを取得し回答を生成

入力した質問と顧客情報をベクトル化し、ベクトル類似上位4つの文章を参照しながら応答するシステムを作成します。
search_kwargs={"k": 4}をsearch_kwargs={"k": 1}に変えると類似度上位1つを参照するようになります。

  embeddings =  GoogleGenerativeAIEmbeddings(model="models/text-embedding-004")
  qa = RetrievalQA.from_chain_type(
      llm=ChatGoogleGenerativeAI(model='gemini-2.0-flash-exp',temperature=0.7, top_p=0.85),
      chain_type="stuff", 
      retriever=chroma_store.as_retriever(search_kwargs={"k": 4}),
      return_source_documents=True
  )

回答と参照したマニュアルを取得するコードです

#実行
  result = qa.invoke({"query":text})

  print(result["result"])
  for document in result["source_documents"]:
        print("取得したpdf最初の40文字 :",document.page_content[:40])

保険の質問でなかった時の動作

保険の質問でない時は通常のgeminiとして動作させるシステムです。
かなり離れていますが、「geminiによるデータベース接続判断」の一番初めのif文とつながっています。

else:
  print("保険に関する質問ではないと判断したため通常のgeminiとして回答します。")
  response = client.models.generate_content(model='gemini-2.0-flash-exp', contents=text)
  print(response.text.strip().lower())

全コード

全てのコードを一回でコピペできるようにしてあります。

コードを表示

コピペしてAPI KRYとファイル名を変更すれば動きます。

api_key="API KEY"
text="田中太郎さん45歳に個人データを参照しておすすめの保険を教えて。"
#pdfはコードで何個でも保存しておけます。
file_name="file.pdf"


!pip install google-genai
!pip install -U --quiet langchain-google-genai
!pip install langchain openai langchain_community
!pip install chromadb tiktoken
!pip install Spire.PDF
from google import genai
from langchain_google_genai import ChatGoogleGenerativeAI
import google.generativeai as generativeai
from langchain.text_splitter import CharacterTextSplitter
import pandas as pd
import numpy as np
import os
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain.chains import RetrievalQA
from langchain.vectorstores import Chroma
from spire.pdf import PdfDocument
from spire.pdf import PdfTextExtractOptions
from spire.pdf import PdfTextExtractor


os.environ['GOOGLE_API_KEY'] = api_key
client = genai.Client(api_key=api_key)
generativeai.configure(api_key=api_key)

#個人情報sql作成
# ライブラリをインポート
import sqlite3
import pandas as pd

# データベース接続を作成(存在しない場合は作成されます)
conn = sqlite3.connect('data.db')

# データフレームを作成
data = {
    'last_name': ["田中", "山田", "田中", "一ノ瀬"],
    'first_name': ["太郎", "太郎", "次郎", "一郎"],
    'age': [45, 45, 12, 111],
    'information': ['15年前に歯牙腫を手術。既婚。子供2人', '健康。既存契約あり', '保険未加入', '要介護状態']
}
df = pd.DataFrame(data)

# データをSQLに変換し、データベースに挿入
df.to_sql('persons', conn, if_exists='replace', index=False)

# データベース接続を閉じる
conn.close()

#pdfをベクトルDBに追加
pdf = PdfDocument()
#ここにファイル名を入力
pdf.LoadFromFile(file_name)




# テキストを保存するための文字列オブジェクトを作成
extracted_text = ""
# シンプルな抽出方法を使用するように設定
extract_options = PdfTextExtractOptions()
#extract_options.IsSimpleExtraction = True
# ドキュメント内のページをループ
for i in range(pdf.Pages.Count):
    # ページを取得
    page = pdf.Pages.get_Item(i)
    # ページをパラメータとして渡してPdfTextExtractorのオブジェクトを作成
    text_extractor = PdfTextExtractor(page)
    # ページからテキストを抽出
    texts = text_extractor.ExtractText(extract_options)
    # 抽出されたテキストを文字列オブジェクトに追加
    extracted_text += texts
pdf.Close()
# 抽出されたテキストをテキストファイルに書き込む
extracted_text=extracted_text[71:].replace(" ", "")

extracted_text=extracted_text.replace("\n", "")
extracted_text=extracted_text.replace("\r", "")
#テキストを分割
text_splitter = CharacterTextSplitter(separator=".",
    chunk_size=6000,
    chunk_overlap=15)

docs = text_splitter.split_text(extracted_text)


cleaned_docs = []
for doc in docs:
    cleaned_doc = doc.replace("PDFforPython.", "")
    cleaned_doc = cleaned_doc.replace("EvaluationWarning:ThedocumentwascreatedwithSpire.PDFforPython.", "")
    cleaned_docs.append(cleaned_doc)

embeddings =  GoogleGenerativeAIEmbeddings(model="models/text-embedding-004")

#データベース作成
chroma_store = Chroma.from_texts(
    cleaned_docs,
    embedding=embeddings,
    collection_name="my_collection"
)
print(len(cleaned_docs))
for doc in cleaned_docs:
    print(doc)

#入力情報 #情報入力をプロンプトのみでできる
#######################以下システム内部###########################
customer_information=" "

#保険の質問かどうか
response = client.models.generate_content(model='gemini-2.0-flash-exp', contents=text+'これは保険に関する質問か、はいかいいえの一言だけで答えて')
if response.text.strip().lower()  == "はい" :
  print("保険に関する質問であると判断したため、関連する保険マニュアルを元に回答します。")
  #詳細が必要な質問かどうか
  response = client.models.generate_content(model='gemini-2.0-flash-exp', contents=text+'これは個人データが追加で必要な質問か、はいかいいえの一言だけで答えて')
  if response.text.strip().lower()  == "はい" :
    #文章から個人情報取得
    print("データベースから顧客の詳細情報を取得します")
    response_json = client.models.generate_content(model='gemini-2.0-flash-exp', contents='''以下の文章の中から顧客1人の名前と年齢を抽出してjson形式のみで返して。例{first_name='一郎',last_name='順規',age=22}'''+text)
    response = client.models.generate_content(model='gemini-2.0-flash-exp', contents='以下の文章からlast_nameを取得して一言で答えて'+str(response_json))
    print("性 :",response.text.strip().lower())
    last_name='"'+response.text.strip().lower()+'"'
    response = client.models.generate_content(model='gemini-2.0-flash-exp', contents='以下の文章から1人目のfirst_nameを取得して一言で答えて'+str(response_json))
    print("名 :",response.text.strip().lower())
    first_name='"'+response.text.strip().lower()+'"'
    response = client.models.generate_content(model='gemini-2.0-flash-exp', contents='以下の文章からageを取得してint型で答えて、取得できないならnullで'+str(response_json))
    print("年齢 :",response.text.strip().lower())
    age='"'+response.text.strip().lower()+'"'
    # データベース接続
    conn = sqlite3.connect('data.db')
    cursor = conn.cursor()
    #情報取得
    try:
      query = f"SELECT information FROM persons WHERE last_name = {last_name}  AND first_name = {first_name} AND age = {age}"
      cursor.execute(query)
      customer_information = cursor.fetchone()
    except:
      print("顧客情報を取得できませんでした。")
    # データベース接続を閉じる
    conn.close()
    content= text + "以下が詳細な個人情報"+ str(customer_information)+" この情報のみの時に最適な答えを出して"
  else:
    print("顧客の詳細情報取得は必要ないと判断しました。")

  # rangchain llm 作成
  qa = RetrievalQA.from_chain_type(
      llm=ChatGoogleGenerativeAI(model='gemini-2.0-flash-exp',temperature=0.7, top_p=0.85),
      chain_type="stuff",  
      retriever=chroma_store.as_retriever(search_kwargs={"k": 4}),
      return_source_documents=True
  )
  # クエリを実行
  result = qa.invoke({"query": content})
  print("取得した顧客情報 :",customer_information)
  for document in result["source_documents"]:
      print("使用したマニュアル最初の40文字 :",document.page_content[:40])
  print("回答 :", result["result"])

else:
  print("保険に関する質問ではないと判断したため通常のgeminiとして回答します。")
  response = client.models.generate_content(model='gemini-2.0-flash-exp', contents=text)
  print(response.text.strip().lower())

宣伝

弊社ではデータ基盤やLLMのご相談や構築も可能ですので、お気軽にお問合せください。
https://solution.rounda.co.jp/

また、中途採用やインターンの問い合わせもお待ちしています!

https://www.wantedly.com/companies/company_5576351/projects
https://pitta.me/matches/qwHyCVnoehfa

Discussion

ログインするとコメントできます