T-CREATOR

Grok RAG 設計入門:社内ドキュメント検索を高精度にする構成パターン

Grok RAG 設計入門:社内ドキュメント検索を高精度にする構成パターン

社内のナレッジベースやドキュメントが増え続ける中、必要な情報を素早く見つけることは日々難しくなっていませんか。従来の全文検索だけでは欲しい答えにたどり着けず、何度も検索キーワードを変えたり、複数のドキュメントを開いて情報を照らし合わせたりする作業に時間を取られてしまいます。

そこで注目されているのが、xAI 社が開発した大規模言語モデル「Grok」を活用した RAG(Retrieval-Augmented Generation)システムです。RAG は検索と生成を組み合わせた技術で、膨大なドキュメントの中から関連情報を取り出し、それを元に自然な回答を生成してくれます。

本記事では、Grok API を使った社内ドキュメント検索システムの設計について、初心者の方にもわかりやすく解説いたします。基本的な仕組みから実装パターン、精度向上のテクニックまで、段階的にご紹介していきますね。

背景

RAG とは何か

RAG(Retrieval-Augmented Generation)は、大規模言語モデルの弱点を補う技術として 2020 年に Facebook AI(現 Meta AI)が提案した手法です。

通常の言語モデルは学習時のデータのみに基づいて回答を生成するため、最新情報や企業固有の情報については正確に答えられません。また、存在しない情報を「もっともらしく」生成してしまう、いわゆる「ハルシネーション(幻覚)」の問題も抱えています。

RAG はこれらの課題を解決するために、回答生成の前に関連ドキュメントを検索(Retrieval)し、その情報を参照しながら回答を生成(Generation)する仕組みです。

Grok の特徴

xAI 社が開発した Grok は、以下の特徴を持つ大規模言語モデルです。

#特徴説明
1長いコンテキスト長最大 128K トークンまで対応し、大量のドキュメントを一度に参照可能
2リアルタイム情報X(旧 Twitter)のデータにアクセスでき、最新情報を反映
3API の利用可能性REST API として提供され、既存システムへの統合が容易
4多言語対応日本語を含む多言語での高精度な処理が可能

この Grok を RAG システムに組み込むことで、社内ドキュメントに特化した高精度な質問応答システムを構築できるのです。

RAG システムの全体像

以下の図は、Grok RAG システムの基本的な処理フローを示しています。

mermaidflowchart TB
    user["ユーザー"] -->|質問| system["RAGシステム"]
    system -->|ベクトル検索| vector_db[("ベクトルDB")]
    vector_db -->|関連文書| system
    system -->|プロンプト生成| grok["Grok API"]
    grok -->|回答生成| system
    system -->|回答| user

    docs["社内ドキュメント"] -->|事前処理| chunk["チャンク分割"]
    chunk -->|埋め込み| embedding["埋め込みモデル"]
    embedding -->|保存| vector_db

図で理解できる要点

  • ユーザーの質問はまずベクトル検索で関連ドキュメントを取得します
  • 取得した情報を Grok に渡して、コンテキストを踏まえた回答を生成します
  • 事前にドキュメントをチャンク分割し、ベクトル化して保存しておく必要があります

課題

従来の検索システムの限界

社内ドキュメント検索において、従来のキーワードベースの全文検索には以下のような課題があります。

キーワード一致の制約

全文検索は入力されたキーワードと完全に一致する、または部分的に一致する文書を返します。しかし、同じ意味でも異なる表現(例:「顧客」と「クライアント」)や、類義語での検索には対応できません。

文脈理解の欠如

複数のキーワードを含む文書が検索結果として返されても、それらのキーワードがどのような関係性で使われているかは考慮されません。例えば「プロジェクト A の予算削減」と「プロジェクト B の予算増加」という異なる文脈が混在してしまうのです。

情報の断片化

必要な情報が複数のドキュメントに分散している場合、ユーザー自身が各文書を開いて内容を統合する必要があります。これは時間がかかるだけでなく、重要な関連情報を見落とすリスクも高めます。

RAG 導入時の技術的課題

RAG システムを構築する際にも、以下のような技術的な課題に直面します。

