0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMアプリに引用元表示を実装する:出典付き回答の設計パターン

0
Last updated at Posted at 2026-10-22

LLMアプリで社内規程や技術ドキュメントを検索し、回答を生成する場合、「その回答はどこに書いてあるのか」を利用者が確認できることが重要です。

回答文だけを表示すると、内容が正しくても検証できません。逆に、検索結果をそのまま並べるだけでは、利用者が必要な箇所を読みにくくなります。

この記事では、RAG(Retrieval-Augmented Generation)型のLLMアプリに引用元表示を実装する際の設計パターンを紹介します。実装のポイントは、検索段階で出典情報を失わず、生成段階では引用IDを明示させ、表示前に引用の妥当性を検証することです。

1. 引用元情報を検索結果に持たせる

まず、ドキュメントを分割して検索インデックスに登録する際、本文だけでなく出典情報も一緒に保存します。

最低限、次のようなメタデータを持たせます。

  • source_id: 文書内で一意なID
  • title: 文書タイトル
  • url: 元ページへのリンク
  • page または section: ページ番号や見出し
  • chunk_id: 検索対象チャンクのID

実際に試した構成では、LLMに渡すコンテキストと、画面表示用のメタデータを別々に管理しました。本文にURLを直接埋め込むより、IDで紐付けたほうがプロンプトを簡潔に保てます。

from dataclasses import dataclass

@dataclass
class SearchResult:
    chunk_id: str
    text: str
    title: str
    url: str
    section: str
    score: float

results = [
    SearchResult(
        chunk_id="chunk-001",
        text="パスワードは90日ごとに変更する必要があります。",
        title="社内セキュリティ規程",
        url="https://example.com/security-policy",
        section="第3章 パスワード管理",
        score=0.91,
    )
]

この時点で、検索結果を単なる文字列に変換してしまわないことが重要です。後続処理で引用リンクを作るため、メタデータは回答生成が終わるまで保持します。

2. コンテキストに引用IDを付ける

検索結果をLLMへ渡す際は、各チャンクに [S1] のような引用IDを付けます。

def build_context(results: list[SearchResult]) -> str:
    blocks = []

    for index, result in enumerate(results, start=1):
        blocks.append(
            f"[S{index}]\n"
            f"タイトル: {result.title}\n"
            f"セクション: {result.section}\n"
            f"本文: {result.text}"
        )

    return "\n\n".join(blocks)

生成用プロンプトには、回答ルールも明記します。

あなたは検索結果に基づいて回答するアシスタントです。

ルール:
- 検索結果に根拠がない内容は断定しない
- 事実を述べる文の末尾に、根拠となる引用IDを付ける
- 引用IDは [S1] の形式で記載する
- 根拠が複数ある場合は [S1][S2] のように記載する
- 回答できない場合は「確認できませんでした」と答える

検索結果:
{context}

質問:
{question}

回答例は次のようになります。

パスワードは90日ごとに変更する必要があります。[S1]

ここで大切なのは、LLMにURLそのものを書かせないことです。URLを生成させると、存在しないURLや誤ったURLが出力される可能性があります。LLMには引用IDだけを生成させ、実際のリンクはアプリケーション側で解決します。

3. 引用IDから表示用リンクを生成する

LLMの回答に含まれる引用IDを抽出し、対応する検索結果から出典一覧を組み立てます。

import re

def extract_source_ids(answer: str) -> list[str]:
    ids = re.findall(r"\[S(\d+)\]", answer)
    return sorted(set(f"S{source_id}" for source_id in ids))

def build_citations(
    answer: str,
    results: list[SearchResult],
) -> list[dict]:
    source_ids = extract_source_ids(answer)
    citations = []

    for source_id in source_ids:
        index = int(source_id[1:]) - 1

        if 0 <= index < len(results):
            result = results[index]
            citations.append({
                "id": source_id,
                "title": result.title,
                "section": result.section,
                "url": result.url,
                "chunk_id": result.chunk_id,
            })

    return citations

