到目前為止,我們討論的檢索場景都是「單輪問答」——使用者問一句,系統回一句。但實際應用中,使用者往往會延續前面的對話繼續追問,這對 RAG 系統的檢索階段帶來了新的挑戰。
真實對話常常長這樣:
使用者:「Postgres 的 pgvector 要怎麼安裝?」
系統:(回答安裝步驟...)
使用者:「那它支援哪些索引類型?」 ← 「它」指的是 pgvector,但這句話本身沒講清楚
如果直接拿「那它支援哪些索引類型?」去做向量搜尋,系統根本不知道「它」是什麼,檢索結果很可能牛頭不對馬嘴。
延續 Day 18 提到的 Query Rewriting,在多輪對話情境下更加重要:把對話歷史一起交給 LLM,讓它把當前問題改寫成「獨立、完整」的問題,再拿去檢索。
def rewrite_with_history(chat_history, current_question):
history_text = "\n".join(
f"{'使用者' if m['role']=='user' else '系統'}:{m['content']}"
for m in chat_history
)
prompt = f"""以下是一段對話歷史,請根據上下文,
將「目前問題」改寫成一個不需要依賴對話歷史、單獨也能被理解的完整問題:
對話歷史:
{history_text}
目前問題:{current_question}
改寫後的完整問題:"""
return call_llm(prompt).strip()
範例輸出:
改寫後:「pgvector 支援哪些索引類型?」
另一種思路是額外維護一個「目前對話中提到的關鍵實體」清單(例如產品名稱、技術名詞),在組裝檢索查詢時自動帶入,而不完全依賴 LLM 改寫。
class ConversationContext:
def __init__(self):
self.mentioned_entities = []
def update(self, question, answer):
entities = extract_entities(question + answer) # 可用 NER 或 LLM 抽取
self.mentioned_entities.extend(entities)
def build_search_query(self, current_question):
context_hint = "、".join(self.mentioned_entities[-3:]) # 取最近提到的幾個實體
return f"{context_hint}{current_question}"
生成階段的 Prompt 也需要調整,通常會把「對話歷史」與「本輪檢索結果」一起放入:
System: 你是一個技術問答助手,請根據下方參考資料與對話歷史回答使用者的最新問題。
對話歷史:
{chat_history}
參考資料(本輪檢索結果):
{retrieved_chunks}
使用者最新問題:{current_question}
要注意:對話歷史一長,加上每輪都要放入檢索結果,很容易讓 Prompt 超過 context window 限制,通常需要做對話歷史的摘要或截斷(只保留最近 N 輪)。
不一定。可以先判斷「這一輪問題是否需要查新的資料」:
這個判斷可以用一個簡單的分類 Prompt 讓 LLM 先做決定,避免每輪都做不必要的檢索,增加延遲與成本。
多輪對話為 RAG 系統帶來的最大挑戰,是「使用者的問題不再是獨立完整的」,需要額外處理指代消解與上下文延續的問題。透過 Query Rewriting、實體追蹤等技巧,可以讓系統在對話情境下依然檢索到正確的內容。明天是檢索優化系列的最後一天,我們要處理長文件與多文件交叉引用的挑戰。