#課題影響
1チャンク分割の最適化不適切なサイズでは文脈が失われるか、ノイズが増加
2埋め込みモデルの選択日本語対応や精度、処理速度のバランスが重要
3検索精度の向上関連性の低い文書が混入すると回答品質が低下
4コスト管理API コールやベクトル DB 利用のコストが増大
5レスポンス速度検索と生成の両方を行うため、応答が遅くなりがち

以下の図は、RAG システムにおける主な課題点を示しています。

mermaidflowchart LR
    input["ユーザー質問"] --> retrieval["検索処理"]
    retrieval --> issue1["課題1:<br/>関連性の低い<br/>文書の混入"]
    retrieval --> issue2["課題2:<br/>チャンクサイズ<br/>の不適切さ"]
    issue1 --> generation["生成処理"]
    issue2 --> generation
    generation --> issue3["課題3:<br/>回答品質の<br/>不安定さ"]
    generation --> issue4["課題4:<br/>レスポンス<br/>速度の遅延"]
    issue3 --> output["回答出力"]
    issue4 --> output

図で理解できる要点

  • 検索段階での精度が、最終的な回答品質に直結します
  • チャンク分割は検索精度と生成品質の両方に影響を与えます
  • 各段階の課題が連鎖的に影響するため、総合的な設計が必要です

これらの課題を解決するためには、システム全体を俯瞰した設計アプローチが求められます。

解決策

Grok RAG の基本アーキテクチャ

Grok を活用した RAG システムは、以下の 4 つの主要コンポーネントで構成されます。それぞれが連携することで、高精度な検索と回答生成を実現するのです。

ドキュメント処理パイプライン

まず、社内ドキュメントを検索可能な形式に変換する処理パイプラインが必要です。このパイプラインは以下のステップで構成されます。

mermaidflowchart LR
    raw["元ドキュメント"] --> loader["ローダー"]
    loader --> parser["パーサー"]
    parser --> chunker["チャンカー"]
    chunker --> embedder["埋め込み"]
    embedder --> store["ベクトルDB"]

    loader -->|PDF/DOCX/MD| parser
    parser -->|テキスト抽出| chunker
    chunker -->|適切なサイズ| embedder
    embedder -->|ベクトル化| store

処理ステップの詳細

  1. ローダー: 各種ファイル形式(PDF、Word、Markdown 等)を読み込みます
  2. パーサー: テキストを抽出し、メタデータ(作成日、作成者等)を付与します
  3. チャンカー: 文書を適切なサイズのチャンクに分割します
  4. 埋め込み: 各チャンクをベクトル表現に変換します
  5. 保存: ベクトル DB に格納し、検索可能にします

ベクトル検索エンジン

ユーザーの質問をベクトル化し、類似度の高いドキュメントチャンクを取得する検索エンジンです。以下の表に主要なベクトル DB の比較を示します。

#ベクトル DB特徴推奨用途
1Pineconeフルマネージド、高速、スケーラブル大規模システム
2Weaviateオープンソース、GraphQL 対応柔軟なクエリが必要な場合
3QdrantRust 製、高性能、フィルタリング強力精密な検索条件が必要な場合
4Chroma軽量、セットアップ簡単プロトタイプ・小規模システム

プロンプトエンジニアリング

検索結果とユーザーの質問を組み合わせて、Grok に送信する効果的なプロンプトを生成します。プロンプトの品質が回答の精度を大きく左右するため、慎重な設計が必要です。

基本的なプロンプトテンプレートの構造は以下のようになります。

typescriptinterface PromptTemplate {
  systemMessage: string; // システムの役割定義
  context: string; // 検索で取得したドキュメント
  question: string; // ユーザーの質問
  constraints: string[]; // 回答時の制約条件
}

Grok 統合レイヤー

Grok API と通信し、回答を取得するレイヤーです。エラーハンドリングやリトライ処理、レスポンスのパースなどを担当します。

精度向上のための構成パターン

RAG システムの検索精度と回答品質を向上させるための、実践的な構成パターンをご紹介します。

パターン 1:ハイブリッド検索

