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?

GAS × Gemini でRAGチャットボットを自作した

0
Posted at

【学習記録 #5 前編】GAS × Gemini でRAGチャットボットを自作した ― 設計・環境構築編

はじめに

前回の記事でDifyを使ったノーコードRAGチャットボットを構築しました。今回は同じ医学の学習用チャットボットを、GAS(Google Apps Script)で1からコードを書いて自作 した全記録です。

「Difyで動くものはできた。でもRAGの中身をブラックボックスのままにしたくない」「GASなら無料でサーバーレスに運用できる」という動機で、自作に挑戦しました。

本記事は前後編に分かれています。

記事 内容
前編(本記事) 設計思想・環境構築・つまずきポイント
後編 実装詳細・デプロイ・テスト・精度改善計画

対象読者:エンジニア経験1年程度の方。GASを触ったことがあるけど、RAGやEmbeddingは初めてという方を想定しています。

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

  1. なぜDifyではなくGASで自作するのか ― Dify版との比較と自作の意義
  2. AIチャットボットのデータの流れ ― クラウドAPI vs ローカル、個人情報の扱い
  3. 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.jsondependencies を見て、驚きました。

{
  "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.jsontsc がコピーしてくれないので、手動でコピーしています。

ポイント: 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チャットボットを自作した ― 実装・デプロイ・精度改善計画編

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?