機械学習
記事内に商品プロモーションを含む場合があります

RAGで「どのドキュメントを参照したか」を出力させてみた

Aru

RAG(Retrieval-Augmented Generation)を用いた回答で、どのドキュメントを参照したのかを出力したいことがありませんか。本記事では、FAISSを用いたベクトル検索の際にメタデータとして参照情報を埋め込み、最終的にLLMの回答に「どの文書を参照したのか(リファレンス)」を付加する方法に挑戦してみました。

RAGで参照したドキュメントを回答に付加したい

大規模言語モデル(LLM)にRAG(Retrieval-Augmented Generation)を組み合わせてユーザーの問い合わせに回答させる場合、「どのドキュメントを参照して回答が生成されたのか」を明示したいケースがあります。

今回は、この「参照元ドキュメントの情報をLLMの回答に自動で付加させる」ことが、システムプロンプトの工夫と、FAISSによるメタデータ活用だけで実現可能かどうかを検証してみました。

この記事では、実際に試してうまくいった手順やポイント、実装上の工夫について解説します。

RAGについては以下の記事を参考にしてください。

FAISSを使ったインデックスの作成

FAISSとは

FAISS(Facebook AI Similarity Search) は、Meta(旧Facebook)が開発したオープンソースの類似検索ライブラリです。主に、ベクトル化されたデータに対する高速な近傍検索 を目的としており、大規模なテキストデータや埋め込みデータの類似性検索に広く利用されています。

特に、自然言語処理の分野では、文章や文書をベクトル化してインデックス化することで、ユーザーの質問に関連する文書を効率よく抽出する用途に適しています。

今回は、このFAISSを使ってあらかじめドキュメント群をベクトル化・インデックス化し、質問に対して類似度の高い文書を取得してRAGに渡すという流れを実装します。

さらに、各文書にメタデータ(ドキュメントIDやファイル名など)を付与することで、どの文書が参照されたかを後から追跡できる仕組みも構築していきます。

FAISSについて詳しく知りたい方は以下の記事を参考にしてください。

Wikiページを読み込む

今回は、ドキュメントとしてWikipediaのページを利用します。以下では、キーワードに対応するWikipediaページを読み込み、テキストとして返す関数 get_wikipedia_page を使用しています。

コードの詳しい説明は以下の記事を参考にしてください。
import requests

def get_wikipedia_page(keyword):
  url = "https://ja.wikipedia.org/w/api.php"
  params = {
      "action": "query",
      "list":"search",
      "srsearch": keyword,
      "srlimit":1,
      # "srnamespace": 1,
      "format": "json",
  }

  response = requests.get(url, params=params)
  data = response.json()
  id = data['query']['search'][0]['pageid']

  params = {
    "action": "query",
    "format": "json",
    "pageids": id,
    "prop": "extracts",
    "exlimit": 1,
    "explaintext": True,
    "redirects": 1,
  }

  response = requests.get(url, params=params)
  data = response.json()
  page = next(iter(data['query']['pages'].values()))

  return page.get("extract", "")

FAISSの読み込み

FAISSをまだインストールしていない場合は、以下のコマンドを実行して必要なパッケージをインストールしてください

pip install langchain-community
pip install faiss-cpu

以下はパッケージの読み込みとせっていを行うコードです。今回は、チャンクサイズを512トークン、オーバーラップを128トークンに設定しました。

from langchain.chains import RetrievalQA
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores.faiss import FAISS
from langchain.docstore.document import Document

CHUNK_SIZE = 512
OVERLAP = 128
SAVE_PATH = "storage"

embedding = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=CHUNK_SIZE,
    chunk_overlap=OVERLAP,
)

インデックスを作成してwikiページを追加する関数

新しくデータベースを作成し、Wikipediaページを追加する関数です。

この関数では、メタデータとしてWikipediaで検索したキーワードチャンク番号を記録しており、それを文書名と番号の代わりとして利用します。

def create_faiss_index_from_wiki(wiki_page_name):
    global embedding, text_splitter
    # Wikiページを読み込み
    wiki_txt = get_wikipedia_page(wiki_page_name)
    texts = text_splitter.split_text(wiki_txt)
    documents = [
        Document(page_content=chunk, metadata={"document_name": wiki_page_name, "chunk_number": i + 1}) for i, chunk in enumerate(texts)
    ]
    db = FAISS.from_documents(documents, embedding)
    return db

