Skip to content

Instantly share code, notes, and snippets.

@hirosichen
Created March 20, 2026 23:18
Show Gist options
  • Select an option

  • Save hirosichen/b74115e108f5a96db45bee73c243880d to your computer and use it in GitHub Desktop.

Select an option

Save hirosichen/b74115e108f5a96db45bee73c243880d to your computer and use it in GitHub Desktop.
Spring AI 課程 Lesson 7:進階 RAG
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Lesson 7:Advanced RAG — 提升檢索準確率\n",
"\n",
"## 本節概要\n",
"\n",
"本課程對應教材:\n",
"- **8.1** Advanced RAG 概覽 - 提升檢索準確率的關鍵技術\n",
"- **8.2** Embedding 模型優化 - 從選擇到效能調校\n",
"- **8.3** Re-ranking 實戰 - 精準重排以獲得最佳答案\n",
"\n",
"學完本課你將能夠:\n",
"1. 理解 Naive RAG 的不足與 Advanced RAG 的改進方向\n",
"2. 使用 Metadata 過濾提升搜尋精確度\n",
"3. 實現查詢改寫(Query Rewriting)\n",
"4. 理解 Re-ranking 的原理與效果\n",
"5. 結合 ChatMemory + RAG 建立有記憶的知識庫助手"
],
"id": "c7ad939d83336f3d"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7.1 為什麼需要 Advanced RAG?\n",
"\n",
"### Naive RAG 的問題\n",
"\n",
"Lesson 6 學到的基礎 RAG 流程(Naive RAG)在真實場景中會遇到:\n",
"\n",
"| 問題 | 範例 |\n",
"|------|------|\n",
"| **檢索不精確** | 問「年假」卻搜到「病假」的文件 |\n",
"| **上下文不足** | 搜到的段落缺少關鍵前後文 |\n",
"| **雜訊太多** | 搜到的 3 個段落中有 2 個不相關 |\n",
"| **使用者問題模糊** | 「那個制度是什麼?」→ AI 不知道「那個」指什麼 |\n",
"\n",
"### Advanced RAG 的改進\n",
"\n",
"```\n",
"Naive RAG:\n",
" 問題 → 搜尋 → AI 回答\n",
"\n",
"Advanced RAG:\n",
" 問題 → [前處理] → 搜尋 → [後處理] → AI 回答\n",
" ↑ ↑\n",
" 查詢改寫 Re-ranking\n",
" 查詢擴展 Metadata 過濾\n",
" 意圖辨識 上下文壓縮\n",
"```\n",
"\n",
"| 階段 | 技術 | 目的 |\n",
"|------|------|------|\n",
"| **Pre-Retrieval** | 查詢改寫、查詢擴展 | 讓搜尋更精確 |\n",
"| **Retrieval** | Metadata 過濾、混合搜尋 | 縮小搜尋範圍 |\n",
"| **Post-Retrieval** | Re-ranking、上下文壓縮 | 篩選最相關的結果 |"
],
"id": "ab3dc44170296fe7"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7.2 環境準備"
],
"id": "9e380fa0ef9c0611"
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:18:22.895863Z",
"start_time": "2026-03-18T15:18:22.070427Z"
}
},
"source": [
"@file:DependsOn(\"org.springframework.ai:spring-ai-openai:1.0.0\")\n",
"@file:DependsOn(\"org.springframework.ai:spring-ai-client-chat:1.0.0\")\n",
"@file:DependsOn(\"org.springframework.ai:spring-ai-vector-store:1.0.0\")\n",
"@file:DependsOn(\"org.springframework.ai:spring-ai-advisors-vector-store:1.0.0\")\n",
"@file:DependsOn(\"org.slf4j:slf4j-simple:2.0.16\")\n",
"\n",
"import org.springframework.ai.openai.OpenAiChatModel\n",
"import org.springframework.ai.openai.OpenAiEmbeddingModel\n",
"import org.springframework.ai.openai.api.OpenAiApi\n",
"import org.springframework.ai.chat.client.ChatClient\n",
"import org.springframework.ai.openai.OpenAiChatOptions\n",
"import org.springframework.ai.vectorstore.SimpleVectorStore\n",
"import org.springframework.ai.vectorstore.SearchRequest\n",
"import org.springframework.ai.document.Document\n",
"import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor\n",
"import org.springframework.ai.chat.memory.MessageWindowChatMemory\n",
"import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor\n",
"import org.springframework.ai.chat.memory.ChatMemory\n",
"import java.io.File\n",
"\n",
"// ===== API Key 設定(二擇一)=====\n",
"val OPENAI_API_KEY = \"\"\n",
"// ===================================\n",
"\n",
"fun loadApiKey(manualKey: String): String {\n",
" if (manualKey.isNotBlank()) return manualKey\n",
" listOf(\".env\", \"../.env\", \"../../.env\").forEach { path ->\n",
" val file = File(path)\n",
" if (file.exists()) {\n",
" file.readLines().forEach { line ->\n",
" if (line.startsWith(\"OPENAI_API_KEY=\")) {\n",
" return line.substringAfter(\"OPENAI_API_KEY=\").trim()\n",
" }\n",
" }\n",
" }\n",
" }\n",
" return System.getenv(\"OPENAI_API_KEY\")\n",
" ?: error(\"請直接填入 API Key,或在專案根目錄建立 .env 檔案\")\n",
"}\n",
"\n",
"val apiKey = loadApiKey(OPENAI_API_KEY)\n",
"val openAiApi = OpenAiApi.builder().apiKey(apiKey).build()\n",
"val chatModel = OpenAiChatModel.builder().openAiApi(openAiApi).build()\n",
"val embeddingModel = OpenAiEmbeddingModel(openAiApi)\n",
"val chatClient = ChatClient.builder(chatModel).build()\n",
"\n",
"println(\"環境準備完成!\")"
],
"id": "aa287abddf473ffd",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"環境準備完成!\n"
]
}
],
"execution_count": 1
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7.3 建立更豐富的知識庫\n",
"\n",
"這次我們建立一個更大、有更多 Metadata 的知識庫,模擬真實企業場景:"
],
"id": "925b8bedd0e92216"
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:18:28.177228Z",
"start_time": "2026-03-18T15:18:24.425599Z"
}
},
"source": [
"val vectorStore = SimpleVectorStore.builder(embeddingModel).build()\n",
"\n",
"val docs = listOf(\n",
" // 休假制度\n",
" Document(\"年假規定:全職員工每年享有 14 天年假。到職滿一年後,每增加一年年資增加 1 天,最多 30 天。年假需提前 3 天申請。\",\n",
" mapOf(\"source\" to \"員工手冊\", \"category\" to \"休假\", \"department\" to \"全公司\", \"updated\" to \"2025-01\")),\n",
" Document(\"病假規定:員工每年有 30 天全薪病假。超過 3 天需附醫生證明。住院期間不計入病假天數。\",\n",
" mapOf(\"source\" to \"員工手冊\", \"category\" to \"休假\", \"department\" to \"全公司\", \"updated\" to \"2025-01\")),\n",
" Document(\"特休假:婚假 8 天、喪假 3-8 天(視親等)、產假 56 天、陪產假 7 天。需附相關證明文件。\",\n",
" mapOf(\"source\" to \"員工手冊\", \"category\" to \"休假\", \"department\" to \"全公司\", \"updated\" to \"2025-01\")),\n",
"\n",
" // 財務制度\n",
" Document(\"報帳流程:業務支出需在 7 天內提交報帳單。5000 元以下主管核准,5000-20000 元經理核准,20000 元以上副總核准。\",\n",
" mapOf(\"source\" to \"財務手冊\", \"category\" to \"財務\", \"department\" to \"全公司\", \"updated\" to \"2025-03\")),\n",
" Document(\"出差補助:國內出差每日膳雜費 700 元,住宿費上限 2500 元。國際出差依目的地國家標準另行核定。\",\n",
" mapOf(\"source\" to \"財務手冊\", \"category\" to \"財務\", \"department\" to \"全公司\", \"updated\" to \"2025-03\")),\n",
"\n",
" // 工程部專屬\n",
" Document(\"程式碼審查規範:所有合併請求(PR)需至少一位資深工程師審查通過。審查重點:程式碼品質、測試覆蓋率、安全性。\",\n",
" mapOf(\"source\" to \"工程部規範\", \"category\" to \"技術\", \"department\" to \"工程部\", \"updated\" to \"2025-02\")),\n",
" Document(\"部署流程:所有程式碼需通過 CI/CD 管線。開發環境可自由部署,測試環境需主管核准,正式環境需技術主管和 PM 雙核准。\",\n",
" mapOf(\"source\" to \"工程部規範\", \"category\" to \"技術\", \"department\" to \"工程部\", \"updated\" to \"2025-02\")),\n",
" Document(\"技術選型原則:新專案優先使用 Spring Boot 3 + Java 21。AI 功能使用 Spring AI 框架。前端使用 React 或 Vue。\",\n",
" mapOf(\"source\" to \"工程部規範\", \"category\" to \"技術\", \"department\" to \"工程部\", \"updated\" to \"2026-01\")),\n",
"\n",
" // 資安\n",
" Document(\"密碼政策:密碼每 90 天更換,長度至少 12 位,需包含大小寫字母、數字和特殊符號。禁止使用最近 5 次用過的密碼。\",\n",
" mapOf(\"source\" to \"資安手冊\", \"category\" to \"資安\", \"department\" to \"全公司\", \"updated\" to \"2025-06\")),\n",
" Document(\"遠端工作資安要求:必須使用公司 VPN 連線。禁止在公共 WiFi 下處理公司資料。離開電腦必須鎖定螢幕。\",\n",
" mapOf(\"source\" to \"資安手冊\", \"category\" to \"資安\", \"department\" to \"全公司\", \"updated\" to \"2025-06\")),\n",
"\n",
" // 績效\n",
" Document(\"績效考核:每半年一次。自評 30%、主管評 50%、同儕評 20%。等級 S/A/B/C/D,B 以上可獲年終獎金。\",\n",
" mapOf(\"source\" to \"人事手冊\", \"category\" to \"績效\", \"department\" to \"全公司\", \"updated\" to \"2025-01\")),\n",
" Document(\"升遷標準:連續兩次考核 A 以上可提報升遷。升遷需經部門主管推薦、人事委員會審核、總經理核准。\",\n",
" mapOf(\"source\" to \"人事手冊\", \"category\" to \"績效\", \"department\" to \"全公司\", \"updated\" to \"2025-01\"))\n",
")\n",
"\n",
"vectorStore.add(docs)\n",
"\n",
"println(\"已載入 ${docs.size} 份文件\")\n",
"println(\"類別:${docs.map { it.metadata[\"category\"] }.distinct().joinToString(\"、\")}\")\n",
"println(\"部門:${docs.map { it.metadata[\"department\"] }.distinct().joinToString(\"、\")}\")"
],
"id": "7acf8fb375bb4ca6",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"已載入 12 份文件\n",
"類別:休假、財務、技術、資安、績效\n",
"部門:全公司、工程部\n"
]
}
],
"execution_count": 2
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7.4 技術一:Metadata 過濾\n",
"\n",
"### 問題:搜尋範圍太大\n",
"\n",
"```\n",
"問:「工程部的部署流程是什麼?」\n",
"\n",
"Naive RAG:搜尋所有 12 份文件 → 可能混入不相關的結果\n",
"Metadata 過濾:只搜尋 department=工程部 的文件 → 更精確\n",
"```"
],
"id": "65e14515540a11f5"
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:18:35.241035Z",
"start_time": "2026-03-18T15:18:34.889600Z"
}
},
"source": [
"import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder\n",
"\n",
"val b = FilterExpressionBuilder()\n",
"\n",
"// 只搜尋工程部的文件\n",
"val engineeringFilter = b.eq(\"department\", \"工程部\").build()\n",
"\n",
"val filteredResults = vectorStore.similaritySearch(\n",
" SearchRequest.builder()\n",
" .query(\"部署流程是什麼?\")\n",
" .topK(3)\n",
" .filterExpression(engineeringFilter)\n",
" .build()\n",
")\n",
"\n",
"println(\"=== 使用 Metadata 過濾(department=工程部)===\")\n",
"for ((i, doc) in filteredResults.withIndex()) {\n",
" println(\"\\n第 ${i + 1} 名 [${doc.metadata[\"source\"]}]:\")\n",
" println(doc.text)\n",
"}"
],
"id": "77469f9ee87d5386",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== 使用 Metadata 過濾(department=工程部)===\n",
"\n",
"第 1 名 [工程部規範]:\n",
"部署流程:所有程式碼需通過 CI/CD 管線。開發環境可自由部署,測試環境需主管核准,正式環境需技術主管和 PM 雙核准。\n",
"\n",
"第 2 名 [工程部規範]:\n",
"技術選型原則:新專案優先使用 Spring Boot 3 + Java 21。AI 功能使用 Spring AI 框架。前端使用 React 或 Vue。\n",
"\n",
"第 3 名 [工程部規範]:\n",
"程式碼審查規範:所有合併請求(PR)需至少一位資深工程師審查通過。審查重點:程式碼品質、測試覆蓋率、安全性。\n"
]
}
],
"execution_count": 3
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:18:45.772490Z",
"start_time": "2026-03-18T15:18:45.545595Z"
}
},
"source": [
"// 對比:不使用過濾\n",
"val unfilteredResults = vectorStore.similaritySearch(\n",
" SearchRequest.builder()\n",
" .query(\"部署流程是什麼?\")\n",
" .topK(3)\n",
" .build()\n",
")\n",
"\n",
"println(\"=== 不使用過濾 ===\")\n",
"for ((i, doc) in unfilteredResults.withIndex()) {\n",
" println(\"\\n第 ${i + 1} 名 [${doc.metadata[\"source\"]}]:\")\n",
" println(doc.text)\n",
"}\n",
"\n",
"println(\"\\n--- 觀察:沒有過濾時,可能搜到不相關部門的文件 ---\")"
],
"id": "9f6bec148a58b836",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== 不使用過濾 ===\n",
"\n",
"第 1 名 [工程部規範]:\n",
"部署流程:所有程式碼需通過 CI/CD 管線。開發環境可自由部署,測試環境需主管核准,正式環境需技術主管和 PM 雙核准。\n",
"\n",
"第 2 名 [財務手冊]:\n",
"報帳流程:業務支出需在 7 天內提交報帳單。5000 元以下主管核准,5000-20000 元經理核准,20000 元以上副總核准。\n",
"\n",
"第 3 名 [工程部規範]:\n",
"技術選型原則:新專案優先使用 Spring Boot 3 + Java 21。AI 功能使用 Spring AI 框架。前端使用 React 或 Vue。\n",
"\n",
"--- 觀察:沒有過濾時,可能搜到不相關部門的文件 ---\n"
]
}
],
"execution_count": 4
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:18:51.648928Z",
"start_time": "2026-03-18T15:18:51.398Z"
}
},
"source": [
"// 組合多個過濾條件\n",
"val combinedFilter = b.and(\n",
" b.eq(\"category\", \"休假\"),\n",
" b.eq(\"department\", \"全公司\")\n",
").build()\n",
"\n",
"val leaveResults = vectorStore.similaritySearch(\n",
" SearchRequest.builder()\n",
" .query(\"請假規定\")\n",
" .topK(5)\n",
" .filterExpression(combinedFilter)\n",
" .build()\n",
")\n",
"\n",
"println(\"=== 組合過濾(category=休假 AND department=全公司)===\")\n",
"for ((i, doc) in leaveResults.withIndex()) {\n",
" println(\"${i + 1}. [${doc.metadata[\"category\"]}] ${doc.text.take(50)}...\")\n",
"}"
],
"id": "6b81a91f23726c27",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== 組合過濾(category=休假 AND department=全公司)===\n",
"1. [休假] 病假規定:員工每年有 30 天全薪病假。超過 3 天需附醫生證明。住院期間不計入病假天數。...\n",
"2. [休假] 年假規定:全職員工每年享有 14 天年假。到職滿一年後,每增加一年年資增加 1 天,最多 30 天。...\n",
"3. [休假] 特休假:婚假 8 天、喪假 3-8 天(視親等)、產假 56 天、陪產假 7 天。需附相關證明文件。...\n"
]
}
],
"execution_count": 5
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Filter 語法\n",
"\n",
"```kotlin\n",
"val b = FilterExpressionBuilder()\n",
"\n",
"b.eq(\"key\", \"value\") // key == value\n",
"b.ne(\"key\", \"value\") // key != value\n",
"b.in(\"key\", \"v1\", \"v2\") // key in [v1, v2]\n",
"b.and(expr1, expr2) // expr1 AND expr2\n",
"b.or(expr1, expr2) // expr1 OR expr2\n",
"```"
],
"id": "ad15baa7f05aae45"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7.5 技術二:查詢改寫(Query Rewriting)\n",
"\n",
"### 問題:使用者的問題太模糊\n",
"\n",
"```\n",
"使用者:「那個要怎麼請?」 → AI 不知道「那個」是什麼\n",
"使用者:「升遷要什麼條件?」 → 太簡短,可能搜不到最佳段落\n",
"```\n",
"\n",
"### 解決方案:用 AI 改寫查詢\n",
"\n",
"```\n",
"原始問題:「那個要怎麼請?」\n",
" ↓ AI 改寫\n",
"改寫後:「年假的申請流程和規定是什麼?需要提前幾天申請?」\n",
" ↓\n",
"用改寫後的問題去搜尋 → 更精確的結果\n",
"```"
],
"id": "482426b362fa8a3e"
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:19:10.349525Z",
"start_time": "2026-03-18T15:19:07.779114Z"
}
},
"source": [
"// 查詢改寫函式\n",
"fun rewriteQuery(originalQuery: String, conversationContext: String = \"\"): String {\n",
" val rewritten: String = chatClient.prompt()\n",
" .system(\"\"\"你是一個查詢改寫助手。\n",
"將使用者的問題改寫為更適合搜尋企業知識庫的查詢。\n",
"規則:\n",
"1. 把模糊的詞替換為具體的詞\n",
"2. 展開縮寫和代稱\n",
"3. 只回傳改寫後的查詢,不要加其他文字\n",
"4. 如果有對話上下文,根據上下文理解指代詞(如「那個」「這個」)\n",
"\n",
"對話上下文:$conversationContext\"\"\")\n",
" .user(originalQuery)\n",
" .call()\n",
" .content() ?: originalQuery\n",
" return rewritten\n",
"}\n",
"\n",
"// 測試查詢改寫\n",
"val queries = listOf(\n",
" \"那個要怎麼請?\" to \"之前在聊年假的事\",\n",
" \"升遷要什麼?\" to \"\",\n",
" \"出差錢怎麼報?\" to \"\"\n",
")\n",
"\n",
"for ((query, context) in queries) {\n",
" val rewritten = rewriteQuery(query, context)\n",
" println(\"原始:$query\")\n",
" println(\"改寫:$rewritten\")\n",
" println()\n",
"}"
],
"id": "c6b8cccaeb0f0301",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"原始:那個要怎麼請?\n",
"改寫:年假申請流程是什麼?\n",
"\n",
"原始:升遷要什麼?\n",
"改寫:升遷需要哪些條件和流程?\n",
"\n",
"原始:出差錢怎麼報?\n",
"改寫:出差費用報銷流程是什麼?\n",
"\n"
]
}
],
"execution_count": 6
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:19:36.965604Z",
"start_time": "2026-03-18T15:19:35.864890Z"
}
},
"source": [
"// 使用改寫後的查詢搜尋,對比效果\n",
"val originalQuery = \"那個要怎麼請?\"\n",
"val rewrittenQuery = rewriteQuery(originalQuery, \"之前在聊年假的事\")\n",
"\n",
"println(\"=== 原始查詢搜尋結果 ===\")\n",
"val origResults = vectorStore.similaritySearch(\n",
" SearchRequest.builder().query(originalQuery).topK(2).build()\n",
")\n",
"for (doc in origResults) {\n",
" println(\" [${doc.metadata[\"category\"]}] ${doc.text.take(60)}...\")\n",
"}\n",
"\n",
"println(\"\\n=== 改寫後查詢搜尋結果 ===\")\n",
"val rewrittenResults = vectorStore.similaritySearch(\n",
" SearchRequest.builder().query(rewrittenQuery).topK(2).build()\n",
")\n",
"for (doc in rewrittenResults) {\n",
" println(\" [${doc.metadata[\"category\"]}] ${doc.text.take(60)}...\")\n",
"}"
],
"id": "f218f853b480b475",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== 原始查詢搜尋結果 ===\n",
" [技術] 程式碼審查規範:所有合併請求(PR)需至少一位資深工程師審查通過。審查重點:程式碼品質、測試覆蓋率、安全性。...\n",
" [休假] 特休假:婚假 8 天、喪假 3-8 天(視親等)、產假 56 天、陪產假 7 天。需附相關證明文件。...\n",
"\n",
"=== 改寫後查詢搜尋結果 ===\n",
" [休假] 年假規定:全職員工每年享有 14 天年假。到職滿一年後,每增加一年年資增加 1 天,最多 30 天。年假需提前 3 天申...\n",
" [休假] 病假規定:員工每年有 30 天全薪病假。超過 3 天需附醫生證明。住院期間不計入病假天數。...\n"
]
}
],
"execution_count": 7
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7.6 技術三:Re-ranking(重新排序)\n",
"\n",
"### 問題:向量搜尋的結果不是最佳排序\n",
"\n",
"```\n",
"向量搜尋 Top 3 結果:\n",
" 1. 密碼政策(相關度 0.85) ← 相關但不是最佳答案\n",
" 2. 遠端工作資安要求(0.82) ← 最佳答案!\n",
" 3. 報帳流程(0.78) ← 不太相關\n",
"\n",
"Re-ranking 後:\n",
" 1. 遠端工作資安要求(重排分 0.95)← 提升到第一名\n",
" 2. 密碼政策(0.70)\n",
" 3. 報帳流程(0.30) ← 降低排名\n",
"```\n",
"\n",
"### 在 Notebook 中模擬 Re-ranking\n",
"\n",
"正式環境可用 Voyage AI、Cohere 等 Re-ranking 服務。\n",
"這裡我們用 AI 來模擬 Re-ranking 的效果:"
],
"id": "eff7f28d08353847"
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:19:57.412712Z",
"start_time": "2026-03-18T15:19:57.301327Z"
}
},
"source": [
"// 用 AI 模擬 Re-ranking\n",
"fun rerank(question: String, documents: List<Document>, topN: Int = 2): List<Document> {\n",
" val docsText = documents.mapIndexed { i, doc ->\n",
" \"文件 ${i + 1}: ${doc.text}\"\n",
" }.joinToString(\"\\n\\n\")\n",
"\n",
" val ranking: String = chatClient.prompt()\n",
" .system(\"\"\"你是一個文件相關性評估助手。\n",
"根據使用者問題,將以下文件按相關性從高到低排序。\n",
"只回傳文件編號,用逗號分隔。例如:2,1,3\n",
"只回傳數字和逗號,不要加其他文字。\"\"\")\n",
" .user(\"\"\"問題:$question\n",
"\n",
"$docsText\"\"\")\n",
" .call()\n",
" .content() ?: \"\"\n",
"\n",
" // 解析排序結果\n",
" val indices = ranking.trim().split(\",\")\n",
" .mapNotNull { it.trim().toIntOrNull()?.minus(1) }\n",
" .filter { it in documents.indices }\n",
" .take(topN)\n",
"\n",
" return indices.map { documents[it] }\n",
"}\n",
"\n",
"println(\"Re-ranking 函式建立完成!\")"
],
"id": "2c426477767d7c7c",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Re-ranking 函式建立完成!\n"
]
}
],
"execution_count": 8
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:20:08.929337Z",
"start_time": "2026-03-18T15:20:07.128282Z"
}
},
"source": [
"// 測試 Re-ranking 效果\n",
"val question = \"遠端工作時要注意什麼安全問題?\"\n",
"\n",
"// Step 1: 向量搜尋(取多一些結果)\n",
"val searchResults = vectorStore.similaritySearch(\n",
" SearchRequest.builder().query(question).topK(5).build()\n",
")\n",
"\n",
"println(\"=== 向量搜尋 Top 5 ===\")\n",
"for ((i, doc) in searchResults.withIndex()) {\n",
" println(\"${i + 1}. [${doc.metadata[\"category\"]}] ${doc.text.take(50)}...\")\n",
"}\n",
"\n",
"// Step 2: Re-ranking\n",
"val reranked = rerank(question, searchResults, topN = 3)\n",
"\n",
"println(\"\\n=== Re-ranking 後 Top 3 ===\")\n",
"for ((i, doc) in reranked.withIndex()) {\n",
" println(\"${i + 1}. [${doc.metadata[\"category\"]}] ${doc.text.take(50)}...\")\n",
"}"
],
"id": "5e6748c6e7329fea",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=== 向量搜尋 Top 5 ===\n",
"1. [資安] 遠端工作資安要求:必須使用公司 VPN 連線。禁止在公共 WiFi 下處理公司資料。離開電腦必須鎖定...\n",
"2. [資安] 密碼政策:密碼每 90 天更換,長度至少 12 位,需包含大小寫字母、數字和特殊符號。禁止使用最近 ...\n",
"3. [技術] 部署流程:所有程式碼需通過 CI/CD 管線。開發環境可自由部署,測試環境需主管核准,正式環境需技術...\n",
"4. [技術] 程式碼審查規範:所有合併請求(PR)需至少一位資深工程師審查通過。審查重點:程式碼品質、測試覆蓋率、...\n",
"5. [技術] 技術選型原則:新專案優先使用 Spring Boot 3 + Java 21。AI 功能使用 Spr...\n",
"\n",
"=== Re-ranking 後 Top 3 ===\n",
"1. [資安] 遠端工作資安要求:必須使用公司 VPN 連線。禁止在公共 WiFi 下處理公司資料。離開電腦必須鎖定...\n",
"2. [資安] 密碼政策:密碼每 90 天更換,長度至少 12 位,需包含大小寫字母、數字和特殊符號。禁止使用最近 ...\n",
"3. [技術] 部署流程:所有程式碼需通過 CI/CD 管線。開發環境可自由部署,測試環境需主管核准,正式環境需技術...\n"
]
}
],
"execution_count": 9
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7.7 組合技:完整的 Advanced RAG Pipeline\n",
"\n",
"把所有技術串在一起:"
],
"id": "a74d923db332ef38"
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:20:18.003424Z",
"start_time": "2026-03-18T15:20:17.915704Z"
}
},
"source": [
"// Advanced RAG Pipeline\n",
"fun advancedRagQuery(\n",
" question: String,\n",
" conversationContext: String = \"\",\n",
" departmentFilter: String? = null\n",
"): String {\n",
" // Step 1: 查詢改寫\n",
" val rewrittenQuery = rewriteQuery(question, conversationContext)\n",
" println(\" [改寫] $question → $rewrittenQuery\")\n",
"\n",
" // Step 2: 向量搜尋(帶 Metadata 過濾)\n",
" val searchBuilder = SearchRequest.builder()\n",
" .query(rewrittenQuery)\n",
" .topK(5)\n",
"\n",
" if (departmentFilter != null) {\n",
" val b = FilterExpressionBuilder()\n",
" searchBuilder.filterExpression(b.eq(\"department\", departmentFilter).build())\n",
" println(\" [過濾] department = $departmentFilter\")\n",
" }\n",
"\n",
" val searchResults = vectorStore.similaritySearch(searchBuilder.build())\n",
" println(\" [搜尋] 找到 ${searchResults.size} 個結果\")\n",
"\n",
" // Step 3: Re-ranking\n",
" val topDocs = if (searchResults.size > 2) {\n",
" rerank(rewrittenQuery, searchResults, topN = 3)\n",
" } else {\n",
" searchResults\n",
" }\n",
" println(\" [重排] 取前 ${topDocs.size} 個最相關段落\")\n",
"\n",
" // Step 4: 組裝 Prompt 送給 AI\n",
" val context = topDocs.joinToString(\"\\n\\n\") { doc ->\n",
" \"[${doc.metadata[\"source\"]} / ${doc.metadata[\"category\"]}]\\n${doc.text}\"\n",
" }\n",
"\n",
" return chatClient.prompt()\n",
" .system(\"\"\"你是公司的智能助手,根據以下文件回答問題。\n",
"只根據文件內容回答,找不到就說不知道。用繁體中文回答。\n",
"\n",
"文件:\n",
"$context\"\"\")\n",
" .user(question)\n",
" .call()\n",
" .content() ?: \"(無回應)\"\n",
"}\n",
"\n",
"println(\"Advanced RAG Pipeline 建立完成!\")"
],
"id": "52d16cee42da2279",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Advanced RAG Pipeline 建立完成!\n"
]
}
],
"execution_count": 10
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:20:33.501979Z",
"start_time": "2026-03-18T15:20:23.768872Z"
}
},
"source": [
"// 測試 Advanced RAG\n",
"println(\"========== 測試 1:模糊問題 ==========\")\n",
"val a1 = advancedRagQuery(\n",
" question = \"那個要怎麼處理?\",\n",
" conversationContext = \"剛才在討論出差的費用\"\n",
")\n",
"println(\"\\n答:$a1\")\n",
"\n",
"println(\"\\n========== 測試 2:部門過濾 ==========\")\n",
"val a2 = advancedRagQuery(\n",
" question = \"程式碼審查的流程是什麼?\",\n",
" departmentFilter = \"工程部\"\n",
")\n",
"println(\"\\n答:$a2\")\n",
"\n",
"println(\"\\n========== 測試 3:一般問題 ==========\")\n",
"val a3 = advancedRagQuery(\n",
" question = \"升遷需要什麼條件?考核怎麼算分?\"\n",
")\n",
"println(\"\\n答:$a3\")"
],
"id": "f48d39e5c8f17f49",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"========== 測試 1:模糊問題 ==========\n",
" [改寫] 那個要怎麼處理? → 出差費用如何處理?\n",
" [搜尋] 找到 5 個結果\n",
" [重排] 取前 3 個最相關段落\n",
"\n",
"答:抱歉,我不太確定你指的是哪個問題或情況。能否提供更多詳細信息或明確的問題,以便我更好地協助你?\n",
"\n",
"========== 測試 2:部門過濾 ==========\n",
" [改寫] 程式碼審查的流程是什麼? → 程式碼審查的標準操作流程是什麼?\n",
" [過濾] department = 工程部\n",
" [搜尋] 找到 3 個結果\n",
" [重排] 取前 3 個最相關段落\n",
"\n",
"答:程式碼審查的流程包括對所有合併請求(PR)進行審查,並需至少一位資深工程師審查通過。審查的重點包括程式碼品質、測試覆蓋率和安全性。\n",
"\n",
"========== 測試 3:一般問題 ==========\n",
" [改寫] 升遷需要什麼條件?考核怎麼算分? → 升遷需要什麼條件?績效考核如何計算分數?\n",
" [搜尋] 找到 5 個結果\n",
" [重排] 取前 3 個最相關段落\n",
"\n",
"答:升遷需要連續兩次考核獲得 A 以上等級,並經過部門主管推薦、人事委員會審核以及總經理核准。\n",
"\n",
"考核分數是通過自評佔 30%、主管評佔 50%、同儕評佔 20%來計算的。\n"
]
}
],
"execution_count": 11
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7.8 終極實戰:有記憶 + RAG 的智能助手\n",
"\n",
"結合 Lesson 5 的 ChatMemory + Lesson 6-7 的 RAG,打造一個有記憶、能查知識庫的助手:"
],
"id": "5972c04707223bac"
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:20:39.304828Z",
"start_time": "2026-03-18T15:20:39.234637Z"
}
},
"source": [
"// 建立有記憶 + RAG 的 ChatClient\n",
"val memory = MessageWindowChatMemory.builder().maxMessages(15).build()\n",
"\n",
"val smartAssistant = ChatClient.builder(chatModel)\n",
" .defaultSystem(\"\"\"你是「公司智能助理」,具備以下能力:\n",
"1. 記住對話中使用者提到的資訊(如姓名、部門)\n",
"2. 根據公司知識庫回答制度和流程問題\n",
"3. 用繁體中文回答,語氣親切專業\n",
"4. 找不到答案時誠實告知,並建議聯繫相關部門\"\"\")\n",
" .defaultAdvisors(\n",
" MessageChatMemoryAdvisor.builder(memory).build(),\n",
" QuestionAnswerAdvisor.builder(vectorStore)\n",
" .searchRequest(SearchRequest.builder().topK(3).build())\n",
" .build()\n",
" )\n",
" .build()\n",
"\n",
"fun ask(message: String): String {\n",
" return smartAssistant.prompt()\n",
" .user(message)\n",
" .advisors { it.param(ChatMemory.CONVERSATION_ID, \"smart-session\") }\n",
" .call()\n",
" .content() ?: \"(無回應)\"\n",
"}\n",
"\n",
"println(\"智能助理已就緒!(ChatMemory + RAG)\")"
],
"id": "159fe716759dfcce",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"智能助理已就緒!(ChatMemory + RAG)\n"
]
}
],
"execution_count": 12
},
{
"cell_type": "code",
"metadata": {
"ExecuteTime": {
"end_time": "2026-03-18T15:20:55.029914Z",
"start_time": "2026-03-18T15:20:42.715364Z"
}
},
"source": [
"// 模擬完整的多輪對話\n",
"val conversation = listOf(\n",
" \"你好!我是小華,工程部的新人,今天剛入職\",\n",
" \"我想了解一下年假有幾天?\",\n",
" \"那病假呢?需要什麼證明嗎?\",\n",
" \"我們部門的程式碼審查規範是什麼?\",\n",
" \"幫我總結一下我剛才問的所有制度重點\"\n",
")\n",
"\n",
"for (msg in conversation) {\n",
" println(\"\\n\" + \"=\".repeat(50))\n",
" println(\"小華:$msg\")\n",
" println(\"-\".repeat(50))\n",
" println(\"助理:${ask(msg)}\")\n",
"}"
],
"id": "227f671a7560cef",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"==================================================\n",
"小華:你好!我是小華,工程部的新人,今天剛入職\n",
"--------------------------------------------------\n",
"助理:你好,小華!歡迎加入工程部!在這裡,我們有一些重要的流程和規範,希望能對你有幫助。\n",
"\n",
"1. **部署流程**:所有程式碼需要通過 CI/CD 管線。在開發環境中,你可以自由部署;但在測試環境部署時,需要主管的核准;而在正式環境中,則需要技術主管和 PM 共同核准。\n",
"\n",
"2. **技術選型原則**:新專案會優先使用 Spring Boot 3 搭配 Java 21。若專案涉及 AI 功能,將使用 Spring AI 框架。前端方面,你可以選擇使用 React 或 Vue。\n",
"\n",
"3. **程式碼審查規範**:所有的合併請求(PR)需至少一位資深工程師審查通過,重點會放在程式碼品質、測試覆蓋率和安全性。\n",
"\n",
"如果有任何其他問題或需要進一步的協助,隨時告訴我!祝你在這裡工作順利!\n",
"\n",
"==================================================\n",
"小華:我想了解一下年假有幾天?\n",
"--------------------------------------------------\n",
"助理:根據公司的年假規定,所有全職員工每年享有 14 天年假。到職滿一年後,每增加一年年資會增加 1 天年假,最多可以累積到 30 天。申請年假時,需提前 3 天提出申請。如果你有更多疑問或需要進一步的協助,建議你可以聯繫人力資源部門了解更多詳情。\n",
"\n",
"==================================================\n",
"小華:那病假呢?需要什麼證明嗎?\n",
"--------------------------------------------------\n",
"助理:根據公司的病假規定,員工每年有 30 天的全薪病假。如果病假超過 3 天,你需要提供醫生的證明。住院期間的時間不會計入病假天數。如果有其他問題或需要進一步的幫助,建議你可以聯繫人力資源部門了解更多詳情。\n",
"\n",
"==================================================\n",
"小華:我們部門的程式碼審查規範是什麼?\n",
"--------------------------------------------------\n",
"助理:在我們工程部,程式碼審查規範為:所有合併請求(PR)需至少一位資深工程師審查通過。審查的重點包括程式碼品質、測試覆蓋率和安全性。如果有其他問題或需要進一步的資訊,隨時告訴我!\n",
"\n",
"==================================================\n",
"小華:幫我總結一下我剛才問的所有制度重點\n",
"--------------------------------------------------\n",
"助理:好的,小華,以下是你剛才問到的制度重點總結:\n",
"\n",
"1. **程式碼審查規範**:所有合併請求(PR)需至少一位資深工程師審查通過,重點在於程式碼品質、測試覆蓋率和安全性。\n",
"\n",
"2. **部署流程**:所有程式碼必須通過 CI/CD 管線。在開發環境中可以自由部署,測試環境需要主管核准,正式環境則需要技術主管和 PM 雙重核准。\n",
"\n",
"3. **績效考核**:每半年進行一次,評估由自評(30%)、主管評(50%)和同儕評(20%)組成,等級分為 S/A/B/C/D,達到 B 級以上可獲得年終獎金。\n",
"\n",
"如果還有其他問題或需要更多資訊,隨時告訴我!\n"
]
}
],
"execution_count": 13
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 觀察重點\n",
"\n",
"1. **記憶**:助理記得使用者叫小華、是工程部新人\n",
"2. **RAG**:制度相關的問題都能從知識庫中找到答案\n",
"3. **多輪理解**:「那病假呢?」— 知道前面在聊休假\n",
"4. **總結能力**:能回顧整個對話中提到的所有制度"
],
"id": "43c1d815d508e7a5"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 7.9 RAG 效能調校建議\n",
"\n",
"| 調校項目 | 建議 |\n",
"|---------|------|\n",
"| **分塊大小** | 200-500 字最佳,太大搜不準、太小缺上下文 |\n",
"| **topK** | 開始用 3-5,根據效果調整 |\n",
"| **Metadata** | 越豐富越好:來源、類別、日期、部門 |\n",
"| **Embedding 模型** | `text-embedding-3-small`(快)vs `text-embedding-3-large`(準) |\n",
"| **查詢改寫** | 對模糊問題效果顯著,但增加一次 AI 呼叫成本 |\n",
"| **Re-ranking** | 正式環境用 Voyage AI / Cohere,效果最好 |\n",
"\n",
"### 選擇策略\n",
"\n",
"```\n",
"資料量小(< 100 份文件):\n",
" → Naive RAG + Metadata 過濾就夠了\n",
"\n",
"資料量中(100 - 10000):\n",
" → + 查詢改寫 + Re-ranking\n",
"\n",
"資料量大(> 10000):\n",
" → + 混合搜尋(向量 + 關鍵字)\n",
" → + 專業 Embedding 模型\n",
" → + 專業 Re-ranking 服務\n",
"```"
],
"id": "3f091ca8ae6044d3"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 本節重點回顧\n",
"\n",
"| 概念 | 重點 |\n",
"|------|------|\n",
"| **Naive vs Advanced RAG** | Advanced 在搜尋前後加入優化步驟 |\n",
"| **Metadata 過濾** | 用 FilterExpression 縮小搜尋範圍 |\n",
"| **查詢改寫** | AI 改寫模糊問題為精確查詢 |\n",
"| **Re-ranking** | 對搜尋結果重新排序,提升相關性 |\n",
"| **ChatMemory + RAG** | 結合記憶和知識庫,打造智能助手 |\n",
"| **效能調校** | 分塊大小、topK、Metadata 是關鍵 |\n",
"\n",
"### 到目前為止的學習路徑\n",
"\n",
"```\n",
"Lesson 1-2: ChatClient 基礎 + 流式輸出\n",
"Lesson 3: 提示詞工程 + 結構化輸出\n",
"Lesson 4: Function Calling(讓 AI 做事)\n",
"Lesson 5: ChatMemory(讓 AI 記住)\n",
"Lesson 6: RAG 基礎(讓 AI 讀文件)\n",
"Lesson 7: Advanced RAG(讓 AI 讀得更準) ← 你在這裡\n",
"```"
],
"id": "9a4d113e2b8b1716"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 下一步\n",
"\n",
"**Lesson 8:多模態處理 — 讓 AI 看見與聽見**\n",
"- 對應教材:5.2 多模態處理、5.3 圖像生成\n",
"- 圖片分析與圖像生成\n",
"- 語音轉文字與語音合成"
],
"id": "42ceda22325b2ff3"
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"## 參考資源\n",
"\n",
"- [Spring AI RAG 文件](https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html)\n",
"- [Spring AI Filter Expression](https://docs.spring.io/spring-ai/reference/api/vectordbs.html#metadata-filters)\n",
"- [Advanced RAG Techniques](https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html)\n",
"- 教材 8.1 Advanced RAG 概覽\n",
"- 教材 8.2 Embedding 模型優化\n",
"- 教材 8.3 Re-ranking 實戰"
],
"id": "b38cc21e4f742eab"
}
],
"metadata": {
"kernelspec": {
"display_name": "Kotlin",
"language": "kotlin",
"name": "kotlin"
},
"language_info": {
"name": "kotlin"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment