はじめに
RAG を触ったことがある人なら、一度はこう思ったはずだ。「回答は流暢なのに、どこまで信じていいか分からない」。デモでは「答えられること」ばかりが強調され、「その答えがどこから来たのか」は置き去りにされがちだ。
私はブックマーク管理ツールに引用付きの Ask 機能を実装した。その経験から言えるのは、引用は UI の飾りではないということだ。引用はハルシネーションに対抗するためのエンジニアリングである。一文ごとに遡れる出处があれば、間違っていてもユーザーは自分で検証できる。この記事では、引用を「表示する」ためではなく「信頼できる」ために必要な設計判断を書く。
引用粒度:どこまで細かく遡れるべきか
最初に悩むのが引用の単位だ。選択肢は大きく 3 つある:
- 文書単位:実装は楽だが、ユーザーがリンクを開いても該当箇所を探せない。「引用があるのに検証できない」は引用がないのと同じだ。
- 文単位:精度は高いが、引用番号が文中に溢れて読み物として破綻する。
- チャンク単位:両者の中間。私の結論はここがスイートスポットだ。
ただし条件がある。チャンクは必ずメタデータを持つこと。ソース URL、タイトル、保存日時、そして原文中の位置(offset)。チャンクが「どの文書のどこ」なのかを失った瞬間、引用はただの飾り番号になる。前回書いたチャンキングの記事でも触れたが、メタデータの断裂は引用システム全体を壊す。
検索側:引用が信頼できる前提は検索が信頼できること
引用の話をすると生成側に目が行きがちだが、実は検索側の設計が先だ。鉄則はひとつ:生成は、検索で返ってきたチャンクの範囲内でのみ行う。順序は「検索 → コンテキストの限定 → 生成」で、この順序を逆にしてはいけない。
ここでハイブリッド検索が効いてくる。固有名詞(人名・製品名)の取りこぼしは BM25 が拾い、言い換えや意味的な近さはベクトル検索が拾う。引用の信頼性は、結局のところ「正しいチャンクがコンテキストに入っているか」に依存する。検索が間違っていれば、その後の工程がどれだけ丁寧でも引用は嘘になる。
やってはいけないアンチパターンがひとつある。生成時にモデルへ「常識で補ってよい」余地を与えることだ。コンテキストの外に出た瞬間、引用番号は捏造され始める。プロンプトでどれだけ縛っても、コンテキスト外の生成を許す設計では引用は守れない。
生成側:モデルにきちんと出典を付けさせる
プロンプト設計
指示は具体的にする。「適切に引用して」では足りない。私が使っているテンプレートの骨子はこうだ:
以下のコンテキストのみを用いて質問に答えてください。
- 各主張の直後に [n] の形式で引用番号を付けてください
- 引用番号は、今回渡したコンテキスト内のチャンクのみを指すものとします
- コンテキストに含まれない情報を補ってはいけません
構造化出力
段落を書かせてから番号を振らせるのではなく、「主張 → 引用 ID」のペアを出力させる。順序を変えるだけで、後処理の検証が格段に楽になる。
生成後の検証
モデルを信じない。プログラムで検証する:
def verify_citations(answer_claims, chunks):
chunk_ids = {c.id for c in chunks}
chunk_text = {c.id: c.text for c in chunks}
for claim in answer_claims:
for cid in claim.citation_ids:
if cid not in chunk_ids:
return False, f"存在しないチャンクを参照: {cid}"
if not supports(chunk_text[cid], claim.text):
return False, f"チャンクが主張を裏付けていない: {cid}"
return True, "OK"
supports の中身は、最初は単純な語彙的重なりでもよい。重要なのは「検証の仕組みがあること」で、完璧な entailment 判定ではない。
失敗談
2 つほど痛い目を見た。
- モデルが引用番号を付けたが、その番号が別クエリ用のチャンクを指していた。番号の形式だけ見れば正しく見える。生成後の検証で「ID は存在するが主張を裏付けていない」として弾かれた。形式の正しさと内容の正しさは別物だ。
- チャンクを細かく切りすぎて、引用先が半文だけのチャンクになった。読者がリンクを開いても前後が分からず「で、何の話?」となる。これは前回の記事で書いたチャンクサイズのトレードオフと直結している。
拒否する勇気:検索で見つからないときは黙る
引用システムで一番効いたのは、実は「答えない」機能だった。top-k の類似度がすべて閾値を下回ったら、「知識庫に関連する内容が見つかりませんでした」とだけ返す。
一度でも自信満々にでっち上げた回答を出すと、ユーザーはそのツール全体を信じなくなる。沈黙は一時の不便だが、捏造は信頼の破産だ。閾値はオフラインの評価セットと人手の抜き打ちチェックで決め、「もう少し広く探す」再試行ボタンを付けておくと体験が丸くなる。
実装例
なお、この仕組みは私が開発しているブックマーク管理ツール「LinkWise」の引用付き Ask として実際に動いている。
効果と教訓
引用を付けて変わったこと:回答の横に「原文を開く」が付いただけで、ユーザーの振る舞いが変わった。正しそうな回答は開かれず、怪しい回答ほど開かれる。つまり引用は「信頼の表明」ではなく「検証の導線」として機能している。
一番の教訓は、引用システムで最も難しいのは生成側ではなく、チャンクのメタデータが収集→分割→インデックスの全工程で途切れないことだ。どこか一箇所でもメタデータが欠落すると、そのチャンクからの引用は死ぬ。地味だが、ここが全ての土台である。
最小構成で再現するなら
週末で試すなら:LangChain / LlamaIndex の citation 対応 + 上のような生成後検証スクリプトで十分動く。最初は検証を甘めにし、誤検出のログを眺めながら締めていくのが近道だ。
最後に、読者のみなさんに質問です。引用の粒度はどのレベルを選んでいますか?その理由もぜひコメントで教えてください。