既存のインデックスにwikiページを追加する関数

既存のインデックスに、新たなWikipediaページを追加する関数です。先ほどの関数とほとんど同じですが、複数のドキュメントをまたいで検索できるかどうかを確認するために、この関数を用意して、あらたなドキュメントを追加できるようにしました。

def append_faiss_index_from_wiki(wiki_page_name, db):
    global embedding, text_splitter
    # Wikiページを読み込み
    wiki_txt = get_wikipedia_page(wiki_page_name)
    texts = text_splitter.split_text(wiki_txt)
    documents = [
        Document(page_content=chunk, metadata={"document_name": wiki_page_name, "chunk_number": i + 1}) for i, chunk in enumerate(texts)
    ]
    db.add_documents(documents)
    return db

インデックスの作成と保存、読み込み

今回は、”ワンパンマン”と”花の慶次”に関するウィキページを読み込んでインデックスとして登録しました。コードでは、”ワンパンマン”のインデックスを作成して、”花の慶次”を追加、インデックスを保存して、再度読み込みを行っています。

# インデックスの作成
db = create_faiss_index_from_wiki("ワンパンマン")

# 別ページを追加
db = append_faiss_index_from_wiki("花の慶次", db)

# インデックスの保存
db.save_local(SAVE_PATH)

# インデックスを読み込む
db = FAISS.load_local(SAVE_PATH, embedding, allow_dangerous_deserialization=True)

以上で、FAISSによるインデックスの作成は完了しました。次はLLMに渡すドキュメントを作成します。

RAGを試す

今回は、LLMとして gemma-3-27b-it を使用しました。このモデルは、プロンプトでの指示に対して比較的忠実に動作する印象があります。小規模なモデル(例:4B や 7B)は、指示を無視する傾向があるため、同様の手法ではうまく動作しない可能性があります。

なお、今回はプログラムコードからLLMを直接呼び出すのではなく、LM Studio に出力された結果をカット&ペーストで手動入力する形で問い合わせを行っています。

プログラムでやるより色々なLLMの応答を試しやすいので、こちらの方法を取りました

システムプロンプト

以下が今回使用したシステムプロンプトです。出力形式に関する指示を詳細に指定しています。

あなたは情報検索した結果から回答を生成する専門家です。
ユーザーからは以下のようなフォーマットで情報と質問が提示されます。

# 入力フォーマット
入力は複数の情報と質問から構成されます。情報には[1],[2]といった情報番号がつけられ、改行して情報テキストが与えられます。
質問の答えは情報を参照して回答してください。参照した情報については、情報名、情報番号を末尾につけてください。

## ユーザの入力のフォーマット
情報:
[1]: 情報名1 情報番号
情報テキスト1

[2]: 情報名2 情報番号
情報テキスト2

質問: XXはなんですか?

# ユーザへの回答のフォーマット
ユーザーへは、箇条書きなどを利用しつつ、情報を整理し、わかりやすく整理して回答してください。
回答文章を作成する際には、各文の末尾に参照した情報の番号を入れてください。
注意:情報に含まれてない質問には、「質問への回答は与えられた情報に含まれていません」と回答してください。

# 例
## ユーザ入力
情報:
[1]: 首都名 10
日本の首都は東京です
[2]: 福岡県について 3
福岡県の県庁所在地は、福岡市です。

質問: 東京と福岡県の県庁はどこにある?

## 回答
・東京の県庁について
  - 東京の県庁の情報はありませんが、日本の首都です[1]
・福岡の県庁について
  - 福岡県の県庁所在地は福岡市です[2]

参照した情報:
[1] 首都名 10
[2] 福岡県について 3

問い合わせを作成する

問い合わせを作成するコードは以下の通りです。similarity_search を用いて問い合わせに対してデータベースから関連ドキュメントを検索し、その結果をもとに RAG を構成しています。

txt = "ワンパンマンの敵のランクを教えてください"

docs = db.similarity_search(txt, k=8)

rag = ""
for i, doc in enumerate(docs) :
  rag += f"""
[{i+1}] : {doc.metadata['document_name']} {doc.metadata['chunk_number']}
{doc.page_content}

"""

prompt = f"""
情報:
{rag}


質問:
{txt}
ASSISTANT:"""

