【学習記録 #5 後編】GAS × Gemini でRAGチャットボットを自作した ― 実装・デプロイ・精度改善計画編
はじめに
[前編]では、GASで自作する理由、AIのデータ処理の仕組み、環境構築のつまずきポイントを記録しました。
後編では、実装の詳細、デプロイ、テスト結果、そして精度改善計画を記録します。
対象読者:エンジニア経験1年程度の方。前編を読んで環境構築まで完了している前提です。
この記事で分かる3点を最初に示します。
- RAGパイプラインの実装詳細 ― Embedding、検索、LLM呼び出しの各工程
- デプロイとテスト ― Web Appとして公開し、動作確認するまで
- 精度改善計画 ― 回答精度を上げるための具体的な改善ロードマップ
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(テストセット作成 + プロンプト改善)から着手予定です。
参考リンク
- [【学習記録 #5 前編】GAS × Gemini でRAGチャットボットを自作した ― 環境構築・つまずきポイント編](TODO: 前編記事のQiita URLを貼る)
- GAS + Gemini で作る社内RAGチャットボット - Qiita(ogaryoさん)
- Google Apps Script 公式
- Gemini API ドキュメント
- Gemini Embedding API
- clasp GitHub
次回予告:[学習記録 #6] 精度改善サイクル ― テストセット作成とプロンプト最適化