見出し画像

RAGなしで十分だった。Next.js × Gemini で作る「公式情報だけ答える」AIチャットボット

はじめに

クライアント企業の公式サイト向けに、公開情報の範囲だけを答える AI チャットボットを Next.js で作りました。

最初はデモ用の固定返答だけでしたが、Gemini API 連携の本番チャットまで仕上げたので、設計と作り方をまとめます。

※ 本記事では企業名・サービス名・ドメインは伏せています。


今回やりたかったこと

  • ページ右下のフローティング UI から質問できる

  • 公式サイトに載っている情報の範囲だけ 答える

  • 天気・一般常識・他社情報などは丁寧に拒否

  • ナレッジにない数値・固有名詞は創作しない

  • 回答は Markdown 表示(リンクはクリック可能)


技術スタック

  • Next.js 15(App Router)

  • Google Gemini API(`@google/genai`)

  • Cloudflare Workers(OpenNext)

  • Firecrawl(サイト → Markdown)

  • TypeScript / Zod


全体アーキテクチャ

[フロント] フローティングチャット UI
    ↓ POST /api/chat
[API] Zod で履歴検証
    ↓
[chatbot.ts] システムプロンプト合成 + Gemini 呼び出し
    ↓
[Gemini API] マルチターン会話

フロントは React のカスタムフックで状態管理し、API 経由で Gemini と会話します。
会話履歴はリクエストごとに渡し、DB への永続化は今回は行っていません(デモ用途)。


UI の構成

チャット UI は次の要素で構成しています。

  • フローティングパネル … ワイド / コンパクト切替、最小化(FAB に格納)

  • 候補タグ … 「IR・投資家情報」「採用情報」など、よくある質問へのショートカット

  • 会話履歴 … ユーザー / アシスタントの吹き出し

  • Markdown 表示 … AI 回答のみレンダリング(リンク・箇条書き・太字)

  • loading / error … 送信中の表示、日本語エラーメッセージ

AI 回答は Markdown で返ってくるため、`renderMarkdown` で HTML 化し、`sanitize-html` でリンク先を `http` / `https` / `mailto` に限定しています。


ナレッジは RAG なし、ビルド時バンドル

公式サイトを Firecrawl で Markdown 化し、リポジトリ内に約 10 ファイル置きました(トップ、会社情報、FAQ、連絡先など)。

規模が小〜中だったので、Vector DB や embedding は使わず、ビルド時に全部システムプロンプトへ埋め込む方式にしました。

npm run bundle:prompts

これで Markdown ファイルが TypeScript の定数(`KNOWLEDGE_PROMPTS`)に変換されます。

Cloudflare Workers では実行時にファイルシステムを読めないため、ビルド時バンドル が必須です。

ナレッジ更新フロー

  1. Firecrawl で MD を再取得

  2. リポジトリ内のナレッジフォルダを更新

  3. `npm run bundle:prompts`

  4. デプロイ


プロンプト設計

AI の振る舞いは Markdown ファイルで管理し、実行時に 1 本のシステムプロンプトへ合成します。

3 層構成

1. chatbot エージェント(役割定義)

  • 公式サイトの AI アシスタントとして振る舞う

  • です・ます調、簡潔に

  • ナレッジにない事実は創作しない

  • 回答は Markdown 形式

2. Skill(ルール・前提知識)

  • company-domain … 事業ドメインの前提(業界用語、文体など)

  • chatbot-guardrails … 回答範囲と拒否ルール(チャット専用)

3. KNOWLEDGE_PROMPTS(公式サイト MD)

  • Firecrawl で取得した各ページの本文を全文連結

  • FAQ・会社概要・連絡先など、公開情報がそのまま AI の参照資料になる

ガードレールの例

範囲外の質問には、例えばこう返すよう Skill に書いておきます。

申し訳ございません。当社の公開情報の範囲でのみお答えできます。IR・事業内容・採用など、当社に関するご質問をお寄せください。

「何でも答える AI」ではなく、プロンプトレベルで回答範囲を固定する のがポイントです。


API 設計

エンドポイントは `POST /api/chat` の 1 本です。

リクエスト

{
  "messages": [
    { "role": "user", "content": "会社概要を教えてください" }
  ]
}

レスポンス

{
  "reply": "(Markdown 形式の回答)"
}

バリデーション

  • 履歴は最大 20 メッセージ

  • 最後のメッセージは必ず user

  • 空文字は拒否

エラー時は日本語メッセージを返し、フロントで表示します。


Gemini 呼び出し

マルチターン会話用の `callChat()` を実装しました。

  • system … `buildChatSystemPrompt()` で合成したプロンプト

  • messages … user / assistant の履歴

  • model … 環境変数 `CHATBOT_MODEL`(未設定時は gemini-2.5-flash)

429 や 503 など一時的なエラーは自動リトライし、ユーザー向けには分かりやすい日本語メッセージに変換します。


環境変数

  • `GEMINI_API_KEY`(必須)… Google AI Studio で取得

  • `CHATBOT_MODEL`(任意)… 既定: gemini-2.5-flash

環境ごとの設定場所

  • ローカル開発 … `.env`

  • Cloudflare プレビュー … `.dev.vars`

  • 本番 … Wrangler secret

npx wrangler secret put GEMINI_API_KEY

デプロイ

Cloudflare Workers(OpenNext)へデプロイしています。

npm run cf:deploy

ビルド時に `bundle:prompts` が走るので、ナレッジ MD も一緒に Workers へ載ります。


うまくいった点

  • ナレッジ 10 ファイル程度なら、RAG なしでも十分実用的

  • プロンプトを Markdown で管理でき、非エンジニアでもルール修正しやすい

  • ガードレール Skill を分離したことで、「何を答えてはいけないか」を明示できた

  • Markdown 表示 + リンク sanitize で、読みやすく安全な UI になった


反省・今後

  • ナレッジが増えると system プロンプトが肥大化 → RAG やページ単位の選択注入を検討

  • ストリーミング未対応(今回は一括返答)

  • チャット履歴の DB 永続化は未実装


まとめ

今回のチャットボットは、次の 3 点が核でした。

  • 小規模な公開情報 … Firecrawl MD をビルド時バンドル

  • 厳しめのガードレール … Skill で回答範囲を固定

  • シンプルな構成 … RAG なし、API 1 本、Cloudflare Workers

RAG は「最初から入れる」ものではなく、ナレッジ量と更新頻度を見て判断 してよいと感じました。

「公式情報だけ答えるチャット」を検討している方の参考になれば幸いです。

いいなと思ったら応援しよう!