0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

学習記録 #5 後編】GAS × Gemini でRAGチャットボットを自作した

0
Posted at

【学習記録 #5 後編】GAS × Gemini でRAGチャットボットを自作した ― 実装・デプロイ・精度改善計画編

はじめに

[前編]では、GASで自作する理由、AIのデータ処理の仕組み、環境構築のつまずきポイントを記録しました。

後編では、実装の詳細デプロイテスト結果、そして精度改善計画を記録します。

対象読者:エンジニア経験1年程度の方。前編を読んで環境構築まで完了している前提です。

この記事で分かる3点を最初に示します。

  1. RAGパイプラインの実装詳細 ― Embedding、検索、LLM呼び出しの各工程
  2. デプロイとテスト ― Web Appとして公開し、動作確認するまで
  3. 精度改善計画 ― 回答精度を上げるための具体的な改善ロードマップ

1. 実装のポイント解説

Claude Codeで一気にコード生成しましたが、生成されたコードの中で特に重要なポイントを解説します。

1-1. Embedding(ベクトル化)

テキストを数値配列に変換する処理です。RAGの精度を左右する最重要コンポーネント。

使用モデル: gemini-embedding-001

テキスト: 「腹水の主症は腹部膨大である」
    │
    │ Gemini Embedding API
    ▼
ベクトル: [0.12, -0.34, 0.56, 0.78, ...] (768次元)

重要ポイント:L2正規化

Gemini Embedding 2はMRL(Matryoshka Representation Learning)により、3072次元→768次元にスケールダウンできます。APIリクエスト時に outputDimensionality: 768 を明示指定することで次元数を削減できます。ただし次元数を変えるとベクトルの長さ(ノルム)が変わるため、L2正規化(ベクトルの長さを1に揃える処理)が必須です。

【L2正規化のイメージ】

正規化前:
  ベクトルA: 長さ 2.5 → [0.5, 1.0, 2.0, ...]
  ベクトルB: 長さ 1.2 → [0.3, 0.8, 0.6, ...]
  → 長さが違うので、類似度計算が不正確になる

正規化後:
  ベクトルA: 長さ 1.0 → [0.2, 0.4, 0.8, ...]
  ベクトルB: 長さ 1.0 → [0.25, 0.67, 0.5, ...]
  → 長さが揃ったので、内積 = コサイン類似度 になる

L2正規化を行うことで、コサイン類似度の計算が単純な内積で済むようになります。

taskType の使い分け:

場面 taskType 理由
資料をベクトル化するとき RETRIEVAL_DOCUMENT ドキュメント用の最適化がかかる
質問をベクトル化するとき RETRIEVAL_QUERY クエリ用の最適化がかかる

同じテキストでもtaskTypeが異なるとベクトルが変わります。ドキュメントとクエリで別の最適化がかかるため、正しく使い分けることで検索精度が向上します。

実装コード(核心部分):

// TODO: 実際のコードに差し替え(embedDocument / embedQuery の核心部分)
function embedText(text: string, taskType: string): number[] {
  const response = UrlFetchApp.fetch(
    `https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=${API_KEY}`,
    {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify({
        model: 'models/gemini-embedding-001',
        content: { parts: [{ text }] },
        taskType,
        outputDimensionality: 768,
      }),
    }
  );
  return JSON.parse(response.getContentText()).embedding.values;
}

1-2. ベクトルDB(Google Sheets)

本格的なベクトルDB(Pinecone、Chroma等)の代わりに、Google Sheetsをベクトルデータベースとして使っています。

Sheetsの構成:

内容
A id doc_001_chunk_0
B type subject or topic
C text チャンクのテキスト
D metadata {"fileName":"鼓脹.pdf","chunkIndex":0}
E embedding [0.12,-0.34,0.56,...]
F textHash a1b2c3d4...(差分ビルド用)

差分ビルド: テキストのMD5ハッシュを保存しておき、次回実行時にハッシュが変わっていないチャンクはスキップ。大量資料でもEmbedding APIの呼び出し回数を最小限に抑えられます。

【差分ビルドの流れ】

初回: 全チャンク → ベクトル化 → Sheets保存(100チャンク)
      処理時間: 約5分

2回目: ハッシュ比較
  ├── 変更なし: 90チャンク → スキップ
  └── 変更あり: 10チャンク → 再ベクトル化
      処理時間: 約30秒(90%節約)

1-3. 検索(コサイン類似度)

質問ベクトルと各チャンクベクトルの「意味の近さ」を計算して、最も関連性の高いチャンクを取得します。

質問: 「腹水の主症は?」
    │
    │ ベクトル化
    ▼
