Key Findings
  • 基礎 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 FilteringSimpleVectorStore 就夠用
100 ~ 10,000 篇+ Query Rewriting + Re-ranking切換 PGVector,加入進階技巧
> 10,000 篇+ Hybrid Search + 專業 Embedding/Re-rankingMilvus/Weaviate + Voyage AI/Cohere

其他調優建議:

  • Chunk 大小 — 200~500 字元為佳,太小失去上下文、太大失去精確度
  • TopK 設定 — 3~5 篇為佳,搭配 Re-ranking 可以先取多一些再篩
  • Metadata 設計 — 越豐富越好:部門、類別、日期、作者、版本
  • Embedding 模型text-embedding-3-small(快、便宜)vs text-embedding-3-large(精準、較貴)

本課重點回顧

概念重點
Metadata Filtering用 FilterExpressionBuilder 縮小搜尋範圍
Query RewritingAI 將模糊查詢改寫為精確搜尋語句
Re-ranking對搜尋結果二次排序,過濾噪音
組合管線Rewrite → Filter → Search → Rerank → Answer
Memory + RAG同時使用 ChatMemory 和 QuestionAnswerAdvisor
效能調優依資料規模選擇策略,平衡精準度與成本

下一步:Lesson 8 — 多模態處理

到目前為止我們處理的都是文字。下一堂課將擴展到多模態——讓 AI 看得懂圖片、生成圖片、聽得懂語音:

  • 圖片分析 — 上傳圖片讓 AI 描述內容、擷取資訊
  • 圖片生成 — 用文字描述生成圖片
  • 語音轉文字 — Speech-to-Text 整合

掌握進階 RAG,繼續前進!

檢索精準度已提升,下一步讓 AI 看得懂圖片、聽得懂語音——多模態處理。

前往 Lesson 8:多模態處理 →