print(prompt)

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

情報:

[1] : ワンパンマン 302
神…人類滅亡クラスの危機。
竜以上…ヒーロー協会が設定しているランクではないが、ヒーロー大全では竜の範疇を超えたランクとして記載されている。ボロスや覚醒したガロウがこのランクに相当する。ガロウ戦時は万全ではなかったとはいえ、タツマキですら対処不可能なレベルであるが、サイタマから見ると他の怪人とは違うレベルだと認識できる程度で強敵とは程遠いようである。
竜…いくつもの町が壊滅する危機。このクラスの怪人は極々稀にしか出ないとされているが、実際はサイタマによって瞬殺されている場合も多く、現場のヒーローの報告により竜相当とされた怪人でも、協会側の都合で実際よりも低いレベルで記録されることがある。
鬼…一つの町の機能停止もしくは壊滅の危機。目安として、A級10人もしくはS級1人で倒せるレベルとされているが、個体や環境によってはS級ヒーローでも苦戦したり、敗北することがある。原作96撃目では、三日月フトマユゲが「月に1度以上で出現する」と発言している。
虎…多くの人命の危機。目安として、B級5人もしくはA級1人で倒せるレベルとされているが、個体や相性次第ではA級ヒーローでも敗北することがある。


[2] : ワンパンマン 232
三枚舌
災害レベル「虎」。長い舌が3枚に分かれている3人組の怪人。三日月フトマユゲを苦戦させ、リュウモン率いるリュウモン組と交戦した。
肉団子コネコネ
災害レベル「鬼」。巨大怪人。ゼイダッツを返り討ちにした。その数日後、大都市を蹂躙しまくっているロボット軍団と共に再び現れたが、ジェノスによって全身を輪切りにされて死亡した。
甲殻王
災害レベル「鬼」。ブルーが率いるチームメンバーを全滅させたが、ブルーによって倒された。
甲殻人
災害レベル「狼」。全員ブルーによって倒された。
ヘビースモーカー
災害レベル「竜」。N市に現れた怪人。有害な毒煙を吐き出すことでじわじわと嬲り殺しにする。タンクトップマスターと対峙し最後の手を出させようとするまで追い詰めるも、駆けつけた閃光のフラッシュによって退治された。しかし、死体から大量の毒煙が発生したことで避難区域を拡大せざるを得なくなり、N市は除染作業がままならない状態になっている。
シュウゴウタイ


[3] : ワンパンマン 229
ケンザンネズミ
災害レベル竜。H市に出現したハリネズミのような姿をした巨大怪獣。近隣から駆け付けたプロヒーローと警察と消防を全滅状態に追い込み、H市の東側を壊滅させたが、サイタマとタツマキの戦闘の巻き添えを受けて死亡。
ネットサーファーズ
災害レベル「鬼」。ネットの掲示板で馬鹿にされすぎて怪人化した3人組の男性。2本の角の先端がアンテナになっている。
へヴィコングと桃テリーと鉄骨を倒すも、ジェノスと閃光のフラッシュにあっさり倒された。
嫌なピエロ
災害レベル「虎」➡︎災害レベル「鬼以上」➡︎災害レベル「竜」
遊園地に現れた衣装一体系怪人のなりたて。使用する武器は大きな木槌のようなものを使い、実力はなりたてである事から最初は災害レベル「虎」程度の実力しかなかったが、異常に速い成長スピードと周りに注目される程自分が強化される特性があり、実力はS級上位クラスに匹敵するA級1位のアマイマスクと互角以上に渡り合った。


[4] : ワンパンマン 231
しかし真の姿になったアマイマスクに顔面が凹むほどの攻撃を受け一時は死にかけたが、アマイマスクが真の姿になったため注目がさらに集まったことでさらにパワーアップし胴体に大きな口がある異形の化け物になり、一時は怪人化したアマイマスクに対抗していた。アマイマスクにとどめを刺すために胴体の口で捕食しようとしたがその隙を突かれ、胴体の口を蹴られて破壊され怯んだ隙に頭も砕かれ、さらに体のほとんどを破壊され完全に絶命した。
災害レベル「竜」の評価はあくまでアマイマスクの評価であるため、実際は不明。
下水ワニ
災害レベル「虎」。二足歩行のワニの怪人の群れ。
1匹は大哲人、大半はネオ微炭酸BOYSと他のネオヒーローたち、特に大きな個体はウェビギャザに倒された。
必殺技は突進攻撃の「デスロール」。
爆速カタツムリ
災害レベル「鬼」。巨大なカタツムリの怪物であり新幹線と同じ速度で走行が可能。高速道路上の車両を巻き込みながら暴走していたが、アクセル率いるネオハンターズにより討伐された。
三枚舌
災害レベル「虎」。長い舌が3枚に分かれている3人組の怪人。三日月フトマユゲを苦戦させ、リュウモン率いるリュウモン組と交戦した。


