0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIが的外れな回答を返す原因の多くは、モデルじゃなくて「検索」の方です — RAGの精度を上げる実践ガイド(チャンク設計・ハイブリッド検索・リランク・RAG評価)

0
Posted at

はじめに — 「もっと賢いモデルに変えたのに、まだ的外れ」の正体

社内のマニュアルやドキュメントを読ませて、AIに質問できるチャットを作ってみた。動いた。でも、なんかちょいちょい的外れな答えが返ってくる。

そこで多くの人がまずやるのが、「モデルをもっと賢いやつに変える」なんですよね。気持ち、めっちゃ分かります。でも、正直に言いますね。それ、たぶん直らないんです。

RAG(検索拡張生成)の失敗を調べていくと、面白い数字が出てきます。2026年時点の業界分析では、RAGが間違えるとき、その原因のうち約73%は「生成」ではなく「検索」の側にある、と言われています。ナイーブに組んだRAGは、そもそも4割くらいの確率で「関連しない文書」を引いてきていて、AIはその間違った資料をもとに、自信満々に答えてしまう。つまり、賢い頭に、間違ったカンニングペーパーを渡し続けてるわけです。

これ、めっちゃもったいなくないですか。頭(モデル)を高いのに替える前に、カンニングペーパー(検索)を整える。そっちのほうが、たいてい安くて、よく効くんです。

この記事では、RAGの検索品質を上げるための4つのレバー、チャンク設計・ハイブリッド検索・リランク・評価を、「そもそもRAGって何してるの?」というところから、コードとプロンプト付きで一緒に見ていきます。AIをまだ触り始めたばかりの人でも、明日から自分のRAGを「測って、直せる」状態を目指します。

