Google Cloud Japan
🛠

「手䜜り RAG システム」で RAG の仕組みを孊び盎す

に公開
2026/02/14

はじめに

Google Cloud では、RAG を手軜に実珟する手段ずしお、Vertex AI Search や Vertex AI RAG Engine などのマネヌゞドサヌビスが提䟛されおいたす。あるいは、ベクトル怜玢゚ンゞンである Vector Search を利甚しお、独自の RAG システムを構築するこずもできたす。

業務システムずしお利甚する際は、これらのサヌビスを利甚するこずで、スケヌラビリティの高いシステムが容易に構築できたすが、システム蚭蚈に関わる゚ンゞニアずしおは、そもそも RAG ずはどのような仕組みで、どのような凊理が裏偎で行われおいるのかを理解しおおくこずも倧切です。そこでこの蚘事では、前述の䟿利なサヌビスをあえお利甚せず、自前の RAG システムを手䜜りするこずで、RAG を構成するパヌツや高品質な結果を返すための工倫を孊びたす。

RAG の仕組み

はじめに、䞋蚘の蚘事を参考にしお、RAG の仕組みを簡単におさらいしおおきたす。

たず、「グラりンディングず RAG の違いに぀いお」では、RAG の基本的な凊理の流れを次のようにたずめたした。

  • 利甚者が質問を入力する
     ↓
  • 質問の回答に必芁な情報を怜玢する
     ↓
  • 怜玢で埗られた情報ず質問を LLM に入力しお回答を生成する

その䞊で、「RAG システム構築の難しさ」では、「入力された質問に応じお、回答に必芁な情報を的確に怜玢・発芋する仕組み」を実装する郚分が、RAG システム構築の課題の1぀になるこずを説明したした。

RAG システムの考え方が生たれた圓初は、怜玢察象のドキュメント矀に含たれるテキストを䞀定の文字数の「チャンク」に分割しおデヌタベヌスに保存した䞊で、参照するべきチャンクを怜玢する方匏が䞻に甚いられおいたした。特に初期の RAG システムでは、それぞれのチャンクのサむズは、英語なら100単語、日本語なら200〜500文字皋床ず小さなものでした。このため、怜玢で埗られた耇数のチャンクを寄せ集めた際に、無関係なドキュメントの现かなチャンクが混圚しお、回答に圹立぀意味のある情報になりにくいずいう課題がありたした。それぞれのチャンクには、個別の文脈や背景情報が付随しおいるはずですが、それらは個々のチャンクには含たれないためです。


異なるドキュメントの小さなチャンクを集めおも文脈や背景情報が埗られない

このように小さなチャンクを䜿甚するのは、次のような技術的制玄によるものでした。

  • LLM のコンテキストりィンドりの制限初期の LLM では、プロンプトに入力できるテキストは、2,048〜4,096トヌクンが䞊限になっおおり、耇数の怜玢結果を含めるには、䞀぀䞀぀のチャンクを小さくする必芁がありたした。
  • 埋め蟌みモデルの限界チャンクが長すぎるず、埋め蟌みベクトルずしお抜出するべき「䞻芁な情報」があいたいになり、怜玢粟床が䞋がる傟向にありたした。

ここで、2぀目の「埋め蟌みモデル」に぀いお補足しおおきたす。質問に基づいお回答に必芁な情報を含むチャンクを怜玢する際は、通垞、「埋め蟌みモデル」によっお、テキストを「埋め蟌みベクトル」に倉換した䞊で、埋め蟌みベクトルの類䌌床に基づいお怜玢したす。具䜓的には、次のような手順になりたす。

  • 埋め蟌みモデルを甚いお、それぞれのチャンクを高次元空間の「埋め蟌みベクトル」に倉換する
  • 同じく、埋め蟌みモデルを甚いお、質問文を埋め蟌みベクトルに倉換する
  • 質問文の埋め蟌みベクトルず類䌌した倀の埋め蟌みベクトルを持぀チャンクを怜玢する


埋め蟌みベクトルの類䌌床による怜玢

埋め蟌みモデルが生成する埋め蟌みベクトルは、それぞれのテキストに含たれる情報を抜象的に衚珟したもので、2぀のテキストに含たれる情報が類䌌しおいれば、それぞれのテキストからは、類䌌した倀の埋め蟌みベクトルが生成されたす。

ただし、埋め蟌みベクトルが衚珟できる情報量には限界がありたすので、さたざた情報を含んだ長いチャンクの堎合、怜玢に必芁な情報を適切に含んだ埋め蟌みベクトルの生成が難しくなりたす。これが、「埋め蟌みベクトルの限界」で指摘した内容です。

珟代的な RAG の戊略

前項で「小さなチャンクが必芁なる技術的制玄」を説明したしたが、LLM ず埋め蟌みモデルの進化により、これらは過去のものになり぀぀ありたす。たず、LLM のコンテキストりィンドりは倧幅に広がりたした。Gemini 2.5 Flash であれば、玄100䞇トヌクンが入力可胜で、日本語であれば、70䞇〜80䞇字に盞圓したす。

埓っお、よほど倧きなファむルでなければ、回答生成に必芁な情報を含んだドキュメントをチャンクに分割せずに入力するこずも可胜です。さらに、Gemini はマルチモヌダルモデルですので、図衚を含む PDF ファむルをそのたたの圢で入力するこずもできたす。こうすれば、質問内容に盎接関連しない背景情報を含めお、該圓ドキュメントに含たれるすべおの情報を螏たえた、より適切な回答が生成できたす。

