RAGで検索漏れが出ると、まず top-k を増やしたくなります。正解文書を拾える確率は上がりますが、それだけで回答が良くなるとは限りません。
理由は、取得件数を増やす操作が retrieval recall と context precision を別方向へ動かす からです。生成モデルを評価する前に、検索段だけを切り出して測ると、この問題をかなり見つけやすくなります。
top-k=10なら安全、ではない
たとえばFAQを検索して回答するRAGを考えます。
質問に対する正解文書が1件あり、検索結果が次の順番だったとします。
rank 1: doc_07 関連文書
rank 2: doc_12 関連文書
rank 3: doc_03 正解文書
rank 4: doc_18 関連が薄い
rank 5: doc_09 関連が薄い
...
rank 10: doc_14 関連が薄い
top-k=1 では正解を取得できません。
そこで top-k=3 にすると正解文書が入ります。ここまでは期待どおりです。
では top-k=10 にすれば、さらに安全でしょうか。
検索漏れという観点だけなら安全側に寄ります。しかし生成モデルへ渡すコンテキストには、正解文書と一緒に7件の余計な候補まで入ります。似た製品、古い手順、別条件のFAQなどが混ざれば、モデルはそれらも回答材料として扱えます。
つまり、
top-kを増やす
↓
正解文書を拾いやすくなる
↓
同時に不要文書も入りやすくなる
↓
生成モデルへ渡す情報の密度が下がる
という関係があります。
OpenAIのRetrieval APIでも、検索件数だけでなく ranking_options の score_threshold によって低スコアの結果を除外できます。公式ドキュメントにも、thresholdを高くすると関連度の高いchunkへ絞れる一方、有用な結果まで除外する可能性があると明記されています。
ここで見るべきなのは「最終回答が正しかったか」だけではありません。検索が必要な文書を取れなかったのか、検索は成功したのに生成で失敗したのかを分けます。
top-kを増やすとrecallとprecisionが逆方向へ動く
小さなデータで確認します。
Python 3.12以降だけで動く例です。外部パッケージは使いません。
from dataclasses import dataclass
@dataclass(frozen=True)
class Question:
query: str
relevant_ids: set[str]
ranked_ids: list[str]
QUESTIONS = [
Question(
query="パスワードを変更するには?",
relevant_ids={"doc_password"},
ranked_ids=[
"doc_login",
"doc_security",
"doc_password",
"doc_profile",
"doc_email",
"doc_plan",
"doc_cancel",
"doc_invoice",
"doc_export",
"doc_api",
],
),
Question(
query="請求書をダウンロードするには?",
relevant_ids={"doc_invoice"},
ranked_ids=[
"doc_invoice",
"doc_plan",
"doc_payment",
"doc_export",
"doc_email",
"doc_profile",
"doc_api",
"doc_cancel",
"doc_login",
"doc_security",
],
),
]
def evaluate(k: int) -> tuple[float, float]:
recalls = []
precisions = []
for q in QUESTIONS:
retrieved = set(q.ranked_ids[:k])
hits = retrieved & q.relevant_ids
recalls.append(len(hits) / len(q.relevant_ids))
precisions.append(len(hits) / len(retrieved))
return sum(recalls) / len(recalls), sum(precisions) / len(precisions)
for k in (1, 3, 10):
recall, precision = evaluate(k)
print(f"k={k:2}: recall={recall:.2f}, precision={precision:.2f}")
出力はこうなります。
k= 1: recall=0.50, precision=0.50
k= 3: recall=1.00, precision=0.33
k=10: recall=1.00, precision=0.10
この例では k=1 から k=3 に変えることで、取り逃していた doc_password が検索結果へ入ります。
しかし k=3 から k=10 に増やしてもrecallはもう改善しません。増えるのは不正解文書です。
ここが top-k を単純な「安全マージン」として扱いにくい理由です。
なお、このprecisionは文書ID単位の単純な評価です。実際のRAGでは1ファイルが複数chunkへ分割されるため、chunk単位で正解ラベルを持たせるのか、file単位で評価するのかを先に決める必要があります。
OpenAIの評価ガイドでも、文書Q&Aの評価例として context recall と context precision を分けて扱っています。Evaluation best practices
回答精度だけを見ると、retrievalの失敗とgenerationの失敗が混ざります。
固定top-kからscore thresholdへ変える
固定 top-k の問題は、検索結果の品質にかかわらず同じ件数を採用することです。
ある質問では上位5件がすべて有用かもしれません。一方、別の質問では1位だけが十分に近く、2位以下は急激に関連度が落ちるかもしれません。
そこで、検索結果にscoreがあるならthresholdを加えます。
次のコードでは、max_k 件まで候補を見つつ、一定score未満を捨てます。
from dataclasses import dataclass
@dataclass(frozen=True)
class Hit:
doc_id: str
score: float
def select_hits(
hits: list[Hit],
max_k: int,
score_threshold: float,
) -> list[Hit]:
return [
hit
for hit in hits[:max_k]
if hit.score >= score_threshold
]
hits = [
Hit("doc_password", 0.91),
Hit("doc_security", 0.78),
Hit("doc_login", 0.73),
Hit("doc_profile", 0.41),
Hit("doc_invoice", 0.18),
]
for threshold in (0.0, 0.5, 0.8):
selected = select_hits(
hits,
max_k=10,
score_threshold=threshold,
)
print(
threshold,
[hit.doc_id for hit in selected],
)
出力は次のとおりです。
0.0 ['doc_password', 'doc_security', 'doc_login', 'doc_profile', 'doc_invoice']
0.5 ['doc_password', 'doc_security', 'doc_login']
0.8 ['doc_password']
ここで大事なのは「0.8が正しい」という話ではありません。
thresholdを上げればprecision側へ寄せられますが、境界付近に正解文書があればrecallを落とします。逆に下げれば正解を残しやすくなる代わりに、ノイズも残ります。
OpenAIのRetrieval APIではVector Store検索時に max_num_results と ranking_options.score_threshold を指定できます。2026年10月1日時点のAPIリファレンスでは max_num_results は1から50、score_threshold は0から1の範囲です。Retrieval
そのため実装では、
質問セット
↓
retrieval
↓
取得文書IDを評価
↓
recall / precision
↓
採用したcontext
↓
LLM
↓
answer accuracy
のように段を分けてログを残します。
たとえば20〜30件程度のFAQから始めるなら、各評価質問に「この質問へ答えるために必要な文書ID」を付けます。そして top-k=1/3/10 や複数のthresholdで同じ質問セットを流します。
検索結果を変更したときに最終回答だけを読み比べるより、「どの正解文書を落としたか」「不要文書を何件入れたか」を先に確認できます。
RAGの調整対象をLLMだけにしないことがポイントです。
thresholdにも正解はない
score thresholdを入れれば問題が終わるわけではありません。
検索scoreは「この値以上なら業務上必ず正しい」という保証ではありません。コーパス、chunkの切り方、質問の種類、rankingの設定が変われば分布も変わります。
さらに、正解文書が複数必要な質問では単純なprecisionだけでは足りません。
たとえば「解約時の返金条件と手続き方法を教えて」という質問に、料金規約と操作手順の2文書が必要なら、片方しか取得できない状態を成功としてはいけません。
評価データを次のように持たせます。
eval_cases = [
{
"query": "解約時の返金条件と手続き方法は?",
"relevant_ids": {
"doc_refund_policy",
"doc_cancel_steps",
},
},
]
この形なら、2件中1件だけ取得した場合のrecallは 0.5 になります。
一方で、検索段のprecisionが低くても最終回答が正しいケースはあります。生成モデルが不要なcontextを無視できたからです。それを理由にretrievalの評価を捨てると、モデル変更や文書追加で急に挙動が崩れたとき、原因を追いにくくなります。
逆もあります。retrieval recallが1.0でも、取得した文章からモデルが誤った回答を作ればgeneration側の問題です。
だから検索評価と回答評価は競合する指標ではなく、障害箇所を分けるための観測点として使います。
実運用へ持っていくなら、固定した小規模評価セットだけでthresholdを決め切らず、実際の質問分布に近いケースを追加して再評価します。評価ガイドも、実際の利用分布を反映したtask-specificなevalと継続的な評価を推奨しています。
top-k=10 は安全設定ではありません。検索漏れを減らす代わりにcontextへ何を追加したのかを測って初めて意味があります。次に確かめるなら、chunkサイズやrankerを固定したまま max_num_results と score_threshold だけを変え、retrieval recall、context precision、最終回答の評価がそれぞれどう動くかを同じ質問セットで比較するのが切り分けやすいです。

