- Published on
第一次學 Kotlin Koog AI 就上手 Day 31:RAG:讓 AI 擁有知識庫的超能力
上一篇文章中我們學習了 Embeddings 技術,它能將文字轉換為向量,並透過相似度比對找到語意相近的內容。今天我們要將這項技術實際應用到一個強大的 AI 架構中:RAG(Retrieval-Augmented Generation,檢索增強產生)
想像你正在為公司開發一個 AI 客服系統。客戶經常詢問產品資訊、退換貨政策、保固條款等問題。傳統的 ChatGPT 雖然聰明,但它的知識有時效性限制,無法掌握最新的產品資訊或公司政策變更。更糟的是,它可能會「幻覺」(產生不準確的資訊),給客戶錯誤的答案
RAG 正是解決這個問題的完美方案。它就像為 AI 配備了一個隨時可查詢的知識庫,確保回答都有可靠的依據
什麼是 RAG
RAG(Retrieval-Augmented Generation)可以用一個簡單的比喻來理解:想像 AI 是一位學生,而 RAG 就是為這位學生準備了一套完整的參考書和搜尋系統
當學生遇到問題時,他不會憑空猜測答案,而是先在參考書中搜尋相關資料,找到最相關的內容後,再結合這些資料給出準確的回答
RAG 的運作流程很直觀
- 檢索(Retrieval):根據使用者的問題,從文件知識庫中找出最相關的內容
- 增強(Augmented):將檢索到的文件內容加入到 AI 的提示中
- 產生(Generation):AI 基於這些可靠的文件內容產生回答
這樣的設計帶來三個重要優勢:資訊的時效性(隨時更新知識庫)、回應的準確性(基於實際文件)、以及可追蹤性(知道答案來源)
重要補充:在實際的 RAG 系統中,文件通常不會整份儲存,而是先經過**分塊(Chunking)**處理,將長文件切割成較小的語意片段。這樣可以提升檢索精確度,確保 AI 獲得最相關的資訊片段。詳細的分塊概念和實作方式可以參考 Day 30 的說明
Koog RAG 架構
在 Koog AI 框架中,RAG 系統建構在我們之前學習的 Embeddings 技術之上。核心元件包括
- RankedDocumentStorage:負責儲存和檢索文件的介面,提供相似度排序功能
- EmbeddingBasedDocumentStorage:基於向量嵌入的文件儲存實作
- JVMTextDocumentEmbedder:將文件轉換為向量嵌入的工具
- InMemoryVectorStorage:記憶體中的向量儲存系統
這些元件協同工作,構成完整的 RAG 系統
簡單的 FAQ RAG 範例
讓我們從一個實際例子開始。假設我們要為電商平台建立客服 FAQ 系統
suspend fun main() {
// 建立 Embedder 和文件儲存系統
val embedder = LLMEmbedder(OllamaClient(), OllamaEmbeddingModels.NOMIC_EMBED_TEXT)
val documentEmbedder = JVMTextDocumentEmbedder(embedder)
val rankedDocumentStorage = EmbeddingBasedDocumentStorage(documentEmbedder, InMemoryVectorStorage())
// 儲存 FAQ 文件
// 建立示範文件內容(實際應用中會從檔案系統讀取)
val faqData = listOf(
"./faq/shipping.txt" to "商品包裏出貨時間為 1-3 個工作天,超商取貨需要額外 1-2 天。",
"./faq/returns.txt" to "商品可在收到後 7 天內退換貨,需保持原包裝完整。",
"./faq/warranty.txt" to "電子產品提供一年保固,保固期內免費維修或更換。",
"./faq/payment.txt" to "支援信用卡、ATM轉帳、超商付款等多種付款方式。",
"./faq/account.txt" to "可透過官網註冊會員帳號,享受更多優惠和服務。"
)
// 建立目錄並存儲文件
faqData.forEach { (filename, content) ->
val path = Path.of(filename)
Files.createDirectories(path.parent)
Files.writeString(path, content)
rankedDocumentStorage.store(path)
println("已載入:$filename")
}
// 搜尋相關文件
val query = "我的包裏什麼時候會到?"
val relevantDocs = rankedDocumentStorage.mostRelevantDocuments(
query = query,
count = 2, // 取前 2 個最相關的文件
similarityThreshold = 0.5 // 相似度閥值(調整為更寬鬆的閥值)
)
// 顯示搜尋結果
println("用戶問題:$query")
println("找到 ${relevantDocs.size} 個相關文件:")
relevantDocs.forEachIndexed { index, doc ->
println("${index + 1}. 文件:${doc.fileName}")
try {
val content = Files.readString(doc)
println("內容:$content")
} catch (e: Exception) {
println("無法讀取檔案:${e.message}")
}
println("-".repeat(50))
}
}
這個基礎範例展示了 RAG 系統的核心概念:將問題轉換為向量,在文件庫中找到最相似的內容,然後提供給 AI 作為回答依據
執行 AI 回應內容
已載入:./faq/shipping.txt
已載入:./faq/returns.txt
已載入:./faq/warranty.txt
已載入:./faq/payment.txt
已載入:./faq/account.txt
用戶問題:我的包裏什麼時候會到?
找到 2 個相關文件:
1. 文件:account.txt
內容:可透過官網註冊會員帳號,享受更多優惠和服務。
--------------------------------------------------
2. 文件:shipping.txt
內容:商品包裏出貨時間為 1-3 個工作天,超商取貨需要額外 1-2 天。
--------------------------------------------------
RAG + AI Agent 整合範例
現在讓我們建立一個完整的 AI 客服系統,結合 RAG 檢索和 AI Agent
// 建立 AI 客服工具集
@LLMDescription("AI 客服工具,能夠搜尋公司知識庫回答客戶問題")
class CustomerServiceTools(
private val documentStorage: EmbeddingBasedDocumentStorage<Path>
) : ToolSet {
@Tool
@LLMDescription("搜尋公司知識庫中與客戶問題相關的文件")
suspend fun searchKnowledgeBase(
@LLMDescription("客戶的問題或查詢內容")
query: String,
@LLMDescription("需要檢索的文件數量,預設為 3")
count: Int = 3
): String {
// 搜尋相關文件
println("AI query document -- $query")
val relevantDocs = documentStorage.mostRelevantDocuments(
query = query,
count = count,
similarityThreshold = 0.5 // 適中的相似度閥值
).toList()
if (relevantDocs.isEmpty()) {
return "抱歉,在知識庫中找不到相關資訊。建議聯繫人工客服。"
}
// 整合搜尋結果
val sb = StringBuilder("根據知識庫搜尋,找到以下相關資訊:\n\n")
relevantDocs.forEachIndexed { index, document ->
try {
val content = Files.readString(document)
sb.append("參考資料 ${index + 1}:${document.fileName}\n")
sb.append("內容:$content\n\n")
} catch (e: Exception) {
println("警告:無法讀取文件 ${document.fileName},原因:${e.message}")
sb.append("參考資料 ${index + 1}:${document.fileName} (檔案讀取失敗)\n")
}
}
return sb.toString()
}
}
使用 AI 客服範例
suspend fun main() {
// 初始化 RAG 系統
val embedder = LLMEmbedder(OllamaClient(), OllamaEmbeddingModels.NOMIC_EMBED_TEXT)
val documentEmbedder = JVMTextDocumentEmbedder(embedder)
val documentStorage = EmbeddingBasedDocumentStorage(documentEmbedder, InMemoryVectorStorage())
// 載入知識庫文件(實際應用中可能從資料庫或 API 載入)
val knowledgeBaseFiles = listOf(
"faq/shipping.txt",
"faq/returns.txt",
"faq/warranty.txt",
"faq/payment.txt",
"faq/product-info.txt"
)
knowledgeBaseFiles.forEach { fileName ->
val path = Path.of("./$fileName")
if (Files.exists(path)) {
documentStorage.store(path)
println("已載入:$fileName")
}
}
// 建立工具註冊器
val toolRegistry = ToolRegistry {
tools(CustomerServiceTools(documentStorage))
}
// 建立 AI 客服 Agent
val customerServiceAgent = AIAgent(
executor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
systemPrompt = """
你是一位專業的客服代表,負責回答客戶問題。
工作原則:
1. 使用 searchKnowledgeBase 工具搜尋相關資訊
2. 基於搜尋結果提供準確、有用的回答
3. 如果知識庫中沒有相關資訊,誠實告知並建議其他解決方案
4. 保持友善、專業的語調
5. 回答要簡潔明瞭,避免冗長
""".trimIndent(),
llmModel = OpenAIModels.Chat.GPT4o,
toolRegistry = toolRegistry
)
// 模擬客戶對話
val customerQueries = listOf(
"我的訂單什麼時候會到貨?",
"如何申請退換貨?",
"產品保固期是多久?",
"支援哪些付款方式?"
)
customerQueries.forEach { query ->
println("\n" + "=".repeat(50))
println("客戶問題:$query")
println("客服回覆:")
try {
val response = customerServiceAgent.run(query)
println(response)
} catch (e: Exception) {
println("處理問題時發生錯誤:${e.message}")
}
}
}
相關的
知識庫文件
我請 AI 幫我產生測試資料,不過因為檔案和文字比較多,我就不放到文章裡面了
這個整合範例展示了如何將 RAG 系統與 AI Agent 結合,建立一個能夠基於實際文件回答問題的 AI 客服
執行 AI 回應內容
不知道為什麼,那個
付款方式
一直檢索不到相關的檔案,蠻神奇的,其它的問題都還蠻正常的
已載入:faq/AI/shipping.txt
已載入:faq/AI/returns.txt
已載入:faq/AI/warranty.txt
已載入:faq/AI/payment.txt
已載入:faq/AI/product-info.txt
==================================================
客戶問題:我的訂單什麼時候會到貨?
客服回覆:
AI query document -- 訂單 到貨 時間
您好,訂單一般標準運送時間為3-5個工作天(不含週末及國定假日),急件配送則可於1-2個工作天送達,偏遠地區及離島會稍微延長運送時間。出貨後會提供物流追蹤碼,您可透過官方網站查詢最新的運送狀態。若需要更詳細的訂單狀態,歡迎提供訂單編號,我們可以幫您查詢。
==================================================
客戶問題:如何申請退換貨?
客服回覆:
AI query document -- 退換貨申請
您好,申請退換貨的流程如下:
1. 請於商品到貨後7天猶豫期內,聯繫客服人員申請退換貨,並提供訂單編號及退換貨原因。
2. 客服確認後會提供退貨地址及相關注意事項。
3. 請於3個工作天內將商品寄回指定地址,並務必保持商品及包裝完整(商品本體、配件、贈品及發票等)。
4. 商品寄回時請妥善包裝,建議使用有追蹤號碼的宅配服務。
5. 若是換貨,請先確認換購商品有庫存,並支付相應的價差及運費(瑕疵商品換貨運費由公司負擔)。
6. 收到退貨商品後,會在3-5個工作天內完成退款或換貨處理。
備註:
- 已使用或損毀商品、個人衛生用品、食品類、客製化及特價促銷商品可能不接受退換貨。
- 瑕疵商品可隨時申請退換貨,無須受7天猶豫期限制,請立即拍照存證並聯繫客服。
若您需要進一步協助,歡迎隨時聯繫客服。
==================================================
客戶問題:產品保固期是多久?
客服回覆:
AI query document -- 產品保固期
您好,產品的保固期限一般電子產品為1年,大型家電為2年,保固期從購買日(以發票日期為準)開始計算。特殊商品如手機、平板等採製造商原廠保固,期限依品牌規定。進口商品保固期可能不同,建議參考商品頁面說明。如需更詳細的保固範圍、申請流程等資訊,歡迎隨時聯繫我們!
==================================================
客戶問題:支援哪些付款方式?
客服回覆:
AI query document -- 付款方式
目前沒有找到具體的付款方式詳細資訊。建議您可以直接聯繫我們客服人員,或查看官網的付款方式頁面以獲得最新的相關資訊。若有其他問題,也歡迎隨時詢問!
實用技巧
相似度閾值調整
相似度閾值是 RAG 系統的關鍵參數,直接影響檢索品質和系統效能
// 嚴格篩選:高品質,但可能漏掉相關內容
val strictResults = documentStorage.mostRelevantDocuments(
query = "退貨流程",
count = 3,
similarityThreshold = 0.85 // 非常高的閾值
)
// 平衡篩選:大多數情況下的最佳選擇
val balancedResults = documentStorage.mostRelevantDocuments(
query = "退貨流程",
count = 3,
similarityThreshold = 0.7 // 適中閾值
)
// 寬鬆篩選:可能包含噪音,但不會漏掉相關內容
val relaxedResults = documentStorage.mostRelevantDocuments(
query = "退貨流程",
count = 5,
similarityThreshold = 0.5 // 低閾值
)
// 實用技巧:動態調整閾值
fun adaptiveSearch(query: String, storage: EmbeddingBasedDocumentStorage<Path>): List<Path> {
// 先用高閾值搜尋
var results = storage.mostRelevantDocuments(query, count = 3, similarityThreshold = 0.8).toList()
// 如果結果太少,降低閾值
if (results.size < 2) {
results = storage.mostRelevantDocuments(query, count = 5, similarityThreshold = 0.6).toList()
}
return results
}
閾值設定建議:
- 0.8-0.9:適用於專業領域,需要非常精確的答案
- 0.6-0.8:一般建議值,平衡品質和實用性
- 0.4-0.6:探索性搜尋,適用於開放式問題
- 0.3以下:可能產生大量噪音,不建議使用
文件儲存最佳實踐
// 結構化的文件管理方式
class KnowledgeBaseManager(
private val documentStorage: EmbeddingBasedDocumentStorage<Path>
) {
private val categoryFolders = mapOf(
"shipping" to listOf(
"shipping-policy.txt" to "運送政策和時間說明",
"delivery-time.txt" to "各地區配送時間表"
),
"returns" to listOf(
"return-process.txt" to "退貨流程和注意事項",
"refund-policy.txt" to "退款政策和處理時間"
),
"products" to listOf(
"product-specs.txt" to "產品規格和功能說明",
"warranty-info.txt" to "保固條款和維修服務"
)
)
suspend fun loadKnowledgeBase(basePath: String = "./knowledge-base"): LoadResult {
var successCount = 0
var failureCount = 0
val failedFiles = mutableListOf<String>()
categoryFolders.forEach { (category, files) ->
files.forEach { (fileName, description) ->
val path = Path.of("$basePath/$category/$fileName")
try {
if (Files.exists(path)) {
documentStorage.store(path)
println("✓ 已載入 $category/$fileName - $description")
successCount++
} else {
println("⚠ 文件不存在:$path")
failedFiles.add("$category/$fileName")
failureCount++
}
} catch (e: Exception) {
println("✗ 載入失敗:$category/$fileName - ${e.message}")
failedFiles.add("$category/$fileName")
failureCount++
}
}
}
return LoadResult(successCount, failureCount, failedFiles)
}
data class LoadResult(
val successCount: Int,
val failureCount: Int,
val failedFiles: List<String>
)
}
// 使用示範
suspend fun Main() {
val embedder = LLMEmbedder(OllamaClient(), OllamaEmbeddingModels.NOMIC_EMBED_TEXT)
val documentEmbedder = JVMTextDocumentEmbedder(embedder)
val documentStorage = EmbeddingBasedDocumentStorage(documentEmbedder, InMemoryVectorStorage())
val manager = KnowledgeBaseManager(documentStorage)
val result = manager.loadKnowledgeBase()
println("\n知識庫載入完成:")
println("成功:${result.successCount} 個文件")
if (result.failureCount > 0) {
println("失敗:${result.failureCount} 個文件")
println("失敗清單:${result.failedFiles.joinToString(", ")}")
}
}
效能優化與擴展性
對於生產環境和大型知識庫,考慮以下效能優化策略
// 使用持久化儲存替代記憶體儲存
val fileVectorStorage = JVMFileVectorStorage(root = Path.of("./vector-storage"))
val documentStorage = EmbeddingBasedDocumentStorage(documentEmbedder, fileVectorStorage)
// 批量處理大量文件
class BatchDocumentProcessor(
private val documentStorage: EmbeddingBasedDocumentStorage<Path>,
private val batchSize: Int = 50
) {
suspend fun processBatch(documentPaths: List<Path>) {
documentPaths.chunked(batchSize).forEach { batch ->
withContext(Dispatchers.IO) {
batch.forEach { path ->
try {
documentStorage.store(path)
println("✓ 已處理:${path.fileName}")
} catch (e: Exception) {
println("✗ 處理失敗:${path.fileName} - ${e.message}")
}
}
}
println("已完成 ${batch.size} 個文件的處理")
}
}
}
// 搜尋結果快取(簡化版本)
class SearchCache {
private val cache = mutableMapOf<String, Pair<List<Path>, Long>>()
private val cacheTimeout = 300_000L // 5 分鐘
fun getCachedResults(query: String): List<Path>? {
val cached = cache[query] ?: return null
return if (System.currentTimeMillis() - cached.second < cacheTimeout) {
cached.first
} else {
cache.remove(query)
null
}
}
fun cacheResults(query: String, results: List<Path>) {
cache[query] = Pair(results, System.currentTimeMillis())
}
}
// AI 搜尋策略
class SmartSearchManager(
private val documentStorage: EmbeddingBasedDocumentStorage<Path>,
private val cache: SearchCache = SearchCache()
) {
suspend fun smartSearch(
query: String,
maxResults: Int = 5,
useCache: Boolean = true
): List<Path> {
// 先檢查快取
if (useCache) {
cache.getCachedResults(query)?.let { return it }
}
// 分階段搜尋策略
val results = when {
// 簡單關鍵字:使用寬鬆閾值
query.split(" ").size <= 2 -> {
documentStorage.mostRelevantDocuments(
query, maxResults, similarityThreshold = 0.6
).toList()
}
// 複雜問題:使用中等閾值
query.length > 20 -> {
documentStorage.mostRelevantDocuments(
query, maxResults, similarityThreshold = 0.7
).toList()
}
// 預設情況
else -> {
documentStorage.mostRelevantDocuments(
query, maxResults, similarityThreshold = 0.65
).toList()
}
}
// 將結果存入快取
if (useCache && results.isNotEmpty()) {
cache.cacheResults(query, results)
}
return results
}
}
效能考量要點
- 記憶體 vs 檔案儲存:記憶體速度快但有容量限制,檔案儲存可持久但速度較慢
- 批量處理:大量文件分批處理,避免記憶體溢出
- 快取策略:常用查詢結果快取,減少重複計算
- 搜尋優化:根據查詢複雜度動態調整參數
文件分塊對 RAG 效果的影響
雖然本文的範例使用整份文件進行 embedding,但在實際應用中,文件分塊是 RAG 系統效能的關鍵因素
分塊大小的影響
// 不同分塊策略的比較範例
class RAGPerformanceComparison {
// 小分塊:精確但可能缺乏上下文
fun smallChunks(): ChunkStrategy = ChunkStrategy(
size = 200, // 200 字元
overlap = 50, // 50 字元重疊
pros = listOf("檢索精確", "回應針對性強"),
cons = listOf("可能缺乏完整上下文", "需要更多分塊")
)
// 中等分塊:平衡效能與品質
fun mediumChunks(): ChunkStrategy = ChunkStrategy(
size = 800, // 800 字元
overlap = 100, // 100 字元重疊
pros = listOf("保持適當上下文", "效能適中"),
cons = listOf("可能包含無關資訊")
)
// 大分塊:上下文豐富但檢索可能不精確
fun largeChunks(): ChunkStrategy = ChunkStrategy(
size = 2000, // 2000 字元
overlap = 200, // 200 字元重疊
pros = listOf("完整上下文", "減少分塊數量"),
cons = listOf("檢索精確度可能降低", "包含更多噪音")
)
}
data class ChunkStrategy(
val size: Int,
val overlap: Int,
val pros: List<String>,
val cons: List<String>
)
總結
RAG 系統為 AI 應用帶來了革命性的改變。透過結合文件檢索和 AI 產生能力,我們能夠建立既準確又可信的 AI 系統。在這篇文章中,我們深入探索了
- RAG 核心概念:理解檢索增強產生的運作機制和優勢
- Koog 框架實作:掌握 EmbeddingBasedDocumentStorage 和 JVMTextDocumentEmbedder
- AI Agent 整合:利用 Tool 機制建立 AI RAG 搜尋功能
- 效能優化:相似度閥值調整、批量處理和快取策略
- 最佳實踐:知識庫管理、錯誤處理和系統擴展考量
RAG 的威力在於它將 Embeddings 的語意理解能力與 AI 的語言產生能力完美結合。相比於 Day 30 單純的文件相似度比對,RAG 能夠將檢索結果轉化為自然流暢的回答,大幅提升使用者體驗
值得特別注意的是,目前 Koog 框架尚未提供內建的文件分塊功能。這在實際應用中是一個重要的限制。這裡的範例都是基於整份文件進行 embedding 和檢索,缺乏分塊會影響 RAG 系統的檢索精確度和效能
RAG 系統讓 AI 能夠基於知識庫回答問題,但當對話持續進行,另一個挑戰隨之而來:如何有效管理不斷增長的對話歷史? 在下一篇文章中,我們將探索 History Compression(歷史記錄壓縮),當你的客服 AI 已經和客戶對話了 50 輪,每一輪都包含問題和回答,歷史記錄可能已經包含數千個 token。我們將學習如何在保留關鍵資訊的同時,有效控制對話長度和成本
參考文件
支持創作
如果這篇文章對您有幫助,歡迎透過 贊助連結 支持我持續創作優質內容。您的支持是我前進的動力!
圖片來源:AI 產生