(出典: RAG Best Practices 2026 (CallMissed) / RAG Is Not Dead: Advanced Retrieval Patterns That Actually Work in 2026 (dev.to)


そもそもRAGって、何をしてるの?

まずここを、ふわっとさせないでおきましょう。RAGを一言でいうと、「カンニングOKの試験」です。

普通のLLMは、頭の中の知識だけで答えます。だから社内文書みたいな「LLMが知らない情報」は答えられないし、知ったかぶり(ハルシネーション)もする。RAGは、質問が来たら先に資料を「検索」して、関連しそうな部分を答えの材料として一緒に渡してあげる仕組みです。カンニングペーパーを見ながら答えていい試験、というわけです。

もうちょっと分解すると、RAGは3ステップで動いています。図書館の司書さんを想像してください。

  1. Retrieve(検索) — 質問に関係ありそうな資料を、書庫から探してくる。司書さんが本を数冊選んで持ってくるところ。
  2. Augment(合成) — 探してきた資料を、質問文とくっつけて「これを参考に答えて」という形に整える。
  3. Generate(生成) — LLMが、その資料を読んで答えを書く。

ここで、無知の無知になりがちな言葉を、先に全部かみ砕いておきます。

  • チャンク(chunk) — 資料を検索できる単位に切り分けた「かたまり」。本まるごとじゃ大きすぎるので、段落やセクション単位に刻む。この「刻み方」が後で効いてきます。
  • 埋め込み(embedding) — 文章を、意味を表す数字の列(ベクトル)に変換したもの。「返品」と「返却」みたいに、言葉は違うけど意味が近いものを、近い座標に置ける。
  • ベクトル検索 — 質問も埋め込みに変換して、意味が近いチャンクを座標の近さで探す方法。
  • トップk(top-k) — 検索で上位何件を持ってくるか、の件数。top-5なら上位5チャンク。
  • BM25 — 昔ながらのキーワード検索の代表選手。単語がどれだけ一致してるかで探す。意味は分からないけど、固有名詞や型番の「完全一致」に強い。

ここまでが土台です。で、問題はいつも「1のRetrieve(検索)」で起きます。司書さんが的外れな本を持ってきたら、どれだけ賢い人(LLM)でも、いい答えは書けない。当たり前ですよね。


なぜ的外れになるのか — 検索が外す「4人の犯人」

検索が失敗するとき、犯人はだいたいこの4人のどれかです。具体例として、社内に「XR-200」という架空の製品マニュアルがあるとして見てみましょう。ユーザーの質問は「XR-200の返品条件は?」だとします。

犯人1: 意味は近いけど、用途が違うチャンクを引く
ベクトル検索は「意味の近さ」で探します。でもこれ、諸刃の剣なんです。「返品条件」で検索すると、「返金の手続き」「交換の流れ」みたいな、意味は近いけど今回聞かれてない部分まで上位に来てしまう。似てるものを引く力が、逆に「ズレたけど似てる」ものを引いてしまう。

犯人2: 固有名詞・型番に弱い
「XR-200」みたいな型番、これベクトル検索がいちばん苦手なやつです。埋め込みにすると「XR-200」も「XR-300」も似た座標になりがちで、別製品の返品条件を堂々と引いてくる。人間からしたら「番号違うやん」なんですけど、意味検索にはそこが見えにくい。

犯人3: チャンクの切り方が雑で、答えが分断される/混ざる
「返品は購入後14日以内、ただし開封済みは対象外」という一文が、チャンクの境界でぶつ切りにされていたら?「14日以内」だけ引けて「開封済みは対象外」が別チャンクに行ってしまい、AIは半分だけ見て答える。逆に、関係ない情報まで1チャンクに詰め込みすぎても、ノイズで薄まる。

犯人4: 並び順が悪くて、正解が下の方に埋もれる
検索は上位20件くらい引けていても、肝心の「正解チャンク」が15位あたりにいると、LLMに渡す3〜5件からこぼれ落ちる。引けてるのに、渡せてない。これ、地味にめちゃくちゃ多いんです。

面白いのは、この4人、直し方がそれぞれ違うんですよね。だから「精度が低い」と一括りにせず、どの犯人かを切り分けるのが最初の一歩になります。そのために、次は「測る」話をします。


直す前に、まず「体温計」を持つ — RAG評価の測り方

ここ、いちばん飛ばされがちなんですけど、いちばん大事です。測れないものは、直せない。

「なんか精度上がった気がする」で改善を進めると、たいてい別のところが悪化してることに気づけません。だから、体温計を先に持ちます。RAGの体温計として2026年に定番なのが、RAG TriadRAGASです。

まず考え方の枠として分かりやすいのが、TruLensのRAG Triad(3点セット)

  • Context Relevance(文脈の関連性) — 引いてきたチャンクは、質問に関連してる? → 犯人1・2を測る
  • Groundedness(接地性) — 答えは、引いた資料にちゃんと基づいてる?(捏造してない?)
  • Answer Relevance(回答の関連性) — 答えは、そもそも質問に答えてる?

この3つがそろって初めて「正しく検索し、正しく根拠にし、正しく答えた」と言えます。どれか1つでも欠けると、そこが壊れてる場所です。

もっと細かく数値で測りたいときは、RAGASというOSSが定番です。中核の4指標だけ、日本語でかみ砕いておきます。

  • Context Precision(文脈の精度) — 引いたチャンクのうち、関連するものがちゃんと上位に来てるか。→ 犯人4(並び順)に効く
  • Context Recall(文脈の再現率) — 正解に必要な情報を、そもそも引けてるか。→ 犯人3(切り方)に効く
  • Faithfulness(忠実性) — 答えが検索結果に忠実か(=資料にないことを言ってないか)
  • Answer Relevance(回答の関連性) — 答えが質問に答えてるか

ここでのコツは、PrecisionとRecallを分けて見ること。Recallが低いなら「そもそも引けてない」=チャンクや検索の問題。Precisionが低いなら「引けてるけど並びが悪い・ノイズが多い」=リランクの問題。犯人を切り分けられるんです。

最小の評価コードは、こんなイメージです(RAGAS)。

# pip install ragas datasets
from ragas import evaluate
from ragas.metrics import (
    context_precision,
    context_recall,
    faithfulness,
    answer_relevancy,
)
from datasets import Dataset

# 評価用の「お手本セット」。question と ground_truth(正解)を人が用意する。
# contexts は、あなたのRAGが実際に検索して引いてきたチャンク。
data = {
    "question": ["XR-200の返品条件は?"],
    "answer": ["購入後14日以内、未開封が条件です。"],          # RAGの出力
    "contexts": [["返品は購入後14日以内。ただし開封済みは対象外。"]],  # 検索で引いたチャンク
    "ground_truth": ["購入後14日以内かつ未開封であれば返品可能。"],   # 人が決めた正解
}

result = evaluate(
    Dataset.from_dict(data),
    metrics=[context_precision, context_recall, faithfulness, answer_relevancy],
)
print(result)
# 例: {'context_precision': 0.91, 'context_recall': 0.88,
#      'faithfulness': 0.95, 'answer_relevancy': 0.90}

ポイントは、改善する前にこの数字(ベースライン)を取っておくこと。これをやらずに手を入れると、良くなったのか悪くなったのか、一生わからないままなんです。ここは人間の仕事。「何をもって正解とするか」を決めるお手本セット(20〜50問でいい)を作るのは、AIには丸投げできない、大事な設計判断です。

(出典: Ragas RAG Evaluation Metrics Complete Guide 2026 (qaskills.sh) / RAG Evaluation: Metrics, Tools, and the Context Gap 2026 (Atlan)


レバー1: チャンク設計 — 「切り方」で正解が生き死にする

体温計を持ったら、いよいよ直していきます。まずいちばん根っこにあるのがチャンク、つまり資料の切り方です。犯人3(分断・混入)に効きます。

いちばんやりがちなのが固定長チャンキング。「500文字ごとに機械的に切る」というやつ。実装は楽なんですけど、意味の切れ目を完全に無視するので、さっきの「14日以内 / 開封済みは対象外」みたいに、一文の途中でぶった切られる事故が起きます。

そこで2026年の定番になってるのがセマンティックチャンキング(意味で切る)。隣り合う文の意味(埋め込み)を比べて、話題が変わったところ(類似度がガクッと下がったところ)で区切るやり方です。ある比較では、固定長ベースラインに対して精度が約71%まで上がった、という報告もあります。

import numpy as np

def semantic_chunk(sentences, embed_fn, threshold=0.6):
    """隣接文の意味が離れたところでチャンクを切る簡易版。
    sentences: 文のリスト / embed_fn: 文を埋め込みベクトルに変換する関数
    threshold: これより類似度が下がったら「話題が変わった」とみなして区切る
    """
    embeddings = [embed_fn(s) for s in sentences]
    chunks, current = [], [sentences[0]]

    def cosine(a, b):
        return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

    for i in range(1, len(sentences)):
        sim = cosine(embeddings[i - 1], embeddings[i])
        if sim < threshold:          # 話題が変わった → ここで区切る
            chunks.append(" ".join(current))
            current = [sentences[i]]
        else:
            current.append(sentences[i])
    chunks.append(" ".join(current))
    return chunks

そしてもう1つ、地味だけど超重要なのがメタデータを添えること。チャンク単体を放流せず、「どの文書の・どの見出しの・何ページ目か・親チャンクは何か」を一緒に持たせます。

chunk = {
    "text": "返品は購入後14日以内。ただし開封済みは対象外。",
    "source": "XR-200_manual.pdf",
    "heading": "5. 返品・交換について",
    "page": 12,
    "parent_id": "sec-5",
}

これがあると、後で「出典を明記する」「製品ごとに絞り込む(=犯人2の型番問題の予防にもなる)」「親チャンクをたどって前後の文脈を補う」といったことが全部できるようになります。切り方は、あとから効いてくる投資なんですよね。


レバー2: ハイブリッド検索 — 意味 × キーワードで穴を埋める

次は犯人2(固有名詞・型番に弱い)を退治します。ここで効くのがハイブリッド検索です。

考え方はシンプル。ベクトル検索(意味に強い・完全一致に弱い)と、BM25(キーワードの完全一致に強い・意味が分からない)を両方走らせて、結果を混ぜる。片方の穴を、もう片方が埋めてくれます。「XR-200」みたいな型番はBM25がガッチリ拾い、「返品したいんだけど」みたいな言い回しはベクトルが意味で拾う。

混ぜ方の定番がRRF(Reciprocal Rank Fusion)。難しそうな名前ですけど、やってることは「それぞれの検索での順位を使って、スコアを足し算する」だけです。上位に来たものほど高得点、を両方の検索でやって合算します。

def reciprocal_rank_fusion(result_lists, k=60):
    """複数の検索結果(順位つき)を、順位ベースで融合する。
    result_lists: [[doc_id, ...(ベクトル検索の順)], [doc_id, ...(BM25の順)]]
    k: 順位の影響を和らげる定数(経験的に60がよく使われる)
    """
    scores = {}
    for ranked in result_lists:
        for rank, doc_id in enumerate(ranked):
            # 上位ほど 1/(k+rank) が大きい → 高スコア
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
    # スコアの高い順に並べ替えて返す
    return sorted(scores, key=scores.get, reverse=True)

# 使用イメージ
vector_hits = ["docA", "docB", "docC"]   # ベクトル検索の結果(順位順)
bm25_hits   = ["docC", "docD", "docA"]   # BM25の結果(順位順)
fused = reciprocal_rank_fusion([vector_hits, bm25_hits])
# → 両方に出てくる docA / docC が自然と上位に上がってくる

RRFのいいところは、スコアのスケールがバラバラな2つの検索を、順位という共通言語でフェアに混ぜられること。実務ではまずこれで十分効きます。多くのベクトルDB(2026時点では主要どころがハイブリッド検索を標準サポート)にも組み込みで入っているので、自前実装じゃなくてもOKです。

(出典: Hybrid Search for RAG: Vector + Keyword + Reranking Guide 2026 (buildmvpfast)


レバー3: リランク — 「20人面接して、上位5人だけ通す」

最後のレバーがリランク(並べ替え)。犯人4(正解が下の方に埋もれる)に、直接効きます。個人的には、いちばん費用対効果が高いレバーだと思っています。

やることは一言でいうと、「まず広めに引いて、そのあと厳しく絞り直す」。経験則として、20件引いて、リランクで5件に絞り、そのうち3〜5件をLLMに渡す、という流れが定番です。

「最初から精度よく引けばいいのでは?」と思いますよね。でもここが面白くて、最初の検索(ベクトルやBM25)は「速く広くざっくり」が得意で、細かい優劣づけは苦手なんです。一方リランクに使うクロスエンコーダというモデルは、「質問とチャンクを1個ずつ突き合わせて、これは本当に関係あるか?」を丁寧に採点するのが得意。遅いけど正確。だから「速い検索で候補を集めて → 正確なリランクで並べ直す」という分業がハマるわけです。

面接にたとえると、書類選考(検索)で20人に絞って、そのあと1人ずつじっくり面接(リランク)して上位5人を通す感じ。効果も地味じゃなくて、推論が重めのクエリでは、同じ20件を並べ替えるだけで、nDCG@10という指標が0.13→0.40(約3倍)に改善した、という報告もあります。候補は変えず、順番を直すだけで、です。

# pip install sentence-transformers
from sentence_transformers import CrossEncoder

# クロスエンコーダ: 質問とチャンクのペアを「関連度スコア」に変換するモデル
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")

def rerank(query, candidates, top_n=5):
    """検索で引いた候補を、質問との関連度で並べ直して上位だけ返す。"""
    pairs = [(query, c["text"]) for c in candidates]
    scores = reranker.predict(pairs)          # ペアごとに関連度を採点
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return [c for c, _ in ranked[:top_n]]     # 上位 top_n だけ通す

# 検索で20件ざっくり引いてから…
candidates = hybrid_search(query, top_k=20)
final = rerank(query, candidates, top_n=5)     # 5件に絞ってLLMへ

注意点として、リランクはひと手間ぶんの時間とコストが増えます(1件ずつ採点するので)。だから「20件を200件」みたいに広げすぎない。20〜50件を5件に、くらいが実務のスイートスポットです。ここは体温計(RAGASのContext Precision)を見ながら、増やして効くか確認していくのがいいです。

(出典: RAG Best Practices 2026 (CallMissed) / StackAI: RAG Best Practices for Enterprise AI


そのまま使えるプロンプト3本

検索品質の改善は、AIにも手伝ってもらえます。人間が「何を測り、どこを直すか」を決めて、手を動かす部分をAIに任せる。そういう役割分担のためのプロンプトを3本、置いておきます。汎用のダミーで書いてるので、{ } を自分の中身に差し替えて使ってください。

プロンプト1: クエリ書き換え(検索前に質問を整える)
ユーザーの生の質問って、検索には向いてないことが多いんです。これを検索が引きやすい形に開いてもらいます。

あなたは検索クエリ設計の専門家です。
次のユーザー質問を、ドキュメント検索でヒット率が上がるように書き換えてください。

# ユーザー質問
{XR-200って返せる?}

# 出力してほしいもの
1. 中核となる検索キーワード(固有名詞・型番はそのまま残す)
2. 言い換えクエリを3つ(同義・別表現。例:「返品」→「返却」「返金」)
3. 絞り込みに使えそうなメタデータ条件(製品名・セクション名など)

キーワードは推測で増やしすぎず、質問に含まれる情報を保つこと。

プロンプト2: チャンク設計レビュー(切り方を診断してもらう)

あなたはRAGの検索品質を診断するレビュアーです。
次のチャンク分割の結果を見て、検索が失敗しそうな箇所を指摘してください。

# チャンク(抜粋)
{ここに実際のチャンクを3〜5個貼る}

# 観点
- 一つの意味のまとまりが、途中でぶつ切りにされていないか
- 逆に、無関係な話題が1チャンクに混ざっていないか
- メタデータ(出典・見出し・ページ)が欠けていないか
- 固有名詞・型番が検索で拾える形で残っているか

指摘は「どのチャンクの・何が問題で・どう直すか」の3点セットで。
根拠が曖昧なものは「推測」と明記すること。

プロンプト3: 検索失敗のトリアージ(犯人を切り分ける)

あなたはRAG障害の切り分けを行うエンジニアです。
以下のRAG評価スコアと事例から、ボトルネックが「検索の再現率(recall)」なのか
「並び順(precision)」なのか「生成(faithfulness)」なのかを切り分けてください。

# 評価スコア
context_recall: {0.55}
context_precision: {0.85}
faithfulness: {0.93}

# 失敗事例
質問: {XR-200の返品条件は?}
引けたチャンク: {返金の手続き / 交換の流れ}   ← 正解チャンクが入っていない
出力: {返金は…(的外れ)}

# 出力
1. 一番の犯人(recall / precision / generation のどれか)と根拠
2. 次に試すべき改善策を、効果が高い順に3つ
3. その改善を測るために見るべき指標

3本目みたいに、スコアと事例をセットで渡して切り分けさせるのがコツです。AIは「並び順が悪いのか、そもそも引けてないのか」を、数字から論理的に絞ってくれます。ここでも最終判断は人間ですが、当たりをつける速度が段違いになります。


人間とAIの役割分担 + 正直な落とし穴

RAGの検索品質って、「AIに全部お任せ」ではうまくいきません。人間が設計・判断する部分と、AIに任せる部分を、はっきり分けておきましょう。

工程 人間が設計・判断すること AIに任せられること
評価軸づくり 何を正解とするか、お手本セットの作成 採点の下書き、スコアの要約
チャンク設計 切り方の方針・メタデータ設計・PII方針 分割の実行、切り方の問題点レビュー
検索 ハイブリッドの重み・絞り込み条件 クエリ書き換え、言い換え生成
リランク 引く件数・絞る件数の上限決定 候補の関連度採点、並べ替え
運用判断 どこまでやるか、撤退するか 失敗事例のトリアージ、原因の要約

そして、正直に落とし穴も置いておきます。ここを知らないと、静かに事故ります。

  • PII・社外秘がチャンクに混ざる — 検索対象に個人情報や秘密情報が入っていると、検索経由でそのまま答えに出てきます。取り込み時にマスキング・除外・アクセス制御を決めるのは人間の仕事。「便利だから全部入れる」は危険。
  • リランクのコストとレイテンシ — 効くからといって候補を広げすぎると、遅く・高くなります。20〜50件を上限に、体温計を見ながら。
  • 埋め込みモデルの不一致 — 取り込み時と検索時で違う埋め込みモデルを使うと、座標系がズレて検索が壊れます。モデルを変えたら、原則インデックスを作り直す。
  • ロングコンテキストで代替できる場面もある — 資料が数ページしかないなら、そもそも全部プロンプトに入れたほうが速くて正確なこともあります。RAGは「資料が多すぎて全部は渡せない」ときの技術。目的が「品質」なのか「コスト・スケール」なのかで、使うかどうかを判断する。
  • 評価なしで回し続ける — これがいちばんの落とし穴。体温計を捨てた瞬間、改善は勘ゲーになります。

撤退ラインもシンプルです。「お手本セットで測って、良くならない改善はやらない」。それだけで、無駄な作り込みでコケるのを、だいぶ防げます。


今日の最初の一歩

いきなり全部やらなくていいです。効く順に、小さく1個ずつ。

  1. 体温計を置く — お手本を20問だけ作って、今のRAGのContext PrecisionとRecallを測る。ベースラインを記録。
  2. リランクを足す — 20件引いて5件に絞る。いちばん費用対効果が高いので、まずここ。
  3. ハイブリッドにする — ベクトルにBM25を足してRRFで融合。型番・固有名詞の取りこぼしが減る。
  4. チャンクを見直す — 固定長をやめて、意味の切れ目で切る。メタデータを添える。

1つ入れるたびに体温計で測る。良くなったら残す、変わらなければ戻す。これだけで、あなたのRAGは「たまに当たる占い」から「毎回頼れる道具」に近づいていきます。


おわりに — 検索を整えるのは、明日の自分への「あざっす」の前借り

最後に、ちょっとだけ引いた話を。

RAGの検索品質を整える作業って、正直、地味です。派手なモデルを差し替えるほうが、よっぽど気持ちいい。でも、僕はこう思うんです。情報を「見つける」ことと、「正しく引く」ことは、まったくの別物。そして、その別物を埋める地味な工事こそが、AIプロダクトを「道具」に変える。

僕はいつも、「この選択、明日の自分があざっすって言ってくれる方はどっち?」を軸にしています。今日、体温計を置いて、検索をちょっと整えておく。それは、明日そのプロダクトを使う人と、半年後にこのコードを触る自分への、あざっすの前借りなんですよね。

賢いモデルは、これからもどんどん出てきます。でも、いい道具をつくるのは、モデルの賢さじゃなくて、「正しい材料を、正しく渡す」という、あなたの設計です。便利な道具を長く育てていく。その一歩として、まずは自分のRAGを一回、測ってみませんか。

明日の自分が、きっと「あざっす」って言ってくれます。

0
2
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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?