前言
在上一篇文章中,我們成功地將一篇長文檔加載并分割成了一系列小的文本片段(TextSegment
)。我們現在有了一堆“知識碎片”,但面臨一個新問題:計算機如何理解這些碎片的內容,并找出與用戶問題最相關的片段呢?
如果用戶問“X-Wing的設置要求是什么?”,我們不能只用簡單的關鍵詞匹配(比如搜索“setup”或“requirements”),因為用戶可能會用不同的詞語提問(“我該如何安裝X-Wing?”)。我們需要一種能夠理解**語義(Semantic Meaning)**的搜索方式。
這就是文本嵌入(Text Embedding) 和 向量數據庫(Vector Database) 發揮作用的地方。今天,我們將深入RAG技術的心臟地帶,學習如何將文本轉化為向量,并將其存儲起來以便進行高效的語義搜索。
第一部分:什么是文本嵌入(Embedding)?語義的“指紋”
想象一下圖書館里的書。圖書管理員不會記住每本書的每一個字,但他們知道哪些書是關于“歷史”的,哪些是關于“科幻”的,哪些是關于“烹飪”的。他們將書的內容“映射”到了一個類別體系中。
文本嵌入模型(Embedding Model)做的是類似但更精細的事情。它是一個專門的AI模型,它的唯一工作就是讀取一段文本,然后輸出一個由幾百上千個數字組成的列表——向量(Vector)。
這個向量,就是這段文本在多維語義空間中的“坐標”或“指紋”。
這個語義空間非常神奇。在其中,意思相近的文本,它們的向量在空間中的距離也相近。經典的例子是:
vector("King") - vector("Man") + vector("Woman") ≈ vector("Queen")
通過將所有文檔片段都轉換成向量,我們就可以通過計算向量之間的“距離”來判斷文本之間的語義相似度,從而實現比關鍵詞搜索高級得多的語義搜索。
在LangChain4j中,這個功能由EmbeddingModel
接口來抽象。
第二部分:什么是向量數據庫(Vector Store)?向量的“家”
現在我們有能力將所有文本片段都轉換成向量了,但我們該把這些向量存放在哪里,又如何高效地搜索它們呢?
這就是向量數據庫(或稱為向量存儲,EmbeddingStore
)的作用。
如果說嵌入模型是“翻譯官”,把文本翻譯成向量;那么向量數據庫就是專門為這些向量建立的“高維空間索引系統”。
它允許我們:
- 存儲大量的向量及其關聯的原始文本。
- 當給定一個新的查詢向量時,能以極高的效率找出數據庫中與它最相似的N個向量(這個過程通常被稱為“最近鄰搜索”)。
LangChain4j通過EmbeddingStore
接口支持多種向量數據庫,從簡單的內存存儲到專業的分布式數據庫:
- InMemoryEmbeddingStore: 完全在內存中運行,非常適合快速原型開發和測試,無需任何外部依賴。
- Chroma, Milvus, Pinecone, Weaviate: 生產級的、可獨立部署的向量數據庫,支持海量數據和高并發查詢。
在今天的教程中,我們將從最簡單的InMemoryEmbeddingStore
開始。
第三部分:實戰 - 將文檔片段嵌入并存儲
我們將繼續完善上一篇的DocumentService
,為其增加嵌入和存儲的功能。
- 確認依賴和配置
好消息是,我們之前添加的langchain4j-open-ai-spring-boot-starter
已經包含了嵌入模型的功能。現在還需要修改application.prroperties增加embedding模型配置
langchain4j.open-ai.chat-model.api-key=${OPENAI_API_KEY:your-api-key-here}
langchain4j.open-ai.chat-model.base-url=https://yibuapi.com/v1/
langchain4j.open-ai.chat-model.model-name=gpt-4o-mini
langchain4j.open-ai.chat-model.temperature=0.7
langchain4j.open-ai.chat-model.max-tokens=1024langchain4j.open-ai.embedding-model.model-name=text-embedding-ada-002
-
修改
DocumentService
我們將注入EmbeddingModel
和EmbeddingStoreIngestor
,并創建一個InMemoryEmbeddingStore
的Bean。第一步:在
config/LangChain4jConfig.java
中創建EmbeddingModel
Beanpackage com.example.aidemoapp.config; // ... other imports import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;@Configuration public class LangChain4jConfig {// ... chatLanguageModel 和 chatMemoryProvider Beans ...@Beanpublic EmbeddingModel embeddingModel() {// 通常嵌入模型也使用相同的api-key和base-url// 注意:OpenAI有專門的嵌入模型名稱,// 比如 "text-embedding-ada-002" 。// 如果不指定,LangChain4j可能會使用一個默認值。// 為清晰起見,最好在properties中也定義它。return OpenAiEmbeddingModel.builder().apiKey(apiKey).baseUrl(baseUrl)// 推薦在application.properties中添加:// langchain4j.open-ai.embedding-model.model-name=text-embedding-ada-002.modelName(embeddingModelName).build();} }
第二步:改造
DocumentService.java
package com.example.aidemoapp.service;import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.DocumentSplitter; import dev.langchain4j.data.document.loader.FileSystemDocumentLoader; import dev.langchain4j.data.document.parser.TextDocumentParser; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service;import java.nio.file.Path; import java.nio.file.Paths; import java.util.List;@Service @RequiredArgsConstructor public class DocumentService {// 注入由Starter自動創建的EmbeddingModelprivate final EmbeddingModel embeddingModel;// 注入我們自己創建的EmbeddingStore Beanprivate final EmbeddingStore embeddingStore;public void loadSplitAndEmbed() {Path documentPath = Paths.get("src/main/resources/documents/product-info.txt");Document document = FileSystemDocumentLoader.loadDocument(documentPath, new TextDocumentParser());// 2. 將文檔分割成片段DocumentSplitter splitter = DocumentSplitters.recursive(300, 10);List<TextSegment> segments = splitter.split(document);System.out.println("Document split into " + segments.size() + " segments.");// 3. 將片段嵌入并存儲到向量數據庫中// LangChain4j提供了一個方便的EmbeddingStoreIngestor來處理這個流程EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().documentSplitter(splitter) // 可以在這里也指定分割器.embeddingModel(embeddingModel).embeddingStore(embeddingStore).build();// 開始攝入文檔ingestor.ingest(document);System.out.println("Document ingested and stored in the embedding store.");} }
代碼解析:
- 我們注入了
EmbeddingModel
和EmbeddingStore
。 - 我們使用了
EmbeddingStoreIngestor
,這是一個高級工具,它將分割、嵌入和存儲這三個步驟打包成了一個簡單的.ingest()
方法調用,非常方便。 - 運行
loadSplitAndEmbed()
方法后,我們的InMemoryEmbeddingStore
中就包含了文檔所有片段的向量信息。
- 我們注入了
第四部分:進行第一次語義搜索
光存儲還不夠,我們需要驗證一下檢索效果。讓我們添加一個搜索方法。
// 在 DocumentService.java 中繼續添加
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.store.embedding.EmbeddingMatch;// ...public List<String> search(String query) {System.out.println("\n--- Performing search for query: '" + query + "' ---");// 1. 將用戶問題也進行嵌入,得到查詢向量Response<Embedding> queryEmbedding = embeddingModel.embed(query);// 2. 在向量存儲中查找最相關的N個匹配項// 參數1: 查詢向量// 參數2: 返回的最大結果數EmbeddingSearchRequest request = EmbeddingSearchRequest.builder().queryEmbedding(queryEmbedding.content()).build();List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(request).matches();// 3. 打印結果System.out.println("Found " + relevant.size() + " relevant segments:");relevant.forEach(match -> {System.out.println("--------------------");System.out.println("Score: " + match.score()); // 相似度得分System.out.println("Text: " + match.embedded().text()); // 原始文本});return relevant.stream().map(match -> match.embedded().text()).collect(Collectors.toList());}
現在,你可以創建一個測試端點來調用loadSplitAndEmbed()
,然后再調用search("What are the setup requirements for X-Wing?")
。你會看到,即使你的問題中沒有“RAM”或“CPU”這些詞,返回的最相關的片段也正是包含“16GB RAM and a 4-core CPU”的那一段!這就是語義搜索的威力。
總結
今天,我們深入了RAG技術的核心腹地。我們學習了:
- **文本嵌入(Embedding)**如何將文字轉換成代表其語義的數學向量。
- **向量數據庫(Vector Store)**如何存儲這些向量并進行高效的相似度搜索。
- 如何使用LangChain4j的
EmbeddingModel
和EmbeddingStoreIngestor
將文檔片段向量化并存入內存向量庫。 - 如何執行一次真正的語義搜索,并找到了與問題最相關的文檔片段。
我們已經成功地“開卷”并“找到了答案所在的頁面”。現在,我們離終點只差最后一步:將找到的這些“參考資料”連同原始問題一起,交給大語言模型,讓它用自然、流暢的語言“總結”出最終的答案。
下一篇預告:
《Java大模型開發入門 (10/15):連接外部世界(下) - 端到端構建完整的RAG問答系統》—— 我們將整合所有學過的知識,打通RAG的“最后一公里”。我們將把檢索到的文本片段與用戶問題組合起來,發送給ChatLanguageModel
,最終構建一個可以針對我們私有文檔進行智能問答的完整端到端應用!