RAGって何? AIに「自社の知識」を教える方法
ミドリ: タツヤさん、社内のマニュアルや規程に基づいて正確に答えてくれるAIを作りたいんですけど、ChatGPTにそのまま聞いても的外れな回答が返ってくるんですよね……。
タツヤ: あるあるだね。しかもAIは自信満々に間違えるから厄介なんだよ。いわゆる「ハルシネーション」ってやつ。
ミドリ: そうなんです! それをどうにかしたくて。
タツヤ: そこで登場するのがRAG(Retrieval-Augmented Generation:検索拡張生成)。LLMが回答を生成するときに、外部のデータソースから関連情報を検索して、その情報をコンテキストとして渡す仕組みだよ。Gartner社の調査によれば、LLMを業務活用している企業の約60%がRAGを採用または導入を計画してる。
ミドリ: 6割も! かなり主流なんですね。
RAGの仕組み:3つのフェーズ
ミドリ(thinking): 具体的にはどういう仕組みなんですか?
タツヤ: 大きく3つのフェーズに分かれる。
フェーズ1:インデックス作成(事前準備)
タツヤ(nodding): まず社内文書やFAQ、マニュアルをベクトル化(Embedding)して、ベクトルデータベースに格納する。これは事前に一度やっておく処理で、新しい文書が追加されたら更新する。
フェーズ2:検索(Retrieval)
タツヤ: ユーザーの質問もベクトル化して、ベクトルDBから意味的に類似した文書を検索・取得する。
ミドリ: キーワードの完全一致じゃなくて、「意味の近さ」で検索するんですね。
タツヤ: そう。だから「有給の取り方」って聞いても「年次有給休暇の申請手続き」っていう文書がちゃんとヒットする。
フェーズ3:生成(Generation)
タツヤ: 検索で取得した文書をコンテキストとしてLLMに渡して、それに基づいた回答を生成する。LLMは自分の学習データじゃなくて、提供されたコンテキスト内の情報に基づいて回答するから、正確性が大幅に上がる。
RAGとFine-tuningの違い
ミドリ: Fine-tuning(ファインチューニング)との違いは何ですか? どっちを選べばいいんでしょう。
タツヤ(nodding): いい質問。比較表で見るとわかりやすいよ。
| 比較項目 | RAG | Fine-tuning |
|---|---|---|
| 知識の更新 | リアルタイムに可能 | 再学習が必要 |
| 初期コスト | 低〜中 | 中〜高 |
| 運用コスト | 検索インフラの維持 | 定期的な再学習 |
| 適したユースケース | 社内ナレッジ検索、FAQ | 特定のタスクやトーンの最適化 |
| ハルシネーション | 大幅に軽減 | 一定の改善 |
| 回答の根拠 | 出典を提示可能 | 提示困難 |
ミドリ(thinking): RAGは「出典を提示できる」のが強いですね。ビジネスだと「なぜその回答なのか」が大事ですもんね。
タツヤ: まさにそこ。多くのケースでは、まずRAGを導入して、必要に応じてFine-tuningを組み合わせるのが効果的だよ。
実装してみよう
ミドリ(thinking): 実際にどうやって作るんですか?
タツヤ: 順番にやっていこう。
ステップ1:環境準備
pip install langchain langchain-openai langchain-community
pip install chromadb faiss-cpu
pip install unstructured pypdf tiktoken
ステップ2:ドキュメントの読み込みとチャンク分割
タツヤ: RAGの精度を左右する最重要ステップがこのチャンキング。文書をどう分割するかで検索精度が決まる。
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# PDFドキュメントの読み込み
loader = DirectoryLoader(
"./documents/",
glob="**/*.pdf",
loader_cls=PyPDFLoader
)
documents = loader.load()
# チャンク分割
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 1チャンクの最大文字数
chunk_overlap=100, # チャンク間のオーバーラップ
separators=["\n\n", "\n", "。", "、", " "], # 日本語向けの区切り文字
length_function=len
)
chunks = text_splitter.split_documents(documents)
print(f"生成されたチャンク数: {len(chunks)}")
ミドリ: チャンクサイズってどのくらいがいいんですか?
タツヤ: ここが悩みどころなんだよね。
- 小さすぎる(100〜200文字) — 検索精度は高いけど、コンテキストが不足しがち
- 大きすぎる(1,000文字以上) — コンテキストは豊富だけど、ノイズが混入しやすい
- 推奨サイズ — 日本語文書なら300〜600文字がベストバランス
オーバーラップはチャンクサイズの15〜20%に設定するのが一般的。文の途中で切れちゃう問題を防げる。
ステップ3:ベクトルデータベースの構築
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
# Embeddingモデルの初期化
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# ベクトルデータベースの作成
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="company_docs"
)
print(f"インデックスに格納したチャンク数: {vectorstore._collection.count()}")
ステップ4:検索と回答生成
タツヤ: ここでRAGチェーンを組み立てる。プロンプトが重要で、「コンテキストに含まれない情報は答えるな」って明示するのがポイント。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# Retriever(検索器)の設定
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4} # 上位4件を取得
)
# プロンプトテンプレート
template = """以下のコンテキスト情報のみに基づいて質問に回答してください。
コンテキストに含まれない情報については「その情報は見つかりませんでした」と回答してください。
コンテキスト:
{context}
質問: {question}
回答:"""
prompt = ChatPromptTemplate.from_template(template)
llm = ChatOpenAI(model="gpt-4o", temperature=0.1)
# RAGチェーンの構築
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# 実行
answer = rag_chain.invoke("有給休暇の申請手順を教えてください")
print(answer)
ミドリ(nodding): なるほど、検索結果をプロンプトに埋め込んでLLMに渡すんですね。
ステップ5:出典情報の付与
タツヤ(smiling): ビジネス利用では「どの文書に基づいた回答か」を明示するのが超重要。
from langchain_core.runnables import RunnableParallel
# 出典情報付きのRAGチェーン
rag_chain_with_sources = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
).assign(
answer=lambda x: (
prompt.invoke({
"context": format_docs(x["context"]),
"question": x["question"]
})
| llm
| StrOutputParser()
).invoke(x["question"])
)
result = rag_chain_with_sources.invoke("出張旅費の精算方法は?")
print("回答:", result["answer"])
print("出典:")
for doc in result["context"]:
print(f" - {doc.metadata.get('source', '不明')} (ページ {doc.metadata.get('page', '不明')})")
ミドリ: 「出典:社内規程第5章 p.12」みたいに出るんですね。これなら信頼できる!
検索精度をもっと上げるテクニック
ミドリ(nodding): 基本はわかりました。もっと精度を上げる方法はありますか?
タツヤ: もちろん。4つの高度なテクニックがある。
ハイブリッド検索
タツヤ: ベクトル検索(意味的類似度)とキーワード検索(BM25)を組み合わせる方法。ベクトル検索は意味の類似性に強いけど、固有名詞や型番の完全一致には弱い。BM25はその逆。両方使えば、検索精度が15〜25%向上するケースが報告されてる。
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# BM25 Retriever
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 4
# ベクトル検索 Retriever
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
# ハイブリッド検索
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6] # ベクトル検索を重視
)
Re-ranking(再ランキング)
タツヤ: 検索結果をクロスエンコーダモデルで再ランキングして、最も関連性の高い文書を上位に並び替える手法。初回検索で多めに候補を取得して(例:20件)、Re-rankingで絞り込む(例:4件)のが効果的。
クエリ変換
ミドリ: ユーザーの質問をそのまま検索に使うのは良くないんですか?
タツヤ: 場合によるけど、検索に最適な形に変換した方が精度は上がる。
- クエリ拡張 — 元の質問に関連キーワードを追加
- HyDE — LLMに「仮想的な回答文書」を生成させて、その文書のベクトルで検索
- マルチクエリ — 1つの質問から複数の視点のクエリを生成して網羅的に検索
メタデータフィルタリング
タツヤ: 文書にメタデータ(部署、文書種別、作成日など)を付けて、検索時にフィルタリングする。ノイズ削減に効く。
retriever = vectorstore.as_retriever(
search_kwargs={
"k": 4,
"filter": {"department": "人事部", "doc_type": "規程"}
}
)
ミドリ: 人事の質問なら人事部の文書だけ検索する、ってことですね。賢い。
本番運用のベストプラクティス
ミドリ: 本番で運用するとき、特に気をつけることは?
評価指標の設定
タツヤ: RAGの品質を定量的に測るために、4つの指標を監視しよう。
- Faithfulness(忠実度) — 回答がコンテキストの情報に基づいているか
- Answer Relevancy(回答関連性) — ユーザーの質問に答えているか
- Context Precision(コンテキスト精度) — 検索結果に関連文書が含まれているか
- Context Recall(コンテキスト再現率) — 必要な情報がすべて取得できているか
RAGASなどの評価フレームワークを使えば、これらを自動的に算出できるよ。
ドキュメントの更新パイプライン
タツヤ: 社内文書は常に更新されるから、以下の仕組みが必要。
- 新規・更新文書の検知 — ファイルの変更監視または定期バッチ
- 差分インデックス更新 — 変更があったドキュメントのみ再ベクトル化
- 古いチャンクの削除 — 削除されたドキュメントに対応するベクトルを削除
- インデックスの整合性チェック — 定期的にソースデータとの一致を検証
セキュリティとアクセス制御
ミドリ: 機密文書を扱う場合は気をつけないとですよね。
タツヤ: 当然。4つのポイントを押さえよう。
- 文書レベルのアクセス権 — ユーザーの権限に応じて検索対象を制限
- 入力のサニタイズ — プロンプトインジェクション対策
- 出力の監査ログ — 誰がどの文書に基づく回答を取得したかを記録
- データの暗号化 — ベクトルDBの保存時・通信時の暗号化
導入効果の数字
ミドリ: 実際にRAGを導入すると、どのくらい効果が出るんですか?
タツヤ: 導入企業のデータを紹介するよ。
- 回答精度 — ハルシネーションが平均70%以上削減
- 対応時間 — 社内問い合わせの平均対応時間が20分→2分に短縮(約90%削減)
- ユーザー満足度 — 社内AI利用率が導入前の3倍に増加
- コスト効率 — 問い合わせ対応コストを年間で約40%削減
ミドリ(smiling): ハルシネーション70%削減はすごい……! 投資回収期間はどのくらいですか?
タツヤ: 社内規程・マニュアル・技術文書を対象としたRAGシステムだと、3〜6ヶ月が一般的。導入効果を実感しやすいプロジェクトだよ。
まとめ:RAGはLLMビジネス活用の「必須アーキテクチャ」
ミドリ: 今日の話をまとめると、どうなりますか?
タツヤ(nodding): ポイントは5つ。
- RAGの3フェーズ(インデックス作成→検索→生成)を理解して、各フェーズでの最適化が検索精度を決める
- チャンキングはRAGの成否を分ける最重要ステップ。日本語文書では300〜600文字が推奨
- ハイブリッド検索、Re-ranking、クエリ変換で検索精度を15〜25%向上させられる
- 出典情報の提示で、ビジネス利用に耐える信頼性を確保できる
- 評価指標の継続的な監視とドキュメント更新パイプラインが本番運用の安定性を支える
ミドリ: 「AIが嘘をつかない仕組み」を作るのがRAGの本質ですね。
タツヤ(nodding): その通り。LLMをビジネスの現場で安全に使うなら、RAGは避けて通れない技術だよ。
次のアクション
ミドリ: 最後に、読者がまずやるべきことを教えてください。
タツヤ: 3ステップで進めよう。
- 対象ドキュメントの棚卸し — RAGシステムに取り込む文書(FAQ、マニュアル、規程など)をまず特定しよう
- プロトタイプの構築 — 本記事のコード例を参考に、小規模なRAGシステムを構築して精度を検証しよう
- 本格導入の設計 — ベクトルDBの選定、セキュリティ設計、運用設計まで含めた包括的なRAG導入は、Shimanto AI Solutionsにご相談ください。数多くのRAGプロジェクトで培ったノウハウをもとに、高精度かつ安定したシステムの構築をサポートします