RAG 上級講座 #7 実践プロジェクト:ドキュメント Q&A ボットのアップグレード

読了 5分

第6回までで、診断・改善・測定の道具がすべてそろいました。最後の記事では、LLM アプリ開発 第13回で作った社内文書 Q&A ボットを、このシリーズの手法でアップグレードします。一度に全部変えるのではなく、一つずつ適用して毎回測定する過程そのものがこの記事の中身です。実務で RAG を改善する仕事は、まさにこの形をしているからです。

出発点 — 第13回のボットとベースライン #

第13回のボットは、このシリーズの基準で見ればもっとも単純な構成です。固定サイズのチャンキング、ベクトル検索一つ、出典表示なし。まず第1回のとおり実際の質問ログからゴールデンセット30件を作り、第6回の評価パイプラインでベースラインを測ります。

step0_baseline.py
result = evaluate(GOLDEN, top_k=5)
# {'recall@k': 0.63, 'mrr': 0.44, 'accuracy': 0.57, 'hallucination_rate': 0.13}

数値はこの例の文書セットでの結果であり、皆さんのデータでは違う値になります。大事なのは絶対値ではなく読み方です。recall@k 0.63 は、三つの質問のうち一つは正解チャンクすら取れていないという意味なので、検索から直すのが順序です。幻覚率 13% は、出典のないボットでは信頼を得にくい水準です。

ステップ1 — チャンキングの差し替え #

第2回の構造ベースのチャンキングに替えます。社内文書が Markdown なので見出し単位で切り、表はまるごと残し、チャンクごとに出典とセクションのメタデータを付けてインデックスを作り直します。

step1_chunking.py
chunks = []
for doc in load_documents("docs/"):
    for c in chunk_by_heading(doc.text, max_chars=1500):
        chunks.append({"text": c, "metadata": {"source": doc.name, "section": heading_of(c)}})
rebuild_index(chunks)

# 再評価: recall@k 0.63 → 0.77, accuracy 0.57 → 0.67

検索アルゴリズムは一行も変えていないのに、recall がもっとも大きく跳ねました。診断で見つかった「表が途中で切れて両方のチャンクが使いものにならなかった」失敗が消えたおかげです。チャンキングをシリーズの前半に置いた理由がこの順序にあります。土台を直さなければ、後の手法は本来の効果を出せません。

ステップ2 — ハイブリッド検索 #

ゴールデンセットを質問タイプ別に分けてみると、残った検索失敗の多くは製品コードと社内略語の質問です。第3回の BM25 + RRF を足します。

step2_hybrid.py
def search(question: str, top_k: int = 5) -> list:
    vec = vector_search_ids(question, top_k=20)
    kw = keyword_search(question, top_k=20)
    return [chunks[i] for i in rrf([vec, kw], top_k=top_k)]

# 再評価: recall@k 0.77 → 0.87(固有名詞の質問グループ 0.50 → 0.90)

全体の数値より、質問グループ別の数値のほうが多くを語ってくれます。記述型の質問グループはほぼそのままで、固有名詞の質問グループが大きく上がりました。導入した手法が狙ったところで働いているという確認です。

ステップ3 — リランキング #

recall は上がってきたのに、MRR が付いてきません。正解チャンクが5件の中にはあるものの、4〜5位に引っかかっている場合が多いという意味です。第4回のクロスエンコーダーによるリランキングを付けます。候補を30件に広げ、リランカーで5件に絞ります。

step3_rerank.py
def search(question: str, top_k: int = 5) -> list:
    candidates = hybrid_search(question, top_k=30)
    pairs = [(question, 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_k]]

# 再評価: mrr 0.58 → 0.74, accuracy 0.70 → 0.77

MRR が上がると accuracy が付いて上がりました。正解チャンクがコンテキストの上のほうにはっきり収まると生成も良くなる、検索と生成がつながっている証拠です。社内ボットなので、リランキングが足す1秒未満の遅延は受け入れることにします。この判断もトレードオフであり、遅延に敏感なサービスなら別の結論になり得ます。

ステップ4 — 引用と「分からない」と答えること #

最後に第5回を適用します。system プロンプトに根拠の制約と「分からないと答える権利」を入れ、チャンクを document ブロックに替えて citations を有効にし、引用比率のゲートを付けます。

step4_citations.py
def qa(question: str) -> str:
    chunks = search(question, top_k=5)
    response = answer_with_citations(question, chunks)   # 第5回の実装
    if not is_grounded(response):
        return "関連する文書を十分に見つけられませんでした。質問を変えてみていただけますか?"
    return render(response)

# 再評価: hallucination_rate 0.10 → 0.03, accuracy 0.77 → 0.80

幻覚率が一桁台前半まで下がり、答えごとに出典が付くことでユーザーが自分で検証できるようになりました。運用者の立場では「見つけられませんでした」という応答のログが新しい診断材料になります。その質問たちが、次に補強する文書とゴールデンセット追加の候補です。

全体の道のり #

ステップrecall@5MRRaccuracy幻覚率
ベースライン(第13回のボット)0.630.440.570.13
+ 構造ベースのチャンキング0.770.550.670.13
+ ハイブリッド検索0.870.580.700.10
+ リランキング0.870.740.770.10
+ 引用・ゲート0.870.740.800.03

この表の読み方が、そのままこのシリーズの要約です。どの行も、すべての列を上げてはいません。各手法は自分が狙った指標を上げ、測定があったからこそその事実を知ることができました。ここからさらに進むなら、第4回のクエリ変換(会話型の質問への対応)が次の候補で、文書が増え続けるなら第6回の評価を定期実行として自動化するのが運用の次のステップです。

シリーズを終えて #

七つの記事を振り返るとこうなります。

  • 改善は診断から始まります。失敗を検索と生成に切り分け、ゴールデンセットでベースラインを作ります(第1回)。
  • 検索品質の土台はチャンキングです。文書構造に沿って切り、表はまるごと残します(第2回)。
  • ベクトル検索とキーワード検索を RRF で合わせ、互いの弱点を埋めます(第3回)。
  • 質問はリライティングで整え、広めに取った候補はリランキングで絞ります(第4回)。
  • 生成には根拠の制約と「分からない」と答える権利を与え、citations で検証可能な出典を付けます(第5回)。
  • recall@k、MRR、正確度、幻覚率の四つの指標を回帰テストとして回します(第6回)。
  • そして適用は一度に一つずつ、測定とともに行います(第7回)。

RAG は一度作って終わりのシステムではなく、文書と質問が変わり続けるあいだ手入れを続けるシステムです。測定があれば、その過程は勘ではなく工学になります。このシリーズがその転換点になることを願っています。ここまでご一緒いただき、ありがとうございました。

X