T-CREATOR

LlamaIndex で社内ナレッジ QA ボット:権限別回答と出典表示で信頼性担保

LlamaIndex で社内ナレッジ QA ボット:権限別回答と出典表示で信頼性担保

社内に散在する情報を一元管理し、誰でも簡単に必要な情報にアクセスできる仕組みを作りたい。そんな悩みを抱える企業は少なくありません。しかし、セキュリティや権限管理を考慮せずに QA ボットを導入すると、機密情報の漏洩リスクが高まってしまいます。

この記事では、LlamaIndex を活用して、ユーザー権限に応じて回答内容を制御し、出典を明示することで信頼性を担保する社内ナレッジ QA ボットの構築方法をご紹介します。実装コードを段階的に解説しますので、初めての方でも安心して取り組めるでしょう。

背景

社内ナレッジ管理の現状

多くの企業では、業務マニュアル、技術文書、プロジェクト資料などが複数のシステムに分散しています。Confluence、SharePoint、Google Drive、Notion など、様々なツールに情報が散在しており、必要な情報を見つけるまでに多くの時間がかかってしまうのです。

さらに、部署や役職によってアクセスできる情報が異なるため、権限管理が複雑化しています。経理部の給与情報、開発チームの技術資料、経営層向けの戦略資料など、それぞれに異なるアクセス権限が必要になりますね。

LlamaIndex が解決できること

LlamaIndex は RAG(Retrieval-Augmented Generation)フレームワークとして、以下の機能を提供します。

  • 柔軟なデータ統合: 複数のデータソースから情報を一元的にインデックス化
  • メタデータフィルタリング: ユーザー権限に基づいた情報の絞り込み
  • 出典トラッキング: 回答の根拠となったドキュメントの追跡
  • カスタマイズ可能な検索: ビジネスニーズに合わせた検索ロジックの実装

以下の図は、LlamaIndex を使った社内ナレッジ QA ボットの基本構造を示しています。

mermaidflowchart TB
    user["ユーザー"] -->|質問| bot["QA ボット"]
    bot -->|権限チェック| auth["権限管理<br/>システム"]
    auth -->|許可された<br/>メタデータ| filter["メタデータ<br/>フィルタ"]
    filter -->|絞り込み| index["LlamaIndex<br/>インデックス"]
    index -->|検索結果<br/>+出典| llm["LLM<br/>(GPT-4 等)"]
    llm -->|回答+出典| bot
    bot -->|回答表示| user

    docs["社内文書"] -->|インデックス化| index
    docs -->|メタデータ付与| index

この図から分かるように、ユーザーからの質問は権限チェックを経て、適切な情報のみが検索対象となります。最終的に LLM が生成する回答には、必ず出典情報が含まれるのです。

課題

権限管理の難しさ

社内ナレッジ QA ボットを構築する際、最大の課題は権限に応じた情報アクセスの制御です。単純に全ての情報をインデックス化してしまうと、アクセス権限のない情報まで回答に含まれてしまう可能性があります。

具体的には、以下のような問題が発生します。

  • 一般社員が経営層向けの戦略資料の内容を閲覧できてしまう
  • 他部署の機密情報が検索結果に表示されてしまう
  • 退職者の権限が残り続け、情報漏洩のリスクが高まる

回答の信頼性担保

LLM による回答は流暢ですが、時として事実と異なる内容(ハルシネーション)を生成してしまいます。社内規定や技術仕様など、正確性が求められる情報では、回答の根拠を明示する仕組みが不可欠ですね。

出典表示がない場合、以下の問題が生じます。

  • 回答が正しいかどうかを検証できない
  • 古い情報と最新情報の区別がつかない
  • 誤った情報に基づいて業務を進めてしまう

下図は、権限管理とトラッキングが不十分な場合の問題を示しています。

