T-CREATOR

Grok で社内 FAQ ボット:ナレッジ連携・権限制御・改善サイクル

Grok で社内 FAQ ボット:ナレッジ連携・権限制御・改善サイクル

社内の問い合わせ対応に、毎日どれくらいの時間を費やしていますか。同じような質問が何度も寄せられ、対応スタッフの負担が増えている企業は少なくありません。xAI 社が開発した AI アシスタント「Grok」を活用すれば、社内 FAQ ボットを構築し、問い合わせ対応を大幅に効率化できます。

本記事では、Grok を使った社内 FAQ ボットの構築方法を、ナレッジベースとの連携、権限制御、継続的な改善サイクルの 3 つの観点から解説します。実際のコード例を交えながら、実務ですぐに活用できる実装方法をご紹介しますので、ぜひ最後までご覧ください。

背景

社内問い合わせ対応の現状

多くの企業では、社内からの問い合わせ対応に多大なリソースを割いています。人事制度、経費精算、IT 機器の使い方、社内システムの操作方法など、質問の内容は多岐にわたるでしょう。

これらの問い合わせに対し、専任スタッフが個別に対応すると、以下のような課題が生じます。

  • 同じ質問への繰り返し対応による業務の非効率化
  • 対応品質のばらつき(担当者によって回答内容が異なる)
  • ナレッジの属人化(特定の人しか答えられない質問の存在)
  • 夜間や休日の問い合わせに対応できない

Grok の特徴と強み

xAI 社が開発した Grok は、2024 年にリリースされた最新の AI アシスタントです。他の AI と比較して、以下の特徴を持っています。

#特徴説明
1リアルタイム情報へのアクセスX(旧 Twitter)のデータを活用し、最新情報を取得可能
2高度な推論能力複雑な質問に対しても文脈を理解して回答
3API による統合既存システムとの連携が容易
4柔軟なカスタマイズプロンプトエンジニアリングで独自の回答スタイルを設定可能

社内 FAQ ボットとして Grok を活用することで、これらの強みを活かした効率的な問い合わせ対応システムを構築できます。

以下の図は、従来の問い合わせ対応と Grok を活用した場合のフローの違いを示しています。

mermaidflowchart LR
  employee["社員"] -->|質問| bot["Grok FAQボット"]
  bot -->|ナレッジ検索| kb[("ナレッジベース")]
  bot -->|回答生成| employee
  kb -->|情報提供| bot

  employee2["社員"] -.->|従来| staff["対応スタッフ"]
  staff -.->|手動検索| manual[("マニュアル・<br/>過去事例")]
  manual -.->|情報| staff
  staff -.->|回答| employee2

図で理解できる要点

  • Grok ボットが自動的にナレッジベースを検索し、即座に回答を生成
  • 従来の手動対応と比較して、大幅な時間短縮を実現
  • 24 時間 365 日の対応が可能に

課題

ナレッジの散在と管理の複雑さ

社内のナレッジは、様々な場所に散在しているケースが多いでしょう。Confluence、SharePoint、Google Drive、社内 Wiki など、複数のプラットフォームに情報が分散していると、FAQ ボットがどこから情報を取得すべきか判断が難しくなります。

また、情報の鮮度管理も重要な課題です。古い情報をもとに回答してしまうと、誤った案内をする可能性があるため注意が必要ですね。

権限制御の難しさ

社内 FAQ ボットでは、誰がどの情報にアクセスできるかを適切に制御する必要があります。例えば、以下のようなケースが考えられます。

  • 人事情報は人事部門のメンバーのみがアクセス可能
  • 経営情報は管理職以上のみが閲覧可能
  • 部門固有の情報は該当部門のメンバーのみに公開

単純に Grok API を呼び出すだけでは、このような細かな権限制御は実現できません。

回答精度の向上と改善サイクル

AI は完璧ではなく、時として不正確な回答や、質問の意図を誤解した回答を返すことがあります。以下の図は、FAQ ボットの主な課題を示しています。

mermaidflowchart TD
  start["FAQボット運用開始"] --> issue1["課題1:<br/>ナレッジ散在"]
  start --> issue2["課題2:<br/>権限制御"]
  start --> issue3["課題3:<br/>回答精度"]

  issue1 --> sub1["複数システムに<br/>情報が分散"]
  issue1 --> sub2["情報の鮮度管理"]

  issue2 --> sub3["部門別アクセス制御"]
  issue2 --> sub4["役職別権限設定"]

  issue3 --> sub5["不正確な回答"]
  issue3 --> sub6["意図の誤解"]
  issue3 --> sub7["継続的な改善が必要"]

課題のポイント

  • 3 つの主要課題が相互に関連し、総合的な対策が必要
  • 単純な API 連携だけでは解決できない
  • 継続的な改善の仕組みが不可欠

これらの課題に対し、どのように対処すべきでしょうか。次のセクションで具体的な解決策を見ていきます。