質問ベクトル: [0.15, -0.31, 0.53, ...]
    │
    │ 全チャンクとコサイン類似度を計算
    ▼
チャンク1: 類似度 0.89 ← 「腹水は腹部膨満を主症とし...」 ✅ 高い
チャンク2: 類似度 0.65 ← 「腹水の病位は...」
チャンク3: 類似度 0.31 ← 「頭痛の分類について...」      ❌ 低い(除外)
    │
    │ 類似度0.5以上のTop 3を返す
    ▼
LLMに渡すコンテキスト: チャンク1 + チャンク2 + チャンク3

実装コード(核心部分):

// TODO: 実際のコードに差し替え(cosineSimilarity の核心部分)
function cosineSimilarity(vecA: number[], vecB: number[]): number {
  // L2正規化済みベクトルなら内積 = コサイン類似度
  return vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);
}

ハイブリッド検索: 2種類のチャンクを別レーンで検索してマージする設計にしています。

【subjectチャンク】科目・テーマごとのまとまり
  → 「腹水について」の情報を広く取得

【topicチャンク】トピックごとのまとまり
  → 「主症」に関する情報を複数科目から取得

→ 両方のレーンの結果をマージして、より網羅的な検索を実現

1-4. LLM呼び出し(Gemini 2.5 Flash)

検索結果(関連チャンク)と質問を組み合わせて、LLMに回答を生成させます。

┌─────────────────────────────────────┐
│ LLMに渡すプロンプト                    │
│                                    │
│ 【SYSTEM】                          │
│ あなたは健康・医学の専門講師です。  │
│ ナレッジベースの情報を最優先で使用...    │
│                                    │
│ 【参考資料(検索結果)】               │
│ [資料1: 鼓脹.pdf]                    │
│ 腹水は腹部膨満を主症とし...            │
│                                    │
│ [資料2: 腹水.pdf]                    │
│ 腹水の症状では...            │
│                                    │
│ 【質問】                             │
│ 腹水の主症は何ですか?                │
└─────────────────────────────────────┘
         │
         │ Gemini 2.5 Flash
         ▼
┌─────────────────────────────────────┐
│ 回答 + 出典                          │
│                                    │
│ 腹水の主症は腹部膨大です。        │
│ 腹部の膨満...   │
│                                    │
│ 出典: 鼓脹.pdf                       │
└─────────────────────────────────────┘

Gemini Pro Preview 系を使って429エラーが出た話:

実際に GAS 版でも Gemini Pro Preview 系モデルで 429 エラーが発生しました(gemini-2.5-pro 系は無料枠が非常に少ない)。

429 RESOURCE_EXHAUSTED
Quota exceeded for metric: generate_content_free_tier_requests
limit: 0, model: gemini-2.5-pro

教訓: Preview版のProモデルは無料枠がほぼない場合がある。検証段階では Gemini 2.5 Flash を使う。

モデル 無料枠
gemini-2.5-pro(仮)※ ❌ なし
Gemini 2.5 Flash ✅ 15req/分、1500req/日

1-5. セキュリティ実装

以下のセキュリティ対策を実装しました。

対策 内容
APIキーの管理 スクリプトプロパティで管理(コードにハードコーディングしない)
入力バリデーション 空メッセージ拒否、2000文字制限
会話履歴のスキーマ検証 roleは "user" / "model" のみ許可
エラーメッセージの隠蔽 詳細はLoggerのみ、ユーザーには汎用メッセージ
【エラーハンドリングの考え方】

  ❌ 悪い例(ユーザーに詳細を見せる):
  「Error: API key AIzaSy... is invalid」
  → APIキーが漏洩する!

  ✅ 良い例(ユーザーには汎用メッセージ):
  「申し訳ありません。処理中にエラーが発生しました」
  → 詳細はLogger.logに記録(開発者だけが確認)

2. デプロイ

2-1. スクリプトプロパティの設定

GASエディタで「プロジェクトの設定」→「スクリプトプロパティ」に以下を登録。

プロパティ名 確認場所
GEMINI_API_KEY https://aistudio.google.com/apikey
DRIVE_FOLDER_ID DriveフォルダURLの /folders/ の後ろ
VECTOR_SHEET_ID スプレッドシートURLの /d/ の後ろ

2-2. 初回実行

1. GASエディタで「initialSetup」を実行 → ヘッダー作成等
2. Google Driveのフォルダに資料をアップロード(2〜3ファイル)
3. 「rebuildEmbeddings」を実行 → ベクトル化
4. スプレッドシートにデータが書き込まれたことを確認

つまずきポイント⑥:clasp open が使えない

Unknown command "clasp open"

claspの新しいバージョンでは open コマンドが廃止されています。