mermaidflowchart LR
    subgraph problem["課題"]
        q1["一般社員"] -->|質問| bot1["権限制御なし<br/>QA ボット"]
        bot1 -->|全情報から検索| all["全社内文書"]
        all -->|機密情報も<br/>含まれる| ans1["不適切な回答"]
        ans1 -.->|出典不明| risk["情報漏洩<br/>リスク"]
    end

    style problem fill:#ffeeee
    style risk fill:#ff6666,color:#fff

このような状態では、セキュリティリスクが高く、企業としての情報管理体制が問われることになります。

解決策

メタデータベースの権限制御

LlamaIndex では、各ドキュメントにメタデータを付与することで、検索時にフィルタリングを行えます。ユーザーの権限情報をメタデータと照合し、アクセス可能な文書のみを検索対象とすることで、安全な QA ボットを実現できるのです。

具体的には、以下の情報をメタデータとして管理します。

#メタデータ項目説明
1department部署名"engineering", "sales", "hr"
2access_levelアクセスレベル"public", "internal", "confidential"
3roles閲覧可能な役職["manager", "director"]
4document_idドキュメント識別子"DOC-2024-001"
5last_updated最終更新日時"2024-01-15T10:30:00Z"

出典トラッキング機能

LlamaIndex の response_modenode 情報を活用することで、回答に使用された元文書を追跡できます。各回答に対して、以下の情報を付与することが可能です。

  • 参照したドキュメント名
  • 該当ページまたはセクション
  • 最終更新日時
  • 信頼度スコア

以下の図は、権限制御と出典トラッキングを実装した理想的なシステムフローを表しています。

mermaidflowchart TB
    user["ユーザー<br/>(Role: Manager)"] -->|質問| check["権限チェック"]
    check -->|権限情報取得| meta["メタデータ<br/>フィルタ作成"]

    meta -->|"access_level≦Manager"| query["Query Engine"]
    query -->|検索| idx["Vector Index"]

    idx -->|関連文書| retrieve["文書取得<br/>(出典情報含む)"]
    retrieve -->|Context + 出典| llm["LLM"]

    llm -->|生成| response["回答"]
    retrieve -.->|出典リスト| response

    response -->|回答+出典表示| user

    style check fill:#e3f2fd
    style retrieve fill:#fff9c4
    style response fill:#c8e6c9

この仕組みにより、ユーザーは自分の権限内で必要な情報にアクセスでき、回答の信頼性も同時に確保できます。

具体例

環境構築とインストール

まず、必要なパッケージをインストールします。LlamaIndex と OpenAI の API を使用しますので、事前に OpenAI の API キーを取得しておいてください。

bash# LlamaIndex と関連パッケージのインストール
yarn add llamaindex openai dotenv
bash# TypeScript の型定義もインストール
yarn add -D @types/node typescript

環境変数ファイルを作成し、API キーを設定します。

bash# .env ファイルの作成
touch .env

.env ファイルには以下の内容を記述してください。

plaintextOPENAI_API_KEY=sk-your-api-key-here

データ構造の定義

権限管理とメタデータを扱うための型定義を行います。TypeScript の型システムを活用することで、安全性の高いコードを書けますね。

typescript// types.ts - 型定義ファイル

/**
 * ユーザーの権限情報を表す型
 */
interface UserPermission {
  userId: string;
  department: string;
  accessLevel:
    | 'public'
    | 'internal'
    | 'confidential'
    | 'secret';
  roles: string[];
}
typescript/**
 * ドキュメントのメタデータを表す型
 */
interface DocumentMetadata {
  documentId: string;
  title: string;
  department: string;
  accessLevel:
    | 'public'
    | 'internal'
    | 'confidential'
    | 'secret';
  allowedRoles: string[];
  lastUpdated: string;
  filePath: string;
}
typescript/**
 * 回答と出典情報を含むレスポンス型
 */
interface QAResponse {
  answer: string;
  sources: SourceInfo[];
  confidence: number;
}

/**
 * 出典情報の型
 */