解決策

ナレッジベース統合アーキテクチャ

散在するナレッジを効率的に活用するため、統合レイヤーを構築します。各ナレッジソースからデータを取得し、統一されたフォーマットで Grok に提供する仕組みです。

以下の図は、ナレッジベース統合のアーキテクチャを示しています。

mermaidflowchart TB
  grok["Grok API"] --> vector["ベクトルDB<br/>(Pinecone/Weaviate)"]

  vector --> sync["同期処理レイヤー"]

  sync --> confluence["Confluence"]
  sync --> sharepoint["SharePoint"]
  sync --> gdrive["Google Drive"]
  sync --> wiki["社内Wiki"]

  metadata["メタデータ管理"] --> vector
  metadata -.->|権限情報| sync

アーキテクチャの要点

  • ベクトル DB で統一的なナレッジ管理を実現
  • 定期的な同期処理で最新情報を維持
  • メタデータに権限情報を付与し、アクセス制御を実現

ベクトルデータベースの選定と設定

ナレッジの検索効率を高めるため、ベクトルデータベースを活用します。ここでは Pinecone を例に説明しますが、Weaviate や Qdrant などでも同様の実装が可能です。

まず、必要なパッケージをインストールします。

typescript// package.json への依存関係追加
{
  "dependencies": {
    "@pinecone-database/pinecone": "^2.0.0",
    "openai": "^4.20.0",
    "dotenv": "^16.3.1"
  }
}

Yarn でインストールを実行します。

bashyarn add @pinecone-database/pinecone openai dotenv

次に、Pinecone クライアントの初期化を行います。環境変数から API キーを読み込み、接続を確立する処理です。

typescript// src/lib/pinecone.ts
import { Pinecone } from '@pinecone-database/pinecone';

// Pinecone クライアントの初期化
export const initPinecone = async () => {
  const pinecone = new Pinecone({
    apiKey: process.env.PINECONE_API_KEY || '',
  });

  return pinecone;
};

インデックスの作成と設定を行います。ここでは、ナレッジドキュメントを 1536 次元のベクトルとして保存する設定をしています。

typescript// src/lib/pinecone.ts (続き)
export const createIndex = async (indexName: string) => {
  const pinecone = await initPinecone();

  // インデックスが存在しない場合のみ作成
  const existingIndexes = await pinecone.listIndexes();

  if (
    !existingIndexes.indexes?.find(
      (index) => index.name === indexName
    )
  ) {
    await pinecone.createIndex({
      name: indexName,
      dimension: 1536, // OpenAI の埋め込みモデルの次元数
      metric: 'cosine', // コサイン類似度を使用
      spec: {
        serverless: {
          cloud: 'aws',
          region: 'us-east-1',
        },
      },
    });
  }

  return pinecone.Index(indexName);
};

ナレッジの同期処理実装

各ナレッジソースからデータを取得し、ベクトル DB に保存する同期処理を実装します。ここでは Confluence からデータを取得する例を示しますね。

まず、Confluence API クライアントの設定を行います。

typescript// src/services/confluence.ts
import axios from 'axios';

// Confluence API クライアントの設定
const confluenceClient = axios.create({
  baseURL: process.env.CONFLUENCE_BASE_URL,
  headers: {
    Authorization: `Bearer ${process.env.CONFLUENCE_API_TOKEN}`,
    'Content-Type': 'application/json',
  },
});

ページコンテンツを取得する関数を実装します。Confluence API を使用して、指定されたスペースの全ページを取得する処理です。

typescript// src/services/confluence.ts (続き)
export interface ConfluencePage {
  id: string;
  title: string;
  content: string;
  spaceKey: string;
  lastModified: string;
  permissions: string[]; // アクセス可能な権限グループ
}

// Confluence からページ一覧を取得
export const fetchConfluencePages = async (
  spaceKey: string
): Promise<ConfluencePage[]> => {
  const response = await confluenceClient.get(
    `/wiki/rest/api/content`,
    {
      params: {
        spaceKey,
        expand:
          'body.storage,version,space,metadata.labels',
        limit: 100, // ページング処理が必要な場合は調整
      },
    }
  );

  return response.data.results.map((page: any) => ({
    id: page.id,
    title: page.title,
    content: page.body.storage.value,
    spaceKey: page.space.key,
    lastModified: page.version.when,
    permissions: extractPermissions(page), // 権限情報の抽出
  }));
};

権限情報を抽出するヘルパー関数です。ページのメタデータから、アクセス可能なグループやロールを抽出します。

typescript// src/services/confluence.ts (続き)
const extractPermissions = (page: any): string[] => {
  // ページのラベルや制限情報から権限グループを抽出
  const labels = page.metadata?.labels?.results || [];
  const permissions: string[] = [];

  // ラベルから権限情報を抽出(例:label-hr, label-engineering など)
  labels.forEach((label: any) => {
    if (label.name.startsWith('access-')) {
      permissions.push(label.name.replace('access-', ''));
    }
  });

  // デフォルトでは全社員がアクセス可能
  if (permissions.length === 0) {
    permissions.push('all-employees');
  }

  return permissions;
};