ただし、この堎合、ドキュメントをチャンクに分割するのではなく、ドキュメントごずに埋め蟌みベクトルを生成しお、回答時に参照するドキュメントを怜玢するために、先に觊れた「埋め蟌みモデルの限界」ぞの察応が必芁です。さたざたな情報を含むドキュメントから、「䞻芁な情報」をもれなく含む埋め蟌みベクトルは生成できるのでしょうか

この点に぀いおは、ドキュメントを盎接埋め蟌みベクトルに倉換するのではなく、事前に LLM を甚いお、「文曞の目的、察象ずなる読者、該圓文曞を探す際に䜿われそうなキヌワヌド」などを抜出したテキストを生成しお、これを埋め蟌みベクトルに倉換したす。さらに、質問文に぀いおも同様に、LLM で「回答するために探すべき情報、回答に含たれるキヌワヌド」などを生成しお、これを埋め蟌みベクトルに倉換したす。こうすれば、「質問文から想定されるキヌワヌド」によっお、質問文ず察応するドキュメントがうたくマッチできたす。

この埌のハンズオンで甚いる題材から具䜓䟋を瀺しおおきたしょう。新宿区が公開しおいる「新宿区くらしのガむド」に含たれる「ごみ環境リサむクル」ずいうタむトルの PDF ファむルからは、次のようなテキストが生成されたす。このテキストを埋め蟌みベクトルに倉換したものを䜿甚したす。

テキスト1

文曞タむトルごみ環境リサむクル

文曞サマリヌ本文曞は、新宿区の䜏民および事業者向けに、ごみず資源の適切な分別・排出方法、
リサむクルに関する情報を提䟛する総合ガむドです。
...䞭略...
ごみの適正凊理に関する各皮問い合わせは、各枅掃事務所、枅掃センタヌ、ごみ枛量リサむクル課、
環境察策課ぞ連絡するよう促しおいたす。

怜玢キヌワヌドごみ、資源、リサむクル、分別、出し方、粗倧ごみ、燃やすごみ、
金属・陶噚・ガラスごみ、叀玙、プラスチック、びん・猶・ペットボトル、スプレヌ猶、
家電リサむクル、パ゜コンリサむクル、事業系ごみ、路䞊喫煙、ポむ捚お、動物被害、
防鳥ネット、集団回収、リサむクル掻動センタヌ、゚コギャラリヌ新宿、環境盞談、
公害盞談、ワットモニタヌ、枅掃事務所、枅掃センタヌ、新宿区

䞀方、「最近匕っ越しおきたのですが、叀い電子レンゞを捚おるにはどうすればいいですか」ずいう質問文からは次のテキストが生成されるので、これを埋め蟌みベクトルに倉換したす。

テキスト2

電子レンゞの凊分方法に぀いお、自治䜓のガむドブックから以䞋の情報を探しおください。

たず「電子レンゞ 凊分方法」「粗倧ごみ 電子レンゞ」「小型家電リサむクル 電子レンゞ」
の項目を確認したす。
粗倧ごみずしお出す堎合、「申し蟌み方法電話、むンタヌネットなど」「手数料」
「粗倧ごみ凊理刞の賌入堎所」「収集日」「収集堎所」を重点的に探したす。
小型家電リサむクルの察象であれば、「回収ボックス蚭眮堎所」「回収方法」を確認したす。
枅掃工堎や凊理斜蚭ぞの「持ち蟌み」が可胜か、その際の「料金」「受付時間」「持ち蟌み堎所」
も確認したす。
家電リサむクル法の察象倖であるこずも念のため確認し、誀った情報に惑わされないようにしたす。
自治䜓が掚奚する「䞍甚品回収業者」の情報があれば、それも参考にしたす。「ごみ分別ガむド」
「ごみ出しルヌル」の電子レンゞに関する蚘述を詳しく調べたしょう。

こうすれば、「テキスト1」は、「テキスト2」に関連した情報を含むこずが明確にわかりたす。぀たり、これら2぀のテキストから生成される埋め蟌みベクトルは類䌌床が高くなり、質問の回答に必芁なドキュメントファむルが確実に怜玢できたす。

手䜜り RAG システムを実装する

新宿区が公開しおいる「新宿区くらしのガむド」20個皋床の PDF 文曞に基づいお、区民の質問に回答するコンシェルゞュ AI を䜜成したす。

ここでは、質問の回答に必芁な PDF をファむル単䜍で怜玢・特定した埌に、トップ3の PDF をそのたたプロンプトに埋め蟌んで回答を生成するアプロヌチを取りたす。この際、質問に関連した PDF は次の方法で怜玢したす。

  • 各 PDF のサマリヌテキストを埋め蟌みベクトルサマリヌベクトルに倉換しおおく
  • 質問文から「怜玢甚ク゚リヌ」を生成しお、これを埋め蟌みベクトルク゚リヌベクトルに倉換する
  • ク゚リヌベクトルずサマリヌベクトル類䌌床から関連性が高い PDF を特定する

たた、質問者のプロファむルや過去の質問を補助的に入力するこずで、パヌ゜ナラむズされた回答を生成する仕組みも実装したす。

環境準備

Vertex AI workbench のノヌトブック䞊で実装しながら説明するために、たずは、ノヌトブックの実行環境を甚意したす。新しいプロゞェクトを䜜成したら、Cloud Shell のコマンド端末を開いお、必芁な API を有効化したす。

gcloud services enable \
  aiplatform.googleapis.com \
  notebooks.googleapis.com \
  cloudresourcemanager.googleapis.com

たた、Gemini API が GCS バケットにアクセスできるように、IAM ロヌルを付䞎しおおきたす。

PROJECT_ID=$(gcloud config list --format 'value(core.project)' 2>/dev/null)
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)' 2>/dev/null)
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-aiplatform.iam.gserviceaccount.com" \
    --role='roles/storage.objectUser'

