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 では実行時にファイルシステムを読めないため、ビルド時バンドル が必須です。
ナレッジ更新フロー
Firecrawl で MD を再取得
リポジトリ内のナレッジフォルダを更新
`npm run bundle:prompts`
デプロイ
プロンプト設計
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 は「最初から入れる」ものではなく、ナレッジ量と更新頻度を見て判断 してよいと感じました。
「公式情報だけ答えるチャット」を検討している方の参考になれば幸いです。
