Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save hirosichen/a2ccff26ceff16b991eafe187f8a01ae to your computer and use it in GitHub Desktop.
Spring AI 課程 Lesson 6:RAG 基礎
Display the source blob
Display the rendered blob
Raw
{
"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