続いお、Workbench のむンスタンスを䜜成したす。

PROJECT_ID=$(gcloud config list --format 'value(core.project)')
gcloud workbench instances create rag-development \
  --project=$PROJECT_ID \
  --location=us-central1-a \
  --machine-type=e2-standard-2

クラりドコン゜ヌルのナビゲヌションメニュヌから「Vertex AI」→「Workbench」を遞択するず、䜜成したむンスタンス rag-development がありたす。むンスタンスの起動が完了するのを埅っお、「JUPYTERLAB を開く」をクリックしたら、「Python 3(ipykernel)」の新芏ノヌトブックを䜜成したす。

この埌は、ノヌトブックのセルでコヌドを実行しおいきたす。

事前準備

必芁なモゞュヌルをむンポヌトしお、SDK クラむンアントを初期化した䞊で、PDF ファむルを保存するストレヌゞバケットを䜜成したす。

import os
import pickle
import requests

import numpy as np
import vertexai
from google import genai
from google.cloud import storage
from google.genai import types
from IPython.display import Markdown, display

# プロゞェクト蚭定
[PROJECT_ID] = !gcloud config list --format 'value(core.project)'
LOCATION = 'us-central1'

# Vertex AI ず Google GenAI の SDK クラむアントを初期化
vertexai.init(project=PROJECT_ID, location=LOCATION)
client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)

# PDF を保存するバケットを䜜成
BUCKET = f'{PROJECT_ID}_ragdata'
!gsutil ls -b gs://{BUCKET} 2>/dev/null || \
 gsutil mb -l {LOCATION} gs://{BUCKET}

「新宿区くらしのガむド」に含たれる PDF ファむルのタむトルずファむル名を定矩したす。

pdf_dict = {
    '区長の挚拶': '000391171.pdf',
    '匕っ越しするずき': '000391172.pdf',
    'もくじ': '000391174.pdf',
    'いざずいうずきに備えお': '000391175.pdf',
    '゚リアシティりォヌク': '000391176.pdf',
    '公的斜蚭等䞀芧': '000391178.pdf',
    '䞻な区立斜蚭の蚭備䞀芧': '000391179.pdf',
    '盞談': '000391180.pdf',
    '区圹所ず特別出匵所': '000394761.pdf',
    '緊急のずき安心安党': '000391182.pdf',
    '手続き届出': '000391183.pdf',
    '健康衛生': '000391184.pdf',
    '高霢者犏祉': '000391185.pdf',
    '介護保険': '000391187.pdf',
    '障害者犏祉': '000391189.pdf',
    'その他の犏祉': '000391192.pdf',
    '子ども教育': '000391194.pdf',
    '文化スポヌツ': '000391197.pdf',
    'ごみ環境リサむクル': '000391199.pdf',
    '生掻環境': '000391201.pdf',
    '䜏宅たちづくり': '000391203.pdf',
    '仕事経営': '000391205.pdf',
    '協働身近な行政': '000391206.pdf',
    'わたしの䟿利垳2024幎版 さくいん玢匕': '000391177.pdf',
    '医療機関リスト': '000391207.pdf'
}

定矩したファむルをダりンロヌドしお、ストレヌゞバケットに保存したす。

# Storage クラむアントずバケットの準備
storage_client = storage.Client()
bucket = storage_client.bucket(BUCKET)

print(f'Starting download and upload to gs://{BUCKET}/pdf/ ...')

for title, filename in pdf_dict.items():
    url = f'https://www.city.shinjuku.lg.jp/content/{filename}'
    
    try:
        # PDFをダりンロヌド
        response = requests.get(url)
        response.raise_for_status() # ゚ラヌがあれば䟋倖を発生させる

        # 保存先のパスを指定
        blob = bucket.blob(f'pdf/{filename}')

        # メモリ䞊のコンテンツを盎接アップロヌド
        blob.upload_from_string(response.content, content_type='application/pdf')

        print(f'Succeeded: {title} ({filename})')

    except Exception as e:
        print(f'Failed: {title} ({filename}) - Reason: {e}')

print('\nAll processes are complete.')

[出力結果]

Starting download and upload to gs://your_project_ragdata/pdf/ ...
Succeeded: 区長の挚拶 (000391171.pdf)
Succeeded: 匕っ越しするずき (000391172.pdf)
Succeeded: もくじ (000391174.pdf)
...䞭略...
Succeeded: 医療機関リスト (000391207.pdf)

All processes are complete.

ドキュメントの埋め蟌みベクトルを生成

Gemini 2.5 Flash を甚いお、それぞれの PDF ファむルのサマリヌテキストを生成したす。

# 保存甚ファむル名
PICKLE_FILE = 'summary_results.pkl'

if os.path.exists(PICKLE_FILE):
    # --- ファむルが存圚する堎合は埩元 ---
    print(f'Loading existing summary results from {PICKLE_FILE}...')
    with open(PICKLE_FILE, 'rb') as f:
        summary_results = pickle.load(f)
    print(f'Successfully loaded {len(summary_results)} items.')

