はじめに

「ChatGPTに自社の製品マニュアルを学習させたい」「社内規定を知っているAIアシスタントを作りたい」——そんな要望を持つ企業が急増しています。

しかし、LLMを自前でファインチューニング(追加学習)するには、大量のGPU・専門知識・膨大なコストが必要です。

そこで注目されているのが RAG(Retrieval-Augmented Generation)です。LLM自体は変更せず、「質問に関係するドキュメントを検索してから回答させる」という仕組みで、独自知識を持ったチャットボットを低コストで実現できます。

本記事では、Amazon Bedrock の Knowledge Bases を使い、S3にドキュメントをアップロードするだけで動作するRAGチャットボットを構築する手順を解説します。


RAGとは何か

RAGは以下の3ステップで動作します。

  1. 検索(Retrieve):ユーザーの質問に関連する文書をベクトルDBから検索
  2. 文脈付与(Augment):検索結果をプロンプトに追加
  3. 生成(Generate):文脈を踏まえてLLMが回答を生成

通常のLLMは学習データの知識しか持ちませんが、RAGを使うとリアルタイムで外部ドキュメントを参照しながら回答できます。

RAGとファインチューニングの違い

RAGファインチューニング
コスト低い高い(GPU必須)
更新の容易さ◎ ドキュメント追加だけ△ 再学習が必要
出典の明示○ できる× 難しい
専門知識不要必要

社内ナレッジや製品情報のように頻繁に更新されるドキュメントを扱う場合はRAGが最適です。


今回構築するシステムの全体像

code
[ユーザー] → [Next.js フロントエンド]
                      ↓
            [API Route(Node.js)]
                      ↓
        [Amazon Bedrock Knowledge Bases]
          ↙                        ↘
[Amazon OpenSearch Serverless]   [Amazon S3]
  (ベクトルDB)               (ドキュメント保管)
                      ↓
              [Claude 3 Sonnet]
                      ↓
              [回答をユーザーへ]

使用するAWSサービス:

サービス役割
Amazon S3ドキュメント(PDF・テキスト)の保管
Amazon Bedrock Knowledge BasesRAGパイプラインの管理
Amazon OpenSearch ServerlessベクトルDB(埋め込みの保存・検索)
Amazon Bedrock (Claude 3 Sonnet)回答生成LLM

前提条件

  • AWSアカウントを持っていること
  • AWS CLIが設定済みであること(aws configure 完了)
  • Node.js v20以上がインストールされていること
  • Amazon Bedrockで Claude 3 SonnetTitan Embeddings V2 のモデルアクセスを有効化済みであること

Bedrockのモデルアクセス有効化

AWSコンソール → Amazon Bedrock → 「モデルアクセス」から以下を有効化します:

  • Anthropic Claude 3 Sonnet(回答生成用)
  • Amazon Titan Embeddings V2(ベクトル化用)

有効化には数分かかる場合があります。


ステップ1:S3バケットにドキュメントをアップロード

まず、チャットボットに学習させたいドキュメントをS3に保存します。

対応ファイル形式

  • PDF(マニュアル・仕様書・報告書)
  • テキストファイル(.txt)
  • Word文書(.docx)
  • Markdown(.md)
  • HTML

バケットの作成

AWSコンソール → S3 → 「バケットを作成」

  • バケット名:my-rag-documents-XXXX(一意の名前)
  • リージョン:ap-northeast-1(東京)
  • その他はデフォルト

ドキュメントのアップロード

ドキュメントをS3にアップロードします。CLIを使う場合:

bash
aws s3 cp ./documents/ s3://my-rag-documents-XXXX/ --recursive

ポイント:ドキュメントの品質がRAGの精度に直結します。読みやすく整理されたテキストほど、検索精度が上がります。


ステップ2:Amazon Bedrock Knowledge Baseを作成

2-1. Knowledge Baseの作成開始

AWSコンソール → Amazon Bedrock → 「Knowledge bases」→「Knowledge baseを作成」

基本設定:

  • 名前:my-knowledge-base
  • 説明:任意
  • IAMロール:「新しいサービスロールを作成」を選択

2-2. データソース(S3)の設定

  • データソースのタイプ:Amazon S3
  • S3 URI:s3://my-rag-documents-XXXX/