[5] : ワンパンマン 303
虎…多くの人命の危機。目安として、B級5人もしくはA級1人で倒せるレベルとされているが、個体や相性次第ではA級ヒーローでも敗北することがある。
狼…危険因子となる生物や集団の出現。目安として、C級3人もしくはB級1人で倒せるレベルとされている。個体や相性によってはC級ヒーロー単独でも対処可能で、童帝によると一般人でも人数と道具が揃えば対処可能。


[6] : ワンパンマン 138
15巻番外編『災害レベル』や原作での怪人協会本部に突入した際のジェノスの台詞など、災害レベル鬼の代表例として描かれている。
海人族(アニメ版では深海族)
深海に棲む種族。単体では災害レベル狼。10体程度となれば災害レベル「虎」相当となる。地上を支配するべく侵攻してきた。かなりの巨体で、人間のような体に水棲生物の頭や鰭、触手を持つ。第1陣として現れた一匹はサイタマに、第2陣の10体はスティンガーによって倒される。
天空王
声 - 上田燿司
災害レベル不明。天狗のような姿の怪人で、空を住処としている様子。地底王・深海王が死んだ今こそ地上を征服する時と4人の息子を率いてヒーロー協会本部を急襲するが、息子共々通りすがりのメルザルガルドに惨殺された。
森林王
災害レベル竜。引きこも侍、エコロ爺、ダイナマイトマン、テジナーマンを捕縛するが、ジェノスに倒された。
森林族
災害レベル虎。Y市に出現した植物の怪人。その内の数体は引きこも侍、エコロ爺、ダイナマイトマン、テジナーマンに倒され、残りの森林族は全てジェノスに焼却された。


[7] : ワンパンマン 227
原作100撃目で協会本部地下13階に収監されていたが、タツマキとフブキの戦闘で檻が破壊されたことで脱走、その直後にフブキの放った「地獄嵐」の巻き添えになり、同じく収監されていたビールッパラーや人喰いカピバラと共に死亡した。
巨大黒鳥
災害レベル鬼。キングの自宅のあるマンションを襲撃した巨大怪鳥。キングの家を訪れていたサイタマに倒される。
タコヅメ男
災害レベル虎。鋭い爪を持った巨大なタコのような怪人で、頭部には人面がある。キングのトレードマークである片目の傷跡は、この怪人によってつけられたもの。まだ修行中のサイタマに倒される。
原始人スッポン
災害レベル不明。流水の中から発見され、研究所で蘇生された原始人。研究所を逃げ出し、近づく人々を無差別に 襲っていたが、アマイマスクに倒される。
闘牛ガエル
災害レベル虎。村田版にのみ登場。スネックの回想に登場した、地中から人間を食べるために出現した巨大なカエル。闘牛のような角を持ち、鼻輪をつけている。明記されてはいないが、スネックに倒されたようである。
ビールッパラー


[8] : ワンパンマン 301
ゴウケツや業火のフレイム曰く、常人の場合は気性が荒くなったり知性を失うこともあるが、身体操作法に長けた者は怪人になっても人間の姿を維持できるとのこと。
災害レベル
怪人の脅威度によってランク分けされ、高い方から順に、神・竜・鬼・虎・狼となっている。主に怪人の強さを表す意味で使われる。また、作中では記載されていないがヒーロー大全では神と竜の間のランクとして竜以上が出てくる。怪人以外に巨大隕石などの自然災害にも災害レベルが適用されるが、疫病などに対しても適用されるかは不明。怪人が現れた際にはヒーロー協会によって即座に災害レベルが設定されるが、実際の災害レベルと一致していない場合も少なく無く、現場にいるヒーローの報告により災害レベルが修正されることもある。また、同じ災害レベルの怪人でも実力はピンからキリまである。
神…人類滅亡クラスの危機。

