作者:來自 Elastic?Josh Long,?Philipp Krenn?及?Laura Trotta
使用 Spring AI 和 Elasticsearch 構建一個完整的 AI 應用程序。
Elasticsearch 原生集成了業界領先的生成式 AI 工具和服務提供商。查看我們關于超越 RAG 基礎或使用 Elastic 向量數據庫構建生產級應用的網絡研討會。
為了為你的用例構建最佳搜索解決方案,現在就開始免費云試用,或者在本地機器上嘗試 Elastic。
Spring AI 現在已正式發布,第一個穩定版本 1.0 已可在 Maven Central 下載。讓我們馬上開始使用它,結合你喜歡的 LLM 和我們最喜歡的向量數據庫,構建一個完整的 AI 應用程序。
什么是 Spring AI?
Spring AI 1.0 是一個面向 Java 的全面 AI 工程解決方案,經過一段時間的開發并受到 AI 領域快速發展的推動,如今正式發布。這個版本為 AI 工程師帶來了許多關鍵的新功能。
Java 和 Spring 正處于迎接 AI 浪潮的有利位置。很多公司都在使用 Spring Boot,這使得將 AI 集成到現有系統中變得非常簡單。你基本上可以毫不費力地將業務邏輯和數據直接連接到那些 AI 模型上。
Spring AI 支持多種 AI 模型和技術,例如:
- 圖像模型:根據文本提示生成圖像。
- 轉錄模型:將音頻源轉換為文本。
- 嵌入模型:將任意數據轉換為向量,這是一種針對語義相似性搜索優化的數據類型。
- 聊天模型:這些你應該很熟悉!你肯定在哪兒已經和其中一個簡單聊過幾句了。
聊天模型是當前 AI 領域最受關注的部分,而且確實如此,它們非常強大!你可以讓它們幫你修改文檔或者寫一首詩。(只是暫時別讓它們講笑話……)它們很厲害,但也確實存在一些問題。
Spring AI 應對 AI 挑戰的解決方案