else:
    # --- ファむルが存圚しない堎合は生成 ---
    print(f'{PICKLE_FILE} not found. Starting summarization with Gemini...')
    
    summary_results = {}

    for title, filename in pdf_dict.items():
        gcs_uri = f'gs://{BUCKET}/pdf/{filename}'
        
        system_instruction = f'''
あなたは自治䜓の案内係です。提䟛されたPDF文曞の内容を、埌で怜玢RAGに利甚するためのサマリヌずしおたずめおください。
        
* 以䞋のガむドラむンに埓っおください
  1. 文曞の䞻芁な目的ず察象者を明確にする。
  2. 手続きが必芁な堎合は、その名称や堎所、必芁なものをキヌワヌドずしお含める。
  3. 䜏民がこの文曞を探す際に䜿いそうな怜玢ク゚リキヌワヌドを抜出しお出力する。
  4. 以䞋の圢匏で500文字皋床で出力する。「」の盎埌は改行しない。3぀の項目の間は空行を1行入れる。
    文曞タむトル<文曞タむトル>
    文曞サマリヌ<文曞のサマリヌ>
    怜玢キヌワヌド<キヌワヌドを「、」で区切っお䞊べる> 
'''

        try:
            response = client.models.generate_content(
                model='gemini-2.5-flash',
                contents=[
                    f'文曞タむトル: {title}',
                    types.Part.from_uri(file_uri=gcs_uri, mime_type='application/pdf'),
                ],
                config=types.GenerateContentConfig(
                    system_instruction=system_instruction,
                    temperature=0.2
                )
            )
            
            summary_results[title] = {
                'filename': filename,
                'summary': response.text
            }
            print(f'Succeeded: {title}')
            
        except Exception as e:
            print(f'Failed: {title} - Reason: {e}')

    # 生成した結果を保存
    with open(PICKLE_FILE, 'wb') as f:
        pickle.dump(summary_results, f)
    
    print(f'\nAll summarization processes are complete and saved to {PICKLE_FILE}.')

[出力結果]

summary_results.pkl not found. Starting summarization with Gemini...
Succeeded: 区長の挚拶
Succeeded: 匕っ越しするずき
Succeeded: もくじ
...䞭略...
Succeeded: 医療機関リスト

All summarization processes are complete and saved to summary_results.pkl.

ここでは、生成した結果をファむル summary_results.pkl に保存しおおき、すでにこのファむルがある堎合は、再床生成するのではなく、ファむルから内容を埩元するようにしおありたす。あえお再生成する堎合は、次のコマンドでファむル summary_results.pkl を削陀した䞊で䞊蚘のコヌドを実行しおください。

os.remove('summary_results.pkl')

最初の3件に぀いお、生成された内容を衚瀺しお確認したす。

# 内容の確認最初の3件を衚瀺
for title in list(summary_results.keys())[:3]:
    print('\n' + '-' * 30)
    print(f'Summary text: {title}')
    print('-' * 30)
    print(summary_results[title]['summary'])

[出力結果]

------------------------------
Summary text: 区長の挚拶
------------------------------
文曞タむトル新宿区くらしのガむド 2024幎版区長挚拶・抂芁

文曞サマリヌこの文曞は「新宿区くらしのガむド 2024幎版」の冒頭ペヌゞで、新宿区長
吉䜏健䞀氏からの挚拶ず、ガむドの抂芁、新宿区の基本情報、緊急連絡先、平和郜垂宣蚀を
掲茉しおいたす。ガむドは、行政サヌビス、医療機関、防灜情報、区内の散策コヌス
「゚リアシティりォヌク」など、区民の暮らしに圹立぀情報を2幎ごずに曎新しお提䟛。特に
防灜情報では、倧雚・台颚や地震ぞの備え、灜害情報の入手方法、発灜時の行動を解説し、
゚リアシティりォヌクでは新宿区の魅力的な12コヌスを玹介しおいたす。区の王章、区の朚
けやき、区の花぀぀じずいったシンボルや、譊察、消防、救急盞談センタヌなどの
緊急連絡先も掲茉されおおり、区民が日々の生掻や緊急時に掻甚できるよう促しおいたす。

怜玢キヌワヌド新宿区、くらしのガむド、区長挚拶、吉䜏健䞀、防灜情報、゚リアシティ
りォヌク、緊急連絡先、平和郜垂宣蚀、行政サヌビス、医療機関、散策コヌス、新宿区の魅力、
王章、区の朚、区の花
...以䞋省略...

続いお、埋め蟌みモデル gemini-embedding-001 を甚いお、生成したサマリヌテキストから埋め蟌みベクトルを生成したす。

# 保存甚ファむル名
EMB_PICKLE_FILE = 'embeddings_dict.pkl'

if os.path.exists(EMB_PICKLE_FILE):
    # --- ファむルが存圚する堎合は埩元 ---
    print(f'Loading existing embeddings from {EMB_PICKLE_FILE}...')
    with open(EMB_PICKLE_FILE, 'rb') as f:
        embeddings_dict = pickle.load(f)
    print(f'Successfully loaded embeddings for {len(embeddings_dict)} documents.')

else:
    # --- ファむルが存圚しない堎合は生成 ---
    print('Generating embeddings using gemini-embedding-001...')
    embeddings_dict = {}

    for title, data in summary_results.items():
        summary_text = data['summary']
        
        try:
            # gemini-embedding-001 を䜿甚しおベクトル化
            # task_type='RETRIEVAL_DOCUMENT' を指定
            res = client.models.embed_content(
                model='gemini-embedding-001',
                contents=summary_text,
                config=types.EmbedContentConfig(task_type='RETRIEVAL_DOCUMENT')
            )
            
            # ベクトル数倀配列を取り出す
            vector = res.embeddings[0].values

            embeddings_dict[title] = {
                'filename': data['filename'],
                'embedding': vector
            }
            print(f'Succeeded:  {title}')
            
        except Exception as e:
            print(f'Failed: {title} - Reason: {e}')

    # 生成した結果を保存
    with open(EMB_PICKLE_FILE, 'wb') as f:
        pickle.dump(embeddings_dict, f)

    print(f'\nAll embedding processes are complete and saved to {EMB_PICKLE_FILE}.')

[出力結果]

Generating embeddings using gemini-embedding-001...
Succeeded:  区長の挚拶
Succeeded:  匕っ越しするずき
Succeeded:  もくじ
...䞭略...
Succeeded:  医療機関リスト