ベクトル検索とキーワード検索を組み合わせることで、両方の長所を活かす手法です。

mermaidflowchart TB
    query["ユーザー質問"]
    query --> vector_search["ベクトル検索"]
    query --> keyword_search["キーワード検索"]

    vector_search --> results1["結果セットA"]
    keyword_search --> results2["結果セットB"]

    results1 --> rerank["リランキング"]
    results2 --> rerank

    rerank --> final["最終結果"]

利点

  • ベクトル検索:意味的に類似した文書を見つけられます
  • キーワード検索:特定の専門用語や固有名詞を確実に捕捉できます
  • リランキング:両方の結果を統合し、最も関連性の高いものを選別します

パターン 2:階層的チャンキング

ドキュメントを複数の粒度でチャンク化し、文脈を保持しながら検索精度を高める手法です。

#レベルチャンクサイズ用途
1ドキュメント全体メタデータ検索
2セクション500-1000 トークン概要把握
3パラグラフ100-300 トークン詳細検索
420-50 トークンピンポイント抽出

検索時には小さなチャンクで精密に探し、回答生成時には親チャンク(より大きな文脈)を参照することで、正確性と文脈理解を両立できます。

パターン 3:メタデータフィルタリング

ドキュメントにメタデータを付与し、検索時にフィルタリングすることで精度を向上させます。

typescriptinterface DocumentMetadata {
  department: string; // 部署
  documentType: string; // ドキュメント種類
  createdAt: Date; // 作成日
  updatedAt: Date; // 更新日
  author: string; // 作成者
  tags: string[]; // タグ
  accessLevel: string; // アクセスレベル
}

メタデータを活用することで、「人事部の 2024 年の規定」「技術部の API 仕様書」といった条件付き検索が可能になります。

パターン 4:クエリ拡張

ユーザーの質問を自動的に拡張・改善してから検索を実行する手法です。

mermaidflowchart LR
    original["元の質問"] --> expand["クエリ拡張"]
    expand --> syn["類義語追加"]
    expand --> reform["言い換え生成"]
    expand --> decompose["質問分解"]

    syn --> search["検索実行"]
    reform --> search
    decompose --> search

具体例

  • 元の質問:「休暇申請の方法」
  • 拡張後:「休暇申請 OR 有給申請 OR 休暇届 OR vacation request」
  • 言い換え:「休暇を取得するための手続き」
  • 分解:「休暇申請のフォーム」「休暇申請の承認フロー」

パターン 5:再ランキングモデル

初期検索で取得した結果を、専用の再ランキングモデルで並び替える手法です。

検索の 2 段階処理により、計算コストを抑えながら精度を向上できます。

  1. 第 1 段階:高速なベクトル検索で候補を 100 件程度取得します
  2. 第 2 段階:再ランキングモデルで上位 10 件程度に絞り込みます

代表的な再ランキングモデルには、Cohere Rerank、BGE Reranker などがあります。

プロンプト設計のベストプラクティス

Grok に送信するプロンプトの品質が、回答の精度を決定します。効果的なプロンプト設計のポイントをご紹介しましょう。

役割の明確化

システムメッセージで Grok の役割を明確に定義します。

typescriptconst systemMessage = `
あなたは社内ドキュメントの専門アシスタントです。
以下の役割を担います:

1. 提供されたドキュメントの内容のみに基づいて回答する
2. 情報が不足している場合は、その旨を明示する
3. 複数のドキュメントから情報を統合して、分かりやすく説明する
4. 回答には必ず参照元のドキュメント名を含める
5. 推測や憶測は避け、事実のみを伝える
`;

このように役割を具体的に指定することで、回答の一貫性と信頼性が向上します。

コンテキストの構造化

検索で取得したドキュメントを、Grok が理解しやすい形式に整理します。

typescriptinterface ContextDocument {
  id: string;
  title: string;
  content: string;
  score: number;
  metadata: DocumentMetadata;
}

