【学習記録 #5 前編】GAS × Gemini でRAGチャットボットを自作した ― 設計・環境構築編
はじめに
前回の記事でDifyを使ったノーコードRAGチャットボットを構築しました。今回は同じ医学の学習用チャットボットを、GAS(Google Apps Script)で1からコードを書いて自作 した全記録です。
「Difyで動くものはできた。でもRAGの中身をブラックボックスのままにしたくない」「GASなら無料でサーバーレスに運用できる」という動機で、自作に挑戦しました。
本記事は前後編に分かれています。
| 記事 | 内容 |
|---|---|
| 前編(本記事) | 設計思想・環境構築・つまずきポイント |
| 後編 | 実装詳細・デプロイ・テスト・精度改善計画 |
対象読者:エンジニア経験1年程度の方。GASを触ったことがあるけど、RAGやEmbeddingは初めてという方を想定しています。
この記事で分かる3点を最初に示します。
- なぜDifyではなくGASで自作するのか ― Dify版との比較と自作の意義
- AIチャットボットのデータの流れ ― クラウドAPI vs ローカル、個人情報の扱い
- GAS + clasp の環境構築 ― つまずきポイント(GAS API有効化、clasp pushエラー等)
1. なぜGASで自作するのか
1-1. Dify版で感じたこと
前回Difyで構築したRAGチャットボットは、数時間で動くものができました。しかし、以下の点が気になりました。
┌───────────────────────────────┐
│ Dify版の良かった点 │
│ ✅ 数時間で動くものができた │
│ ✅ GUIでチャンク設定を変更可能 │
│ ✅ LLMモデルの切り替えが簡単 │
└───────────────────────────────┘
┌───────────────────────────────┐
│ Dify版で気になった点 │
│ ❓ RAGの中身がブラックボックス │
│ ❓ データがDifyのサーバーに送られる │
│ ❓ カスタマイズに限界がある │
│ ❓ 将来的に医療情報を扱えない │
└───────────────────────────────┘
1-2. GAS自作のメリット
| 比較項目 | Dify版 | GAS版 |
|---|---|---|
| 構築時間 | 数時間 | 半日〜1日 |
| 学習価値 | △ ブラックボックス | ◎ RAGの全工程を理解 |
| カスタマイズ | △ GUI制約あり | ◎ コードで何でも可能 |
| データ保管 | Difyのサーバー | 自分のGoogleアカウント |
| コスト | 無料〜$59/月 | 完全無料 |
| 常時稼働 | ✅ | ✅(GAS Web App) |
| スキルシートに書ける | △ | ◎「RAG構築経験」 |
1-3. 自作で学べたこと(結論の先出し)
GASで自作したことで、以下が「手触り」として理解できました。
1. Embeddingとは何か → テキストを数値配列に変換する処理
2. ベクトル検索とは何か → コサイン類似度で「意味の近さ」を計算
3. チャンクの切り方が精度を左右する → 大きすぎても小さすぎてもダメ
4. L2正規化の意味 → ベクトルの長さを揃えないと類似度計算がおかしくなる
5. RAGのボトルネック → 検索精度がLLMの回答品質の上限を決める
2. AIはデータをどう処理するのか
自作を始める前に、AIがデータをどう扱っているか を正しく理解する必要がありました。特に将来的に医療情報を扱いたい場合、この理解は必須です。
2-1. クラウドAPI方式(Dify/Gemini/Claude等)
┌──────────────────────┐
│ あなたの環境 │
│ 質問 + 資料の抜粋 │
│ │ │
└─────────┼─────────────┘
│ インターネットで送信
▼
┌──────────────────────┐
│ Google/Anthropic │
│ のサーバー │
│ 🤖 AIが処理 │
│ │ │
└─────────┼─────────────┘
│ 回答を返す
▼
┌──────────────────────┐
│ あなたの環境 │
│ 回答を表示 │
└──────────────────────┘
重要: 質問だけでなく、ナレッジベースから検索された資料の抜粋も外部サーバーに送信される。これは個人情報や医療情報を扱う場合に法的リスクになります。
2-2. ローカルLLM方式
┌──────────────────────┐
│ あなたのPC内部 │
│ │
│ 質問 → 🤖 ローカルAI │
│ 資料 → 処理 │
│ │ │
│ ▼ │
│ 回答を表示 │
│ │
│ 外部に一切出ない ✅ │
└──────────────────────┘
2-3. 今回のGAS版はどちら?
今回のGAS版は クラウドAPI方式 です。Gemini APIにデータを送信しています。
GAS版のデータの流れ:
Google Drive(資料)
│
│ GASでテキスト抽出
▼
Gemini Embedding API ← テキストが送信される
│
│ ベクトルを返す
▼
Google Sheets(ベクトルDB)
│
│ ユーザーの質問時
▼
コサイン類似度検索(GAS内部)← ここはローカル処理
│
│ 関連チャンク + 質問
▼
Gemini LLM API ← テキストが送信される
│
│ 回答を返す
▼
ユーザーに表示
つまり: 公開情報(教科書、論文等)のみを扱う分には問題ありませんが、個人情報やカルテを入れるのはNGです。
2-4. 将来のハイブリッド構成
個人情報を扱う場合の解決策として、匿名化ハイブリッド構成 を検討しました。
【ローカル環境】カルテ → 個人情報を除去 → 匿名化クエリ
│
│ 個人情報を含まない検索クエリだけ送信
▼
【クラウド環境】公開資料のナレッジベースで検索 → 回答
│
│ 回答をローカルに戻す
▼
【ローカル環境】回答とカルテを照合して最終判断
例えば:
❌ 悪い例(個人情報を含む):
「田中太郎さん、45歳男性、糖尿病15年、不眠と腰痛を訴える」
✅ 良い例(匿名化済み):
「2型糖尿病の中年男性にみられる不眠+腰痛に対する
東洋医学的治療法を知りたい」
今回のGAS版では匿名化モジュールのスタブ(将来用の空関数)だけ用意してあり、将来的に実装する設計になっています。
3. GAS版の全体アーキテクチャ
3-1. 構成図
3-2. 各ファイルの役割
| ファイル | 役割 | 主な処理 |
|---|---|---|
config.ts |
設定管理 | APIキーや定数をスクリプトプロパティから取得 |
main.ts |
エントリーポイント | doGet、入力バリデーション |
ingest.ts |
資料取込 | Drive→テキスト抽出→チャンク分割 |
embedding.ts |
ベクトル化 | Gemini Embedding API呼び出し + L2正規化 |
vectorStore.ts |
ベクトルDB | Sheetsへの保存・読み込み |
search.ts |
類似検索 | コサイン類似度 + ハイブリッド検索 |
chat.ts |
RAGパイプライン | 検索→コンテキスト構築→LLM呼び出し |
index.html |
チャットUI | ブラウザで動くチャット画面 |
3-3. RAGパイプラインのフロー
ユーザーの質問
│
▼
① 質問をベクトル化(embedding.ts)
│
▼
② ベクトルDBから類似チャンクを検索(search.ts)
│
▼
③ システムプロンプト + 関連チャンク + 質問 を組み立て(chat.ts)
│
▼
④ Gemini LLMに送信して回答生成(chat.ts)
│
▼
⑤ 回答 + 出典情報 をユーザーに表示(index.html)
4. 環境構築 -- つまずきポイント全記録
4-1. 必要なもの
| 必要なもの | 用途 | 費用 |
|---|---|---|
| Googleアカウント | GAS、Drive、Sheets | 無料 |
| Node.js(v22以上) | clasp実行環境 | 無料 |
| Gemini APIキー | Embedding + LLM | 無料枠あり |
| Google Driveフォルダ | 資料の保管 | 無料 |
| Googleスプレッドシート | ベクトルDB | 無料 |
4-2. clasp インストール + ログイン
npm install -g @google/clasp
clasp login
つまずきポイント①:権限の承認
clasp login するとブラウザが開き、Googleアカウントへのアクセス許可を求められます。
┌─────────────────────────────────────┐
│ clasp – The Apps Script CLI が │
│ Google アカウントへのアクセスを │
│ 求めています │
│ │
│ ☐ すべて選択 ← これにチェック! │
│ │
│ [キャンセル] [続行] │
└─────────────────────────────────────┘
「すべて選択」にチェックして「続行」 をクリック。claspがDriveやGASプロジェクトを操作するために全ての権限が必要です。
4-3. プロジェクト初期化
mkdir gas-rag-chatbot
cd gas-rag-chatbot
npm init -y
npm install -D typescript @types/google-apps-script
4-4. GASプロジェクト作成
clasp create --type standalone --title "鍼灸学習Bot"
つまずきポイント②:--type webapp が使えない
最初 clasp create --type webapp を試したところ、以下のエラーが出ました。
Invalid container file type
解決策: --type standalone で作成する。standaloneでも後からWebアプリとしてデプロイできるため、機能的に問題ありません。
つまずきポイント③:GAS APIが無効
User has not enabled the Apps Script API.
Enable it by visiting https://script.google.com/home/usersettings
解決策: https://script.google.com/home/usersettings にアクセスして「Google Apps Script API」をオンにする。反映に数分かかることがあるので、有効化後少し待ってからリトライ。
4-5. TypeScript設定
npx tsc --init
基本の tsconfig.json を生成した後、GAS向けに書き直します(後述の調査を経て最終的に以下の設定に落ち着きました)。
4-6. 最大の罠:clasp 3.x は TypeScript をコンパイルしない
これが今回一番のはまりポイントです。同じエラーに3回つまずき、原因を調査して初めて根本的な問題がわかりました。
エラー発生
clasp push を実行すると、こんなエラーが返ってきました。
Syntax error: SyntaxError: Unexpected token ':'
line: 22 file: anonymize.gs
TypeScriptの型注釈(: string など)がそのままGASに送られ、JavaScriptとして解釈できずにクラッシュしていました。
// anonymize.ts の22行目
function anonymizeQuery(karteText: string, userQuestion: string): string {
// ^^^^^^ ^^^^^^
// ここの ':' が GAS で構文エラーになる
GASはJavaScriptしか理解できないので、TypeScriptの型注釈を事前に除去する必要があります。
試行①:"module": "none" を追加(失敗)
ネットで調べると「tsconfig.json に "module": "none" を追加するとGASで動く」という情報が出てきます。試してみました。
{
"compilerOptions": {
"module": "none"
}
}
しかし今度は別のエラー。
tsconfig.json: Option 'module=None' is deprecated and will stop
functioning in TypeScript 7.0. Specify compilerOption
'"ignoreDeprecations": "6.0"' to silence this error.
TypeScript 6ではこのオプション自体がdeprecated扱いになっており、コンパイルエラーになります。
試行②:"ignoreDeprecations": "6.0" を追加(失敗)
エラーメッセージの指示通りに追加。
{
"compilerOptions": {
"module": "none",
"ignoreDeprecations": "6.0"
}
}
tsc --noEmit はエラーなしで通過。「これで直った!」と思って clasp push を実行すると……
Syntax error: SyntaxError: Unexpected token ':'
line: 22 file: anonymize.gs
同じエラーが再発。tsconfig を何度直しても変わりません。
調査:clasp の内部を確認する
「tsconfig の問題ではないのかもしれない」と思い、clasp 自体を調べることにしました。
# claspのバージョン確認
clasp --version
# → 3.3.0
# claspの依存関係を確認
cat $(npm root -g)/@google/clasp/package.json
package.json の dependencies を見て、驚きました。
{
"dependencies": {
"@formatjs/intl": "^3.1.6",
"@modelcontextprotocol/sdk": "^1.12.1",
"chalk": "^5.4.1",
"chokidar": "^4.0.3",
"commander": "^13.1.0",
"googleapis": "^148.0.0",
"inquirer": "^12.6.0",
...
}
}
typescript が dependencies に存在しない。
根本原因:clasp 3.x は TypeScript を捨てた
clasp 2.x には TypeScript コンパイラが組み込まれていました。しかし clasp 3.x でTypeScriptへの依存が完全に削除されました。
clasp 3.x の実際の挙動はこうです。
.ts ファイルを発見
│
│ コンパイルしない
│ 拡張子だけ .gs に変換
▼
型注釈がそのままの状態で GAS にアップロード
│
▼
GAS「これ JavaScript じゃないぞ → Syntax Error」
tsconfig を何回書き直しても無意味だったわけです。clasp がそもそも TypeScript を読んでいないのですから。
解決策:tsc で事前コンパイルして dist/ から push する
clasp に頼るのをやめ、プロジェクト自身の TypeScript(tsc)でコンパイルし、生成された JavaScript を clasp に push させる 構成に変えました。
src/*.ts
│
│ tsc(プロジェクトのTypeScriptコンパイラ)
▼
dist/*.js ← 型注釈が除去された純粋なJavaScript
│
│ clasp push(JSファイルをそのままアップロード)
▼
GAS「これはJavaScript! → OK」
ファイル構成の変更
gas-rag-chatbot/
├── src/ ← TypeScript ソース(ここを編集する)
│ ├── config.ts
│ ├── main.ts
│ └── html/
│ └── index.html
├── dist/ ← tsc が出力するJS(clasp はここから push する)
│ ├── config.js
│ ├── main.js
│ └── html/
│ └── index.html
├── tsconfig.json ← outDir: "./dist" を追加
├── .clasp.json ← rootDir: "dist" に変更
└── package.json ← ビルドスクリプトを追加
tsconfig.json(最終版)
{
"compilerOptions": {
"target": "ES2019",
"lib": ["ES2019"],
"module": "none",
"ignoreDeprecations": "6.0",
"strict": true,
"rootDir": "./src",
"outDir": "./dist",
"types": ["google-apps-script"],
"skipLibCheck": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
| 設定 | 値 | 理由 |
|---|---|---|
| target | ES2019 | GAS V8エンジンはES2019まで確実にサポート |
| module | none | GASはモジュールシステムを使わない |
| ignoreDeprecations | 6.0 | TypeScript 6で module: none がdeprecationエラーになるため |
| rootDir | ./src | TypeScriptソースの場所 |
| outDir | ./dist | コンパイル済みJSの出力先 |
| types | google-apps-script | DriveApp・SpreadsheetApp等の型定義 |
.clasp.json(rootDir を dist に変更)
{
"scriptId": "your-script-id",
"rootDir": "dist",
"scriptExtensions": [".js", ".gs"],
"htmlExtensions": [".html"],
"jsonExtensions": [".json"]
}
package.json(ビルドスクリプトを追加)
{
"scripts": {
"build": "tsc && cp -r src/html dist/ && cp appsscript.json dist/",
"push": "npm run build && clasp push",
"clean": "rm -rf dist"
}
}
HTMLファイルと appsscript.json は tsc がコピーしてくれないので、手動でコピーしています。
ポイント: src/html/index.html は GAS の HTMLService で 'html/index' と参照しているため、dist/html/index.html にコピーすることで参照パスが一致します。
4-7. clasp push 成功
npm run push
> tsc && cp -r src/html dist/ && cp appsscript.json dist/ && clasp push
Pushed 12 files.
└─ dist/anonymize.js
└─ dist/appsscript.json
└─ dist/chat.js
└─ dist/config.js
└─ dist/embedding.js
└─ dist/html/index.html
└─ dist/html/style.html
└─ dist/ingest.js
└─ dist/main.js
└─ dist/search.js
└─ dist/utils.js
└─ dist/vectorStore.js
型注釈が除去された JavaScript がGASにアップロードされ、構文エラーが解消しました。
つまずきポイント⑤:マニフェスト上書き確認
clasp push 時に以下の確認が出ます。
Manifest file has been updated. Do you want to push and overwrite? y/n
y を入力。appsscript.json にDriveやSheetsの権限(oauthScopes)が追加されているため、上書きが必要です。
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 はTS非対応。tscで事前コンパイル+dist/から push |
| 5 | マニフェスト上書き | Do you want to push and overwrite? |
y で上書き許可 |
clasp 3.x を使う場合の注意
「clasp push すればTypeScriptをコンパイルしてくれる」という情報はclasp 2.x時代のものです。clasp 3.x(2025年以降)ではTypeScriptが依存関係から削除されており、自分でtscを実行してからpushする必要があります。
clasp --version で 3.x 系と表示される場合は、本記事の npm run push アプローチを使ってください。
まとめ(前編)
前編では以下を整理しました。
- Dify版との比較:GAS自作は学習価値が高く、RAGの全工程を理解できる
- AIのデータ処理の仕組み:クラウドAPI方式ではデータが外部に送信される。医療情報を扱う場合はローカル処理かハイブリッド構成が必要
- 環境構築のつまずき:clasp + TypeScriptの設定で5つのつまずきポイントがあり、特に clasp 3.x がTypeScriptをコンパイルしなくなった という変更が最大のはまりポイント
後編では、実装の詳細(Embedding、検索、LLM呼び出し)、デプロイ手順、テスト結果、そして精度改善計画を記録します。
後編に続く:[学習記録 #5 後編] GAS × Gemini でRAGチャットボットを自作した ― 実装・デプロイ・精度改善計画編