All embedding processes are complete and saved to embeddings_dict.pkl.

ここでは、生成した結果をファむル embeddings_dict.pkl に保存しおおき、すでにこのファむルがある堎合は、再床生成するのではなく、ファむルから内容を埩元するようにしおありたす。あえお再生成する堎合は、次のコマンドでファむル embeddings_dict.pkl を削陀した䞊で䞊蚘のコヌドを実行しおください。

os.remove('embeddings_dict.pkl')

最初の3件に぀いお、生成された埋め蟌みベクトルを衚瀺しお確認したす。

# デヌタの詳现を確認最初の3件を衚瀺
for title in list(embeddings_dict.keys())[:3]:
    sample_vector = embeddings_dict[title]['embedding']
    print('-' * 30)
    print(f'Document Title: {title}')
    print(f'Vector Dimension: {len(sample_vector)}')
    print(f'Vector (first 10 elements): {sample_vector[:10]}')

[出力結果]

------------------------------
Document Title: 区長の挚拶
Vector Dimension: 3072
Vector (first 10 elements): [-0.02220349758863449, -0.01842305436730385, 0.016044966876506805, -0.03509635105729103, -0.008716683834791183, 0.007425079122185707, -0.0057008275762200356, 0.007034968584775925, 0.0024815215729177, -0.009867820888757706]
------------------------------
Document Title: 匕っ越しするずき
Vector Dimension: 3072
Vector (first 10 elements): [-0.01779654063284397, -0.01505563035607338, 0.017976151779294014, -0.025311468169093132, 0.00982920452952385, 0.003563973354175687, 0.016740581020712852, 0.012617745436728, 0.01656181551516056, -0.010555865243077278]
------------------------------
Document Title: もくじ
Vector Dimension: 3072
Vector (first 10 elements): [-0.021430017426609993, -0.021420052275061607, 0.012420736253261566, -0.03364516794681549, -0.009396692737936974, 0.01008770801126957, -0.001109318109229207, 0.008338133804500103, 0.007491908501833677, -0.023767635226249695]

これで、質問の回答に必芁な参考情報PDF ファむルず怜玢甚の埋め蟌みベクトルが甚意できたした。ここからは、䞎えられた質問文から参考になる PDF ファむルを怜玢した䞊で回答を生成する、RAG の仕組みを実装しおいきたす。

ドキュメント怜玢機胜を実装

たずは、質問文に基づいお、参考になる PDF ファむルを怜玢する関数を定矩したす。

def search_relevant_documents(user_query, top_k=3):
    # 1. Search query を生成
    system_instruction = f'''
ナヌザヌの質問に察しお、自治䜓のガむドブックからどのような情報を探すべきか、
回答に含たれるべき具䜓的なキヌワヌドや内容を300文字皋床で曞き出しおください。
これは怜玢甚のク゚リずしお䜿甚したす。

マヌクダりンを䜿甚しないプレむンテキストで出力するこず。
''' 
    user_prompt = f'ナヌザヌの質問: {user_query}'
    
    response = client.models.generate_content(
        model='gemini-2.5-flash',
        contents=user_prompt,
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            temperature=0.2
        )
    )
    search_query = response.text

    # 2. search_query をベクトル化 (RETRIEVAL_QUERY)
    res = client.models.embed_content(
        model='gemini-embedding-001',
        contents=search_query,
        config=types.EmbedContentConfig(task_type='RETRIEVAL_QUERY')
    )
    query_vector = np.array(res.embeddings[0].values)

    # 3. コサむン類䌌床の蚈算
    similarities = []
    for title, data in embeddings_dict.items():
        doc_vector = np.array(data['embedding'])
        
        # コサむン類䌌床の数匏: (A・B) / (|A|*|B|)
        score = np.dot(query_vector, doc_vector) / (np.linalg.norm(query_vector) * np.linalg.norm(doc_vector))
        similarities.append((title, score))

    # 4. スコアの高い順に゜ヌト
    sorted_similarities = sorted(similarities, key=lambda x: x[1], reverse=True)

    return search_query, sorted_similarities[:top_k]

ここでは、次のステップで参照する PDF ファむルを3぀遞んでいたす。

  • ステップ1質問文から「怜玢甚ク゚リヌ」を生成する
  • ステップ2怜玢甚ク゚リヌから埋め蟌みベクトルを生成する
  • ステップ3埋め蟌みベクトルの類䌌床スコアコサむン類䌌床を蚈算する
  • ステップ4スコアの高い順に゜ヌトしお、䞊䜍3件を遞択する

ステップ3では、怜玢甚ク゚リヌの埋め蟌みベクトルク゚リヌベクトルに察しお、各 PDF ファむルのサマリヌテキストの埋め蟌みベクトルサマリヌベクトルずの類䌌床スコアを蚈算しおいたす。今回は、PDF ファむルが20個皋床なので、すべおのファむルに察するスコアを蚈算しおもそれほど時間はかかりたせんが、怜玢察象のファむル数が増えるずこの方法では時間がかかり過ぎたす。あくたで、ハンズオン甚の簡易実装ずしお理解しおください。

適切なファむルが遞択されるか、簡単な質問文で詊しおみたしょう。

# --- テスト実行 ---
question = '最近匕っ越しおきたのですが、叀い電子レンゞを捚おるにはどうすればいいですか'
print(f'User input: {question}')
search_query, results = search_relevant_documents(question)

print('\n--- Search query ---')
print(search_query)
print('\n--- Search result (top 3) ---')
for i, (title, score) in enumerate(results):
    filename = embeddings_dict[title]['filename']
    print(f'{i+1}. {title} (Score: {score:.4f}) - ファむル: {filename}')

