導入
チャット形式のRAGでは、「それは何年に完成したの?」のような**前の発言に依存する質問(フォローアップ質問)**がよく来ます。この質問文だけをそのまま検索に投げても、「それ」が何を指すのか検索エンジンには分からず、うまくヒットしません。
説明
この問題への定番の対処が、クエリ書き換え(query rewriting)、あるいは**要約結合(condensation)**と呼ばれる手法です。会話履歴とフォローアップ質問をLLMに渡し、単独で意味が通る質問に書き換えさせてから検索する、という手順を挟みます。
全体の流れをまとめると、こうなります。
flowchart TD hist["会話履歴 + フォローアップ質問<br/>例:「それは何年に完成したの?」"] --> rewrite["書き換え(rewrite)<br/>LLMに独立した質問を作らせる"] rewrite --> query["独立した検索クエリ<br/>例:「東京タワーが完成したのは何年?」"] query --> search["検索(第3章のretriever)"] search --> contexts["関連文書(contexts)"] contexts --> answer["生成(第4章のgenerator)<br/>回答"]
「履歴+フォローアップ質問 → 独立した検索クエリを作る」という1ステップを検索の前に挟むだけで、あとの流れ(検索して、プロンプトを組み立てて、答えを生成する)は今までと同じです。書き換え用のプロンプトを組み立てる擬似コードを見てみましょう。
def build_rewrite_prompt(history, follow_up_question):
conversation = "\n".join(f"{turn['role']}: {turn['text']}" for turn in history)
return f"""Given the conversation history and a follow-up question, rewrite the
follow-up question to be a standalone question that makes sense without the history.
Conversation history:
{conversation}
Follow-up question: {follow_up_question}
Standalone question:"""
history = [
{"role": "user", "text": "When was Tokyo Tower built?"},
{"role": "assistant", "text": "Tokyo Tower was completed in 1958."},
]
rewrite_prompt = build_rewrite_prompt(history, "How tall is it?")
# rewrite_prompt をLLMに渡すと、たとえば
# "How tall is Tokyo Tower?" のような独立した質問が返ってくる
build_prompt(レッスン14)とほとんど同じ考え方です。違いは、渡すものが「文書」ではなく「会話履歴」であること、そして返ってくるのが「回答」ではなく「書き換えられた検索クエリ」であることです。こうしてできた独立クエリを、これまで通り検索(retrieve)→ プロンプト組み立て(build_prompt)→ 生成(LLM呼び出し)に渡せば、マルチターンのRAGが完成します。
このクエリ書き換えの考え方は、次の第5章で扱う「クエリ変換」というテーマにもつながっていきます。
まとめ
- フォローアップ質問をそのまま検索すると、指示語(「それ」など)のせいでうまく検索できないことがある。
- クエリ書き換え(query rewriting):会話履歴とフォローアップ質問をLLMに渡し、単独で意味が通る質問に直してから検索する。
- 書き換え後は、これまで通り「検索 → プロンプト組み立て → 生成」の流れに乗せられる。