function formatContext(docs: ContextDocument[]): string {
  return docs
    .map(
      (doc, index) => `
【ドキュメント${index + 1}】
タイトル: ${doc.title}
関連度: ${(doc.score * 100).toFixed(1)}%
作成日: ${doc.metadata.createdAt}

内容:
${doc.content}
---
  `
    )
    .join('\n');
}

構造化されたコンテキストにより、Grok は各情報源を区別して処理できるようになります。

制約条件の設定

回答時の制約を明示的に指定することで、期待する形式の回答を得られます。

typescriptconst constraints = [
  '回答は500文字以内で簡潔にまとめてください',
  '箇条書きを使用して読みやすくしてください',
  '専門用語には簡単な説明を付けてください',
  '参照したドキュメント名を末尾に記載してください',
  '情報がない場合は「情報が見つかりませんでした」と回答してください',
];

Few-shot プロンプティング

良い回答の例を示すことで、期待する出力形式を学習させます。

typescriptconst fewShotExamples = `
【良い回答例】

質問: 経費精算の期限を教えてください

回答:
経費精算の期限は以下の通りです:

- 月次経費: 翌月10日まで
- 出張費: 出張終了後14日以内
- 立替金: 発生日から30日以内

期限を過ぎた場合は、上長の承認が別途必要となります。

参照: 経費規定2024年版(第3章)、経費精算マニュアル
`;

このような例を含めることで、Grok は形式を理解し、一貫した回答を生成しやすくなります。

具体例

システム構成の実装例

ここからは、実際に Grok RAG システムを構築する具体的なコード例をご紹介します。Node.js + TypeScript での実装を前提に、段階的に解説していきますね。

必要なパッケージのインストール

まず、プロジェクトのセットアップと必要なパッケージをインストールします。

bash# プロジェクトディレクトリの作成
mkdir grok-rag-system
cd grok-rag-system

# package.jsonの初期化
yarn init -y

次に、必要な依存パッケージをインストールしましょう。

bash# Grok API クライアント(OpenAI互換)
yarn add openai

# ベクトルDB (この例ではChromaを使用)
yarn add chromadb

# テキスト処理
yarn add langchain

# 環境変数管理
yarn add dotenv

# TypeScript関連
yarn add -D typescript @types/node ts-node

TypeScript の設定ファイルも作成します。

bash# tsconfig.jsonの生成
yarn tsc --init

環境変数の設定

プロジェクトルートに .env ファイルを作成し、API キーなどの設定を記述します。

bash# Grok API設定
GROK_API_KEY=your-grok-api-key-here
GROK_API_BASE=https://api.x.ai/v1

# ベクトルDB設定
CHROMA_HOST=localhost
CHROMA_PORT=8000

# システム設定
CHUNK_SIZE=500
CHUNK_OVERLAP=50
TOP_K_RESULTS=5

型定義

システム全体で使用する型を定義します。まず types​/​index.ts を作成しましょう。

typescript// types/index.ts

// ドキュメントのメタデータ
export interface DocumentMetadata {
  department: string;
  documentType: string;
  createdAt: string;
  updatedAt: string;
  author: string;
  tags: string[];
  accessLevel: string;
  filePath: string;
}

次に、チャンク化されたドキュメントの型を定義します。

typescript// types/index.ts (続き)

// チャンク化されたドキュメント
export interface DocumentChunk {
  id: string;
  content: string;
  metadata: DocumentMetadata;
  embedding?: number[];
}

検索結果の型も定義しておきます。

typescript// types/index.ts (続き)

// 検索結果
export interface SearchResult {
  chunk: DocumentChunk;
  score: number;
  rank: number;
}

RAG システムの設定用の型も必要です。

typescript// types/index.ts (続き)

// RAG設定
export interface RAGConfig {
  chunkSize: number;
  chunkOverlap: number;
  topK: number;
  temperature: number;
  maxTokens: number;
}

ドキュメントローダーの実装

社内ドキュメントを読み込み、テキストを抽出するローダーを実装します。

typescript// loaders/DocumentLoader.ts

import { readFileSync } from 'fs';
import { DocumentMetadata } from '../types';