2-3. 埋め込みモデルの選択

埋め込みモデルにはドキュメントをベクトルに変換する役割があります。

  • Amazon Titan Embeddings V2(推奨)

- 1,536次元のベクトル

- 日本語対応

- コストパフォーマンスが高い

2-4. ベクトルDBの設定

Knowledge Basesはベクトルストアとして Amazon OpenSearch Serverless を自動で作成できます。

  • 「新しいベクトルストアをすばやく作成する」を選択
  • OpenSearch Serverlessコレクションが自動作成されます

注意:OpenSearch Serverlessは最低料金が発生します(約$700/月〜)。コストを抑えたい場合は後述のコスト最適化を参照してください。

2-5. 確認と作成

設定を確認して「Knowledge baseを作成」をクリックします。作成には5〜10分かかります。


ステップ3:データソースの同期

Knowledge Baseを作成したら、S3のドキュメントをベクトル化して検索インデックスに取り込む「同期」作業が必要です。

同期の実行

AWSコンソール → 作成したKnowledge Base → 「データソース」タブ → 「同期」ボタンをクリック

同期が完了すると、ドキュメントがチャンク(小さな断片)に分割され、それぞれのベクトルがOpenSearch Serverlessに保存されます。

同期のステータス確認

  • 完了(Completed):検索可能な状態
  • 処理中(In progress):しばらく待つ
  • 失敗(Failed):IAM権限やファイル形式を確認

ステップ4:APIの実装

Next.jsのAPI Routeを使って、Bedrockを呼び出すバックエンドを実装します。

プロジェクトのセットアップ

bash
npx create-next-app@latest rag-chatbot --typescript --tailwind --app
cd rag-chatbot
npm install @aws-sdk/client-bedrock-agent-runtime

環境変数の設定

プロジェクトルートに .env.local を作成します:

code
AWS_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
BEDROCK_KNOWLEDGE_BASE_ID=XXXXXXXXXX
BEDROCK_MODEL_ARN=arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0

API Routeの作成

app/api/chat/route.ts を作成します:

typescript
import { NextRequest, NextResponse } from "next/server";
import {
  BedrockAgentRuntimeClient,
  RetrieveAndGenerateCommand,
} from "@aws-sdk/client-bedrock-agent-runtime";

const client = new BedrockAgentRuntimeClient({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

export async function POST(req: NextRequest) {
  const { message, sessionId } = await req.json();

  if (!message) {
    return NextResponse.json({ error: "メッセージが必要です" }, { status: 400 });
  }

  const command = new RetrieveAndGenerateCommand({
    input: { text: message },
    retrieveAndGenerateConfiguration: {
      type: "KNOWLEDGE_BASE",
      knowledgeBaseConfiguration: {
        knowledgeBaseId: process.env.BEDROCK_KNOWLEDGE_BASE_ID!,
        modelArn: process.env.BEDROCK_MODEL_ARN!,
        retrievalConfiguration: {
          vectorSearchConfiguration: {
            numberOfResults: 5,
          },
        },
        generationConfiguration: {
          promptTemplate: {
            textPromptTemplate:
              "あなたは親切なアシスタントです。以下の情報を参考に、日本語で回答してください。\n\n$search_results$\n\n質問:$query$",
          },
        },
      },
    },
    sessionId: sessionId || undefined,
  });

  const response = await client.send(command);

  const answer = response.output?.text ?? "回答を生成できませんでした。";
  const citations = response.citations?.map((c) =>
    c.retrievedReferences?.map((r) => r.location?.s3Location?.uri)
  ).flat().filter(Boolean);

  return NextResponse.json({
    answer,
    citations,
    sessionId: response.sessionId,
  });
}

ポイント解説:

  • RetrieveAndGenerateCommand:検索と生成を1回のAPIコールで完結させるコマンド
  • numberOfResults: 5:参照するドキュメントチャンクの数
  • sessionId:会話履歴を維持するためのセッションID
  • citations:回答の根拠となったS3ファイルのURIが返ってくる

ステップ5:フロントエンドの実装

シンプルなチャット画面を app/page.tsx に実装します:

typescript
"use client";
import { useState, useRef } from "react";

type Message = {
  role: "user" | "assistant";
  content: string;
  citations?: string[];
};

export default function ChatPage() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const sessionIdRef = useRef<string | null>(null);

  const sendMessage = async () => {
    if (!input.trim() || loading) return;
    const userMessage = input;
    setInput("");
    setMessages((prev) => [...prev, { role: "user", content: userMessage }]);
    setLoading(true);

    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message: userMessage, sessionId: sessionIdRef.current }),
    });
    const data = await res.json();
    sessionIdRef.current = data.sessionId;

    setMessages((prev) => [
      ...prev,
      { role: "assistant", content: data.answer, citations: data.citations },
    ]);
    setLoading(false);
  };

  return (
    <div className="max-w-2xl mx-auto p-4 h-screen flex flex-col">
      <h1 className="text-xl font-bold mb-4">RAGチャットボット</h1>
      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
        {messages.map((m, i) => (
          <div key={i} className={m.role === "user" ? "text-right" : "text-left"}>
            <div className={
              m.role === "user"
                ? "inline-block bg-blue-500 text-white rounded-lg px-4 py-2"
                : "inline-block bg-gray-100 rounded-lg px-4 py-2"
            }>
              {m.content}
            </div>
            {m.citations && m.citations.length > 0 && (
              <div className="text-xs text-gray-400 mt-1">
                参照: {m.citations.join(", ")}
              </div>
            )}
          </div>
        ))}
        {loading && <div className="text-gray-400">回答を生成中...</div>}
      </div>
      <div className="flex gap-2">
        <input
          className="flex-1 border rounded-lg px-4 py-2"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && sendMessage()}
          placeholder="質問を入力..."
        />
        <button
          onClick={sendMessage}
          className="bg-blue-500 text-white px-4 py-2 rounded-lg"
        >
          送信
        </button>
      </div>
    </div>
  );
}

