- RAG(檢索增強生成)[4]讓 AI 在回答問題前先「翻閱參考資料」——像是開卷考試,AI 不再只靠訓練時的記憶,還能引用你提供的最新文件
- Embedding 是 RAG 的基礎:把文字轉換為 1536 維的數學向量,語意相近的文字在向量空間中距離相近,實現「理解意思」而非「比對關鍵字」的搜尋
- Spring AI 的 QuestionAnswerAdvisor 只需一行程式碼就能實現完整的 RAG 流程——自動搜尋、自動組裝上下文、自動回答
- 正式環境選擇向量資料庫:開發用 SimpleVectorStore,中小規模用 PGVector,大規模用 Milvus / Chroma / Weaviate
什麼是 RAG?
AI 模型有三個天生的限制:知識截止日(不知道訓練後發生的事)、沒有私有資料(不知道你的公司規章、產品文件)、幻覺問題(編造看似合理但錯誤的答案)。
RAG(Retrieval-Augmented Generation,檢索增強生成)[4]的核心思想很簡單——讓 AI 在回答問題之前,先搜尋相關文件。就像學生考試時可以翻課本一樣:
═══ RAG 完整流程 ═══
【離線階段:建立知識庫】
文件 → 切割成小段落 → 轉換為向量(Embedding) → 存入向量資料庫
【線上階段:智能問答】
使用者提問
│
▼
問題 → 轉換為向量 → 在向量資料庫中搜尋最相關的段落
│
▼
「相關段落 + 原始問題」一起送給 AI
│
▼
AI 根據參考資料生成精確回答
RAG 與其他 AI 增強方式的比較:
| 方式 | 原理 | 成本 | 適用場景 |
|---|---|---|---|
| RAG | 檢索外部文件輔助回答 | 低 | 知識庫問答、企業文件 |
| Fine-tuning | 重新訓練模型權重 | 高 | 特定領域風格、專業術語 |
| Prompt Engineering | 在提示詞中附加資訊 | 最低 | 少量上下文、簡單指引 |
Step 1:環境準備
開啟課程專案的 Lesson6/Lesson6_RAG.ipynb。這堂課引入了向量相關的依賴:
@file:DependsOn("org.springframework.ai:spring-ai-openai:1.0.0")
@file:DependsOn("org.springframework.ai:spring-ai-client-chat:1.0.0")
@file:DependsOn("org.springframework.ai:spring-ai-vector-store:1.0.0")
@file:DependsOn("org.springframework.ai:spring-ai-advisors-vector-store:1.0.0")
@file:DependsOn("org.slf4j:slf4j-simple:2.0.16")
import org.springframework.ai.openai.OpenAiChatModel
import org.springframework.ai.openai.OpenAiEmbeddingModel
import org.springframework.ai.openai.api.OpenAiApi
import org.springframework.ai.chat.client.ChatClient
val apiKey = System.getenv("OPENAI_API_KEY")
?: error("請先設定 OPENAI_API_KEY(參考 Lesson 0)")
val openAiApi = OpenAiApi.builder().apiKey(apiKey).build()
val chatModel = OpenAiChatModel.builder().openAiApi(openAiApi).build()
val embeddingModel = OpenAiEmbeddingModel.builder().openAiApi(openAiApi).build()
val chatClient = ChatClient.builder(chatModel).build()
println("✓ 環境準備完成(含 Embedding Model)")
新增的關鍵依賴:
- spring-ai-vector-store — 向量資料庫抽象層(SimpleVectorStore、Document、SearchRequest)
- spring-ai-advisors-vector-store — QuestionAnswerAdvisor(一行程式碼的 RAG)
Step 2:理解 Embedding(向量嵌入)
Embedding[2] 是 RAG 的基礎——把一段文字轉換為一個高維數學向量(OpenAI 的 text-embedding-3-small 產生 1536 維的向量)。語意相近的文字,向量距離也相近:
val texts = listOf(
"Spring AI 是一個 Java 框架,用於建構 AI 應用",
"Spring Boot 整合人工智慧的開發工具",
"今天天氣真好,適合去公園散步"
)
texts.forEach { text ->
val embedding = embeddingModel.embed(text)
println("文字: ${text.take(20)}...")
println("向量維度: ${embedding.size}")
println("前 5 個數值: ${embedding.take(5).map { "%.4f".format(it) }}")
println()
}
預期結果:前兩段文字(都跟 Spring AI 相關)的向量會非常接近,而第三段(天氣散步)的向量距離會很遠。這就是語意搜尋的原理——不是比對關鍵字,而是理解意思。
Step 3:建立向量資料庫
Spring AI 的 SimpleVectorStore[3] 是一個記憶體內的向量資料庫,適合開發和學習:
import org.springframework.ai.vectorstore.SimpleVectorStore
val vectorStore = SimpleVectorStore.builder(embeddingModel).build()
println("✓ 向量資料庫已建立")
Step 4:載入企業知識庫
模擬一個企業人事知識庫,包含各種公司規章制度:
import org.springframework.ai.document.Document
val documents = listOf(
Document(
"年假規定:到職滿一年者享有 7 天特休假,滿三年者 10 天,滿五年者 14 天,滿十年者 20 天。" +
"特休假應於當年度使用完畢,未休完可折算工資。請假需提前三個工作天申請。",
mapOf("source" to "員工手冊", "chapter" to "休假制度")
),
Document(
"病假規定:全年病假不超過 30 天,需附醫療證明。連續病假超過 3 天需提供診斷書。" +
"病假期間薪資照發(半薪),超過 30 天部分為留職停薪。",
mapOf("source" to "員工手冊", "chapter" to "休假制度")
),
Document(
"出差費用報銷:國內出差每日住宿上限 3,000 元,餐費上限 800 元。" +
"國際出差依目的地城市標準核銷。所有費用需於出差結束後 14 天內檢附收據報銷。",
mapOf("source" to "財務規章", "chapter" to "費用報銷")
),
Document(
"遠距工作政策:員工每週可申請最多 2 天遠距工作。需提前一天向主管申請。" +
"遠距工作期間需保持通訊軟體在線,並參加所有排定的視訊會議。核心工作時間為 10:00-16:00。",
mapOf("source" to "員工手冊", "chapter" to "工作模式")
),
Document(
"績效考核:每年進行兩次績效評估(6月和12月)。評分分為五個等級:" +
"卓越(A)、優良(B)、達標(C)、需改進(D)、不及格(E)。連續兩次 D 或一次 E 將進入績效改善計畫。",
mapOf("source" to "人資規章", "chapter" to "績效管理")
),
Document(
"新人報到流程:報到日需攜帶身分證、學歷證明、銀行帳戶資料。第一天由 HR 帶領完成" +
"系統帳號開通、門禁卡領取、座位分配。第一週為新人訓練週,需完成公司文化、資安規範等必修課程。",
mapOf("source" to "員工手冊", "chapter" to "入職流程")
),
Document(
"資安規範:公司電腦禁止安裝未經 IT 部門核准的軟體。所有密碼需每 90 天更換一次," +
"長度至少 12 字元,包含大小寫字母、數字和特殊符號。嚴禁使用公司設備存取非工作相關的雲端儲存服務。",
mapOf("source" to "資安政策", "chapter" to "資訊安全")
),
Document(
"會議室預約:透過內部系統預約,最長可預約 2 小時。超過 30 分鐘未到自動釋放。" +
"大型會議室(10人以上)需提前 3 天預約,需部門主管核准。",
mapOf("source" to "行政規章", "chapter" to "辦公室管理")
)
)
// 載入到向量資料庫
vectorStore.add(documents)
println("✓ 已載入 ${documents.size} 篇企業文件到知識庫")
Step 5:向量搜尋——語意比對
用自然語言查詢,向量資料庫會回傳語意最相關的文件:
import org.springframework.ai.vectorstore.SearchRequest
val results = vectorStore.similaritySearch(
SearchRequest.builder()
.query("我可以請幾天假?")
.topK(3) // 回傳最相關的 3 篇文件
.build()
)
results.forEachIndexed { i, doc ->
println("【第 ${i + 1} 名】")
println("來源: ${doc.metadata["source"]} — ${doc.metadata["chapter"]}")
println("內容: ${doc.text.take(80)}...")
println()
}
預期輸出:
【第 1 名】
來源: 員工手冊 — 休假制度
內容: 年假規定:到職滿一年者享有 7 天特休假,滿三年者 10 天,滿五年者 14 天...
【第 2 名】
來源: 員工手冊 — 休假制度
內容: 病假規定:全年病假不超過 30 天,需附醫療證明...
【第 3 名】
來源: 員工手冊 — 工作模式
內容: 遠距工作政策:員工每週可申請最多 2 天遠距工作...
注意:查詢「我可以請幾天假」,文件中沒有「請假」兩個字,但向量搜尋理解了「請假」和「特休假」、「病假」的語意關聯。這就是語意搜尋比關鍵字搜尋強大的原因。
Step 6:手動實作 RAG
理解 RAG 完整流程的最好方式是手動實作一次:
fun ragQuery(question: String): String {
// Step 1: 搜尋最相關的文件
val relevantDocs = vectorStore.similaritySearch(
SearchRequest.builder().query(question).topK(3).build()
)
// Step 2: 組裝上下文(含來源資訊)
val context = relevantDocs.joinToString("\n\n") { doc ->
"【來源: ${doc.metadata["source"]} — ${doc.metadata["chapter"]}】\n${doc.text}"
}
// Step 3: 送給 AI 回答
return chatClient.prompt()
.system("""你是一位企業人事助手,根據提供的參考資料回答問題。
規則:
1. 只根據參考資料回答,不要編造
2. 引用資料來源
3. 如果資料中找不到答案,誠實說明""")
.user("""
參考資料:
$context
問題:$question
""".trimIndent())
.call().content() ?: "(無回應)"
}
// 測試各種問題
val questions = listOf(
"新人到職第一天需要帶什麼?",
"遠距工作有什麼限制?",
"密碼多久要換一次?要多長?",
"公司的股票代碼是什麼?" // 知識庫中沒有的資訊
)
questions.forEach { q ->
println("❓ $q")
println("💬 ${ragQuery(q)}")
println()
}
預期輸出(第四題):
❓ 公司的股票代碼是什麼?
💬 抱歉,在目前提供的參考資料中找不到公司股票代碼的相關資訊。
建議您聯繫財務部門或查閱公司官方網站取得此資訊。
AI 在找不到資料時誠實說明而非編造——這正是 RAG 系統控制幻覺的關鍵設計。
Step 7:QuestionAnswerAdvisor——一行程式碼的 RAG
手動實作幫助理解原理,但正式開發時,Spring AI 的 QuestionAnswerAdvisor[1] 可以用一行程式碼完成同樣的事:
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor
val ragClient = ChatClient.builder(chatModel)
.defaultSystem("你是一位企業人事助手,根據提供的參考資料回答問題。找不到答案時誠實說明。")
.defaultAdvisors(
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().topK(3).build())
.build()
)
.build()
// 使用方式和一般 ChatClient 完全相同!
val answer = ragClient.prompt()
.user("員工績效考核是怎麼進行的?")
.call().content()
println(answer)
預期輸出:
根據公司人資規章,績效考核每年進行兩次(6月和12月),
評分分為五個等級:卓越(A)、優良(B)、達標(C)、需改進(D)、不及格(E)。
需要注意的是,連續兩次 D 或一次 E 將進入績效改善計畫。
(來源:人資規章 — 績效管理)
QuestionAnswerAdvisor 在背後自動完成了:搜尋向量資料庫 → 取得相關文件 → 組裝上下文 → 送給 AI。對使用者完全透明。
兩種方式的比較:
| 方式 | 程式碼量 | 彈性 | 適用場景 |
|---|---|---|---|
| 手動 RAG | 較多 | 完全自訂搜尋邏輯、Prompt 模板、後處理 | 需要自訂流程的進階場景 |
| QuestionAnswerAdvisor | 一行 | 使用預設模板,可配置搜尋參數 | 標準 RAG、快速上線 |
Step 8:文件切割策略
真實世界的文件通常很長(幾千甚至幾萬字),直接存入向量資料庫會有問題:向量只能代表一段文字的「整體語意」,太長的文字會失去細節。因此需要切割。
| 策略 | 方式 | 優點 | 缺點 |
|---|---|---|---|
| 固定長度切割 | 每 N 個 Token 切一段 | 實作簡單 | 可能切斷句子 |
| 段落切割 | 按段落/章節分割 | 保留語意完整性 | 段落大小不一 |
| 語意切割 | 偵測主題變化點分割 | 語意最準確 | 計算成本高 |
| 滑動視窗 | 固定長度 + 重疊區段 | 兼顧效率與連貫性 | 重疊導致儲存增加 |
Spring AI 內建的 TokenTextSplitter 使用滑動視窗策略:
// Spring AI 預設設定
// 每個 chunk: 800 tokens
// 重疊區段: 350 tokens(確保跨 chunk 的資訊不會遺失)
import org.springframework.ai.transformer.splitter.TokenTextSplitter
val splitter = TokenTextSplitter()
val longDocument = Document("這是一篇很長的文件..." /* 數千字 */)
val chunks = splitter.split(longDocument)
// 產生多個小段落,每段約 800 tokens
向量資料庫選型指南
根據專案規模選擇合適的向量資料庫[3]:
| 資料庫 | 適用規模 | 特點 |
|---|---|---|
| SimpleVectorStore | 開發 / 測試 | 記憶體內,重啟即消失,零設定 |
| PGVector | 中小規模(< 100 萬筆) | PostgreSQL 擴充,熟悉的 SQL 生態 |
| Milvus | 大規模(百萬~十億筆) | 分散式、高效能、GPU 加速 |
| Chroma | 中小規模 | Python 生態友好、簡單易用 |
| Weaviate | 大規模 | 內建混合搜尋(向量 + 關鍵字) |
| Neo4j Vector | 需要關係圖譜 | 結合圖資料庫與向量搜尋 |
本系列建議: 開發階段用 SimpleVectorStore,到 Lesson 10(Spring Boot 整合)時切換為 PGVector。切換只需替換 VectorStore 實作,上層程式碼完全不變。
本課重點回顧
| 概念 | 重點 |
|---|---|
| RAG | 檢索增強生成,讓 AI「翻課本」回答問題 |
| Embedding | 文字→1536維向量,語意相近=距離相近 |
| VectorStore | 儲存向量的資料庫,支援語意搜尋 |
| Document | Spring AI 的文件物件(文字 + metadata) |
| SearchRequest | 配置搜尋參數(topK、相似度閾值) |
| QuestionAnswerAdvisor | 一行程式碼實現完整 RAG 流程 |
| 文件切割 | 長文件切成小段落,TokenTextSplitter(800 tokens + 350 重疊) |
課程回顧:Lesson 0~6 完整技術棧
到這裡,你已經掌握了 Spring AI 的六大核心能力:
- Lesson 0 — 環境設定(JDK + IntelliJ + Kotlin Notebook)
- Lesson 1 — 基礎呼叫(ChatClient + ChatModel)
- Lesson 2 — 串流輸出(Streaming + Temperature + 模型切換)
- Lesson 3 — Prompt 工程(PromptTemplate + 結構化輸出)
- Lesson 4 — 工具呼叫(Function Calling + @Tool)
- Lesson 5 — 對話記憶(ChatMemory + Advisor)
- Lesson 6 — RAG(Embedding + VectorStore + 語意搜尋)
下一步:Lesson 7 — 進階 RAG
基礎 RAG 已經很強大,但進階技巧能讓它更精確:
- Re-ranking — 對初步搜尋結果進行二次排序,提升相關性
- Hybrid Search — 向量搜尋 + 關鍵字搜尋的組合
- Metadata Filtering — 根據文件屬性(部門、日期、類型)預先過濾
掌握基礎 RAG,繼續前進!
基礎 RAG 已上手,下一步學習 Metadata 過濾、Query Rewriting 與 Re-ranking,大幅提升檢索精準度。