export class DocumentLoader {
  // ファイルパスからドキュメントを読み込む
  loadDocument(filePath: string): { content: string; metadata: DocumentMetadata } {
    const content = readFileSync(filePath, 'utf-8');
    const metadata = this.extractMetadata(filePath, content);

    return { content, metadata };
  }

メタデータ抽出のロジックを実装します。

typescript// loaders/DocumentLoader.ts (続き)

  private extractMetadata(filePath: string, content: string): DocumentMetadata {
    // ファイル名から部署を推測 (例: /docs/hr/policy.md -> hr)
    const department = this.extractDepartment(filePath);

    // ファイルの種類を判定
    const documentType = this.determineDocumentType(filePath);

    return {
      department,
      documentType,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      author: 'system',
      tags: this.extractTags(content),
      accessLevel: 'internal',
      filePath
    };
  }

部署の抽出とタグ生成のヘルパーメソッドを追加します。

typescript// loaders/DocumentLoader.ts (続き)

  private extractDepartment(filePath: string): string {
    const match = filePath.match(/\/docs\/([^\/]+)\//);
    return match ? match[1] : 'general';
  }

  private determineDocumentType(filePath: string): string {
    if (filePath.includes('policy')) return 'ポリシー';
    if (filePath.includes('manual')) return 'マニュアル';
    if (filePath.includes('spec')) return '仕様書';
    return 'ドキュメント';
  }

  private extractTags(content: string): string[] {
    // 簡易的なタグ抽出(実際にはより高度なNLP処理が推奨)
    const tags: string[] = [];
    if (content.includes('API')) tags.push('API');
    if (content.includes('セキュリティ')) tags.push('セキュリティ');
    if (content.includes('承認')) tags.push('承認フロー');
    return tags;
  }
}

チャンク分割の実装

ドキュメントを適切なサイズのチャンクに分割する処理を実装します。

typescript// chunkers/TextChunker.ts

import { DocumentChunk, DocumentMetadata } from '../types';
import { v4 as uuidv4 } from 'uuid';

export class TextChunker {
  private chunkSize: number;
  private chunkOverlap: number;

  constructor(chunkSize: number = 500, chunkOverlap: number = 50) {
    this.chunkSize = chunkSize;
    this.chunkOverlap = chunkOverlap;
  }

文字数ベースでのチャンク分割ロジックを実装します。

typescript// chunkers/TextChunker.ts (続き)

  // テキストをチャンクに分割
  splitText(
    content: string,
    metadata: DocumentMetadata
  ): DocumentChunk[] {
    const chunks: DocumentChunk[] = [];
    let startIndex = 0;

    while (startIndex < content.length) {
      // チャンクの終了位置を計算
      const endIndex = Math.min(
        startIndex + this.chunkSize,
        content.length
      );

      // 文の途中で切れないよう調整
      const adjustedEndIndex = this.findSentenceBoundary(
        content,
        endIndex
      );

      // チャンクを作成
      const chunkContent = content.slice(startIndex, adjustedEndIndex);

      chunks.push({
        id: uuidv4(),
        content: chunkContent,
        metadata: { ...metadata }
      });

      // 次のチャンクの開始位置を設定(オーバーラップを考慮)
      startIndex = adjustedEndIndex - this.chunkOverlap;
    }

    return chunks;
  }

文の境界を見つけるヘルパーメソッドを追加します。

typescript// chunkers/TextChunker.ts (続き)

  private findSentenceBoundary(text: string, position: number): number {
    // 位置がテキストの終端なら、そのまま返す
    if (position >= text.length) {
      return text.length;
    }

    // 文の終わり(。、.、\n)を探す
    const sentenceEnders = ['。', '.', '\n'];
    let searchPosition = position;

    // 最大50文字先まで探索
    const searchLimit = Math.min(position + 50, text.length);

    while (searchPosition < searchLimit) {
      if (sentenceEnders.includes(text[searchPosition])) {
        return searchPosition + 1;
      }
      searchPosition++;
    }

    // 見つからなければ元の位置を返す
    return position;
  }
}

ベクトル DB クライアントの実装

Chroma DB とやり取りするクライアントを実装します。

typescript// db/VectorDBClient.ts

import { ChromaClient, Collection } from 'chromadb';
import { DocumentChunk, SearchResult } from '../types';

export class VectorDBClient {
  private client: ChromaClient;
  private collection: Collection | null = null;