interface SourceInfo {
  documentId: string;
  title: string;
  excerpt: string;
  lastUpdated: string;
  relevanceScore: number;
}

これらの型定義により、コード全体で一貫したデータ構造を扱えるようになります。

メタデータ付きドキュメントの読み込み

実際のドキュメントをメタデータと共にインデックス化する処理を実装します。ここでは、JSON 形式でドキュメント情報を管理する例を示します。

typescript// documentLoader.ts - ドキュメント読み込み処理

import { Document } from 'llamaindex';
import * as fs from 'fs';
import * as path from 'path';

/**
 * ドキュメントをメタデータ付きで読み込む関数
 * 各ドキュメントに権限情報を付与します
 */
async function loadDocumentsWithMetadata(
  documentsDir: string
): Promise<Document[]> {
  const documents: Document[] = [];

  // documents.json からメタデータを読み込み
  const metadataPath = path.join(
    documentsDir,
    'documents.json'
  );
  const metadataList: DocumentMetadata[] = JSON.parse(
    fs.readFileSync(metadataPath, 'utf-8')
  );

  return documents;
}
typescript/**
 * 各ドキュメントファイルを読み込み、メタデータを付与
 */
for (const meta of metadataList) {
  const content = fs.readFileSync(
    path.join(documentsDir, meta.filePath),
    'utf-8'
  );

  // Document オブジェクトを作成し、メタデータを設定
  const doc = new Document({
    text: content,
    metadata: {
      document_id: meta.documentId,
      title: meta.title,
      department: meta.department,
      access_level: meta.accessLevel,
      allowed_roles: meta.allowedRoles,
      last_updated: meta.lastUpdated,
    },
  });

  documents.push(doc);
}

このコードでは、ファイルシステムから文書を読み込み、JSON で定義されたメタデータを各 Document オブジェクトに付与しています。

インデックスの作成

読み込んだドキュメントを Vector Index として構築します。LlamaIndex は内部で埋め込みベクトルを生成し、効率的な検索を可能にします。

typescript// indexBuilder.ts - インデックス構築処理

import {
  VectorStoreIndex,
  StorageContext,
} from 'llamaindex';
import { OpenAI } from 'llamaindex/llm/openai';

/**
 * ドキュメントから Vector Index を構築する関数
 * 埋め込みベクトルを生成し、検索可能な状態にします
 */
async function buildIndex(
  documents: Document[]
): Promise<VectorStoreIndex> {
  // OpenAI の埋め込みモデルを設定
  const llm = new OpenAI({
    model: 'gpt-4',
    temperature: 0.1,
  });

  return index;
}
typescript  // Vector Index を作成
  // この処理で各ドキュメントが埋め込みベクトルに変換されます
  const index = await VectorStoreIndex.fromDocuments(documents, {
    llm,
  });

  console.log(`インデックス構築完了: ${documents.length} 件の文書を登録しました`);

  return index;
}

インデックス構築時には、各ドキュメントのテキストが埋め込みベクトルに変換され、意味的な検索が可能になります。

権限フィルタの実装

ユーザーの権限に基づいて、アクセス可能なドキュメントのみを検索対象とするフィルタを実装します。この機能が、権限別回答の核心部分となりますね。

typescript// permissionFilter.ts - 権限フィルタ処理

/**
 * ユーザー権限に基づいたメタデータフィルタを生成する関数
 * このフィルタにより、権限外の情報へのアクセスを防ぎます
 */
function createPermissionFilter(
  userPermission: UserPermission
): Record<string, any> {
  const accessLevelMap = {
    public: 0,
    internal: 1,
    confidential: 2,
    secret: 3,
  };

  const userLevel =
    accessLevelMap[userPermission.accessLevel];

  return filter;
}
typescript  // アクセスレベルによるフィルタリング
  // ユーザーのレベル以下の文書のみを対象とする
  const filter = {
    filters: [
      {
        key: 'department',
        value: userPermission.department,
        operator: 'eq',
      },
      {
        key: 'access_level',
        value: Object.keys(accessLevelMap).filter(
          (level) => accessLevelMap[level] <= userLevel
        ),
        operator: 'in',
      },
    ],
  };

  return filter;
}