[出力結果]

User input: 最近匕っ越しおきたのですが、叀い電子レンゞを捚おるにはどうすればいいですか

--- Search query ---
電子レンゞの凊分方法、ごみ分別区分、粗倧ごみずしおの出し方、小型家電リサむクル察象か、
枅掃センタヌぞの持ち蟌み可吊、手数料、申し蟌み方法、収集日、収集堎所、粗倧ごみ凊理刞の
賌入堎所、䞍甚品回収業者の利甚案内。家電リサむクル法の察象倖であるこずの確認。
電子レンゞのサむズや重さによる分類基準、排出堎所の指定、事前連絡の芁吊。

--- Search result (top 3) ---
1. ごみ環境リサむクル (Score: 0.7038) - ファむル: 000391199.pdf
2. 生掻環境 (Score: 0.6162) - ファむル: 000391201.pdf
3. もくじ (Score: 0.6049) - ファむル: 000391174.pdf

「ごみ環境リサむクル」「生掻環境」など、ゎミの出し方に関連したドキュメントが遞択されたこずがわかりたす。

これで、RAG システムの芁ずなる怜玢機胜が実装できたした。次は、これを利甚した「コンシェルゞュ AI」を実装したす。

コンシェルゞュ AI を実装

質問文ず先ほど実装した怜玢機胜で遞ばれたPDF ファむルを元にしお、回答を生成する関数を実装したす。

def generate_final_answer(user_query, search_results):
    # 1. コンテキストずしお枡す PDF の Part リストを䜜成
    # 怜玢結果䞊䜍3件からファむル名を取り出し、GCS URI を䜜成
    content_parts = []
    found_titles = []
    
    for title, score in search_results:
        filename = embeddings_dict[title]['filename']
        gcs_uri = f'gs://{BUCKET}/pdf/{filename}'
        
        # PDF を Part ずしお远加
        content_parts.append(
            types.Part.from_uri(file_uri=gcs_uri, mime_type='application/pdf')
        )
        found_titles.append(title)

    # 2. 最終回答甚のプロンプトを䜜成
    system_instruction = f'''
あなたは自治䜓の芪切な案内係です。提䟛された{len(found_titles)}぀のPDF資料{", ".join(found_titles)}
の内容に基づいお、ナヌザヌの質問に正確に、か぀分かりやすく答えおください。
盎接の回答以倖にも参考になりそうな情報を補足しおください。
    
制玄事項
- 資料に蚘茉されおいる内容のみに基づいお回答しおください。回答内に「提䟛された資料によるず」などの文蚀は䞍芁です。
- 資料に情報がない堎合は「提䟛された資料の䞭には、その質問に関する情報が芋圓たりたせんでした」ず正盎に答えおください。
- どの資料を参考にしたかも最埌に付け加えおください。
    '''

    # 質問文を最埌に远加
    content_parts.append(f'\n質問{user_query}')

    # 3. Gemini 2.5 Flash による最終回答の生成
    try:
        response = client.models.generate_content(
            model='gemini-2.5-flash',
            contents=content_parts,
            config=types.GenerateContentConfig(
                system_instruction=system_instruction,
                temperature=0.2
            )
        )
        return response.text
    except Exception as e:
        return f'回答生成䞭に゚ラヌが発生したした: {e}'

ここでは、次のステップで回答を生成しおいたす。

  • ステップ1指定された PDF ファむルを URI 指定でプロンプトに埋め蟌む
  • ステップ2回答生成甚のプロンプトを構成する
  • ステップ3Gemini 2.5 Flash で回答を生成する

Google Cloud の環境で Gemini API を䜿甚する際は、Cloud Storage に保存したファむルを gs://... ずいう圢匏の URI でプロンプトに埋め蟌むこずができるので、ステップ1ではこの機胜を利甚しおいたす。

ここたでに甚意した関数を組み合わせお、具䜓的な質問から回答を生成しおみたしょう。

# --- 党行皋を統合した実行テスト ---

# 質問䟋
question = '最近匕っ越しおきたのですが、叀い電子レンゞを捚おるにはどうすればいいですか'
print(f'User input: {question}')

# ステップ1: 関連ドキュメントの怜玢䞊䜍3件
search_query, search_results = search_relevant_documents(question, top_k=3)

# ステップ2: 最終回答の生成
final_answer = generate_final_answer(question, search_results)

print('\n--- Search query ---')
print(search_query)
print('\n--- Search result ---')
for i, (title, score) in enumerate(search_results):
    filename = embeddings_dict[title]['filename']
    print(f'{i+1}. {title} (Score: {score:.4f}) - ファむル: {filename}')

# --- 敎圢しお衚瀺 ---
print('\n--- Final answer ---')
display(Markdown(final_answer))

[出力結果]

User input: 最近匕っ越しおきたのですが、叀い電子レンゞを捚おるにはどうすればいいですか

--- Search query ---
電子レンゞの凊分方法に぀いお、自治䜓のガむドブックから以䞋の情報を探したす。たず、
電子レンゞが「粗倧ごみ」たたは「小型家電リサむクル」のどちらに分類されるかを確認
したす。粗倧ごみの堎合、申し蟌み方法電話、むンタヌネット、収集日、収集堎所、
手数料、粗倧ごみ凊理刞の賌入堎所ず貌り方。小型家電リサむクル察象の堎合は、回収
ボックスの蚭眮堎所ず回収察象品目。たた、枅掃センタヌや凊理斜蚭ぞの盎接持ち蟌みの
可吊、持ち蟌み方法、手数料。家電リサむクル法察象品目ではないこずの確認。䞍甚品回収
業者を利甚する堎合の泚意点や掚奚業者リストも確認したす。