  constructor() {
    this.client = new ChromaClient({
      path: `http://${process.env.CHROMA_HOST}:${process.env.CHROMA_PORT}`
    });
  }

コレクションの初期化メソッドを実装します。

typescript// db/VectorDBClient.ts (続き)

  // コレクションの初期化または取得
  async initializeCollection(collectionName: string): Promise<void> {
    try {
      this.collection = await this.client.getOrCreateCollection({
        name: collectionName,
        metadata: { description: '社内ドキュメント検索用コレクション' }
      });
    } catch (error) {
      throw new Error(`コレクションの初期化に失敗しました: ${error}`);
    }
  }

ドキュメントチャンクを追加するメソッドを実装します。

typescript// db/VectorDBClient.ts (続き)

  // チャンクをベクトルDBに追加
  async addChunks(chunks: DocumentChunk[]): Promise<void> {
    if (!this.collection) {
      throw new Error('コレクションが初期化されていません');
    }

    // チャンクのデータを抽出
    const ids = chunks.map(chunk => chunk.id);
    const documents = chunks.map(chunk => chunk.content);
    const metadatas = chunks.map(chunk => chunk.metadata);

    // ベクトルDBに追加(Chromaが自動的に埋め込みを生成)
    await this.collection.add({
      ids,
      documents,
      metadatas
    });
  }

類似検索を実行するメソッドを実装します。

typescript// db/VectorDBClient.ts (続き)

  // クエリに類似したチャンクを検索
  async search(
    queryText: string,
    topK: number = 5,
    filter?: Record<string, any>
  ): Promise<SearchResult[]> {
    if (!this.collection) {
      throw new Error('コレクションが初期化されていません');
    }

    // 検索実行
    const results = await this.collection.query({
      queryTexts: [queryText],
      nResults: topK,
      where: filter
    });

    // 結果を整形
    return this.formatSearchResults(results);
  }

検索結果を整形するヘルパーメソッドを追加します。

typescript// db/VectorDBClient.ts (続き)

  private formatSearchResults(results: any): SearchResult[] {
    const searchResults: SearchResult[] = [];

    // Chromaの結果形式を変換
    if (results.ids && results.ids[0]) {
      for (let i = 0; i < results.ids[0].length; i++) {
        searchResults.push({
          chunk: {
            id: results.ids[0][i],
            content: results.documents[0][i],
            metadata: results.metadatas[0][i]
          },
          score: 1 - (results.distances?.[0]?.[i] || 0), // 距離をスコアに変換
          rank: i + 1
        });
      }
    }

    return searchResults;
  }
}

Grok API クライアントの実装

Grok API と通信するクライアントを実装します。Grok は OpenAI 互換 API のため、OpenAI SDK を利用できます。

typescript// api/GrokClient.ts

import OpenAI from 'openai';
import { SearchResult } from '../types';

export class GrokClient {
  private client: OpenAI;
  private model: string = 'grok-beta';

  constructor() {
    this.client = new OpenAI({
      apiKey: process.env.GROK_API_KEY,
      baseURL: process.env.GROK_API_BASE
    });
  }

プロンプトを構築するメソッドを実装します。

typescript// api/GrokClient.ts (続き)

  // 検索結果からプロンプトを構築
  private buildPrompt(
    question: string,
    searchResults: SearchResult[]
  ): { system: string; user: string } {
    // システムメッセージ
    const system = `
あなたは社内ドキュメントの専門アシスタントです。
提供されたドキュメントの内容のみに基づいて、正確かつ簡潔に回答してください。

【回答の制約】
- ドキュメントに記載されていない情報は推測しない
- 複数のドキュメントから情報を統合して説明する
- 回答は500文字以内で簡潔にまとめる
- 参照したドキュメント名を末尾に記載する
- 情報が不足している場合は、その旨を明示する
    `.trim();

    // コンテキストの構築
    const context = searchResults.map((result, index) => `
【ドキュメント${index + 1}】
タイトル: ${result.chunk.metadata.filePath}
関連度: ${(result.score * 100).toFixed(1)}%

内容:
${result.chunk.content}
---
    `).join('\n');

    // ユーザーメッセージ
    const user = `
以下のドキュメントを参照して、質問に回答してください。

${context}

【質問】
${question}
    `.trim();

    return { system, user };
  }

回答を生成するメソッドを実装します。

typescript// api/GrokClient.ts (続き)