このフィルタにより、部署とアクセスレベルの両方を考慮した権限制御が実現できます。例えば、internal レベルのユーザーは publicinternal の文書にアクセスできますが、confidentialsecret にはアクセスできません。

Query Engine の構築

権限フィルタを適用した Query Engine を作成します。この Engine が、実際にユーザーの質問に対する回答を生成する役割を担います。

typescript// queryEngine.ts - Query Engine 構築

import {
  VectorStoreIndex,
  MetadataFilters,
} from 'llamaindex';

/**
 * 権限制御付き Query Engine を作成する関数
 * ユーザーの権限に応じて検索範囲を制限します
 */
function createQueryEngine(
  index: VectorStoreIndex,
  userPermission: UserPermission
) {
  // 権限フィルタを生成
  const permissionFilter =
    createPermissionFilter(userPermission);

  return queryEngine;
}
typescript  // Query Engine を作成し、フィルタを適用
  const queryEngine = index.asQueryEngine({
    similarityTopK: 5, // 上位5件の関連文書を取得
    filters: new MetadataFilters({
      filters: permissionFilter.filters,
    }),
    responseMode: 'compact', // コンパクトな回答モード
  });

  return queryEngine;
}

similarityTopK パラメータで取得する文書数を指定できます。多すぎると処理時間が長くなり、少なすぎると回答の品質が下がる可能性があるため、適切な値を設定することが重要です。

出典付き回答の生成

Query Engine を使って質問に回答し、出典情報を抽出する処理を実装します。この機能により、回答の信頼性を大きく向上させられますね。

typescript// qaBot.ts - QA ボットのメイン処理

/**
 * 質問に対して出典付きで回答する関数
 * 回答の根拠となったドキュメント情報も併せて返します
 */
async function answerWithSources(
  queryEngine: any,
  question: string
): Promise<QAResponse> {
  // 質問を実行
  const response = await queryEngine.query({
    query: question,
  });

  return qaResponse;
}
typescript  // 回答から出典情報を抽出
  const sources: SourceInfo[] = response.sourceNodes.map((node: any) => ({
    documentId: node.metadata.document_id,
    title: node.metadata.title,
    excerpt: node.text.substring(0, 200) + '...', // 最初の200文字を抜粋
    lastUpdated: node.metadata.last_updated,
    relevanceScore: node.score,
  }));

  const qaResponse: QAResponse = {
    answer: response.response,
    sources: sources,
    confidence: calculateConfidence(sources),
  };

  return qaResponse;
}
typescript/**
 * 出典の関連性スコアから回答の信頼度を計算
 * スコアが高いほど、回答の信頼性が高いと判断できます
 */
function calculateConfidence(
  sources: SourceInfo[]
): number {
  if (sources.length === 0) return 0;

  const avgScore =
    sources.reduce((sum, s) => sum + s.relevanceScore, 0) /
    sources.length;

  return Math.min(avgScore * 100, 100); // 0-100 のスケールに変換
}

信頼度の計算により、回答の品質をユーザーに示すことができます。スコアが低い場合は、追加の確認を促すなどの対応が可能です。

統合実装例

これまでに作成した各モジュールを統合し、実際に動作する QA ボットを構築します。以下は、全体の流れを示すメイン処理です。

typescript// main.ts - メイン処理

import * as dotenv from 'dotenv';

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

/**
 * 社内ナレッジ QA ボットのメイン処理
 * 初期化からクエリ実行までの一連の流れを実装します
 */