解決策: .clasp.json のscriptIdを使って直接URLを開く。

https://script.google.com/d/{scriptId}/edit

2-3. Web Appとしてデプロイ

1. GASエディタで「デプロイ」→「新しいデプロイ」
2. 種類: ウェブアプリ
3. 次のユーザーとして実行: 自分
4. アクセスできるユーザー: 自分のみ
5. 「デプロイ」をクリック
6. 表示されたURLをブラウザで開く

3. テスト結果

3-1. テスト環境

項目 内容
資料 鍼灸・東洋医学の講義資料(2〜3ファイル)
Embeddingモデル gemini-embedding-001(768次元)
LLMモデル Gemini 2.5 Flash
チャンクサイズ 1500文字 / オーバーラップ150文字
検索方式 ハイブリッド検索(subjectチャンクレーン + topicチャンクレーンを別々に検索してマージ)

3-2. テスト質問と結果

質問: 「鼓脹の主症は何ですか?」
回答: (TODO: 実際の回答テキストを貼る)
精度: まあ悪くない 🟡

質問: (TODO: 2問目の質問)
回答: (TODO: 実際の回答テキストを貼る)
精度: (TODO: 🟢/🟡/🔴)

質問: (TODO: 3問目の質問)
回答: (TODO: 実際の回答テキストを貼る)
精度: (TODO: 🟢/🟡/🔴)

3-3. Dify版との比較(同じ資料・同じ質問)

比較項目 Dify版 GAS版
回答速度 TODO: 実測値に差し替え(例: 平均2.1秒) TODO: 実測値に差し替え(例: 平均7.8秒)
回答精度 良い まあ悪くない
出典表示 あり あり
日本語品質 良い 良い
カスタマイズ性

4. 精度改善計画

詳細は次回記事(学習記録 #6)で取り上げます。

現時点の精度は「まあ悪くない」レベルです。ここから精度を上げるための改善計画を立てました。

4-1. 改善ロードマップ

Level テーマ 概要
Level 1 チャンク戦略の改善 チャンクサイズ最適化・セマンティックチャンキング・メタデータ充実
Level 2 プロンプトの改善 Few-shot追加・回答フォーマット指定・ハルシネーション抑制強化
Level 3 検索精度の改善 Rerankモデル導入・HyDE・クエリ拡張・Top K動的調整
Level 4 評価の仕組み化 テストセット作成・自動評価スクリプト・A/Bテスト・ログ分析

5. つまずきポイントまとめ(全体)

前後編を通じてのつまずきポイント一覧です。

# つまずき エラーメッセージ 解決策
1 clasp権限承認 (ブラウザ承認画面) 「すべて選択」→「続行」
2 webapp指定不可 Invalid container file type --type standalone に変更
3 GAS API未有効 User has not enabled the Apps Script API 設定ページでAPIをオン
4 TS型注釈エラー Unexpected token ':' clasp 3.x はTypeScriptをコンパイルしない。tscで事前コンパイルしてdist/からpush(詳細は前編参照)
5 マニフェスト上書き Do you want to push and overwrite? y で許可
6 clasp open不可 Unknown command "clasp open" scriptIdで直接URL作成(→ 2-2参照)
7 Gemini Pro無料枠 429 RESOURCE_EXHAUSTED Gemini 2.5 Flashに変更(→ 1-4参照)

まとめ

GAS × Gemini でRAGチャットボットを自作した全記録をまとめました。

達成したこと

  • Dify版とGAS版の2つのRAGチャットボット を同日に構築・テスト完了
  • GAS + TypeScript + clasp の開発環境を構築
  • Gemini Embedding(gemini-embedding-001)でベクトル化
  • Google SheetsをベクトルDBとして使用
  • Gemini 2.5 FlashでLLM回答生成
  • Web Appとしてデプロイし、ブラウザからチャット可能に

学んだこと

1. RAGの全工程を「手触り」で理解できた
   → Embedding、ベクトル検索、コンテキスト構築の仕組み

2. clasp 3.x は TypeScript をコンパイルしない
   → `npm run push`(tsc → dist/ → clasp push)の3ステップが必要(前編参照)

3. Gemini APIの無料枠はモデルによって大きく違う
   → Pro系は要注意、Flash系を使う

4. 精度は「動いてから」改善するもの
   → まず動くものを作り、テストして改善するサイクルが重要

5. データの行き先を意識する癖がついた
   → クラウドAPIに送信 = 外部にデータが出る、を常に意識

次のステップ

精度改善計画のLevel 1(テストセット作成 + プロンプト改善)から着手予定です。


参考リンク


次回予告:[学習記録 #6] 精度改善サイクル ― テストセット作成とプロンプト最適化

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?