  // 質問に対する回答を生成
  async generateAnswer(
    question: string,
    searchResults: SearchResult[],
    temperature: number = 0.3,
    maxTokens: number = 1000
  ): Promise<string> {
    try {
      // プロンプトを構築
      const { system, user } = this.buildPrompt(question, searchResults);

      // Grok APIを呼び出し
      const response = await this.client.chat.completions.create({
        model: this.model,
        messages: [
          { role: 'system', content: system },
          { role: 'user', content: user }
        ],
        temperature,
        max_tokens: maxTokens
      });

      // 回答を抽出
      const answer = response.choices[0]?.message?.content || '';

      if (!answer) {
        throw new Error('Grokからの回答が空です');
      }

      return answer;

    } catch (error) {
      console.error('Grok API呼び出しエラー:', error);
      throw new Error(`回答生成に失敗しました: ${error}`);
    }
  }
}

RAG システムの統合

これまで実装した各コンポーネントを統合し、RAG システムとして機能させます。

typescript// RAGSystem.ts

import { DocumentLoader } from './loaders/DocumentLoader';
import { TextChunker } from './chunkers/TextChunker';
import { VectorDBClient } from './db/VectorDBClient';
import { GrokClient } from './api/GrokClient';
import { RAGConfig } from './types';

export class RAGSystem {
  private loader: DocumentLoader;
  private chunker: TextChunker;
  private vectorDB: VectorDBClient;
  private grokClient: GrokClient;
  private config: RAGConfig;

  constructor(config?: Partial<RAGConfig>) {
    // デフォルト設定
    this.config = {
      chunkSize: Number(process.env.CHUNK_SIZE) || 500,
      chunkOverlap: Number(process.env.CHUNK_OVERLAP) || 50,
      topK: Number(process.env.TOP_K_RESULTS) || 5,
      temperature: 0.3,
      maxTokens: 1000,
      ...config
    };

    // 各コンポーネントを初期化
    this.loader = new DocumentLoader();
    this.chunker = new TextChunker(
      this.config.chunkSize,
      this.config.chunkOverlap
    );
    this.vectorDB = new VectorDBClient();
    this.grokClient = new GrokClient();
  }

システムを初期化するメソッドを実装します。

typescript// RAGSystem.ts (続き)

  // システムの初期化
  async initialize(collectionName: string = 'company-docs'): Promise<void> {
    await this.vectorDB.initializeCollection(collectionName);
    console.log('RAGシステムが初期化されました');
  }

ドキュメントをインデックス化するメソッドを実装します。

typescript// RAGSystem.ts (続き)

  // ドキュメントをシステムに追加
  async indexDocument(filePath: string): Promise<void> {
    // ドキュメントを読み込み
    const { content, metadata } = this.loader.loadDocument(filePath);

    // チャンクに分割
    const chunks = this.chunker.splitText(content, metadata);

    // ベクトルDBに追加
    await this.vectorDB.addChunks(chunks);

    console.log(`ドキュメントをインデックス化しました: ${filePath}`);
    console.log(`  - チャンク数: ${chunks.length}`);
  }

質問に回答するメソッドを実装します。

typescript// RAGSystem.ts (続き)