讓我們來看看這些問題以及 Spring AI 中的解決方案。
問題 | 解決方案 | |
---|---|---|
一致性 | 聊天模型思想開放,容易分心 | 你可以給它們一個 system prompt 來控制它們整體的行為和結構 |
內存 | AI 模型沒有記憶,所以它們無法將同一個用戶的多條消息關聯起來 | 你可以給它們一個記憶系統來存儲對話中相關的部分 |
隔離 | AI 模型生活在隔離的小沙盒中,但如果你給它們訪問工具的權限——當它們認為有必要時可以調用的函數,它們可以做非常驚人的事情。 | Spring AI 支持工具調用,這讓你可以告訴 AI 模型它環境中的工具,之后它可以請求你調用這些工具。這種多輪交互會為你透明地處理。 |
私有數據 | AI 模型很智能,但它們不是無所不知!它們不知道你專有數據庫中的內容 —— 而且我們也認為你不會想讓它們知道! | 你需要通過填充 prompts 來指導它們的回答 —— 基本上是用強大的字符串拼接操作符,在模型查看問題之前,把文本放進請求里。可以看作是背景信息。你怎么決定什么該發送,什么不該發送?用 vector store 選擇只有相關的數據,再發送給模型。這叫做檢索增強生成,或者 RAG。 |
幻覺 | AI chat 模型喜歡聊天!有時候它們非常自信,以至于會編造內容。 | 你需要使用評估 —— 用一個模型驗證另一個模型的輸出——來確認結果是否合理。 |
當然,沒有哪個 AI 應用是孤立的。現代 AI 系統和服務在與其他系統和服務集成時表現最佳。Model Context Protocol (MCP) 使你能夠將 AI 應用連接到其他基于 MCP 的服務,無論它們使用什么語言編寫。你可以將這些組合成推動更大目標的 agentic 工作流。
最棒的是?你可以在熟悉的 Spring Boot 習慣用法和抽象基礎上完成這一切:Spring Initializr 上幾乎所有功能都有方便的 starter 依賴。
Spring AI 提供方便的 Spring Boot 自動配置,讓你享受約定優于配置的體驗。Spring AI 還支持通過 Spring Boot 的 Actuator 和 Micrometer 項目進行可觀察性。它還能很好地兼容 GraalVM 和虛擬線程,幫助你構建超快、高效且可擴展的 AI 應用。
為什么選擇 Elasticsearch
Elasticsearch 是一個全文搜索引擎,你可能已經知道了。那么為什么我們用它來做這個項目?因為它也是一個向量存儲!而且非常不錯,數據和全文本緊密存放。其他顯著優點:
-
超級容易設置
-
開源
-
橫向可擴展
-
你們組織的大部分自由格式數據可能已經存在 Elasticsearch 集群中
-
功能完善的搜索引擎能力
-
完全集成在 Spring AI 中!
綜合考慮,Elasticsearch 滿足作為優秀向量存儲的所有條件,所以讓我們開始設置并構建應用吧!
開始使用 Elasticsearch
我們需要 Elasticsearch 和 Kibana,Kibana 是你用來與數據庫中數據交互的 UI 控制臺。
多虧了 Docker 鏡像和 Elastic.co 首頁的便利,你可以在本地機器上試用所有東西。去那里,向下滾動找到 curl 命令,運行它并直接輸入你的終端:
curl -fsSL https://elastic.co/start-local | sh ______ _ _ _ | ____| | | | (_) | |__ | | __ _ ___| |_ _ ___ | __| | |/ _` / __| __| |/ __|| |____| | (_| \__ \ |_| | (__ |______|_|\__,_|___/\__|_|\___|
-------------------------------------------------
🚀 Run Elasticsearch and Kibana for local testing
-------------------------------------------------
?? Do not use this script in a production environment
?? Setting up Elasticsearch and Kibana v9.0.0...
- Generated random passwords
- Created the elastic-start-local folder containing the files:- .env, with settings- docker-compose.yml, for Docker services- start/stop/uninstall commands
- Running docker compose up --wait
[+] Running 25/26? kibana_settings Pulled 16.7s ? kibana Pulled 26.8s ? elasticsearch Pulled 17.4s
[+] Running 6/6? Network elastic-start-local_default Created 0.0s ? Volume "elastic-start-local_dev-elasticsearch" Created 0.0s ? Volume "elastic-start-local_dev-kibana" Created 0.0s ? Container es-local-dev Healthy 12.9s ? Container kibana_settings Exited 11.9s ? Container kibana-local-dev Healthy 21.8s
🎉 Congrats, Elasticsearch and Kibana are installed and running in Docker!
🌐 Open your browser at http://localhost:5601Username: elasticPassword: w1GB15uQ
🔌 Elasticsearch API endpoint: http://localhost:9200
🔑 API key: SERqaGlKWUJLNVJDODc1UGxjLWE6WFdxSTNvMU5SbVc5NDlKMEhpMzJmZw==
Learn more at https://github.com/elastic/start-local
? ~
這將會拉取并配置 Elasticsearch 和 Kibana 的 Docker 鏡像,幾分鐘后你就可以在本地機器上運行它們,連接憑據也會一起提供。
你還會得到兩個不同的 URL 用來與你的 Elasticsearch 實例交互。按照提示操作,打開瀏覽器訪問 http://localhost:5601。
注意:控制臺上打印的用戶名 elastic 和密碼:你登錄時需要用到它們(在上面的示例輸出中,用戶名和密碼分別是 elastic 和 w1GB15uQ)。
整合應用
訪問 Spring Initializr 頁面,生成一個包含以下依賴項的新 Spring AI 項目:
-
Elasticsearch Vector Store
-
Spring Boot Actuator
-
GraalVM
-
OpenAI
-
Web
確保選擇最新版本的 Java(理想是 Java 24 —— 寫本文時是這個版本 —— 或更高)以及你選擇的構建工具。這里的示例使用 Apache Maven。
點擊 Generate,下載并解壓項目,然后導入到你喜歡的 IDE 中。(我們用的是 IntelliJ IDEA。)
首先,指定 Spring Boot 應用的連接詳情。在 application.properties 中寫入以下內容:
spring.elasticsearch.uris=http://localhost:9200
spring.elasticsearch.username=elastic
spring.elasticsearch.password=w1GB15uQ
我們還會使用 Spring AI 的 vector store 功能來初始化 Elasticsearch 端所需的數據結構,所以請指定:
spring.ai.vectorstore.elasticsearch.initialize-schema=true
在這個演示中我們將使用 OpenAI,具體是 Embedding Model 和 Chat Model(你可以使用任何 Spring AI 支持的服務)。
Embedding Model 用于在將數據存入 Elasticsearch 之前創建數據的向量。要使用 OpenAI,我們需要指定 API key:
spring.ai.openai.api-key=...
你可以將它定義為環境變量,比如 SPRING_AI_OPENAI_API_KEY,避免將憑證寫入源代碼。
我們要上傳文件,所以一定要自定義允許上傳到 servlet 容器的數據大小:
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20M
快完成了!在我們開始寫代碼之前,先預覽一下這個流程。
我們在機器上下載了一個文件(一個棋盤游戲規則列表),重命名為 test.pdf,放在 ~/Downloads/test.pdf。
該文件將發送到 /rag/ingest 端點(根據你的本地設置替換路徑):
http --form POST http://localhost:8080/rag/ingest path@/Users/jlong/Downloads/test.pdf
這可能需要幾秒鐘……
在后臺,數據被發送到 OpenAI,OpenAI 會為數據創建 embeddings;然后這些數據,包括向量和原始文本,都寫入到 Elasticsearch。
這些數據和所有的 embeddings 就是魔法所在。然后我們可以通過 VectorStore 接口查詢 Elasticsearch。
完整流程如下:
- HTTP 客戶端將你選擇的 PDF 上傳到 Spring 應用。
- Spring AI 負責從 PDF 提取文本,并將每頁分成 800 字符的塊。
- OpenAI 為每個塊生成向量表示。
- 分塊文本和向量都會存儲到 Elasticsearch。
最后,我們將發出一個查詢:
http :8080/rag/query question=="where do you place the reward card after obtaining it?"
然后我們會得到一個相關的答案:
After obtaining a Reward card, you place it facedown under the Hero card of the hero who received it.
Found at page: 28 of the manual
不錯!這整個過程是怎么工作的?
- HTTP 客戶端把問題提交給 Spring 應用。
- Spring AI 從 OpenAI 獲取問題的向量表示。
- 利用這個向量,它在 Elasticsearch 中存儲的文本塊里搜索相似文檔,并檢索最相似的文檔。
- 然后 Spring AI 把問題和檢索到的上下文發送給 OpenAI,生成大語言模型(LLM)的答案。
- 最后,它返回生成的答案和檢索到的上下文引用。
讓我們深入看看 Java 代碼,了解它到底是怎么工作的。
首先是 Main 類:它是任何普通 Spring Boot 應用的標準主類。
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args);}
}
沒什么特別的。繼續...
接下來,是一個基礎的 HTTP 控制器:
@RestController
class RagController {private final RagService rag;RagController(RagService rag) {this.rag = rag;}@PostMapping("/rag/ingest")ResponseEntity<?> ingestPDF(@RequestBody MultipartFile path) {rag.ingest(path.getResource());return ResponseEntity.ok().body("Done!");}@GetMapping("/rag/query")ResponseEntity<?> query(@RequestParam String question) {String response = rag.directRag(question);return ResponseEntity.ok().body(response);}
}
控制器只是調用了我們構建的一個服務,用來處理文件的攝取并寫入 Elasticsearch 向量存儲,然后方便地對同一個向量存儲進行查詢。
我們來看一下這個服務:
@Service
class RagService {private final ElasticsearchVectorStore vectorStore;private final ChatClient ai;RagService(ElasticsearchVectorStore vectorStore, ChatClient.Builder clientBuilder) {this.vectorStore = vectorStore;this.ai = clientBuilder.build();}void ingest(Resource path) {PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(path);List<Document> batch = new TokenTextSplitter().apply(pdfReader.read());vectorStore.add(batch);}// TBD
}
這段代碼處理所有的攝取:給定一個 Spring Framework 的 Resource,它是一個字節容器,我們使用 Spring AI 的 PagePdfDocumentReader 讀取 PDF 數據(假設是 .PDF 文件——確保在接受任意輸入之前盡量驗證!),然后用 Spring AI 的 TokenTextSplitter 進行分詞,最后將得到的 List 添加到向量存儲實現 ElasticsearchVectorStore 中。
你可以通過 Kibana 確認:在發送文件到 /rag/ingest 端點后,打開瀏覽器訪問 localhost:5601,然后在左側菜單中導航到 Dev Tools。在那里你可以發出查詢,和 Elasticsearch 實例中的數據交互。
現在是有趣的部分:我們如何根據用戶查詢把數據取回來?
下面是在名為 directRag 的方法中對查詢的初步實現。
String directRag(String question) {// Query the vector store for documents related to the questionList<Document> vectorStoreResult =vectorStore.doSimilaritySearch(SearchRequest.builder().query(question).topK(5).similarityThreshold(0.7).build());// Merging the documents into a single stringString documents = vectorStoreResult.stream().map(Document::getText).collect(Collectors.joining(System.lineSeparator()));// Exit if the vector search didn't find any resultsif (documents.isEmpty()) {return "No relevant context found. Please change your question.";}// Setting the prompt with the contextString prompt = """You're assisting with providing the rules of the tabletop game Runewars.Use the information from the DOCUMENTS section to provide accurate answers to thequestion in the QUESTION section.If unsure, simply state that you don't know.DOCUMENTS:""" + documents+ """QUESTION:""" + question;// Calling the chat model with the questionString response = ai.prompt().user(prompt).call().content();return response +System.lineSeparator() +"Found at page: " +// Retrieving the first ranked page number from the document metadatavectorStoreResult.getFirst().getMetadata().get(PagePdfDocumentReader.METADATA_START_PAGE_NUMBER) +" of the manual";}
代碼很簡單,但我們分幾步來看:
-
用 VectorStore 執行相似度搜索。
-
從所有結果中,獲取底層的 Spring AI Documents 并提取它們的文本,全部拼接成一個結果。
-
把 VectorStore 的結果連同指示模型如何處理它們的提示語和用戶的問題一起發給模型。等待響應并返回。
這就是 RAG(檢索增強生成)。意思是我們用向量庫中的數據來輔助模型的處理和分析。現在你知道怎么做了,希望你永遠不需要親自寫!至少不用像這樣寫:Spring AI 的 Advisors 可以讓這個過程更簡單。
Advisors 允許你對模型的請求做前處理和后處理,同時為你的應用和向量庫之間提供抽象層。在構建文件中添加以下依賴:
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
在類里添加另一個方法,叫 advisedRag(String question):
String advisedRag(String question) {return this.ai.prompt().user(question).advisors(new QuestionAnswerAdvisor(vectorStore)).call().content();
}
所有的 RAG 模式邏輯都封裝在 QuestionAnswerAdvisor 中。其他的就像對 ChatModel 的任何請求一樣!很棒!
結論
在這個演示中,我們使用了 Docker 鏡像,并且在本地機器上完成了所有操作,但目標是構建適合生產環境的 AI 系統和服務。你可以做很多事情來實現這個目標。
首先,你可以添加 Spring Boot Actuator 來監控 token 的消耗。token 是衡量模型請求復雜度(有時也是成本)的代理指標。
你已經在類路徑上添加了 Spring Boot Actuator,只需指定以下屬性來顯示所有指標(由出色的 Micrometer.io 項目捕獲):
management.endpoints.web.exposure.include=*
重啟你的應用。發起一次查詢,然后訪問:http://localhost:8080/actuator/metrics。搜索 “token”,你會看到應用使用的 token 信息。一定要關注這些信息。當然,你也可以使用 Micrometer 對 Elasticsearch 的集成,將這些指標推送過去,讓 Elasticsearch 成為你的時間序列數據庫!
你還應該考慮,每次請求像 Elasticsearch、OpenAI 或其他網絡服務時,都是 IO 操作,并且這些 IO 通常會阻塞執行的線程。Java 21 及以后版本帶來了非阻塞的虛擬線程,大幅提升了可擴展性。用以下方式啟用:
spring.threads.virtual.enabled=true
最后,你會想把你的應用和數據托管在一個能讓它們茁壯成長和擴展的地方。我們相信你可能已經考慮過在哪里運行你的應用,但你的數據會托管在哪里?我們推薦 Elastic Cloud。它安全、私密、可擴展且功能豐富。我們最喜歡的部分是,如果你愿意,可以選擇無服務器版本,由 Elastic 來負責報警,而不是你!
原文:Spring AI and Elasticsearch as your vector database - Elasticsearch Labs