質問:
ワンパンマンの敵のランクを教えてください

結果

システムプロンプトをLMStudioのシステムプロンプトに貼り付け、上記のRAGをユーザ入力として入力した場合の応答は以下になります。

ワンパンマンの敵のランクと、ドキュメントの名前(ワンパンマン)と参照したチャンク番号がきちんと出力されています。

・神: 人類滅亡クラスの危機です[1, 8]。
・竜以上: ヒーロー協会が設定しているランクではなく、竜の範疇を超えたランクとして記載されています。ボロスや覚醒したガロウがこのランクに相当します[1]。
・竜: いくつもの町が壊滅する危機です。ケンザンネズミ、ヘビースモーカー、嫌なピエロなどが該当します[1, 4, 6, 7]。
・鬼: 一つの町の機能停止もしくは壊滅の危機です。肉団子コネコネ、甲殻王、ネットサーファーズ、爆速カタツムリ、巨大黒鳥などが該当します[2, 4, 6, 7]。
・虎: 多くの人命の危機です。三枚舌、下水ワニ、タコヅメ男、森林族などが該当します[2, 4, 6, 7]。
・狼: 危険因子となる生物や集団の出現です。海人族(単体)、甲殻人などが該当します[5, 6]。

参照した情報:
[1] ワンパンマン 302
[2] ワンパンマン 232
[4] ワンパンマン 231
[5] ワンパンマン 303
[6] ワンパンマン 138
[7] ワンパンマン 227
[8] ワンパンマン 301

「ワンパンマンと慶次とは?」に対する回答

複数の文章が参照できるか、「ワンパンマンと慶次とは?」という問い合わせをしてみました。結果は以下になります。参照した情報が少し間違っているのが気になりますが・・・一応、参照番号と末尾に参照したドキュメントが追加されました。

・ワンパンマンについて[1, 3, 4, 6, 7, 8]

ONEによる日本の漫画で、ウェブサイトで連載され、累計1000万人以上が閲覧した作品です[1]。
村田雄介によるリメイク版は『となりのヤングジャンプ』で連載されています[3]。
テレビアニメも放送されており、第1期は2015年、第2期は2019年に放送されました。第3期が2025年10月より放送予定です[7]。
主人公は一撃で敵を倒すヒーローで、A級ヒーローランキングも存在します[6]。
・花の慶次について[1, 5, 8]

原哲夫による日本の漫画で、『一夢庵風流記』を原作としています[1]。
戦国の世を生きた漢・前田慶次の生き様を描いた作品です[1]。
武器として千子村正、朱槍、唐剣、短刀などが登場します[5]。
参照した情報:
[1] 花の慶次 1
[3] ワンパンマン 1
[4] ワンパンマン 317
[5] 花の慶次 8
[6] ワンパンマン 74
[7] ワンパンマン 322
[8] ワンパンマン 329

Gemma-3-27Bはそれなりに賢いですが、指示に合わせた出力はもうちょっとかもしれません。

Command-A-03-2025を使った場合の回答

command-aを使って質問に回答させてみました。結果、gemma3よりうまく三勝番号をつけることができました。

コマンドAは111BとGemma3の27Bより3倍以上大きなモデルです。プロンプト指示をうまく解釈する能力は、やはり大きなモデルの方が高いようです。

まとめ

この記事では、RAG を用いて、参照されたドキュメントの名称とチャンク番号を LLM に出力させる方法に挑戦しました。LLM には Gemma-3-27B を使用し、ある程度は期待通りの動作が得られましたが、依然として誤りや不一致が発生するケースも見られました。

現時点では、RAG の仕組みだけで正確に参照元を追跡・表示させるのはやや難しい面があると感じています。やはり、出力整形や追加のポストプロセッシング処理が必要になりそうです。

引用をつける方法は公式ページにもあります。そちらも参考にしてみてください。

https://python.langchain.com/docs/how_to/qa_citations/

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUT ME
ある/Aru
ある/Aru
IT&機械学習エンジニア/ファイナンシャルプランナー(CFP®)
専門分野は並列処理・画像処理・機械学習・ディープラーニング。プログラミング言語はC, C++, Go, Pythonを中心として色々利用。現在は、Kaggle, 競プロなどをしながら悠々自適に活動中
記事URLをコピーしました