做Java開發(fā)一年多,我最大的感受就是:AI開發(fā)的風(fēng)口真的來(lái)了。
從公司內(nèi)部的智能客服,到對(duì)外業(yè)務(wù)的AI助手,越來(lái)越多項(xiàng)目需要集成大模型能力。而在Java生態(tài)里,Spring AI無(wú)疑是最順手的框架——它不像Python那樣零散,能完美貼合我們熟悉的Spring開發(fā)習(xí)慣,從基礎(chǔ)到企業(yè)級(jí)場(chǎng)景都能覆蓋。
不過很多朋友跟我反饋:入門Demo看了不少,真要做項(xiàng)目的時(shí)候,還是會(huì)卡在幾個(gè)關(guān)鍵點(diǎn):
? RAG知識(shí)庫(kù)怎么搭才能檢索準(zhǔn)確?
? Function工具調(diào)用怎么寫才能自動(dòng)觸發(fā)?
? 像ChatGPT那樣的流式對(duì)話怎么實(shí)現(xiàn)?
? 不同場(chǎng)景要切換不同大模型,怎么做到動(dòng)態(tài)切換不重啟?
這篇文章我把四個(gè)最常用的進(jìn)階場(chǎng)景整理成了可運(yùn)行的實(shí)戰(zhàn)代碼,一步步講清楚,新手也能直接抄作業(yè)。
一、Spring AI RAG 知識(shí)庫(kù)實(shí)戰(zhàn):從能用變好用
RAG(檢索增強(qiáng)生成)絕對(duì)是企業(yè)落地AI最常見的場(chǎng)景——把公司的產(chǎn)品手冊(cè)、內(nèi)部文檔、歷史問答導(dǎo)入向量庫(kù),讓大模型基于私有知識(shí)回答,既不會(huì)胡說(shuō)八道,又能解決數(shù)據(jù)隱私問題。Spring AI對(duì)RAG的封裝非常友好,我們直接上代碼:
1. 環(huán)境準(zhǔn)備與依賴導(dǎo)入
我這里用最常用的PostgreSQL+PgVector做向量存儲(chǔ),導(dǎo)入依賴:
xml
<dependencies>
? ? <!-- Spring AI PgVector 向量庫(kù)支持 -->
? ? <dependency>
? ? ? ? <groupId>org.springframework.ai</groupId>
? ? ? ? <artifactId>spring-ai-pgvector-store</artifactId>
? ? </dependency>
? ? <!-- PostgreSQL驅(qū)動(dòng) -->
? ? <dependency>
? ? ? ? <groupId>org.postgresql</groupId>
? ? ? ? <artifactId>postgresql</artifactId>
? ? </dependency>
</dependencies>
2. 基礎(chǔ)配置
在application.yml中添加數(shù)據(jù)庫(kù)和大模型配置:
yaml
spring:
? datasource:
? ? url: jdbc:postgresql://localhost:5432/rag_knowledge
? ? username: postgres
? ? password: your_db_password
? ai:
? ? # 用OpenAI做嵌入生成,也可以換成文心、通義等其他模型
? ? openai:
? ? ? api-key: ${OPENAI_API_KEY}
? ? ? embedding:
? ? ? ? options:
? ? ? ? ? model: text-embedding-3-small
? ? # PgVector向量庫(kù)配置
? ? vectorstore:
? ? ? pgvector:
? ? ? ? index-name: product_knowledge_base
? ? ? ? dimension: 1536 # 和嵌入模型維度對(duì)應(yīng)
3. 核心代碼實(shí)現(xiàn)
實(shí)現(xiàn)文檔導(dǎo)入和RAG問答兩個(gè)核心接口:
java
@RestController
@RequestMapping("/rag")
public class RAGKnowledgeController {
? ? private final VectorStore vectorStore;
? ? private final ChatClient chatClient;
? ? // 自動(dòng)注入VectorStore和ChatClient
? ? public RAGKnowledgeController(VectorStore vectorStore, ChatClient.Builder chatClientBuilder) {
? ? ? ? this.vectorStore = vectorStore;
? ? ? ? this.chatClient = chatClientBuilder.build();
? ? }
? ? /**
? ? * 上傳文檔,拆分后導(dǎo)入向量庫(kù)
? ? */
? ? @PostMapping("/upload")
? ? public String uploadDocument(@RequestBody String content) {
? ? ? ? // 讀取文本,Spring AI自動(dòng)幫我們做拆分
? ? ? ? TextReader textReader = new StringTextReader(content);
? ? ? ? List<Document> splitDocuments = textReader.read();
? ? ? ? // 生成嵌入向量,寫入向量庫(kù)
? ? ? ? vectorStore.add(splitDocuments);
? ? ? ? return "導(dǎo)入成功,共拆分%s個(gè)文檔塊".formatted(splitDocuments.size());
? ? }
? ? /**
? ? * 基于RAG的知識(shí)庫(kù)問答
? ? */
? ? @GetMapping("/query")
? ? public String queryKnowledge(@RequestParam String question) {
? ? ? ? // 1. 語(yǔ)義檢索,撈出最相關(guān)的5個(gè)文檔塊
? ? ? ? List<Document> searchResults = vectorStore.similaritySearch(question, 5);
? ? ? ? // 2. 拼接上下文,構(gòu)造Prompt
? ? ? ? String context = searchResults.stream()
? ? ? ? ? ? ? ? .map(Document::getContent)
? ? ? ? ? ? ? ? .reduce("", (acc, doc) -> acc + "\n\n" + doc);
? ? ? ? String prompt = """
? ? ? ? ? ? ? ? 你是公司的產(chǎn)品客服助手,請(qǐng)嚴(yán)格基于下面給的上下文回答用戶的問題。
? ? ? ? ? ? ? ? 如果上下文中沒有找到答案,請(qǐng)直接回答"抱歉,這個(gè)問題我暫時(shí)無(wú)法解答",不要自己編造內(nèi)容。
? ? ? ? ? ? ? ? ------------------------------
? ? ? ? ? ? ? ? 上下文內(nèi)容:%s
? ? ? ? ? ? ? ? ------------------------------
? ? ? ? ? ? ? ? 用戶問題:%s
? ? ? ? ? ? ? ? """.formatted(context, question);
? ? ? ? // 3. 調(diào)用大模型生成回答
? ? ? ? return chatClient.call(prompt);
? ? }
}
4. RAG優(yōu)化小技巧
很多人搭完基礎(chǔ)RAG會(huì)覺得“效果不好”,其實(shí)只要做兩個(gè)小優(yōu)化就能提升很多:
? ? 自定義分段規(guī)則:不要用默認(rèn)的固定長(zhǎng)度拆分,長(zhǎng)文檔按照標(biāo)題、章節(jié)分段,把同一主題的內(nèi)容放在一個(gè)塊里,檢索精度能提升20%;
? ? 結(jié)果重排序:先用向量檢索撈出top20,再用輕量排序模型(比如BGE-reranker)對(duì)結(jié)果重排序,把最相關(guān)的內(nèi)容放到前面,效果提升非常明顯。
二、Function工具調(diào)用:讓AI能調(diào)用你的業(yè)務(wù)接口
大模型本身只能生成文字,要讓它幫你查數(shù)據(jù)庫(kù)、調(diào)接口、操作業(yè)務(wù)系統(tǒng),就需要用到Function Calling(工具調(diào)用)。Spring AI 1.x版本把這個(gè)能力簡(jiǎn)化到了極致,只需要加個(gè)注解就能用:
1. 定義工具函數(shù)
給你的業(yè)務(wù)方法加上@Tool注解,Spring AI會(huì)自動(dòng)掃描注冊(cè):
java
@Component
public class BusinessTools {
? ? @Tool(description = "查詢指定城市的當(dāng)前天氣氣溫")
? ? public String getCityWeather(
? ? ? ? ? ? @ToolParam(description = "要查詢的城市名稱,比如北京、上海") String city
? ? ) {
? ? ? ? // 這里調(diào)用第三方天氣API,返回真實(shí)結(jié)果
? ? ? ? return "當(dāng)前%s的氣溫是26攝氏度,天氣晴朗,紫外線中等".formatted(city);
? ? }
? ? @Tool(description = "查詢公司員工的工號(hào)和部門信息")
? ? public String getEmployeeInfo(
? ? ? ? ? ? @ToolParam(description = "員工姓名") String name
? ? ) {
? ? ? ? // 這里調(diào)用內(nèi)部員工數(shù)據(jù)庫(kù)查詢
? ? ? ? if ("張三".equals(name)) {
? ? ? ? ? ? return "張三的工號(hào)是00123,所屬部門是產(chǎn)品研發(fā)部";
? ? ? ? }
? ? ? ? return "未找到該員工的信息";
? ? }
}
2. 啟用工具調(diào)用
只需要在構(gòu)建ChatClient的時(shí)候默認(rèn)注入所有工具就可以了:
java
@RestController
@RequestMapping("/function")
public class FunctionCallController {
? ? private final ChatClient chatClient;
? ? // Spring會(huì)自動(dòng)把所有帶@Tool的工具注入進(jìn)來(lái)
? ? public FunctionCallController(ChatClient.Builder builder, List<ToolCallback> toolCallbacks) {
? ? ? ? this.chatClient = builder
? ? ? ? ? ? ? ? .defaultTools(toolCallbacks)
? ? ? ? ? ? ? ? .build();
? ? }
? ? @GetMapping("/chat")
? ? public String chat(@RequestParam String question) {
? ? ? ? // 大模型會(huì)自動(dòng)判斷是否需要調(diào)用工具,拿到結(jié)果后再生成自然語(yǔ)言回答
? ? ? ? return chatClient.call(question);
? ? }
}
就這么簡(jiǎn)單!當(dāng)用戶問“北京今天多少度?張三在哪個(gè)部門?”,大模型會(huì)自動(dòng)調(diào)用對(duì)應(yīng)的工具,拿到結(jié)果后再整理成自然語(yǔ)言回答,整個(gè)過程都是自動(dòng)處理的,完全不需要你手動(dòng)判斷。
三、流式對(duì)話:實(shí)現(xiàn)逐字輸出的聊天體驗(yàn)
傳統(tǒng)的問答是一次性返回完整回答,大模型生成長(zhǎng)回答的時(shí)候用戶要等好幾秒,體驗(yàn)特別差。流式對(duì)話就是像ChatGPT那樣,回答逐字輸出,用戶不用等,Spring AI只需要幾行代碼就能實(shí)現(xiàn):
java
@RestController
@RequestMapping("/stream")
public class StreamChatController {
? ? private final ChatClient chatClient;
? ? public StreamChatController(ChatClient.Builder builder) {
? ? ? ? this.chatClient = builder.build();
? ? }
? ? /**
? ? * 流式對(duì)話接口,返回SSE流
? ? */
? ? @GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
? ? public Flux<String> streamChat(@RequestParam String question) {
? ? ? ? // Spring AI直接返回Flux流,自動(dòng)處理分塊輸出
? ? ? ? return chatClient.stream(question);
? ? }
}
前端用EventSource對(duì)接就可以了,非常簡(jiǎn)單:
javascript
// 前端對(duì)接代碼示例
const responseContainer = document.getElementById('response');
const question = "請(qǐng)介紹一下Spring AI的核心優(yōu)勢(shì)";
const eventSource = new EventSource(`/api/stream/chat?question=${encodeURIComponent(question)}`);
// 收到消息就追加到頁(yè)面上
eventSource.onmessage = (event) => {
? ? responseContainer.innerHTML += event.data;
};
// 流結(jié)束關(guān)閉連接
eventSource.onclose = () => eventSource.close();
不管是做聊天機(jī)器人還是智能客服,流式對(duì)話基本都是標(biāo)配了,沒想到Spring AI實(shí)現(xiàn)起來(lái)這么簡(jiǎn)單吧?
四、多模型動(dòng)態(tài)切換:一套接口對(duì)接多個(gè)大模型
很多項(xiàng)目都會(huì)有切換模型的需求:比如簡(jiǎn)單問答用便宜的小模型降成本,復(fù)雜推理用能力強(qiáng)的大模型,國(guó)內(nèi)業(yè)務(wù)要用國(guó)產(chǎn)大模型,怎么做到不重啟服務(wù)動(dòng)態(tài)切換呢?
其實(shí)非常簡(jiǎn)單,Spring AI提供了ModelRegistry統(tǒng)一管理所有模型,我們只需要根據(jù)前端傳的模型ID動(dòng)態(tài)獲取就可以了:
java
@RestController
@RequestMapping("/model")
public class DynamicModelController {
? ? private final ModelRegistry modelRegistry;
? ? private final ChatClient.Builder chatClientBuilder;
? ? public DynamicModelController(ModelRegistry modelRegistry, ChatClient.Builder chatClientBuilder) {
? ? ? ? this.modelRegistry = modelRegistry;
? ? ? ? this.chatClientBuilder = chatClientBuilder;
? ? }
? ? @GetMapping("/chat")
? ? public String chat(
? ? ? ? ? ? @RequestParam String question,
? ? ? ? ? ? @RequestParam(defaultValue = "openai-gpt-3.5-turbo") String modelId
? ? ) {
? ? ? ? // 1. 根據(jù)模型ID從注冊(cè)中心獲取對(duì)應(yīng)的ChatModel
? ? ? ? ChatModel chatModel = modelRegistry.getChatModel(modelId)
? ? ? ? ? ? ? ? .orElseThrow(() -> new IllegalArgumentException("暫不支持該模型:" + modelId));
? ? ? ? // 2. 動(dòng)態(tài)構(gòu)建ChatClient,調(diào)用大模型
? ? ? ? ChatClient chatClient = chatClientBuilder
? ? ? ? ? ? ? ? .chatModel(chatModel)
? ? ? ? ? ? ? ? .build();
? ? ? ? return chatClient.call(question);
? ? }
}
配置文件里只需要把多個(gè)模型的配置都加上,Spring AI會(huì)自動(dòng)注冊(cè)到ModelRegistry:
yaml
spring:
? ai:
? ? # OpenAI配置
? ? openai:
? ? ? api-key: ${OPENAI_API_KEY}
? ? ? chat:
? ? ? ? options:
? ? ? ? ? model: gpt-3.5-turbo
? ? # 百度文心一言配置
? ? ernie:
? ? ? api-key: ${ERNIE_API_KEY}
? ? ? chat:
? ? ? ? options:
? ? ? ? ? model: ernie-3.5-8k
? ? # 阿里通義千問配置
? ? qwen:
? ? ? api-key: ${QWEN_API_KEY}
? ? ? chat:
? ? ? ? options:
? ? ? ? ? model: qwen-max
這樣就實(shí)現(xiàn)了一個(gè)接口對(duì)接多個(gè)大模型,需要哪個(gè)用哪個(gè),完全不用重啟服務(wù),靈活適配不同業(yè)務(wù)場(chǎng)景。
寫在最后:持續(xù)更新進(jìn)階教程
上面這四個(gè)場(chǎng)景——RAG知識(shí)庫(kù)搭建、Function工具調(diào)用、流式對(duì)話、多模型動(dòng)態(tài)切換,是我做Spring AI項(xiàng)目用得最多的進(jìn)階功能,所有代碼都是經(jīng)過實(shí)際項(xiàng)目驗(yàn)證的,大家可以直接拿去用。
接下來(lái)我會(huì)持續(xù)更新更多Spring AI進(jìn)階實(shí)戰(zhàn)內(nèi)容:
?? 企業(yè)級(jí)RAG怎么做文檔權(quán)限控制?
?? 多Agent協(xié)作系統(tǒng)怎么搭?
?? 怎么用Spring AI對(duì)接本地部署的大模型?
?? AI應(yīng)用怎么優(yōu)化延遲、降低成本?