iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0
自我挑戰組

與 AI 共舞:打造更高效的日常系列 第 23

Spring AI 應用解析:探索 Naive RAG 的核心流程與基本實作

  • 分享至 

  • xImage
  •  

前言

連續放了兩天颱風假,想到明天星期五又要回去上班,心情真是複雜。還好山陀兒颱風在今天下午五點已經減弱為輕度颱風,希望明天的上班路程不會受到太多影響。不過,iThome 鐵人賽的挑戰還是要繼續,今天就來分享一些我在技術上的心得和體驗吧。

解決 WebFlux 的挑戰

昨天我嘗試新增檔案上傳和建立 ETL Pipeline 的功能,並用 WebFlux 的方式改寫這個 API。但是過程並不順利。一開始我以為是自己對 Reactive Programming 的理解不夠深入,導致程式在檔案還沒上傳完成的時候就去讀取它,才會出現「檔案不存在」的錯誤。

問題的根源

經過深入調查,我才發現自己犯了一個小疏忽。在 Spring AI 的 TextReader 中,它可以接受兩個參數:

  1. Resource:Spring 抽象化的資源,可以開啟一個 InputStream
  2. String:需要 Resource 的 URL 而不是檔案路徑。

我原本以為只要提供檔案的路徑就可以了,但實際上需要將路徑轉換成 URI 格式,TextReader 才能正確讀取檔案。原本的程式碼只是直接傳入路徑,沒有進行 URI 轉換,這就是錯誤的根源。修正之後,終於成功地將檔案上傳的 API 端點改寫成 WebFlux 風格。

具體的實作步驟

  1. 建立暫存檔案:利用 File.createTempFile 建立一個暫存檔案。
  2. 寫入檔案內容:透過 FilePart.transferTo 將上傳的檔案寫入暫存檔案。由於 transferTo 回傳的是 Mono<Void>,因此我加入 then(Mono.fromCallable),以便在檔案寫入完成後回傳暫存檔案的路徑供後續步驟使用。
  3. 讀取檔案內容:在下一個 flatMap 中,使用 Mono.fromCallable 建立對應的 TextReader,記得將檔案路徑轉換成 URI 格式(file.toURI())。接著,使用 TextReader.get() 取得文件列表 List<Document>
  4. 文件拆解與向量化:建立一個 TokenTextSplitter,將文件拆解成多個 chunks。然後,透過 VectorStore.add 將這些區塊進行 embedding,並儲存至向量資料庫。
@PostMapping("/ai/upload")
fun uploadFileWithoutEntity(
    @RequestPart("file") filePart: Mono<FilePart>
): Mono<ResponseEntity<Map<String, String>>> {
    val tempFile = Files.createTempFile("upload-", ".txt")
    return filePart.flatMap { part ->
        part.transferTo(tempFile)
            .then(Mono.fromCallable { tempFile })
            .flatMap { file ->
                Mono.fromCallable {
                    val textReader = TextReader(file.toUri().toString())
                    textReader.get()
                }.flatMap { documents ->
                        Mono.fromCallable {
                            val tokenTextSplitter = TokenTextSplitter()
                            tokenTextSplitter.apply(documents)
                        }
                    }.flatMap { splitDocuments ->
                        Mono.fromCallable { vectorStore.add(splitDocuments) }
                    }.map { ResponseEntity.ok(mapOf("upload" to "ok")) }
            }
    }.onErrorResume { ex ->
        val errorMessage = "Error uploading file: ${ex.message}"
        Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(mapOf("error" to errorMessage)))
    }
}

實作最基本的 RAG

除了檔案上傳的功能之外,我還新增了一個 API 來實作基本功能。這個實作雖然相對簡單,但對於理解 RAG 的運作方式非常有幫助。

建立資料檢索服務

首先,新增一個 DataRetrievalService,其中包含 searchData 方法。這個方法會利用 VectorStore.similaritySearch,將使用者查詢進行 embedding 後,透過 Cosine Similarity(餘弦相似度)演算法,找出最相似的向量,並提取對應的 context。

@Service
class DataRetrievalService @Autowired constructor(
    private val vectorStore: VectorStore
) {
    fun searchData(query: String?): List<Document> {
        return vectorStore.similaritySearch(query)
    }
}

構建 RAG API

  1. 取得相關內容:使用 DataRetrievalService.searchData 搜尋並取得與使用者查詢相關的 context。
  2. 建立 Prompt 模板:設計一個 PromptTemplate,用來引導 LLM 據提供的 context 回答使用者的問題。Prompt 需明確指示 LLM 必須參考 context 中的特定段落來回答問題,並且在無法回答時,需回應「 I'm sorry, I don't have the information you are looking for.」以降低 LLM 產生幻覺的機率。
  3. 獲取模型回應:利用 ChatModel.call 方法,將建構好的 Prompt 傳遞給 LLM,並取得 LLM 的回應。
@GetMapping("/ai/ask")
fun ask(
    @RequestParam(
        value = "message",
        defaultValue = "Tell me a joke"
    ) message: String?
): Map<*, *> {
    val context = this.dataRetrievalService.searchData(message)
    val promptTemplate = PromptTemplate(askPromptBlueprint)
    val prompt = promptTemplate.create(mapOf("context" to context, "query" to message))
    return java.util.Map.of("answer", chatModel.call(prompt))
}

淺談 Naive RAG 的運作原理

上述流程即是最基本的 RAG,也稱為 Naive RAG。其主要步驟如下:

  • 查詢向量化(Query Embedding):將使用者輸入的查詢進行 Embedding,轉換成查詢向量。
  • 相似度檢索(Similarity Search):利用相似度演算法(例如 Cosine Similarity),在向量資料庫中尋找最相似的向量,並擷取對應的內容。
  • 生成回應(Response Generation):將查詢和相關內容一併傳遞給 LLM,生成更精準的答案。

這種方法可以被形象地比喻為「Open Book」,也就是先從知識庫中找出參考資訊,再提供給 LLM 進行作答。相較於直接對模型進行 Fine-tuning,這種方式的成本要低得多。

雖然 Naive RAG 已能滿足基本需求,但它也存在一些不足之處。因此,業界發展出各種 Advanced RAG 方法,例如導入更複雜的檢索演算法、強化上下文理解能力等等。這些進階方法目前不在我的研究範圍內。

圖片來源:Retrieval Augmented Generation (RAG) for LLMs | Prompt Engineering Guide

總結

目前,我已經可以使用 Spring AI 實現簡單的 Naive RAG 應用。明天會繼續探索,看看可以做出哪些新的嘗試。雖然目前還沒有具體的想法,但我會一步一步地推進。

最後,提醒大家明天上班路上要注意風雨,平安順利!


上一篇
淺談 AI 繪圖工具:MidJourney 的實用技巧分享
下一篇
探索 ChatGPT 4o with Canvas:從撰寫爬蟲到創作新體驗
系列文
與 AI 共舞:打造更高效的日常30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言