7月2日、Towards Data Scienceが「The Untaught Lessons of RAG Question Parsing: Structure Before You Search」と題した記事を公開した。この記事では、RAGパイプラインにおける質問解析フェーズを「文字列処理」ではなく「型付きデータ構造」として扱うことで、サイレントな部分回答や検索精度の劣化を防ぐ6つの設計原則について詳しく紹介されている。問題の核心は「静かに壊れる」という点にある。エラーは出ない。テストも通る。しかし本番でユーザーの質問の半分が無視されている——そのような状況を生み出す構造的な欠陥と、その修正方法がこの記事のテーマだ。
RAGが「静かに壊れる」場所
多くのRAGチュートリアルでは、ユーザーの質問文字列をそのままベクトル検索に投げ、コサイン類似度でtop-kチャンクを取得し、それをLLMに渡す構成を採用している。この記事はその構成を「ナイーブベースライン」と呼び、根本的な問題を指摘する。
ユーザーの質問はサーチクエリではない。
「保険料の金額と更新期限は?」という質問は2つの独立した問いを含んでいる。しかしナイーブな実装ではこの文字列をそのままembeddingし、「なんとなく似ているチャンク」を返す。結果として片方の質問には答えるが、もう片方はサイレントにスキップされる。本番環境でRAGが静かに壊れる箇所の多くはここだ、と記事は主張する。
提案するアーキテクチャでは、質問をquestion_dfの1行として構造化する。5つの型付きカラム(keywords、scope、shape、decomposition、clarification)と2つの派生ブリーフ(RetrievalQueryとGenerationBrief。それぞれ「検索フェーズへの指示書」「生成フェーズへの指示書」に相当する元記事固有の概念)に変換してから検索に渡す。

6つの「教わらない教訓」
Lesson 1:質問をリレーショナルスキーマとして扱う
一般的な「クエリ理解」や「クエリ書き換え」の手法は、質問を別の文字列に変換するだけにとどまる。この記事が提案するのは、質問をドキュメント側のline_dfやtoc_dfと対称的なリレーショナル構造として扱うことだ。両側が関係データであれば、検索はその結合フィルタリングとして実装できる。
プロンプトテンプレートに特殊ケースの処理を積み重ねる代わりに、パーサー境界で一度だけ構造化する。6ヶ月後にプロンプトが60行の特殊条件だらけになる「腐敗」を源流で防ぐ、という考え方だ。
Lesson 2:分岐コードではなくスキーマで拡張する
多くのコードベースはif intent == "..."の連鎖で質問処理を実装し、月を経るごとに硬直化する。スキーマアプローチでは、新機能の追加はquestion_dfへのカラム追加で済む。「否定処理(negation handling)」を追加する場合、ナイーブな実装ではコードブランチの追加・テスト・回帰テストが必要だが、このアプローチではnegation_presentブーリアン(boolean)カラムを1つ追加し、ディスパッチャーがそのカラムを読むだけだ。
Lesson 3:下流のブリックごとに2つのブリーフを分ける
元記事では、パイプラインを構成する個々の処理コンポーネントを「ブリック(brick)」と呼ぶ。各ブリックへの指示書が「ブリーフ(brief)」であり、検索用のRetrievalQueryと生成用のGenerationBriefの2種類に分けて渡す設計を提案している。
「保険料はドル建てで、ユーロ表記は除外して」という質問の場合、検索ブリーフに必要なのはキーワードとスコープだけだ。「EUR除外」という条件は生成ブリーフにのみ渡せばよい。1つのプロンプトにすべてを詰め込む設計では、各ブリックが不要な情報を読み飛ばすか、再解析するかを強いられる。
Lesson 4:エキスパート辞書がembeddingに勝る場面
「毎月いくら払うの?」という質問をembeddingで検索すると、汎用的な「payment」関連ページが返ってくる。しかし保険コーパス向けのconcept_keywords_dfがあれば、「pay each month」→["premium", "monthly contribution", "monthly installment"]というマッピングを検索前に適用できる。コストは低く、ドメイン固有の語彙に対してembeddingよりも確実に機能する。
同様のアプローチはドメインを問わず機能する。医療分野なら「blood thinner」→["anticoagulant", "warfarin", "heparin", "DOAC"]、財務分野なら「top line」→["revenue", "net revenue", "GAAP revenue"]という具合だ。
Lesson 5:複合質問の4パターンを明示的に処理する
複合質問を1つの文字列として扱うと、片方の質問への回答がサイレントにドロップされる。この記事は複合パターンを4種類に分類する。
- independent(独立):2つの問いが互いに依存しない
- sequential(逐次):part Aの答えをpart Bの入力として使う
- unified(統合):2つの問いが1つの答えに収束する
- conditional(条件付き):一方の問いは他方の結果に依存する
「免責事項はキャップを超えた場合どうなるか、またそのキャップはいくらか」はsequentialパターンだ。パーサーがこれを識別すれば、2つのサブ質問を順番に処理し、それぞれに引用付きの回答を返すか、見つからなければ明示的に「not-found」とマークできる。
Lesson 6:決定論的ディスパッチャーを使う
LLMにルーティングを「考えさせる」アプローチ(LLM-decides)は、同じ質問を2回実行しても異なるルーティングトレースを生成しうる。監査が必要なエンタープライズ環境では致命的だ。決定論的ディスパッチャーはコードのルール(decide.py 47行目が発火、route = "factual_lookup"、検索メソッド["keyword", "toc"]が起動)として動作し、監査ログが毎回同一になる。記事はエンタープライズ向けにはこちらを推奨し、LLM-decidesアプローチを明示的に除外する。
コンテキストウィンドウのサイジングも構造化する
上記6つの教訓と直交する補足的なトピックとして、記事はコンテキストウィンドウのサイジングについても言及している。question_dfはもう2つのカラム(lines_before_anchorとlines_after_anchor)を持つ。コンテキストの単位は行数(文字数は粒度が細かすぎ、ページ数は粗すぎる)で管理する。
- 事実参照型(保険料、有効日付など):アンカー前後に数行で十分
- リスト型質問:前方コンテキストは不要、後方ウィンドウを長く取る
top-kというマジックナンバーをパイプライン全体に伝搬させるのではなく、質問の形状(shape)と分解パターン(decomposition)からコンテキスト幅を導出する設計だ。
実装リソース
記事は4本のサブ記事と対応するノートブックへの入口として機能する。
- Article 6A:質問解析の全体フロー
- Article 6B:5カラムの抽出ロジック
- Article 6C:ディスパッチャーの実装
- 実行可能なノートブック:doc-intel/notebooks-vol1
6つのレッスンに共通するのは、インラインの文字列処理として扱われていた工程を、型付きのブリックとして独立させるという一手だ。質問が行とカラムを持つ構造になれば、パイプライン全体でフィルタリング・型チェック・ディスパッチが可能になる。フラットな文字列では実現できない設計だ。
詳細はThe Untaught Lessons of RAG Question Parsing: Structure Before You Searchを参照していただきたい。