APIレスポンスは、本文と引用を分けて返す設計にすると、Web UIやモバイルアプリで扱いやすくなります。

{
  "answer": "パスワードは90日ごとに変更する必要があります。[S1]",
  "citations": [
    {
      "id": "S1",
      "title": "社内セキュリティ規程",
      "section": "第3章 パスワード管理",
      "url": "https://example.com/security-policy",
      "chunk_id": "chunk-001"
    }
  ]
}

画面上では、本文中の [S1] をクリック可能なバッジに置き換え、回答の下に「出典」一覧を表示する形が分かりやすいです。長いURLを本文に表示せず、タイトルとセクションを見せると、可読性も保てます。

4. 引用の妥当性を検証する

引用IDが存在するだけでは、十分な品質とはいえません。LLMが検索結果にないIDを出力したり、回答の主張と関係ない出典を付けたりする可能性があるためです。

まずは、存在しない引用IDを除外します。

def validate_citations(
    answer: str,
    results: list[SearchResult],
) -> tuple[str, list[str]]:
    valid_ids = {
        f"S{index}"
        for index in range(1, len(results) + 1)
    }

    referenced_ids = extract_source_ids(answer)
    invalid_ids = [
        source_id
        for source_id in referenced_ids
        if source_id not in valid_ids
    ]

    return answer, invalid_ids

本番運用では、次のようなチェックも有効です。

  • 引用IDが1つもない回答を検出する
  • 検索結果にない引用IDをエラーとして記録する
  • 引用元チャンクのスコアが低い場合に注意表示する
  • 回答の各段落に引用があるか確認する
  • 出典ページが削除されていないか定期的に確認する

引用が不正な場合は、無理にリンクを表示せず、「出典を特定できませんでした」と扱うほうが安全です。引用機能は回答をもっともらしく見せるための装飾ではなく、利用者が検証するための機能だからです。

5. 実装時に注意したこと

チャンクを細かくしすぎない

チャンクが短すぎると、主語や条件が別チャンクに分かれてしまいます。たとえば「管理者は申請できる」という文と、「緊急時に限る」という条件が分離すると、LLMが条件を落とす可能性があります。

見出し、段落、箇条書きの単位を意識し、前後の文脈が残るように分割します。

引用は主張単位で付ける

回答全体の末尾に引用を1つだけ付けるより、主張や段落ごとに引用を付けたほうが検証しやすくなります。

通常の申請は平日の営業時間内に受け付けます。[S1]
緊急申請は管理者の承認が必要です。[S2]

出典の版を保存する

同じURLの内容が更新される場合、後から見たページと回答生成時の根拠が異なることがあります。文書の更新日時、版番号、取得日時もメタデータとして保存しておくと、監査や問い合わせ対応に役立ちます。

「出典付き」と「正しい」は別

引用元が表示されていても、LLMが出典内容を誤って要約する可能性は残ります。そのため、重要な業務では、回答生成後に検索チャンクとの一致を確認する処理や、人による確認フローも必要です。

まとめ

引用元表示の処理全体の流れを図にすると次の通りです。

LLMアプリの引用元表示は、次の流れで設計すると実装しやすくなります。

  1. 検索結果にURLや文書位置などのメタデータを持たせる
  2. コンテキストに [S1] のような引用IDを付ける
  3. LLMにはURLではなく引用IDを生成させる
  4. アプリ側で引用IDを実際の出典リンクに変換する
  5. 不正な引用や根拠のない回答を検証する

私が検証した範囲では、引用をLLMの自由生成に任せず、検索結果のメタデータとアプリケーションロジックで管理する構成が最も扱いやすいと感じました。利用者が回答の根拠をすぐ確認できるようになると、LLMアプリを業務で使う際の安心感と運用性が大きく向上します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?