  // 質問に回答
  async query(
    question: string,
    filter?: Record<string, any>
  ): Promise<{ answer: string; sources: string[] }> {
    // 関連ドキュメントを検索
    const searchResults = await this.vectorDB.search(
      question,
      this.config.topK,
      filter
    );

    if (searchResults.length === 0) {
      return {
        answer: '関連するドキュメントが見つかりませんでした。',
        sources: []
      };
    }

    // Grokで回答を生成
    const answer = await this.grokClient.generateAnswer(
      question,
      searchResults,
      this.config.temperature,
      this.config.maxTokens
    );

    // 参照元を抽出
    const sources = searchResults.map(
      result => result.chunk.metadata.filePath
    );

    return { answer, sources };
  }
}

使用例

実際に RAG システムを使用するサンプルコードを示します。

typescript// example/main.ts

import { RAGSystem } from '../RAGSystem';
import dotenv from 'dotenv';

// 環境変数を読み込み
dotenv.config();

async function main() {
  // RAGシステムを初期化
  const rag = new RAGSystem();
  await rag.initialize('company-knowledge-base');

  console.log('RAGシステムの準備が完了しました\n');

ドキュメントをインデックス化する処理を追加します。

typescript// example/main.ts (続き)

// サンプルドキュメントをインデックス化
const documents = [
  './docs/hr/vacation-policy.md',
  './docs/hr/expense-policy.md',
  './docs/tech/api-specification.md',
  './docs/tech/security-guidelines.md',
];

console.log('ドキュメントをインデックス化しています...\n');
for (const doc of documents) {
  await rag.indexDocument(doc);
}

console.log('\nインデックス化が完了しました\n');

実際に質問を実行する処理を追加します。

typescript// example/main.ts (続き)

  // 質問の例
  const questions = [
    '有給休暇の申請方法を教えてください',
    '経費精算の期限はいつですか',
    'APIのエンドポイントはどこに定義されていますか'
  ];

  console.log('--- 質問応答の開始 ---\n');

  for (const question of questions) {
    console.log(`Q: ${question}`);

    const result = await rag.query(question);

    console.log(`A: ${result.answer}\n`);
    console.log('参照元:');
    result.sources.forEach((source, index) => {
      console.log(`  ${index + 1}. ${source}`);
    });
    console.log('\n' + '='.repeat(60) + '\n');
  }
}

// メイン処理を実行
main().catch(console.error);

実行方法

作成したシステムを実行するには、以下のコマンドを使用します。

bash# TypeScriptをコンパイルして実行
yarn ts-node example/main.ts

実行すると、以下のような出力が得られます。

makefileRAGシステムの準備が完了しました

ドキュメントをインデックス化しています...

ドキュメントをインデックス化しました: ./docs/hr/vacation-policy.md
  - チャンク数: 8
ドキュメントをインデックス化しました: ./docs/hr/expense-policy.md
  - チャンク数: 12

インデックス化が完了しました

--- 質問応答の開始 ---

Q: 有給休暇の申請方法を教えてください
A: 有給休暇の申請は、社内システムの「休暇申請フォーム」から行います。
申請は希望日の5営業日前までに提出し、上長の承認を得る必要があります。
緊急の場合は直接上長に連絡してください。

参照元:
  1. ./docs/hr/vacation-policy.md

============================================================

まとめ

本記事では、Grok API を活用した RAG システムの設計と実装について、基礎から実践まで詳しく解説してまいりました。

RAG は従来の検索システムの限界を超え、社内ドキュメントから必要な情報を的確に取り出し、文脈を理解した自然な回答を生成できる強力な技術です。特に Grok の長いコンテキスト長と高い日本語処理能力を活用することで、企業の知識管理を大きく効率化できるでしょう。

実装においては、以下のポイントが重要となります。

#ポイント効果
1適切なチャンク分割文脈を保持しつつ検索精度を向上
2メタデータの活用条件付き検索で精度を高める
3ハイブリッド検索意味検索とキーワード検索の長所を統合
4プロンプト設計回答の品質と一貫性を確保
5再ランキング最も関連性の高い情報を優先

また、システム運用においては、定期的なドキュメントの更新、ユーザーフィードバックの収集、検索精度のモニタリングが継続的な改善につながります。

Grok RAG システムは、社内の知識を活性化し、従業員の生産性を向上させる有効な手段です。本記事で紹介した構成パターンと実装例を参考に、ぜひ皆さんの組織でも導入を検討してみてください。

まずは小規模なプロトタイプから始め、段階的に機能を拡張していくアプローチをお勧めいたします。

関連リンク