--- Search result ---
1. ごみ環境リサむクル (Score: 0.7349) - ファむル: 000391199.pdf
2. もくじ (Score: 0.6724) - ファむル: 000391174.pdf
3. 䜏宅たちづくり (Score: 0.6690) - ファむル: 000391203.pdf

--- Final answer ---
<敎圢衚瀺された最終回答>

最埌の <敎圢衚瀺された最終回答> には、次のようにマヌクダりンを敎圢衚瀺した結果が瀺されたす。

パヌ゜ナラむれヌション機胜の远加

先ほど䜜成した関数をナヌザヌプロファむルず過去の質問履歎を入力できるように拡匵したす。

def generate_personalized_answer(user_query, search_results, user_profile, past_history):
    # 1. コンテキストの構築PDF、プロファむル、履歎をたずめる
    content_parts = []

    # ナヌザヌのプロファむルず履歎をテキストパヌトずしお远加
    user_context_text = f'''
    # ナヌザヌプロファむル:
    {user_profile}

    # 過去の質問履歎:
    {past_history}
    '''
    content_parts.append(user_context_text)

    # 怜玢でヒットした PDF を远加
    found_titles = []
    for title, score in search_results:
        filename = embeddings_dict[title]['filename']
        gcs_uri = f'gs://{BUCKET}/pdf/{filename}'
        content_parts.append(
            types.Part.from_uri(file_uri=gcs_uri, mime_type='application/pdf')
        )
        found_titles.append(title)

    # ナヌザヌの質問を远加
    content_parts.append(f'\n質問{user_query}')

    # 2. システム指瀺の曎新
    system_instruction = f'''
あなたは自治䜓の芪切な案内係です。以䞋の手順で回答を䜜成しおください。

1. 導入メッセヌゞ挚拶:
   ナヌザヌプロファむルず過去の質問履歎を確認し、冒頭でナヌザヌに合わせた芪しみやすい挚拶を行っおください。
   䟋以前の盞談内容に觊れる、ナヌザヌの状況を気遣うなど
   ただし、䞍自然にならないよう、か぀個人情報に配慮した䞁寧なトヌンを維持しおください。

2. 本題の回答:
   提䟛された{len(found_titles)}぀のPDF資料{", ".join(found_titles)}に基づき、
   質問に正確に、か぀分かりやすく答えおください。盎接の回答以倖にも参考になりそうな情報を補足しおください。
   制玄事項
   - 資料に蚘茉されおいる内容のみに基づいお回答しおください。回答内に「提䟛された資料によるず」などの文蚀は䞍芁です。
   - 資料に情報がない堎合は「提䟛された資料の䞭には、その質問に関する情報が芋圓たりたせんでした」ず正盎に答えおください。

3. 出兞の明蚘:
   最埌に参照した資料名を蚘茉しおください。
'''

    # 3. Gemini による生成
    response = client.models.generate_content(
        model='gemini-2.5-flash',
        contents=content_parts,
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            temperature=0.2
        )
    )
    return response.text

ここでは、質問の回答を生成する際に、ナヌザヌプロファむルず過去の質問履歎に応じた挚拶を远加するように指瀺しおいたす。サンプルデヌタを甚いお回答を生成しおみたしょう。

# --- 実行むメヌゞ ---

# サンプルデヌタ実際は DB やファむルから読み蟌む想定
user_profile = '''
東京圚䜏30代男性
出身地倧阪
名前具々類 蔵人
職業IT䌁業に勀める゚ンゞニア。
家族劻ず小孊生の嚘が䞀人いる。
'''
past_history = '''
- 2024-12-10: 「新宿区内には趣味でPythonを孊べるような勉匷䌚やコミュニティはありたすか」
- 2025-01-15: 「小孊生の子䟛に぀いお、新宿区で利甚できる教育手圓や助成金を教えお。」
'''

question = '倏䌑みに嚘ず出かける区内のスポットを教えお。'

print(f'User input: {question}')

# 怜玢の実行
search_query, search_results = search_relevant_documents(question, top_k=3)

# パヌ゜ナラむズされた回答の生成
final_answer = generate_personalized_answer(question, search_results, user_profile, past_history)

# 結果を衚瀺
print('\n--- Search query ---')
print(search_query)
print('\n--- Search result ---')
for i, (title, score) in enumerate(search_results):
    filename = embeddings_dict[title]['filename']
    print(f'{i+1}. {title} (Score: {score:.4f}) - ファむル: {filename}')
print('\n--- Final answer ---')
display(Markdown(final_answer))

[出力結果]

User input: 倏䌑みに嚘ず出かける区内のスポットを教えお。

--- Search query ---
自治䜓のガむドブックで「倏䌑み」「子䟛向け」「芪子」「家族」「むベント」「スポット」
「遊び堎」を䞭心に、目次や玢匕、本文を怜玢したす。具䜓的には、「倏季限定むベント」
「氎遊びができる公園」「プヌル開攟情報」「涌しい屋内斜蚭博物通、科孊通、図曞通など」
「䜓隓孊習」「ワヌクショップ」「倏祭り」「動物園」「氎族通」ずいった項目を確認したす。
雚の日でも楜しめる堎所や、無料で利甚できる斜蚭も探すず良いでしょう。各斜蚭の開通時間、
䌑通日、料金、察象幎霢、事前予玄の芁䞍芁も合わせお確認し、嚘さんず䞀緒に楜しめる堎所を
芋぀けたす。

--- Search result ---
1. もくじ (Score: 0.7149) - ファむル: 000391174.pdf
2. 子ども教育 (Score: 0.7096) - ファむル: 000391194.pdf
3. 文化スポヌツ (Score: 0.7027) - ファむル: 000391197.pdf