取得したコンテンツをベクトル化して Pinecone に保存する処理です。OpenAI の埋め込みモデルを使用してテキストをベクトルに変換します。

typescript// src/services/embedding.ts
import { OpenAI } from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// テキストをベクトル化
export const createEmbedding = async (
  text: string
): Promise<number[]> => {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  });

  return response.data[0].embedding;
};

ナレッジを Pinecone に保存する処理です。ベクトル化したコンテンツと、メタデータ(権限情報など)を一緒に保存します。

typescript// src/services/knowledge-sync.ts
import { createIndex } from '../lib/pinecone';
import { fetchConfluencePages } from './confluence';
import { createEmbedding } from './embedding';

export const syncConfluenceKnowledge = async () => {
  const index = await createIndex('company-knowledge');
  const pages = await fetchConfluencePages('COMPANY');

  // バッチ処理で効率的に保存
  const batchSize = 100;
  for (let i = 0; i < pages.length; i += batchSize) {
    const batch = pages.slice(i, i + batchSize);

    const vectors = await Promise.all(
      batch.map(async (page) => {
        const embedding = await createEmbedding(
          `${page.title}\n\n${page.content}`
        );

        return {
          id: `confluence-${page.id}`,
          values: embedding,
          metadata: {
            title: page.title,
            source: 'confluence',
            spaceKey: page.spaceKey,
            lastModified: page.lastModified,
            permissions: page.permissions, // 権限情報を保存
            content: page.content.substring(0, 1000), // 検索結果表示用
          },
        };
      })
    );

    await index.upsert(vectors);
  }

  console.log(
    `Synced ${pages.length} pages from Confluence`
  );
};

権限制御の実装

FAQ ボットで適切な権限制御を実現するため、ユーザーの所属情報と照合して検索結果をフィルタリングします。

まず、ユーザー情報の型定義を行います。

typescript// src/types/user.ts
export interface User {
  id: string;
  email: string;
  department: string;
  role: string; // 'employee' | 'manager' | 'executive'
  groups: string[]; // 所属グループ(例:['engineering', 'all-employees'])
}

ユーザーがアクセス可能なナレッジを検索するクエリ処理です。権限フィルターを適用して、適切な情報のみを返却します。

typescript// src/services/knowledge-search.ts
import { createIndex } from '../lib/pinecone';
import { createEmbedding } from './embedding';
import { User } from '../types/user';

export const searchKnowledge = async (
  query: string,
  user: User,
  topK: number = 5
) => {
  const index = await createIndex('company-knowledge');

  // クエリをベクトル化
  const queryEmbedding = await createEmbedding(query);

  // ユーザーの権限グループを取得
  const userPermissions = [...user.groups, 'all-employees'];

  // 権限フィルターを適用して検索
  const searchResults = await index.query({
    vector: queryEmbedding,
    topK,
    includeMetadata: true,
    filter: {
      permissions: { $in: userPermissions }, // 権限によるフィルタリング
    },
  });

  return searchResults.matches || [];
};

検索結果を整形して返却する処理です。スコアが低い結果は除外し、信頼性の高い情報のみを提供します。

typescript// src/services/knowledge-search.ts (続き)
export interface SearchResult {
  title: string;
  content: string;
  source: string;
  score: number;
}

export const formatSearchResults = (
  matches: any[]
): SearchResult[] => {
  return matches
    .filter((match) => match.score > 0.7) // 類似度が70%以上のみ
    .map((match) => ({
      title: match.metadata.title,
      content: match.metadata.content,
      source: match.metadata.source,
      score: match.score,
    }));
};

Grok API との統合

Grok を使用して、検索結果をもとに自然な回答を生成します。ここでは、xAI API を使用して Grok と連携する方法を示しますね。

まず、Grok API クライアントの設定を行います。

typescript// src/services/grok.ts
import axios from 'axios';

