- 基礎 RAG 的四大瓶頸:檢索不精準(相關但不對的文件)、上下文不足(只看 topK 可能漏掉)、噪音過多(無關文件干擾回答)、模糊查詢(使用者問題含糊)
- Metadata Filtering 透過文件屬性(部門、類別、日期)預先縮小搜尋範圍——問工程部門的問題就只搜尋工程文件,精準度大幅提升
- Query Rewriting 用 AI 將模糊的使用者問題改寫為精確的搜尋查詢——「那個怎麼申請?」變成「年假申請流程是什麼?」
- Re-ranking 對初步搜尋結果進行二次排序,過濾掉向量相似但語意不相關的文件——從「大致相關」提升到「精確命中」
為什麼需要進階 RAG?
Lesson 6 建立的基礎 RAG 已經能運作,但在真實企業環境中,你會遇到這些問題[3]:
| 問題 | 症狀 | 進階 RAG 解法 |
|---|---|---|
| 檢索不精準 | 搜尋「部署流程」卻拿到「財務報銷」文件 | Metadata Filtering |
| 模糊查詢 | 使用者問「那個怎麼申請?」——什麼是「那個」? | Query Rewriting |
| 噪音過多 | Top 5 結果中只有 2 個真正相關 | Re-ranking |
| 上下文斷裂 | AI 不記得上一輪的問題 | ChatMemory + RAG |
進階 RAG 在基礎流程上增加了三個階段[3]:
═══ 進階 RAG Pipeline ═══
【Pre-Retrieval 檢索前】
Query Rewriting → Query Expansion → 意圖識別
【Retrieval 檢索中】
Metadata Filtering → Hybrid Search(向量 + 關鍵字)
【Post-Retrieval 檢索後】
Re-ranking → Context Compression → 去重
Step 1:建立更豐富的知識庫
進階 RAG 需要更豐富的 metadata 才能發揮威力。開啟 Lesson7/Lesson7_AdvancedRAG.ipynb,建立包含 12 篇文件、5 個類別、2 個部門的企業知識庫:
import org.springframework.ai.document.Document
import org.springframework.ai.vectorstore.SimpleVectorStore
val vectorStore = SimpleVectorStore.builder(embeddingModel).build()
val documents = listOf(
// 休假制度
Document(
"年假規定:到職滿一年者享有 7 天特休假,滿三年者 10 天,滿五年者 14 天。" +
"請假需提前三個工作天申請,需主管核准。年假可遞延至隔年第一季使用。",
mapOf("source" to "員工手冊", "category" to "leave",
"department" to "company-wide", "updated" to "2026-01")
),
Document(
"病假規定:全年病假不超過 30 天,需附醫療證明。連續病假超過 3 天需提供診斷書。",
mapOf("source" to "員工手冊", "category" to "leave",
"department" to "company-wide", "updated" to "2026-01")
),
// 財務
Document(
"出差費用報銷:國內住宿上限 3,000 元/天,餐費 800 元/天。" +
"需於出差結束 14 天內檢附電子發票報銷。超過 5,000 元需部門主管簽核。",
mapOf("source" to "財務規章", "category" to "finance",
"department" to "company-wide", "updated" to "2026-02")
),
// 技術規範(工程部專屬)
Document(
"程式碼審查規範:所有 PR 必須經過至少一位資深工程師審查。" +
"審查重點包含:程式碼品質、測試覆蓋率(最低 80%)、安全性檢查、效能影響評估。",
mapOf("source" to "工程規範", "category" to "tech",
"department" to "engineering", "updated" to "2026-03")
),
Document(
"部署流程:透過 CI/CD Pipeline 自動部署。開發環境每次 commit 自動部署," +
"正式環境需通過 staging 驗證後,由 Tech Lead 核准才能部署。緊急修復走 hotfix 流程。",
mapOf("source" to "工程規範", "category" to "tech",
"department" to "engineering", "updated" to "2026-03")
),
Document(
"Git 分支策略:採用 Git Flow。main 分支為正式版本,develop 為開發分支。" +
"功能開發使用 feature/ 前綴,修復使用 fix/ 前綴。PR 合併前需通過所有 CI 檢查。",
mapOf("source" to "工程規範", "category" to "tech",
"department" to "engineering", "updated" to "2026-03")
),
// 資安
Document(
"密碼規範:所有系統密碼需每 90 天更換,長度至少 12 字元," +
"包含大小寫字母、數字和特殊符號。禁止使用與過去 5 次相同的密碼。",
mapOf("source" to "資安政策", "category" to "security",
"department" to "company-wide", "updated" to "2026-01")
),
Document(
"VPN 使用規定:遠端存取公司內部系統必須透過 VPN。VPN 帳號與 AD 帳號整合," +
"閒置 30 分鐘自動斷線。禁止將 VPN 設定分享給非授權人員。",
mapOf("source" to "資安政策", "category" to "security",
"department" to "company-wide", "updated" to "2026-02")
),
// 更多文件...
)
vectorStore.add(documents)
println("✓ 已載入 ${documents.size} 篇文件(含豐富 metadata)")
Step 2:Metadata Filtering——精準縮小搜尋範圍
Metadata Filtering[2] 的核心思想:在向量搜尋之前,先用文件屬性篩選。問工程部門的問題,就只在工程文件中搜尋。
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder
val b = FilterExpressionBuilder()
// 只搜尋工程部門的技術文件
val engineeringFilter = b.and(
b.eq("department", "engineering"),
b.eq("category", "tech")
).build()
val filteredResults = vectorStore.similaritySearch(
SearchRequest.builder()
.query("部署到正式環境的流程是什麼?")
.topK(3)
.filterExpression(engineeringFilter)
.build()
)
filteredResults.forEach { doc ->
println("【${doc.metadata["source"]}】${doc.text.take(60)}...")
}
對比有無 Filter 的差異:
| 搜尋方式 | 結果 | 精準度 |
|---|---|---|
| 無 Filter | 部署流程、出差報銷、密碼規範 | ❌ 混入無關文件 |
| 有 Filter(engineering + tech) | 部署流程、Git 分支、程式碼審查 | ✅ 全部相關 |
FilterExpressionBuilder 支援的運算子:
b.eq("field", "value") // 等於
b.ne("field", "value") // 不等於
b.in("field", "a", "b") // 包含在列表中
b.and(expr1, expr2) // 且
b.or(expr1, expr2) // 或
Step 3:Query Rewriting——改寫模糊查詢
使用者常常問模糊的問題,尤其在多輪對話中——「那個怎麼申請?」、「上次說的那個制度是什麼?」。Query Rewriting 用 AI 將模糊查詢改寫為精確的搜尋語句:
fun rewriteQuery(originalQuery: String, conversationContext: String = ""): String {
val rewritePrompt = """你是一個查詢改寫專家。
請根據對話上下文,將使用者的模糊查詢改寫為一個精確、獨立的搜尋語句。
只回傳改寫後的查詢,不要加任何解釋。
對話上下文: $conversationContext
使用者查詢: $originalQuery
改寫後的查詢:"""
return chatClient.prompt()
.user(rewritePrompt)
.call().content()?.trim() ?: originalQuery
}
// 測試:模糊查詢 + 對話上下文
val context = "我們剛才討論了年假制度"
val vague = "那個怎麼申請?"
val rewritten = rewriteQuery(vague, context)
println("原始查詢: $vague")
println("改寫後: $rewritten")
// 改寫後: "年假申請流程和需要注意的事項是什麼?"
改寫前後的搜尋結果比較:
原始查詢「那個怎麼申請?」→ 搜尋結果:出差報銷、會議室預約、年假... (混亂)
改寫後「年假申請流程」 → 搜尋結果:年假規定(精準命中!)
Step 4:Re-ranking——二次排序
向量搜尋回傳的結果是按向量距離排序的,但向量相似度不完全等於「回答這個問題的相關性」。Re-ranking 用 AI 重新評估每個結果的相關性:
fun rerank(question: String, documents: List<Document>, topN: Int = 3): List<Document> {
val docsText = documents.mapIndexed { i, doc ->
"文件 ${i + 1}: ${doc.text.take(200)}"
}.joinToString("\n")
val rankResult = chatClient.prompt()
.user("""你是一個文件相關性評估專家。
請根據問題,從以下文件中選出最相關的 $topN 篇,
只回傳文件編號(用逗號分隔),按相關性從高到低排列。
問題: $question
$docsText
最相關的文件編號:""")
.call().content() ?: ""
// 解析 AI 回傳的排序結果
val indices = rankResult.replace(" ", "")
.split(",")
.mapNotNull { it.trim().toIntOrNull()?.minus(1) }
.filter { it in documents.indices }
.take(topN)
return indices.map { documents[it] }
}
Re-ranking 的效果:
向量搜尋 Top 5:
1. 部署流程(相關度 0.89) ← 精準
2. Git 分支策略(相關度 0.85) ← 相關但非直接答案
3. 出差報銷(相關度 0.82) ← ❌ 不相關(噪音)
4. 程式碼審查(相關度 0.81) ← 相關
5. VPN 規定(相關度 0.80) ← ❌ 不相關
Re-ranking 後 Top 3:
1. 部署流程 ← 精準
2. 程式碼審查 ← 相關(部署前需審查)
3. Git 分支策略 ← 相關(分支影響部署)
Step 5:組合管線——完整的進階 RAG
將三個技巧組合成一個完整的進階 RAG Pipeline:
fun advancedRagQuery(
question: String,
conversationContext: String = "",
departmentFilter: String? = null
): String {
// Step 1: Query Rewriting(如果有對話上下文)
val searchQuery = if (conversationContext.isNotEmpty()) {
rewriteQuery(question, conversationContext)
} else question
println("🔄 搜尋查詢: $searchQuery")
// Step 2: 建立搜尋請求(含 Metadata Filter)
val searchBuilder = SearchRequest.builder()
.query(searchQuery)
.topK(5) // 多取一些,後面 re-rank 再篩
if (departmentFilter != null) {
val b = FilterExpressionBuilder()
searchBuilder.filterExpression(
b.eq("department", departmentFilter).build()
)
println("🏷️ 部門過濾: $departmentFilter")
}
// Step 3: 向量搜尋
val rawResults = vectorStore.similaritySearch(searchBuilder.build())
println("📄 初步搜尋: ${rawResults.size} 篇文件")
// Step 4: Re-ranking
val rankedResults = rerank(searchQuery, rawResults, topN = 3)
println("🏆 Re-ranking 後: ${rankedResults.size} 篇文件")
// Step 5: 組裝上下文並回答
val context = rankedResults.joinToString("\n\n") { doc ->
"【${doc.metadata["source"]} — ${doc.metadata["category"]}】\n${doc.text}"
}
return chatClient.prompt()
.system("""你是一位企業知識庫助手。根據參考資料精確回答,引用來源。
找不到答案時誠實說明。""")
.user("參考資料:\n$context\n\n問題: $question")
.call().content() ?: "(無回應)"
}
測試三種場景:
// 場景 1:模糊查詢 + 對話上下文
println(advancedRagQuery(
question = "那個怎麼申請?期限是多久?",
conversationContext = "我們剛才在討論年假制度"
))
// 場景 2:部門過濾
println(advancedRagQuery(
question = "PR 審查要注意什麼?",
departmentFilter = "engineering"
))
// 場景 3:跨類別查詢
println(advancedRagQuery(
question = "新進工程師需要知道的資安規範有哪些?"
))
Step 6:終極實戰——Memory + RAG 智慧助手
將 Lesson 5 的 ChatMemory 與本課的 RAG 結合,打造一個有記憶的知識庫助手:
import org.springframework.ai.chat.memory.MessageWindowChatMemory
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor
import org.springframework.ai.chat.memory.ChatMemory
val smartMemory = MessageWindowChatMemory.builder()
.maxMessages(15)
.build()
val smartAssistant = ChatClient.builder(chatModel)
.defaultSystem("""你是公司的「AI 人事助手小幫手」。
你會記住使用者的姓名、部門和之前的對話。
根據知識庫中的文件回答問題,找不到時誠實說明。
回答要友善、實用,適時給予延伸建議。""")
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(smartMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(3).build())
.build()
)
.build()
fun ask(sessionId: String, question: String): String {
return smartAssistant.prompt()
.user(question)
.advisors { it.param(ChatMemory.CONVERSATION_ID, sessionId) }
.call().content() ?: "(無回應)"
}
模擬一個新進工程師「小華」的五輪對話:
val sid = "xiaohua-001"
println("🧑 " + "嗨!我叫小華,我是工程部的新人,今天第一天上班。")
println("🤖 " + ask(sid, "嗨!我叫小華,我是工程部的新人,今天第一天上班。"))
println()
println("🧑 " + "我可以請幾天年假?")
println("🤖 " + ask(sid, "我可以請幾天年假?"))
println()
println("🧑 " + "那病假呢?")
println("🤖 " + ask(sid, "那病假呢?"))
println()
println("🧑 " + "對了,我們工程部的 Code Review 流程是怎樣的?")
println("🤖 " + ask(sid, "對了,我們工程部的 Code Review 流程是怎樣的?"))
println()
println("🧑 " + "幫我整理一下我今天問過的所有問題的重點。")
println("🤖 " + ask(sid, "幫我整理一下我今天問過的所有問題的重點。"))
第五輪的預期輸出:
🤖 好的小華!以下是你今天諮詢的重點整理:
1. 📅 年假:到職滿一年 7 天,可遞延至隔年 Q1
2. 🏥 病假:全年 30 天,超過 3 天需診斷書,半薪
3. 🔍 Code Review:所有 PR 需資深工程師審查,
重點在品質、測試覆蓋率 80%、安全性、效能
作為工程部新人,建議你也了解一下部署流程和 Git 分支策略!
有任何問題都可以隨時問我。祝你第一天順利 🎉
這個助手同時具備了:記憶(記得小華的名字和部門)、知識檢索(從企業文件中找答案)、多輪理解(「那病假呢?」能理解是延續前一個話題)、摘要能力(整理所有問過的問題)。
RAG 效能調優指南
根據資料規模選擇合適的策略:
| 資料規模 | 推薦策略 | 說明 |
|---|---|---|
| < 100 篇 | 基礎 RAG + Metadata Filtering | SimpleVectorStore 就夠用 |
| 100 ~ 10,000 篇 | + Query Rewriting + Re-ranking | 切換 PGVector,加入進階技巧 |
| > 10,000 篇 | + Hybrid Search + 專業 Embedding/Re-ranking | Milvus/Weaviate + Voyage AI/Cohere |
其他調優建議:
- Chunk 大小 — 200~500 字元為佳,太小失去上下文、太大失去精確度
- TopK 設定 — 3~5 篇為佳,搭配 Re-ranking 可以先取多一些再篩
- Metadata 設計 — 越豐富越好:部門、類別、日期、作者、版本
- Embedding 模型 —
text-embedding-3-small(快、便宜)vstext-embedding-3-large(精準、較貴)
本課重點回顧
| 概念 | 重點 |
|---|---|
| Metadata Filtering | 用 FilterExpressionBuilder 縮小搜尋範圍 |
| Query Rewriting | AI 將模糊查詢改寫為精確搜尋語句 |
| Re-ranking | 對搜尋結果二次排序,過濾噪音 |
| 組合管線 | Rewrite → Filter → Search → Rerank → Answer |
| Memory + RAG | 同時使用 ChatMemory 和 QuestionAnswerAdvisor |
| 效能調優 | 依資料規模選擇策略,平衡精準度與成本 |
下一步:Lesson 8 — 多模態處理
到目前為止我們處理的都是文字。下一堂課將擴展到多模態——讓 AI 看得懂圖片、生成圖片、聽得懂語音:
- 圖片分析 — 上傳圖片讓 AI 描述內容、擷取資訊
- 圖片生成 — 用文字描述生成圖片
- 語音轉文字 — Speech-to-Text 整合


