Key Findings
  • 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儲存向量的資料庫,支援語意搜尋
DocumentSpring AI 的文件物件(文字 + metadata)
SearchRequest配置搜尋參數(topK、相似度閾值)
QuestionAnswerAdvisor一行程式碼實現完整 RAG 流程
文件切割長文件切成小段落,TokenTextSplitter(800 tokens + 350 重疊)

課程回顧:Lesson 0~6 完整技術棧

到這裡,你已經掌握了 Spring AI 的六大核心能力:

  1. Lesson 0 — 環境設定(JDK + IntelliJ + Kotlin Notebook)
  2. Lesson 1 — 基礎呼叫(ChatClient + ChatModel)
  3. Lesson 2 — 串流輸出(Streaming + Temperature + 模型切換)
  4. Lesson 3 — Prompt 工程(PromptTemplate + 結構化輸出)
  5. Lesson 4 — 工具呼叫(Function Calling + @Tool)
  6. Lesson 5 — 對話記憶(ChatMemory + Advisor)
  7. Lesson 6 — RAG(Embedding + VectorStore + 語意搜尋)

下一步:Lesson 7 — 進階 RAG

基礎 RAG 已經很強大,但進階技巧能讓它更精確:

  • Re-ranking — 對初步搜尋結果進行二次排序,提升相關性
  • Hybrid Search — 向量搜尋 + 關鍵字搜尋的組合
  • Metadata Filtering — 根據文件屬性(部門、日期、類型)預先過濾

掌握基礎 RAG,繼續前進!

基礎 RAG 已上手,下一步學習 Metadata 過濾、Query Rewriting 與 Re-ranking,大幅提升檢索精準度。

前往 Lesson 7:進階 RAG →