// Grok API クライアントの設定
const grokClient = axios.create({
  baseURL: 'https://api.x.ai/v1',
  headers: {
    Authorization: `Bearer ${process.env.XAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
});

検索結果を含むプロンプトを構築し、Grok に回答を生成させる関数です。

typescript// src/services/grok.ts (続き)
import { SearchResult } from './knowledge-search';

export const generateAnswer = async (
  question: string,
  context: SearchResult[]
): Promise<string> => {
  // コンテキスト情報を整形
  const contextText = context
    .map(
      (result, index) =>
        `[参考情報${index + 1}] ${result.title}\n${
          result.content
        }`
    )
    .join('\n\n');

  // プロンプトを構築
  const prompt = `あなたは社内FAQボットです。以下の参考情報をもとに、質問に正確かつ親切に回答してください。

参考情報:
${contextText}

質問: ${question}

回答する際の注意点:
- 参考情報に基づいて回答してください
- 情報が不足している場合は、その旨を伝えてください
- 具体的で実用的な回答を心がけてください
- 敬語で丁寧に回答してください`;

  const response = await grokClient.post(
    '/chat/completions',
    {
      model: 'grok-beta',
      messages: [
        {
          role: 'system',
          content:
            '社内FAQボットとして、従業員の質問に正確かつ親切に回答します。',
        },
        {
          role: 'user',
          content: prompt,
        },
      ],
      temperature: 0.3, // 正確性を重視
      max_tokens: 1000,
    }
  );

  return response.data.choices[0].message.content;
};

エラーハンドリングを含む統合処理です。API 呼び出しが失敗した場合の処理を実装します。

typescript// src/services/grok.ts (続き)
export const handleGrokError = (error: any): string => {
  if (error.response) {
    // API からエラーレスポンスが返された場合
    const status = error.response.status;

    switch (status) {
      case 401:
        return 'Error 401: API認証に失敗しました。APIキーを確認してください。';
      case 429:
        return 'Error 429: リクエスト制限に達しました。しばらく待ってから再度お試しください。';
      case 500:
        return 'Error 500: サーバーエラーが発生しました。時間をおいて再度お試しください。';
      default:
        return `Error ${status}: 予期しないエラーが発生しました。`;
    }
  }

  return '接続エラーが発生しました。ネットワーク接続を確認してください。';
};

改善サイクルの構築

FAQ ボットの回答精度を継続的に向上させるため、フィードバック収集と分析の仕組みを実装します。

以下の図は、改善サイクルの全体像を示しています。

mermaidflowchart LR
  user["社員"] -->|質問| bot["FAQボット"]
  bot -->|回答| user
  user -->|フィードバック| fb[("フィードバックDB")]
  fb -->|分析| analytics["分析処理"]
  analytics -->|改善点抽出| improve["改善実施"]
  improve -->|ナレッジ更新| kb[("ナレッジベース")]
  improve -->|プロンプト調整| bot
  kb -->|最新情報| bot

改善サイクルのポイント

  • ユーザーフィードバックを体系的に収集
  • 定期的な分析で改善点を特定
  • ナレッジとプロンプトの両面から改善

フィードバックデータの型定義です。

typescript// src/types/feedback.ts
export interface Feedback {
  id: string;
  questionId: string;
  question: string;
  answer: string;
  userId: string;
  rating: number; // 1-5の評価
  comment?: string;
  helpful: boolean;
  timestamp: Date;
}

フィードバックを保存する処理です。PostgreSQL を使用した例を示します。

typescript// src/services/feedback.ts
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export const saveFeedback = async (
  feedback: Omit<Feedback, 'id' | 'timestamp'>
): Promise<void> => {
  const query = `
    INSERT INTO feedbacks (
      question_id, question, answer, user_id,
      rating, comment, helpful
    ) VALUES ($1, $2, $3, $4, $5, $6, $7)
  `;

  await pool.query(query, [
    feedback.questionId,
    feedback.question,
    feedback.answer,
    feedback.userId,
    feedback.rating,
    feedback.comment || null,
    feedback.helpful,
  ]);
};

フィードバックを分析し、改善が必要な項目を抽出する処理です。

typescript// src/services/feedback-analysis.ts
export interface AnalysisResult {
  averageRating: number;
  helpfulRate: number;
  commonIssues: string[];
  improvementAreas: string[];
}

export const analyzeFeedback = async (
  days: number = 7
): Promise<AnalysisResult> => {
  const query = `
    SELECT
      AVG(rating) as avg_rating,
      COUNT(*) FILTER (WHERE helpful = true)::float / COUNT(*) as helpful_rate,
      question,
      comment
    FROM feedbacks
    WHERE timestamp > NOW() - INTERVAL '${days} days'
    GROUP BY question, comment
    ORDER BY avg_rating ASC
    LIMIT 20
  `;

  const result = await pool.query(query);

  // 共通の問題を抽出
  const commonIssues = result.rows
    .filter((row) => row.avg_rating < 3)
    .map((row) => row.question);

  // 改善が必要な領域を特定
  const improvementAreas = identifyImprovementAreas(
    result.rows
  );

  return {
    averageRating: result.rows[0]?.avg_rating || 0,
    helpfulRate: result.rows[0]?.helpful_rate || 0,
    commonIssues,
    improvementAreas,
  };
};

改善領域を特定するヘルパー関数です。低評価のコメントから改善点を抽出します。

typescript// src/services/feedback-analysis.ts (続き)
const identifyImprovementAreas = (
  rows: any[]
): string[] => {
  const areas: Set<string> = new Set();

  rows.forEach((row) => {
    if (row.comment) {
      const comment = row.comment.toLowerCase();

      // キーワードベースで改善領域を特定
      if (
        comment.includes('古い') ||
        comment.includes('更新')
      ) {
        areas.add('ナレッジの鮮度管理');
      }
      if (
        comment.includes('わかりにくい') ||
        comment.includes('不明確')
      ) {
        areas.add('回答の明確性向上');
      }
      if (
        comment.includes('見つからない') ||
        comment.includes('検索')
      ) {
        areas.add('検索精度の改善');
      }
      if (
        comment.includes('権限') ||
        comment.includes('アクセス')
      ) {
        areas.add('権限制御の見直し');
      }
    }
  });

  return Array.from(areas);
};

具体例

完全な FAQ ボット実装

これまでの解説を踏まえ、実際に動作する FAQ ボットのエンドポイントを実装します。Next.js の API Routes を使用した例です。

まず、型定義とインターフェースを整理します。

typescript// src/app/api/faq/types.ts
export interface FAQRequest {
  question: string;
  userId: string;
}

export interface FAQResponse {
  answer: string;
  sources: Array<{
    title: string;
    source: string;
  }>;
  questionId: string;
}

API Route のハンドラー実装です。ユーザー認証、権限チェック、ナレッジ検索、回答生成の一連の処理を行います。

typescript// src/app/api/faq/route.ts
import { NextRequest, NextResponse } from 'next/server';
import {
  searchKnowledge,
  formatSearchResults,
} from '@/services/knowledge-search';
import {
  generateAnswer,
  handleGrokError,
} from '@/services/grok';
import { User } from '@/types/user';
import { v4 as uuidv4 } from 'uuid';

export async function POST(request: NextRequest) {
  try {
    // リクエストボディを解析
    const { question, userId }: FAQRequest =
      await request.json();

    // バリデーション
    if (!question || !userId) {
      return NextResponse.json(
        { error: 'Error 400: 質問とユーザーIDは必須です' },
        { status: 400 }
      );
    }

    // ユーザー情報を取得(実際は認証システムから取得)
    const user: User = await getUserById(userId);

    if (!user) {
      return NextResponse.json(
        { error: 'Error 401: ユーザーが見つかりません' },
        { status: 401 }
      );
    }

    // ナレッジベースから関連情報を検索(権限フィルター適用)
    const searchResults = await searchKnowledge(
      question,
      user
    );
    const formattedResults =
      formatSearchResults(searchResults);

    // 検索結果がない場合のハンドリング
    if (formattedResults.length === 0) {
      return NextResponse.json({
        answer:
          '申し訳ございません。ご質問に関連する情報が見つかりませんでした。別の言い方で質問していただくか、担当部署にお問い合わせください。',
        sources: [],
        questionId: uuidv4(),
      });
    }

    // Grokで回答を生成
    const answer = await generateAnswer(
      question,
      formattedResults
    );

    // レスポンスを返却
    const response: FAQResponse = {
      answer,
      sources: formattedResults.map((r) => ({
        title: r.title,
        source: r.source,
      })),
      questionId: uuidv4(),
    };

    return NextResponse.json(response);
  } catch (error) {
    console.error('FAQ API Error:', error);
    const errorMessage = handleGrokError(error);

    return NextResponse.json(
      { error: errorMessage },
      { status: 500 }
    );
  }
}

ユーザー情報を取得するヘルパー関数です。実際の実装では、認証システムやデータベースから取得します。

typescript// src/services/user.ts
import { User } from '@/types/user';

export const getUserById = async (
  userId: string
): Promise<User | null> => {
  // 実際はデータベースや認証サービスから取得
  // ここではサンプルデータを返す
  const mockUser: User = {
    id: userId,
    email: 'user@example.com',
    department: 'engineering',
    role: 'employee',
    groups: ['engineering', 'all-employees'],
  };

  return mockUser;
};

フィードバック収集のエンドポイント

ユーザーからのフィードバックを受け付ける API を実装します。

typescript// src/app/api/feedback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { saveFeedback } from '@/services/feedback';

export async function POST(request: NextRequest) {
  try {
    const feedbackData = await request.json();

    // バリデーション
    if (!feedbackData.questionId || !feedbackData.rating) {
      return NextResponse.json(
        { error: 'Error 400: 質問IDと評価は必須です' },
        { status: 400 }
      );
    }

    // 評価が1-5の範囲内かチェック
    if (
      feedbackData.rating < 1 ||
      feedbackData.rating > 5
    ) {
      return NextResponse.json(
        {
          error:
            'Error 400: 評価は1から5の範囲で指定してください',
        },
        { status: 400 }
      );
    }

    // フィードバックを保存
    await saveFeedback(feedbackData);

    return NextResponse.json({
      success: true,
      message: 'フィードバックをありがとうございます',
    });
  } catch (error) {
    console.error('Feedback API Error:', error);
    return NextResponse.json(
      {
        error:
          'Error 500: フィードバックの保存に失敗しました',
      },
      { status: 500 }
    );
  }
}

フロントエンド実装

React を使用した FAQ ボットの UI コンポーネントを実装します。チャット形式のインターフェースを提供しますね。

まず、メッセージの型定義を行います。

typescript// src/components/FAQBot/types.ts
export interface Message {
  id: string;
  type: 'user' | 'bot';
  content: string;
  sources?: Array<{
    title: string;
    source: string;
  }>;
  timestamp: Date;
  questionId?: string;
}

FAQ ボットのメインコンポーネントです。質問の送信、回答の受信、フィードバックの送信機能を実装します。

typescript// src/components/FAQBot/FAQBot.tsx
'use client';

import { useState } from 'react';
import { Message } from './types';
import { v4 as uuidv4 } from 'uuid';

export const FAQBot = () => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  // 質問を送信する処理
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!input.trim()) return;

    // ユーザーメッセージを追加
    const userMessage: Message = {
      id: uuidv4(),
      type: 'user',
      content: input,
      timestamp: new Date(),
    };

    setMessages((prev) => [...prev, userMessage]);
    setInput('');
    setIsLoading(true);

    try {
      // FAQ APIを呼び出し
      const response = await fetch('/api/faq', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          question: input,
          userId: 'current-user-id', // 実際は認証情報から取得
        }),
      });

      if (!response.ok) {
        throw new Error(
          `HTTP error! status: ${response.status}`
        );
      }

      const data = await response.json();

      // ボットの回答を追加
      const botMessage: Message = {
        id: uuidv4(),
        type: 'bot',
        content: data.answer,
        sources: data.sources,
        timestamp: new Date(),
        questionId: data.questionId,
      };

      setMessages((prev) => [...prev, botMessage]);
    } catch (error) {
      console.error('Error:', error);

      // エラーメッセージを表示
      const errorMessage: Message = {
        id: uuidv4(),
        type: 'bot',
        content:
          'エラーが発生しました。もう一度お試しください。',
        timestamp: new Date(),
      };

      setMessages((prev) => [...prev, errorMessage]);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className='faq-bot-container'>
      <div className='messages'>
        {messages.map((message) => (
          <MessageItem key={message.id} message={message} />
        ))}
        {isLoading && <LoadingIndicator />}
      </div>

      <form onSubmit={handleSubmit} className='input-form'>
        <input
          type='text'
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder='質問を入力してください...'
          disabled={isLoading}
        />
        <button
          type='submit'
          disabled={isLoading || !input.trim()}
        >
          送信
        </button>
      </form>
    </div>
  );
};

メッセージアイテムコンポーネントです。ユーザーとボットのメッセージを表示し、フィードバックボタンを提供します。

typescript// src/components/FAQBot/MessageItem.tsx
import { Message } from './types';
import { FeedbackButtons } from './FeedbackButtons';

interface MessageItemProps {
  message: Message;
}

export const MessageItem = ({
  message,
}: MessageItemProps) => {
  return (
    <div className={`message ${message.type}`}>
      <div className='message-content'>
        {message.content}
      </div>

      {/* ボットの回答の場合、ソースを表示 */}
      {message.type === 'bot' &&
        message.sources &&
        message.sources.length > 0 && (
          <div className='sources'>
            <p>参考情報:</p>
            <ul>
              {message.sources.map((source, index) => (
                <li key={index}>
                  {source.title} ({source.source})
                </li>
              ))}
            </ul>
          </div>
        )}

      {/* ボットの回答の場合、フィードバックボタンを表示 */}
      {message.type === 'bot' && message.questionId && (
        <FeedbackButtons
          questionId={message.questionId}
          question={message.content}
          answer={message.content}
        />
      )}

      <div className='timestamp'>
        {message.timestamp.toLocaleTimeString()}
      </div>
    </div>
  );
};

フィードバックボタンコンポーネントです。ユーザーが回答を評価できるようにします。

typescript// src/components/FAQBot/FeedbackButtons.tsx
import { useState } from 'react';

interface FeedbackButtonsProps {
  questionId: string;
  question: string;
  answer: string;
}

export const FeedbackButtons = ({
  questionId,
  question,
  answer,
}: FeedbackButtonsProps) => {
  const [submitted, setSubmitted] = useState(false);

  // フィードバックを送信
  const sendFeedback = async (
    helpful: boolean,
    rating: number
  ) => {
    try {
      await fetch('/api/feedback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          questionId,
          question,
          answer,
          userId: 'current-user-id',
          rating,
          helpful,
        }),
      });

      setSubmitted(true);
    } catch (error) {
      console.error('Failed to send feedback:', error);
    }
  };

  if (submitted) {
    return (
      <p className='feedback-thanks'>
        フィードバックありがとうございます
      </p>
    );
  }

  return (
    <div className='feedback-buttons'>
      <p>この回答は役に立ちましたか?</p>
      <button onClick={() => sendFeedback(true, 5)}>
        👍 はい
      </button>
      <button onClick={() => sendFeedback(false, 2)}>
        👎 いいえ
      </button>
    </div>
  );
};

定期的な改善レポート生成

週次で FAQ ボットのパフォーマンスを分析し、改善レポートを生成するバッチ処理を実装します。

typescript// src/scripts/generate-improvement-report.ts
import { analyzeFeedback } from '@/services/feedback-analysis';

export const generateImprovementReport = async () => {
  // 過去7日間のフィードバックを分析
  const weeklyAnalysis = await analyzeFeedback(7);

  console.log('=== 週次改善レポート ===');
  console.log(
    `平均評価: ${weeklyAnalysis.averageRating.toFixed(
      2
    )}/5.0`
  );
  console.log(
    `役立った割合: ${(
      weeklyAnalysis.helpfulRate * 100
    ).toFixed(1)}%`
  );

  console.log('\n共通の問題:');
  weeklyAnalysis.commonIssues.forEach((issue, index) => {
    console.log(`${index + 1}. ${issue}`);
  });

  console.log('\n改善が必要な領域:');
  weeklyAnalysis.improvementAreas.forEach((area, index) => {
    console.log(`${index + 1}. ${area}`);
  });

  // 改善アクションを提案
  const actions =
    generateImprovementActions(weeklyAnalysis);

  console.log('\n推奨される改善アクション:');
  actions.forEach((action, index) => {
    console.log(`${index + 1}. ${action}`);
  });
};

改善アクションを生成するヘルパー関数です。分析結果に基づいて具体的な改善提案を行います。

typescript// src/scripts/generate-improvement-report.ts (続き)
import { AnalysisResult } from '@/services/feedback-analysis';

const generateImprovementActions = (
  analysis: AnalysisResult
): string[] => {
  const actions: string[] = [];

  // 平均評価が低い場合
  if (analysis.averageRating < 3.5) {
    actions.push('プロンプトの見直しと最適化を実施する');
    actions.push(
      'ナレッジベースの内容を確認し、不足情報を追加する'
    );
  }

  // 改善領域に基づくアクション
  analysis.improvementAreas.forEach((area) => {
    if (area === 'ナレッジの鮮度管理') {
      actions.push(
        '古いドキュメントを特定し、更新または削除する'
      );
      actions.push(
        '同期処理の頻度を見直す(現在: 日次 → 推奨: 4時間ごと)'
      );
    }
    if (area === '検索精度の改善') {
      actions.push(
        'ベクトル化モデルを最新版にアップグレードする'
      );
      actions.push('同義語辞書を拡充する');
    }
    if (area === '権限制御の見直し') {
      actions.push(
        '権限グループの設定を確認し、適切なアクセス制御を実施する'
      );
    }
  });

  return actions;
};

このバッチ処理を定期実行するための設定例です(cron 形式)。

bash# crontab の設定例
# 毎週月曜日の朝9時にレポートを生成
0 9 * * 1 cd /path/to/project && yarn ts-node src/scripts/generate-improvement-report.ts

Docker での運用

本番環境での運用を見据え、Docker で FAQ ボットをコンテナ化する設定を示します。

dockerfile# Dockerfile
FROM node:20-alpine AS base

# 依存関係のインストール
FROM base AS deps
WORKDIR /app

COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# ビルドステージ
FROM base AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 環境変数の設定(ビルド時)
ENV NEXT_TELEMETRY_DISABLED 1

RUN yarn build

# 実行ステージ
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

# 必要なファイルのみをコピー
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

Docker Compose で関連サービスをまとめて起動する設定です。

yaml# docker-compose.yml
version: '3.8'

services:
  faq-bot:
    build: .
    ports:
      - '3000:3000'
    environment:
      - DATABASE_URL=postgresql://user:password@postgres:5432/faqbot
      - PINECONE_API_KEY=${PINECONE_API_KEY}
      - XAI_API_KEY=${XAI_API_KEY}
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    depends_on:
      - postgres
    networks:
      - faq-network

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=faqbot
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - faq-network

  # ナレッジ同期のバッチ処理
  knowledge-sync:
    build: .
    command: yarn ts-node src/scripts/sync-knowledge.ts
    environment:
      - PINECONE_API_KEY=${PINECONE_API_KEY}
      - CONFLUENCE_API_TOKEN=${CONFLUENCE_API_TOKEN}
    networks:
      - faq-network

volumes:
  postgres-data:

networks:
  faq-network:
    driver: bridge

運用監視とアラート設定

FAQ ボットの稼働状況を監視し、問題が発生した際にアラートを送信する仕組みを実装します。

typescript// src/services/monitoring.ts
import axios from 'axios';

// メトリクスの型定義
export interface Metrics {
  totalQuestions: number;
  averageResponseTime: number;
  errorRate: number;
  averageRating: number;
}

// メトリクスを収集
export const collectMetrics =
  async (): Promise<Metrics> => {
    const query = `
    SELECT
      COUNT(*) as total_questions,
      AVG(EXTRACT(EPOCH FROM (response_time - question_time))) as avg_response_time,
      COUNT(*) FILTER (WHERE error = true)::float / COUNT(*) as error_rate,
      AVG(rating) as avg_rating
    FROM faq_logs
    WHERE timestamp > NOW() - INTERVAL '1 hour'
  `;

    // データベースからメトリクスを取得
    const result = await pool.query(query);

    return {
      totalQuestions: parseInt(
        result.rows[0].total_questions
      ),
      averageResponseTime: parseFloat(
        result.rows[0].avg_response_time
      ),
      errorRate: parseFloat(result.rows[0].error_rate),
      averageRating: parseFloat(result.rows[0].avg_rating),
    };
  };

閾値を超えた場合に Slack へアラートを送信する処理です。

typescript// src/services/monitoring.ts (続き)
export const checkAndAlert = async () => {
  const metrics = await collectMetrics();

  // アラート条件のチェック
  const alerts: string[] = [];

  if (metrics.errorRate > 0.1) {
    alerts.push(
      `⚠️ エラー率が高い: ${(
        metrics.errorRate * 100
      ).toFixed(1)}%`
    );
  }

  if (metrics.averageResponseTime > 5) {
    alerts.push(
      `⚠️ 応答時間が遅い: ${metrics.averageResponseTime.toFixed(
        2
      )}秒`
    );
  }

  if (metrics.averageRating < 3.0) {
    alerts.push(
      `⚠️ 評価が低い: ${metrics.averageRating.toFixed(
        2
      )}/5.0`
    );
  }

  // アラートがある場合、Slackに送信
  if (alerts.length > 0) {
    await sendSlackAlert(alerts);
  }
};

const sendSlackAlert = async (alerts: string[]) => {
  const message = `
FAQボット監視アラート

${alerts.join('\n')}

詳細を確認し、対応してください。
  `;

  await axios.post(process.env.SLACK_WEBHOOK_URL || '', {
    text: message,
  });
};

まとめ

本記事では、Grok を活用した社内 FAQ ボットの構築方法を、ナレッジベース連携、権限制御、改善サイクルの 3 つの観点から詳しく解説しました。

以下の表に、実装の要点をまとめます。

#実装項目主な技術・ツール効果
1ナレッジベース統合Pinecone, OpenAI Embeddings散在する情報を統一的に管理・検索
2権限制御メタデータフィルタリング部門・役職に応じた適切な情報提供
3Grok API 連携xAI API自然で正確な回答生成
4フィードバック収集PostgreSQL継続的な品質改善の基盤
5改善サイクル分析スクリプトデータドリブンな最適化
6監視・アラートSlack 連携問題の早期発見と対応

FAQ ボットの導入により、以下のような効果が期待できます。

まず、問い合わせ対応時間が大幅に削減されます。従来は 1 件あたり平均 10 分かかっていた対応が、ボットによる即座の回答で完結するケースが増えるでしょう。対応スタッフは、より複雑で専門的な問い合わせに集中できるようになりますね。

次に、回答品質の均一化が実現します。担当者によるばらつきがなくなり、常に最新のナレッジに基づいた正確な情報を提供できるようになります。

さらに、24 時間 365 日の対応が可能になることで、社員の利便性が向上します。深夜や休日でも、疑問が生じたタイミングで即座に解決できる環境が整うでしょう。

継続的な改善も重要なポイントです。フィードバックデータを分析することで、どのような質問が多いのか、どの領域のナレッジが不足しているのかが明確になります。これにより、ナレッジベース全体の品質向上にもつながるでしょう。

実装にあたっては、いくつかの注意点があります。

第一に、ナレッジベースの初期構築には時間がかかります。既存のドキュメントを整理し、適切な権限情報を付与する作業が必要です。段階的に対象範囲を広げていくアプローチをおすすめします。

第二に、プライバシーとセキュリティへの配慮が不可欠です。機密情報が適切に保護され、権限のないユーザーにアクセスされないよう、厳密な権限制御を実装してください。

第三に、AI の限界を認識し、適切なエスカレーションフローを用意することが重要です。ボットで対応できない複雑な質問については、人間の担当者にスムーズに引き継げる仕組みを整えましょう。

Grok を使った社内 FAQ ボットは、業務効率化の強力なツールとなります。本記事で紹介した実装方法を参考に、ぜひ自社に最適な FAQ ボットを構築してみてください。継続的な改善を重ねることで、さらに効果的なシステムへと進化させていけるはずです。

関連リンク