async function main() {
  console.log('社内ナレッジ QA ボット起動中...\n');

  // ドキュメントを読み込み
  const documents = await loadDocumentsWithMetadata(
    './documents'
  );
  console.log(
    `${documents.length} 件の文書を読み込みました\n`
  );

  // インデックスを構築
  const index = await buildIndex(documents);
  console.log('インデックス構築完了\n');

  await runDemo(index);
}
typescript/**
 * デモンストレーション実行
 * 異なる権限を持つユーザーでの動作を確認します
 */
async function runDemo(index: VectorStoreIndex) {
  // 一般社員の権限設定
  const generalUser: UserPermission = {
    userId: 'user001',
    department: 'engineering',
    accessLevel: 'internal',
    roles: ['employee'],
  };

  // マネージャーの権限設定
  const manager: UserPermission = {
    userId: 'manager001',
    department: 'engineering',
    accessLevel: 'confidential',
    roles: ['employee', 'manager'],
  };

  await testQuery(index, generalUser);
  await testQuery(index, manager);
}
typescript/**
 * 実際にクエリを実行し、結果を表示
 */
async function testQuery(
  index: VectorStoreIndex,
  user: UserPermission
) {
  console.log(
    `\n=== ユーザー: ${user.userId} (${user.accessLevel}) ===`
  );

  const queryEngine = createQueryEngine(index, user);
  const question =
    '新しいデプロイメントプロセスについて教えてください';

  console.log(`質問: ${question}\n`);

  const response = await answerWithSources(
    queryEngine,
    question
  );

  displayResponse(response);
}
typescript/**
 * 回答と出典情報を見やすく表示
 */
function displayResponse(response: QAResponse) {
  console.log('【回答】');
  console.log(response.answer);
  console.log(
    `\n信頼度: ${response.confidence.toFixed(1)}%\n`
  );

  console.log('【出典】');
  response.sources.forEach((source, index) => {
    console.log(`${index + 1}. ${source.title}`);
    console.log(`   文書ID: ${source.documentId}`);
    console.log(`   更新日: ${source.lastUpdated}`);
    console.log(
      `   関連性: ${(source.relevanceScore * 100).toFixed(
        1
      )}%`
    );
    console.log(`   抜粋: ${source.excerpt}\n`);
  });
}

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

サンプルドキュメントの準備

実際に動作させるために、サンプルの文書とメタデータを準備します。JSON ファイルで管理することで、柔軟な権限設定が可能です。

json// documents/documents.json - ドキュメントメタデータ

[
  {
    "documentId": "DOC-2024-001",
    "title": "デプロイメント手順書(一般向け)",
    "department": "engineering",
    "accessLevel": "internal",
    "allowedRoles": ["employee"],
    "lastUpdated": "2024-01-15T10:30:00Z",
    "filePath": "deployment_guide_public.txt"
  },
  {
    "documentId": "DOC-2024-002",
    "title": "本番環境アクセス権限管理",
    "department": "engineering",
    "accessLevel": "confidential",
    "allowedRoles": ["manager", "director"],
    "lastUpdated": "2024-01-20T14:00:00Z",
    "filePath": "prod_access_guide.txt"
  }
]

各文書ファイル(deployment_guide_public.txt など)には、実際の業務マニュアルや技術文書の内容を記述します。

実行結果の例

実装したシステムを実行すると、以下のような結果が得られます。

plaintext社内ナレッジ QA ボット起動中...

2 件の文書を読み込みました

インデックス構築完了: 2 件の文書を登録しました

=== ユーザー: user001 (internal) ===
質問: 新しいデプロイメントプロセスについて教えてください

【回答】
デプロイメントは Git からコードをプルし、テストを実行した後、
ステージング環境で動作確認を行います。問題がなければ、
承認を得て本番環境へデプロイする流れとなります。

信頼度: 87.3%

【出典】
1. デプロイメント手順書(一般向け)
   文書ID: DOC-2024-001
   更新日: 2024-01-15T10:30:00Z
   関連性: 87.3%
   抜粋: デプロイメントプロセスは以下の手順で実施します...

