Created
March 20, 2026 23:18
-
-
Save hirosichen/a2ccff26ceff16b991eafe187f8a01ae to your computer and use it in GitHub Desktop.
Spring AI 課程 Lesson 6:RAG 基礎
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "# Lesson 6:RAG 系統 — 讓 AI 讀懂你的文件\n", | |
| "\n", | |
| "## 本節概要\n", | |
| "\n", | |
| "本課程對應教材:\n", | |
| "- **7.1** RAG 原理與實現 - 讓 AI 讀懂你的文件\n", | |
| "- **7.2** 內容向量化 - Embedding 技術詳解\n", | |
| "- **7.3** 知識來源處理 - 支援 PDF、Word、Excel\n", | |
| "- **7.5** 資料品質優化 - 提升檢索準確度\n", | |
| "\n", | |
| "學完本課你將能夠:\n", | |
| "1. 理解 RAG 的原理與完整流程\n", | |
| "2. 使用 Embedding 將文字轉為向量\n", | |
| "3. 使用 SimpleVectorStore 建立向量資料庫\n", | |
| "4. 實現從文件載入到問答的完整 RAG 系統" | |
| ], | |
| "id": "9cb095fd5ef667ed" | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.1 什麼是 RAG?\n", | |
| "\n", | |
| "### AI 的知識限制\n", | |
| "\n", | |
| "| 限制 | 範例 |\n", | |
| "|------|------|\n", | |
| "| **訓練資料有截止日** | 不知道你公司最新的產品規格 |\n", | |
| "| **沒有你的私有資料** | 不知道你公司的內部規章制度 |\n", | |
| "| **會產生幻覺** | 一本正經地給你編造不存在的資訊 |\n", | |
| "\n", | |
| "### RAG = Retrieval-Augmented Generation(檢索增強生成)\n", | |
| "\n", | |
| "**生活化比喻:開卷考試**\n", | |
| "\n", | |
| "```\n", | |
| "沒有 RAG(閉卷考試):\n", | |
| " 學生只能靠腦中記憶回答 → 記不住就亂猜\n", | |
| "\n", | |
| "有 RAG(開卷考試):\n", | |
| " 學生可以翻書找答案 → 先找到相關頁面,再根據內容回答\n", | |
| "```\n", | |
| "\n", | |
| "### RAG 完整流程\n", | |
| "\n", | |
| "```\n", | |
| "階段一:建立知識庫(離線)\n", | |
| " 文件 → 切割成段落 → Embedding 向量化 → 存入向量資料庫\n", | |
| "\n", | |
| "階段二:問答(線上)\n", | |
| " 使用者問題 → Embedding → 向量搜尋最相關的段落\n", | |
| " ↓\n", | |
| " [相關段落 + 使用者問題] → AI 模型 → 回答\n", | |
| "```\n", | |
| "\n", | |
| "```\n", | |
| "┌──────────────────────────────────────────────────┐\n", | |
| "│ RAG 流程圖 │\n", | |
| "│ │\n", | |
| "│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │\n", | |
| "│ │ PDF │ │ 切割成 │ │ Embedding │ │\n", | |
| "│ │ Word │───→│ 小段落 │───→│ 向量化 │ │\n", | |
| "│ │ 文件 │ │ │ │ │ │\n", | |
| "│ └─────────┘ └──────────┘ └──────┬───────┘ │\n", | |
| "│ │ │\n", | |
| "│ ↓ │\n", | |
| "│ ┌──────────────┐ │\n", | |
| "│ │ 向量資料庫 │ │\n", | |
| "│ └──────┬───────┘ │\n", | |
| "│ │ │\n", | |
| "│ ┌─────────┐ ┌──────────┐ │ │\n", | |
| "│ │ 使用者 │ │ Embedding │ ┌──────┴───────┐ │\n", | |
| "│ │ 問題 │───→│ 向量化 │───→│ 相似度搜尋 │ │\n", | |
| "│ └─────────┘ └──────────┘ └──────┬───────┘ │\n", | |
| "│ │ │\n", | |
| "│ ↓ │\n", | |
| "│ ┌──────────────┐ │\n", | |
| "│ ┌─────────────────────────│ 相關段落 │ │\n", | |
| "│ │ └──────────────┘ │\n", | |
| "│ ↓ │\n", | |
| "│ ┌─────────────────────────────────────────────┐ │\n", | |
| "│ │ AI 模型:根據相關段落 + 問題 → 生成回答 │ │\n", | |
| "│ └─────────────────────────────────────────────┘ │\n", | |
| "└──────────────────────────────────────────────────┘\n", | |
| "```" | |
| ], | |
| "id": "810648e620cb3259" | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "### RAG vs Fine-tuning vs Prompt Engineering\n", | |
| "\n", | |
| "| 方法 | 做法 | 優點 | 缺點 | 適用場景 |\n", | |
| "|------|------|------|------|----------|\n", | |
| "| **Prompt Engineering** | 把資料塞進提示詞 | 最簡單 | 受限於 Token 上限 | 資料少 |\n", | |
| "| **RAG** | 從知識庫檢索再回答 | 資料可隨時更新 | 需要建立向量資料庫 | 企業知識庫 |\n", | |
| "| **Fine-tuning** | 重新訓練模型 | 模型內化知識 | 成本高、更新慢 | 特定領域 |" | |
| ], | |
| "id": "e41e296b125c5762" | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.2 環境準備" | |
| ], | |
| "id": "6aa85ee9516b1aa" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:05.415968Z", | |
| "start_time": "2026-03-18T15:10:04.665570Z" | |
| } | |
| }, | |
| "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\nimport org.springframework.ai.openai.OpenAiChatModel\nimport org.springframework.ai.openai.OpenAiEmbeddingModel\nimport org.springframework.ai.openai.api.OpenAiApi\nimport org.springframework.ai.chat.client.ChatClient\nimport org.springframework.ai.openai.OpenAiChatOptions\nimport org.springframework.ai.vectorstore.SimpleVectorStore\nimport org.springframework.ai.document.Document\nimport org.springframework.ai.vectorstore.SearchRequest\nimport java.io.File\n\n// ===== API Key 設定(二擇一)=====\nval OPENAI_API_KEY = \"\"\n// ===================================\n\nfun 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\nval apiKey = loadApiKey(OPENAI_API_KEY)\nval openAiApi = OpenAiApi.builder().apiKey(apiKey).build()\nval chatModel = OpenAiChatModel.builder().openAiApi(openAiApi).build()\nval embeddingModel = OpenAiEmbeddingModel(openAiApi)\nval chatClient = ChatClient.builder(chatModel).build()\n\nprintln(\"環境準備完成!\")\nprintln(\"Chat Model: ${chatModel.javaClass.simpleName}\")\nprintln(\"Embedding Model: ${embeddingModel.javaClass.simpleName}\")", | |
| "id": "f8e8a8ec6502a7c8", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "環境準備完成!\n", | |
| "Chat Model: OpenAiChatModel\n", | |
| "Embedding Model: OpenAiEmbeddingModel\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 1 | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.3 什麼是 Embedding(向量化)?\n", | |
| "\n", | |
| "### 文字 → 數字向量\n", | |
| "\n", | |
| "```\n", | |
| "\"Spring AI 是一個框架\" → [0.12, -0.34, 0.56, ..., 0.78] (1536 維向量)\n", | |
| "\"Spring Boot 很好用\" → [0.11, -0.32, 0.55, ..., 0.77] (相似!)\n", | |
| "\"今天天氣很好\" → [0.89, 0.23, -0.67, ..., 0.12] (不相似)\n", | |
| "```\n", | |
| "\n", | |
| "**語意相近的文字,向量也會相近。** 這就是向量搜尋的基礎。\n", | |
| "\n", | |
| "| 概念 | 說明 |\n", | |
| "|------|------|\n", | |
| "| **Embedding** | 把文字轉換為固定長度的數字向量 |\n", | |
| "| **維度** | 向量的長度(OpenAI text-embedding-3-small = 1536 維) |\n", | |
| "| **相似度** | 兩個向量的距離越近,語意越相似 |\n", | |
| "| **向量資料庫** | 專門儲存和搜尋向量的資料庫 |" | |
| ], | |
| "id": "226bef8ede8cae08" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:06.246087Z", | |
| "start_time": "2026-03-18T15:10:05.427639Z" | |
| } | |
| }, | |
| "source": [ | |
| "// 體驗 Embedding:把文字轉為向量\n", | |
| "val texts = listOf(\n", | |
| " \"Spring AI 是 Spring 生態系的 AI 框架\",\n", | |
| " \"Spring Boot 讓 Java 開發更簡單\",\n", | |
| " \"今天午餐吃什麼好呢\"\n", | |
| ")\n", | |
| "\n", | |
| "for (text in texts) {\n", | |
| " val embedding = embeddingModel.embed(text)\n", | |
| " println(\"文字:$text\")\n", | |
| " println(\" 向量維度:${embedding.size}\")\n", | |
| " println(\" 前 5 個值:${embedding.take(5).map { String.format(\"%.4f\", it) }}\")\n", | |
| " println()\n", | |
| "}" | |
| ], | |
| "id": "57fb032425ac3c06", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "文字:Spring AI 是 Spring 生態系的 AI 框架\n", | |
| " 向量維度:1536\n", | |
| " 前 5 個值:[0.0052, -0.0120, -0.0079, 0.0004, 0.0058]\n", | |
| "\n", | |
| "文字:Spring Boot 讓 Java 開發更簡單\n", | |
| " 向量維度:1536\n", | |
| " 前 5 個值:[0.0037, 0.0012, 0.0091, -0.0227, -0.0151]\n", | |
| "\n", | |
| "文字:今天午餐吃什麼好呢\n", | |
| " 向量維度:1536\n", | |
| " 前 5 個值:[0.0097, 0.0012, 0.0278, -0.0225, -0.0133]\n", | |
| "\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 2 | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.4 建立向量資料庫\n", | |
| "\n", | |
| "Spring AI 提供 `SimpleVectorStore`,不需要額外安裝資料庫,適合學習和開發:" | |
| ], | |
| "id": "1f63276e327ca6c1" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:06.342244Z", | |
| "start_time": "2026-03-18T15:10:06.248912Z" | |
| } | |
| }, | |
| "source": [ | |
| "// 建立 SimpleVectorStore(記憶體內向量資料庫)\n", | |
| "val vectorStore = SimpleVectorStore.builder(embeddingModel).build()\n", | |
| "\n", | |
| "println(\"向量資料庫建立成功!\")" | |
| ], | |
| "id": "31dd5292b436c832", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "向量資料庫建立成功!\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 3 | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.5 模擬企業知識庫\n", | |
| "\n", | |
| "載入一組模擬的企業文件到向量資料庫:" | |
| ], | |
| "id": "3aaf11ad63e943eb" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:07.959242Z", | |
| "start_time": "2026-03-18T15:10:06.346593Z" | |
| } | |
| }, | |
| "source": [ | |
| "// 模擬企業內部文件\n", | |
| "val companyDocs = listOf(\n", | |
| " Document(\"公司年假規定:全職員工每年享有 14 天年假。到職滿一年後,每增加一年年資增加 1 天,最多 30 天。年假需提前 3 天申請,主管核准後生效。未休完的年假可折算薪資。\",\n", | |
| " mapOf(\"source\" to \"員工手冊\", \"chapter\" to \"休假制度\")),\n", | |
| "\n", | |
| " Document(\"病假規定:員工每年有 30 天全薪病假。超過 3 天需附醫生證明。住院期間不計入病假天數。病假期間薪資照付。\",\n", | |
| " mapOf(\"source\" to \"員工手冊\", \"chapter\" to \"休假制度\")),\n", | |
| "\n", | |
| " Document(\"報帳流程:員工出差或業務支出需在 7 天內提交報帳單。金額 5000 元以下由直屬主管核准,5000-20000 元由部門經理核准,20000 元以上需副總核准。需附上發票或收據正本。\",\n", | |
| " mapOf(\"source\" to \"財務制度\", \"chapter\" to \"報帳\")),\n", | |
| "\n", | |
| " Document(\"遠端工作政策:員工每週可申請 2 天遠端工作。需提前一天在系統上申請。遠端工作日需在上午 9 點前上線打卡,並確保即時通訊工具暢通。\",\n", | |
| " mapOf(\"source\" to \"員工手冊\", \"chapter\" to \"工作制度\")),\n", | |
| "\n", | |
| " Document(\"績效考核制度:公司每半年進行一次績效考核。考核分為自評(30%)、主管評(50%)、同儕評(20%)。考核結果分為 S、A、B、C、D 五個等級,與年終獎金和升遷掛鉤。\",\n", | |
| " mapOf(\"source\" to \"人事制度\", \"chapter\" to \"績效考核\")),\n", | |
| "\n", | |
| " Document(\"新人入職流程:新員工入職第一天需至人事部報到,領取員工證和電腦設備。第一週為入職培訓期,包含公司文化介紹、部門介紹、系統操作訓練。培訓結束後需通過測驗。\",\n", | |
| " mapOf(\"source\" to \"人事制度\", \"chapter\" to \"入職\")),\n", | |
| "\n", | |
| " Document(\"資訊安全規定:員工不得將公司資料上傳至個人雲端空間。離開座位需鎖定電腦螢幕。密碼需每 90 天更換一次,長度至少 12 位,包含大小寫字母和數字。\",\n", | |
| " mapOf(\"source\" to \"資安制度\", \"chapter\" to \"安全規範\")),\n", | |
| "\n", | |
| " Document(\"會議室預約規則:會議室需透過內部系統預約。預約時間最長 2 小時,超時需重新預約。會議開始後 15 分鐘無人使用將自動釋出。\",\n", | |
| " mapOf(\"source\" to \"行政制度\", \"chapter\" to \"會議室\"))\n", | |
| ")\n", | |
| "\n", | |
| "// 載入到向量資料庫\n", | |
| "vectorStore.add(companyDocs)\n", | |
| "\n", | |
| "println(\"已載入 ${companyDocs.size} 份企業文件到向量資料庫!\")\n", | |
| "println(\"文件來源:${companyDocs.map { it.metadata[\"source\"] }.distinct().joinToString(\"、\")}\")" | |
| ], | |
| "id": "3155ca00cbcfe8cf", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "已載入 8 份企業文件到向量資料庫!\n", | |
| "文件來源:員工手冊、財務制度、人事制度、資安制度、行政制度\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 4 | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.6 向量搜尋 — 找到最相關的文件\n", | |
| "\n", | |
| "先測試向量搜尋功能,看看能不能找到正確的文件段落:" | |
| ], | |
| "id": "fcb17825ca5f0e32" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:08.270742Z", | |
| "start_time": "2026-03-18T15:10:07.963018Z" | |
| } | |
| }, | |
| "source": [ | |
| "// 測試向量搜尋\n", | |
| "val query = \"我可以請幾天年假?\"\n", | |
| "val results = vectorStore.similaritySearch(\n", | |
| " SearchRequest.builder().query(query).topK(3).build()\n", | |
| ")\n", | |
| "\n", | |
| "println(\"查詢:$query\")\n", | |
| "println(\"\\n找到 ${results.size} 個相關段落:\\n\")\n", | |
| "\n", | |
| "for ((i, doc) in results.withIndex()) {\n", | |
| " println(\"--- 第 ${i + 1} 名 [${doc.metadata[\"source\"]} / ${doc.metadata[\"chapter\"]}] ---\")\n", | |
| " println(doc.text)\n", | |
| " println()\n", | |
| "}" | |
| ], | |
| "id": "2928c7f7d5c7828e", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "查詢:我可以請幾天年假?\n", | |
| "\n", | |
| "找到 3 個相關段落:\n", | |
| "\n", | |
| "--- 第 1 名 [員工手冊 / 休假制度] ---\n", | |
| "公司年假規定:全職員工每年享有 14 天年假。到職滿一年後,每增加一年年資增加 1 天,最多 30 天。年假需提前 3 天申請,主管核准後生效。未休完的年假可折算薪資。\n", | |
| "\n", | |
| "--- 第 2 名 [員工手冊 / 休假制度] ---\n", | |
| "病假規定:員工每年有 30 天全薪病假。超過 3 天需附醫生證明。住院期間不計入病假天數。病假期間薪資照付。\n", | |
| "\n", | |
| "--- 第 3 名 [員工手冊 / 工作制度] ---\n", | |
| "遠端工作政策:員工每週可申請 2 天遠端工作。需提前一天在系統上申請。遠端工作日需在上午 9 點前上線打卡,並確保即時通訊工具暢通。\n", | |
| "\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 5 | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:08.544092Z", | |
| "start_time": "2026-03-18T15:10:08.286577Z" | |
| } | |
| }, | |
| "source": [ | |
| "// 再測試一個不同主題的搜尋\n", | |
| "val query2 = \"報帳需要什麼文件?多少錢以上需要副總核准?\"\n", | |
| "val results2 = vectorStore.similaritySearch(\n", | |
| " SearchRequest.builder().query(query2).topK(2).build()\n", | |
| ")\n", | |
| "\n", | |
| "println(\"查詢:$query2\")\n", | |
| "println(\"\\n找到 ${results2.size} 個相關段落:\\n\")\n", | |
| "\n", | |
| "for ((i, doc) in results2.withIndex()) {\n", | |
| " println(\"--- 第 ${i + 1} 名 [${doc.metadata[\"source\"]}] ---\")\n", | |
| " println(doc.text)\n", | |
| " println()\n", | |
| "}" | |
| ], | |
| "id": "84334c498cf0b3d9", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "查詢:報帳需要什麼文件?多少錢以上需要副總核准?\n", | |
| "\n", | |
| "找到 2 個相關段落:\n", | |
| "\n", | |
| "--- 第 1 名 [財務制度] ---\n", | |
| "報帳流程:員工出差或業務支出需在 7 天內提交報帳單。金額 5000 元以下由直屬主管核准,5000-20000 元由部門經理核准,20000 元以上需副總核准。需附上發票或收據正本。\n", | |
| "\n", | |
| "--- 第 2 名 [資安制度] ---\n", | |
| "資訊安全規定:員工不得將公司資料上傳至個人雲端空間。離開座位需鎖定電腦螢幕。密碼需每 90 天更換一次,長度至少 12 位,包含大小寫字母和數字。\n", | |
| "\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 6 | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.7 完整 RAG 實現 — 手動版\n", | |
| "\n", | |
| "先用手動方式理解 RAG 的完整流程:" | |
| ], | |
| "id": "6c57e9e09dfc8352" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:08.612205Z", | |
| "start_time": "2026-03-18T15:10:08.548586Z" | |
| } | |
| }, | |
| "source": [ | |
| "// 手動 RAG:搜尋 → 組裝 Prompt → 送給 AI\n", | |
| "fun ragQuery(question: String): String {\n", | |
| " // Step 1: 向量搜尋相關文件\n", | |
| " val relevantDocs = vectorStore.similaritySearch(\n", | |
| " SearchRequest.builder().query(question).topK(3).build()\n", | |
| " )\n", | |
| "\n", | |
| " // Step 2: 組裝上下文\n", | |
| " val context = relevantDocs.joinToString(\"\\n\\n\") { doc ->\n", | |
| " \"[來源:${doc.metadata[\"source\"]} / ${doc.metadata[\"chapter\"]}]\\n${doc.text}\"\n", | |
| " }\n", | |
| "\n", | |
| " // Step 3: 送給 AI(帶上下文)\n", | |
| " return chatClient.prompt()\n", | |
| " .system(\"\"\"你是公司的 HR 助手,請根據以下公司文件回答問題。\n", | |
| "規則:\n", | |
| "1. 只根據提供的文件內容回答,不要編造資訊\n", | |
| "2. 如果文件中找不到答案,請說「這個問題我在文件中找不到相關資訊」\n", | |
| "3. 用繁體中文回答,語氣親切\n", | |
| "4. 回答末尾附上資訊來源\n", | |
| "\n", | |
| "公司文件:\n", | |
| "$context\"\"\")\n", | |
| " .user(question)\n", | |
| " .call()\n", | |
| " .content() ?: \"(無回應)\"\n", | |
| "}\n", | |
| "\n", | |
| "println(\"手動 RAG 函式建立完成!\")" | |
| ], | |
| "id": "325bc055e382c97d", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "手動 RAG 函式建立完成!\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 7 | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:23.385319Z", | |
| "start_time": "2026-03-18T15:10:08.623560Z" | |
| } | |
| }, | |
| "source": [ | |
| "// 測試手動 RAG\n", | |
| "val questions = listOf(\n", | |
| " \"我到職兩年了,可以請幾天年假?\",\n", | |
| " \"出差報帳的流程是什麼?超過兩萬元怎麼處理?\",\n", | |
| " \"我可以每天都在家工作嗎?\",\n", | |
| " \"公司的股票代碼是什麼?\" // 知識庫裡沒有的問題\n", | |
| ")\n", | |
| "\n", | |
| "for (q in questions) {\n", | |
| " println(\"\\n\" + \"=\".repeat(50))\n", | |
| " println(\"問:$q\")\n", | |
| " println(\"-\".repeat(50))\n", | |
| " println(\"答:${ragQuery(q)}\")\n", | |
| "}" | |
| ], | |
| "id": "2a16f067eb0162c6", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "\n", | |
| "==================================================\n", | |
| "問:我到職兩年了,可以請幾天年假?\n", | |
| "--------------------------------------------------\n", | |
| "答:根據公司的年假規定,到職滿一年後,每增加一年年資會增加 1 天年假。您到職兩年,所以可以享有 14 天的基本年假加上 1 天的年資增加,共計 15 天年假。\n", | |
| "\n", | |
| "資訊來源:[員工手冊 / 休假制度]\n", | |
| "\n", | |
| "==================================================\n", | |
| "問:出差報帳的流程是什麼?超過兩萬元怎麼處理?\n", | |
| "--------------------------------------------------\n", | |
| "答:出差報帳的流程是:員工在出差或有業務支出後,需在 7 天內提交報帳單。金額超過兩萬元的報帳單需要副總核准。此外,所有報帳單需附上發票或收據正本。 \n", | |
| "資訊來源:[來源:財務制度 / 報帳]\n", | |
| "\n", | |
| "==================================================\n", | |
| "問:我可以每天都在家工作嗎?\n", | |
| "--------------------------------------------------\n", | |
| "答:根據公司的遠端工作政策,員工每週可以申請 2 天遠端工作,而不是每天都在家工作。需要提前一天在系統上申請,並且在遠端工作日需在上午 9 點前上線打卡,確保即時通訊工具暢通。\n", | |
| "\n", | |
| "資訊來源:員工手冊 / 工作制度\n", | |
| "\n", | |
| "==================================================\n", | |
| "問:公司的股票代碼是什麼?\n", | |
| "--------------------------------------------------\n", | |
| "答:這個問題我在文件中找不到相關資訊。 \n", | |
| "[來源:資安制度 / 安全規範] [來源:人事制度 / 績效考核] [來源:員工手冊 / 工作制度]\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 8 | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "### 觀察要點\n", | |
| "\n", | |
| "1. **有相關文件的問題** → AI 根據文件準確回答\n", | |
| "2. **沒有相關文件的問題** → AI 誠實說「找不到」(而非編造)\n", | |
| "3. **回答包含來源** → 讓使用者可以驗證資訊" | |
| ], | |
| "id": "c7d7a76812ebc632" | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.8 使用 QuestionAnswerAdvisor — Spring AI 內建 RAG\n", | |
| "\n", | |
| "Spring AI 提供 `QuestionAnswerAdvisor`,自動完成搜尋 + 組裝的流程:" | |
| ], | |
| "id": "f118a484bad6e9f5" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:23.448225Z", | |
| "start_time": "2026-03-18T15:10:23.395565Z" | |
| } | |
| }, | |
| "source": "import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor\n\n// 使用 QuestionAnswerAdvisor(Spring AI 內建 RAG)\nval ragClient = ChatClient.builder(chatModel)\n .defaultSystem(\"\"\"你是公司的 HR 助手,根據檢索到的文件回答問題。\n用繁體中文回答,語氣親切。如果找不到相關資訊,請誠實告知。\"\"\")\n .defaultAdvisors(\n QuestionAnswerAdvisor.builder(vectorStore)\n .searchRequest(SearchRequest.builder().topK(3).build())\n .build()\n )\n .build()\n\nprintln(\"RAG ChatClient 建立成功(使用 QuestionAnswerAdvisor)!\")", | |
| "id": "51bd22f6e07bb06b", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "RAG ChatClient 建立成功(使用 QuestionAnswerAdvisor)!\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 9 | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:30.455682Z", | |
| "start_time": "2026-03-18T15:10:23.451951Z" | |
| } | |
| }, | |
| "source": [ | |
| "// 使用 RAG ChatClient 問答 — 一行搞定!\n", | |
| "val ragQuestions = listOf(\n", | |
| " \"新人入職第一天要做什麼?\",\n", | |
| " \"密碼多久需要換一次?有什麼要求?\",\n", | |
| " \"績效考核怎麼評分的?\"\n", | |
| ")\n", | |
| "\n", | |
| "for (q in ragQuestions) {\n", | |
| " val answer: String = ragClient.prompt()\n", | |
| " .user(q)\n", | |
| " .call()\n", | |
| " .content() ?: \"(無回應)\"\n", | |
| "\n", | |
| " println(\"\\n\" + \"=\".repeat(50))\n", | |
| " println(\"問:$q\")\n", | |
| " println(\"-\".repeat(50))\n", | |
| " println(\"答:$answer\")\n", | |
| "}" | |
| ], | |
| "id": "f2805b4f3cfc762c", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "\n", | |
| "==================================================\n", | |
| "問:新人入職第一天要做什麼?\n", | |
| "--------------------------------------------------\n", | |
| "答:新人入職的第一天需要到人事部報到,並領取員工證和電腦設備。這是他們第一週入職培訓期的一部分,這段時間會包括公司文化介紹、部門介紹以及系統操作訓練。希望這些資訊對你有所幫助!如果有其他問題,隨時可以問喔。\n", | |
| "\n", | |
| "==================================================\n", | |
| "問:密碼多久需要換一次?有什麼要求?\n", | |
| "--------------------------------------------------\n", | |
| "答:密碼需要每 90 天更換一次,並且必須至少有 12 位字符,包含大小寫字母和數字。希望這對你有幫助!如果還有其他問題,隨時告訴我哦!\n", | |
| "\n", | |
| "==================================================\n", | |
| "問:績效考核怎麼評分的?\n", | |
| "--------------------------------------------------\n", | |
| "答:績效考核的評分是根據三個部分來進行的:自評佔 30%,主管評佔 50%,同儕評佔 20%。考核結果會分為 S、A、B、C、D 五個等級,並且這些結果會影響到年終獎金和升遷。希望這能幫助你了解公司的績效考核制度!如果還有其他問題,隨時可以問哦!\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 10 | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "### 手動 RAG vs QuestionAnswerAdvisor\n", | |
| "\n", | |
| "| | 手動 RAG | QuestionAnswerAdvisor |\n", | |
| "|--|---------|---------------------|\n", | |
| "| **程式碼量** | 多(自己搜尋、組裝) | 少(一行設定) |\n", | |
| "| **靈活性** | 完全自訂 | 使用預設模板 |\n", | |
| "| **適用場景** | 需要自訂 RAG 邏輯 | 標準知識庫問答 |\n", | |
| "| **與 ChatClient 整合** | 手動 | 自動(Advisor 機制) |" | |
| ], | |
| "id": "49e65a65b27706f1" | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.9 文本分塊策略\n", | |
| "\n", | |
| "在實際應用中,長文件需要先切割成適當大小的段落:\n", | |
| "\n", | |
| "### 為什麼需要分塊?\n", | |
| "\n", | |
| "```\n", | |
| "一份 100 頁的 PDF...\n", | |
| " ❌ 整份存入向量資料庫 → 搜尋不精確\n", | |
| " ✅ 切成小段落存入 → 精確找到相關段落\n", | |
| "```\n", | |
| "\n", | |
| "### 分塊策略比較\n", | |
| "\n", | |
| "| 策略 | 說明 | 適用場景 |\n", | |
| "|------|------|----------|\n", | |
| "| **固定大小** | 每 500 字切一塊 | 通用 |\n", | |
| "| **按段落** | 以段落為單位 | 結構化文件 |\n", | |
| "| **按語意** | AI 自動判斷語意邊界 | 品質最高但最慢 |\n", | |
| "| **滑動視窗** | 固定大小 + 重疊 | 避免斷句 |\n", | |
| "\n", | |
| "### Spring AI 的 TextSplitter\n", | |
| "\n", | |
| "```java\n", | |
| "// Spring Boot 中的用法\n", | |
| "var splitter = new TokenTextSplitter();\n", | |
| "// 預設:每段 800 token,重疊 350 token\n", | |
| "\n", | |
| "List<Document> chunks = splitter.split(documents);\n", | |
| "vectorStore.add(chunks);\n", | |
| "```" | |
| ], | |
| "id": "faf81c8bb634e4a9" | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 6.10 練習:建立你自己的知識庫\n", | |
| "\n", | |
| "修改以下文件內容,建立你自己的 RAG 知識庫:" | |
| ], | |
| "id": "1154a084ebf29ec9" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "metadata": { | |
| "ExecuteTime": { | |
| "end_time": "2026-03-18T15:10:42.238239Z", | |
| "start_time": "2026-03-18T15:10:30.460718Z" | |
| } | |
| }, | |
| "source": "// === 練習:替換為你自己的知識內容 ===\n\nval myDocs = listOf(\n Document(\"Spring AI 是 Spring 生態系中的 AI 整合框架,提供統一的 API 存取各種 AI 模型。支援 OpenAI、Anthropic Claude、Ollama 等多種模型。核心功能包含 ChatClient、Embedding、RAG、Tool Calling。\",\n mapOf(\"source\" to \"Spring AI 簡介\")),\n\n Document(\"ChatClient 是 Spring AI 的高階 API,使用 Fluent 風格的鏈式呼叫。支援同步呼叫(call)和流式輸出(stream)。可以透過 Advisor 機制擴展功能,如 ChatMemory 和 RAG。\",\n mapOf(\"source\" to \"ChatClient 教學\")),\n\n Document(\"Tool Calling 讓 AI 可以呼叫你定義的 Java/Kotlin 函式。使用 @Tool 註解標記工具方法,@ToolParam 描述參數。AI 會自動判斷何時需要呼叫工具,並傳入正確的參數。\",\n mapOf(\"source\" to \"Tool Calling 教學\")),\n\n Document(\"RAG(Retrieval-Augmented Generation)是檢索增強生成技術。流程為:文件 → Embedding 向量化 → 存入向量資料庫 → 使用者提問時搜尋相關段落 → 送給 AI 生成回答。\",\n mapOf(\"source\" to \"RAG 教學\"))\n)\n\nval myVectorStore = SimpleVectorStore.builder(embeddingModel).build()\nmyVectorStore.add(myDocs)\n\nval myRagClient = ChatClient.builder(chatModel)\n .defaultSystem(\"你是 Spring AI 課程助教,根據教材內容回答。用繁體中文回答。\")\n .defaultAdvisors(\n QuestionAnswerAdvisor.builder(myVectorStore)\n .searchRequest(SearchRequest.builder().topK(2).build())\n .build()\n )\n .build()\n\n// 測試\nval testQuestions = listOf(\n \"ChatClient 支援哪些呼叫方式?\",\n \"怎麼讓 AI 呼叫我自己寫的函式?\",\n \"RAG 的完整流程是什麼?\"\n)\n\nfor (q in testQuestions) {\n val a: String = myRagClient.prompt().user(q).call().content() ?: \"\"\n println(\"問:$q\")\n println(\"答:$a\\n\")\n}", | |
| "id": "b9a987bb2e487b26", | |
| "outputs": [ | |
| { | |
| "name": "stdout", | |
| "output_type": "stream", | |
| "text": [ | |
| "問:ChatClient 支援哪些呼叫方式?\n", | |
| "答:ChatClient 支援同步呼叫(call)和流式輸出(stream)兩種呼叫方式。\n", | |
| "\n", | |
| "問:怎麼讓 AI 呼叫我自己寫的函式?\n", | |
| "答:要讓 AI 呼叫你自己寫的函式,你可以使用 Tool Calling 功能。在 Spring AI 框架中,使用 `@Tool` 註解來標記你希望 AI 呼叫的方法,並且使用 `@ToolParam` 註解來描述方法的參數。這樣,AI 就可以自動判斷何時需要呼叫你的工具,並且傳入正確的參數。如果你需要更多的詳細操作步驟或範例,請參考相關的 Spring AI 文件或教材。\n", | |
| "\n", | |
| "問:RAG 的完整流程是什麼?\n", | |
| "答:RAG(Retrieval-Augmented Generation)的完整流程是:首先,將文件轉換為 Embedding 向量,然後將這些向量存入向量資料庫。當使用者提出問題時,系統會搜尋相關的段落,再將這些段落送給 AI 生成回答。\n", | |
| "\n" | |
| ] | |
| } | |
| ], | |
| "execution_count": 11 | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 本節重點回顧\n", | |
| "\n", | |
| "| 概念 | 重點 |\n", | |
| "|------|------|\n", | |
| "| **RAG** | 檢索增強生成:先搜尋相關文件,再讓 AI 根據文件回答 |\n", | |
| "| **Embedding** | 把文字轉為數字向量,語意相近的向量距離也近 |\n", | |
| "| **VectorStore** | 儲存向量的資料庫,支援相似度搜尋 |\n", | |
| "| **SimpleVectorStore** | Spring AI 內建的記憶體向量資料庫,適合開發 |\n", | |
| "| **SearchRequest** | 控制搜尋參數(topK = 回傳幾個結果) |\n", | |
| "| **Document** | Spring AI 的文件物件,包含文字和 metadata |\n", | |
| "| **QuestionAnswerAdvisor** | Spring AI 內建 RAG Advisor,自動搜尋 + 組裝 |\n", | |
| "| **文本分塊** | 長文件切成小段落,提升搜尋精確度 |\n", | |
| "\n", | |
| "### 正式環境的向量資料庫\n", | |
| "\n", | |
| "```\n", | |
| "開發測試 → SimpleVectorStore(記憶體內)\n", | |
| "中小型正式環境 → PGVector(PostgreSQL 擴充套件)\n", | |
| "大型正式環境 → Milvus / Chroma / Weaviate\n", | |
| "圖資料庫整合 → Neo4j Vector\n", | |
| "```" | |
| ], | |
| "id": "208e9922ae307ca0" | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 下一步\n", | |
| "\n", | |
| "**恭喜完成 Lesson 0 ~ 6 的基礎課程!**\n", | |
| "\n", | |
| "你已經掌握了 Spring AI 的核心功能:\n", | |
| "\n", | |
| "| Lesson | 內容 |\n", | |
| "|--------|------|\n", | |
| "| 0 | 環境安裝 |\n", | |
| "| 1 | ChatClient / ChatModel 基礎 |\n", | |
| "| 2 | 流式輸出與模型參數 |\n", | |
| "| 3 | 提示詞工程與結構化輸出 |\n", | |
| "| 4 | Function Calling |\n", | |
| "| 5 | ChatMemory 對話記憶 |\n", | |
| "| 6 | RAG 知識庫問答 |\n", | |
| "\n", | |
| "進階課程方向:\n", | |
| "- **Lesson 7**:進階 RAG — Re-ranking、Embedding 優化\n", | |
| "- **Lesson 8**:多模態 — 圖片分析、語音處理\n", | |
| "- **Lesson 9**:Spring Boot 完整專案 — 從 Notebook 到正式應用" | |
| ], | |
| "id": "187aef89dbc23db9" | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "## 參考資源\n", | |
| "\n", | |
| "- [Spring AI RAG 文件](https://docs.spring.io/spring-ai/reference/api/rag.html)\n", | |
| "- [Spring AI Embedding 文件](https://docs.spring.io/spring-ai/reference/api/embeddings.html)\n", | |
| "- [Spring AI VectorStore 文件](https://docs.spring.io/spring-ai/reference/api/vectordbs.html)\n", | |
| "- 教材 7.1 RAG 原理與實現\n", | |
| "- 教材 7.2 內容向量化\n", | |
| "- 教材 7.3 知識來源處理" | |
| ], | |
| "id": "8aa40522377ac653" | |
| } | |
| ], | |
| "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