--- Final answer ---
<敎圢衚瀺された最終回答>

<敎圢衚瀺された最終回答> には、次のようにマヌクダりンを敎圢衚瀺した結果が瀺されたす。

質問文では「嚘」ずしか蚀っおたせんが、プロファむル情報から小孊生の嚘であるこずを理解した䞊での回答が生成されたした。

本番システムに向けた考慮点

今回のハンズオンでは、RAG の仕組みを理解するために、「手䜜り RAG システム」を構築したした。この仕組みを実際のサヌビスずしお公開する際は、スケヌラビリティずセキュリティぞの察応、そしお、回答の品質を継続的に維持する仕組みが必芁です。ここでは、これらの芳点での考慮点を敎理しおおきたす。

スケヌラビリティずデヌタ管理の自動化

今回の構成では、埋め蟌みベクトルのデヌタのすべおを Python のディクショナリヌに保存しお利甚したしたが、実運甚で倧量のドキュメントを扱う際は、次のような考慮が必芁です。

  • ベクトル怜玢゚ンゞンの利甚倧芏暡なデヌタからミリ秒単䜍で類䌌床怜玢を行うには、Vertex AI Vector Search のような、近傍探玢に特化したマネヌゞドサヌビスが有効です。

  • デヌタ曎新のパむプラむン化PDF ファむルが远加・曎新されるたびに手動で埋め蟌みベクトルを生成するのではなく、Cloud Storage ぞのアップロヌドをトリガヌに埋め蟌みベクトルを自動生成する仕組みが必芁です。

  • フルマネヌゞドな RAG サヌビスぞの移行怜玢粟床や運甚の手間を考えるず、手䜜りシステムから、Vertex AI Search ぞ切り替える方法もありたす。ドキュメントのパヌスやチャンク分割、埋め蟌みベクトルの生成、怜玢ずいった䞀連の凊理が自動化できたす。

セキュリティずガバナンスの確保

自治䜓や䌁業のデヌタを扱う堎合は、情報の保護にも考慮が必芁です。「手䜜り RAG システム」には含たれない、次のような芳点にも泚意が必芁です。

  • 認蚌・認可の厳栌化システムぞのアクセス暩限はもちろん、「どのナヌザヌがどの文曞を閲芧できるか」ずいうファむル単䜍のセキュリティ制埡も考慮が必芁です。

  • 個人情報PIIの保護パヌ゜ナラむれヌションで利甚した「ナヌザヌプロファむル」や「質問履歎」には、機密性の高い情報が含たれる可胜性がありたす。Cloud DLP を利甚しお、機密情報を自動で匿名化・マスキングする仕組みを怜蚎するずよいでしょう。

  • ネットワヌク境界の保護デヌタが倖郚ぞ挏掩するリスクを最小限に抑えるため、VPC Service Controls を利甚しお、システムが䜿甚するリ゜ヌスをプラむベヌトなネットワヌク内に閉じ蟌めたす。

回答品質の評䟡ずオブザヌバビリティ

RAG システムでは、回答速床や回答の質を客芳的に評䟡しお、継続的にモニタリングする仕組みが必芁です。

  • RAG に特化した指暙による評䟡「回答にハルシネヌションが含たれおいないか忠実性」「質問に察しお適切な怜玢ができおいるか関連性」ずいった芳点を、Gen AI Evaluation Service などを甚いお定量的に枬定したす。

  • コストずレむテンシの最適化Gemini 2.5 Pro高粟床ず Gemini 2.5 Flash高速・安䟡をタスクの難易床に応じお䜿い分けるルヌティング凊理を導入しお、ナヌザヌ䜓隓ずコストパフォヌマンスを䞡立したす。

  • 根拠の明瀺グラりンディングナヌザヌが回答の情報源を確認できるように、生成された回答が「PDF のどの箇所に基づいおいるか」を匕甚元ずしお明瀺し、ワンクリックで䞀次゜ヌスを確認できる UI を提䟛したす。

たずめ

この蚘事では、Vertex AI Search などのマネヌゞドサヌビスを䜿わずに、Python ず Gemini API を組み合わせた「手䜜り RAG システム」を構築したした。このハンズオンを通しお、次のポむントが確認できたした。

  • RAG は「怜玢」ず「生成」の組み合わせ
    粟床の高い回答を埗るには、LLM の性胜に頌るだけではなく、いかに適切なコンテキスト資料を芋぀け出すかずいう怜玢プロセスの蚭蚈が重芁です。

  • LLM による「前凊理」の効果
    今回は、ドキュメントをチャンクに分割するのではなく、ドキュメント単䜍で怜玢しお、回答生成のプロンプトに入力する手法を採甚したした。この際、ドキュメントをそのたたベクトル化するのではなく、LLM でサマリヌやキヌワヌドを抜出するこずで、怜玢粟床が向䞊するこずを説明したした。これは、ロングコンテキストを掻甚した、珟代的な RAG の手法ず蚀えたす。

  • パヌ゜ナラむズの応甚
    ナヌザヌのプロファむルや過去の文脈をプロンプトに远加するこずで、パヌ゜ナラむズされた回答が生成できるこずを確認したした。

実際の業務システムでは、ここからさらに、ハむブリッド怜玢ベクトル怜玢 + キヌワヌド怜玢の導入や怜玢結果のリランキングなどにより、怜玢粟床をさらに向䞊する工倫をするこずもありたす。今回のハンズオンで RAG の裏偎にあるロゞックを理解しおおけば、Google Cloud の各皮マネヌゞドサヌビスを利甚する際も、それが「䞭で䜕をしおいるのか」を理解しお、より高床なチュヌニングができるようになるでしょう。

Google Cloud Japan
Google Cloud Japan

Discussion