一般社員は internal レベルまでしかアクセスできないため、confidential の本番環境アクセス権限管理の情報は回答に含まれません。一方、マネージャーが同じ質問をすると、より詳細な情報が回答されるのです。

セキュリティ強化のポイント

実運用では、さらなるセキュリティ対策が必要になります。以下の点に注意して実装してください。

#対策項目実装方法重要度
1ログ記録全てのクエリと回答をログに記録★★★
2権限の定期見直しユーザーの異動や退職時の権限更新★★★
3アクセス監査不審なアクセスパターンの検出★★☆
4データ暗号化メタデータとインデックスの暗号化★★★
5API レート制限過度なクエリ実行の防止★★☆
typescript// logger.ts - ログ記録の実装例

import * as fs from 'fs';

/**
 * クエリと回答をログファイルに記録
 * 監査証跡として保存し、セキュリティインシデント調査に活用します
 */
function logQuery(
  userId: string,
  question: string,
  response: QAResponse,
  timestamp: Date
) {
  const logEntry = {
    timestamp: timestamp.toISOString(),
    userId,
    question,
    answerLength: response.answer.length,
    sourcesCount: response.sources.length,
    confidence: response.confidence,
    sourceDocuments: response.sources.map(
      (s) => s.documentId
    ),
  };

  fs.appendFileSync(
    'query_logs.jsonl',
    JSON.stringify(logEntry) + '\n'
  );
}

ログ記録により、誰がいつどのような質問をし、どのドキュメントが参照されたかを追跡できます。これは情報漏洩の調査や、システムの改善にも役立つでしょう。

パフォーマンス最適化

大量の文書を扱う場合、検索パフォーマンスが課題となります。以下の最適化手法を検討してください。

typescript// optimization.ts - パフォーマンス最適化

import {
  VectorStoreIndex,
  PineconeVectorStore,
} from 'llamaindex';

/**
 * 外部ベクトルストアを使用したスケーラブルな実装
 * Pinecone などのベクトルデータベースを活用します
 */
async function buildScalableIndex(
  documents: Document[]
): Promise<VectorStoreIndex> {
  // Pinecone に接続
  const vectorStore = new PineconeVectorStore({
    apiKey: process.env.PINECONE_API_KEY,
    environment: 'us-west1-gcp',
    indexName: 'company-knowledge',
  });

  return index;
}
typescript  // ベクトルストアを使用したインデックス作成
  const storageContext = await StorageContext.fromDefaults({
    vectorStore,
  });

  const index = await VectorStoreIndex.fromDocuments(documents, {
    storageContext,
  });

  console.log('スケーラブルなインデックスを構築しました');

  return index;
}

Pinecone や Weaviate などの専用ベクトルデータベースを使用することで、数百万件の文書でも高速に検索できるようになります。

まとめ

LlamaIndex を活用した社内ナレッジ QA ボットの構築方法をご紹介しました。メタデータベースの権限制御と出典トラッキング機能により、セキュリティと信頼性を両立したシステムを実現できます。

本記事で解説した重要ポイントは以下の通りです。

  • メタデータフィルタリングにより、ユーザー権限に応じた情報アクセス制御を実現
  • 出典情報の表示により、回答の根拠を明示し、信頼性を担保
  • 段階的な実装により、初心者でも理解しやすい構成
  • セキュリティ対策として、ログ記録や定期的な権限見直しが重要
  • パフォーマンス最適化には、外部ベクトルストアの活用が効果的

社内の情報資産を有効活用しながら、セキュリティリスクを最小限に抑えることが可能です。まずは小規模なドキュメントセットから始めて、徐々に拡張していくアプローチをお勧めします。

実装を進める中で、ビジネス要件に応じて検索精度の調整や、追加のメタデータ項目の設定など、カスタマイズを行ってください。LlamaIndex の柔軟性を活かせば、様々なユースケースに対応できるでしょう。

関連リンク