動作確認

開発サーバーを起動して動作確認します:

bash
npm run dev

ブラウザで http://localhost:3000 を開き、アップロードしたドキュメントに関する質問を入力してみましょう。S3に保存した内容をもとに回答が返ってくれば成功です。


コスト最適化のポイント

OpenSearch Serverlessのコスト

OpenSearch Serverlessは最低2OCU(OpenSearch Compute Unit)が必要で、コストが高くなりがちです。開発・検証フェーズでは以下の代替手段を検討してください。

ベクトルストア月額目安特徴
OpenSearch Serverless$300〜フルマネージド、スケーラブル
Pinecone(外部)無料〜$70小規模なら無料枠あり
pgvector(Aurora)$50〜PostgreSQL互換

リクエストコスト

  • Claude 3 Sonnet:入力$3/100万トークン、出力$15/100万トークン
  • Titan Embeddings V2:$0.02/100万トークン(同期時のみ発生)

ドキュメントの更新方法

新しいドキュメントをS3にアップロードしたら、Knowledge Baseの「同期」を再実行するだけで最新情報を反映できます。コードの変更は不要です。

bash
# CLIで同期を実行する場合
aws bedrock-agent start-ingestion-job \
  --knowledge-base-id XXXXXXXXXX \
  --data-source-id YYYYYYYYYY

よくある問題と対処法

Q. 同期でエラーが出る

IAMロールにS3の読み取り権限が付与されているか確認してください。Knowledge Base作成時に自動作成されたIAMロールの権限が、バケットポリシーと一致していない場合があります。

Q. 回答の精度が低い

  • ドキュメントの品質を見直す(箇条書きより段落文章が有効)
  • numberOfResults を増やして参照チャンク数を増やす
  • プロンプトテンプレートを調整して回答形式を指定する

Q. 日本語の精度が悪い

Titan Embeddings V2は日本語対応していますが、英語に比べると精度が落ちる場合があります。Cohere Embed Multilingual(Bedrock経由)も選択肢として検討してください。


まとめ

Amazon BedrockのKnowledge Basesを使うと、ベクトルDB・埋め込み処理・検索パイプラインをAWSが管理してくれるため、コード量を最小限にRAGシステムを構築できます。

  • S3にドキュメントをアップロード
  • Knowledge Baseを作成して同期
  • RetrieveAndGenerateCommand 1つで検索+生成

ドキュメントを差し替えるだけで知識を更新できるのがRAGの強みです。社内FAQ・製品マニュアル・規定集など、さまざまな用途に応用してみてください。