?三、SpringAI
Spring AI | LangChain4j | |
Chat | 支持 | 支持 |
Function | 支持 | 支持 |
RAG | 支持 | 支持 |
對話模型 | 15+ | 15+ |
向量模型 | 10+ | 15+ |
向量數據庫 | 15+ | 20+ |
多模態模型 | 5+ | 1 |
JDK | 17 | 8 |
1. 對話機器人
1.1 快速入門
步驟①:引入依賴(先去掉openai的starter依賴,因為要配置API_KEY)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.3</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.itheima</groupId><artifactId>heima-ai</artifactId><version>0.0.1-SNAPSHOT</version><name>heima-ai</name><description>heima-ai</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>17</java.version><spring-ai.version>1.0.0-M6</spring-ai.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-ollama-spring-boot-starter</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
注意將lombok的版本改為1.18.30,否則可能出現下面的錯誤
java: java.lang.NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field 'com.sun.tools.javac.tree.JCTree qualid'
②配置模型
- 如果用的是ollama
spring:application:name: heima-aiai:ollama:base-url: http://localhost:11434chat:model: deepseek-r1:7b
- 如果用的是openai
spring:ai:openai:base-url: https://dashscope.aliyuncs.com/compatible-modeapi-key: ${OPENAI_API_KEY}chat:options:model: qwen-max # 模型名稱temperature: 0.8 # 模型溫度,值越大,輸出結果越隨機
③配置客戶端
package com.itheima.ai.config;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class CommonConfiguration {@Beanpublic ChatClient chatClient(OllamaChatModel model) {return ChatClient.builder(model).defaultSystem("你是一個熱心、可愛的智能助手,你的名字叫小團團,請以小團團的身份和語氣回答問題。").build();}
}
④發起調用
package com.itheima.ai.controller;import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {private final ChatClient chatClient;@RequestMapping("/chat")public String chat(String prompt) {return chatClient.prompt().user(prompt).call().content();}
}
⑤啟動HeimaAiApplication 和 ollama 測試
http://localhost:8080/ai/chat?prompt=%E4%BD%A0%E6%98%AF%E8%B0%81
?
再次改用流式輸出進行測試:
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")public Flux<String> chat(String prompt) {return chatClient.prompt().user(prompt).stream().content();}
?
1.2 會話日志
SpringAI利用AOP原理提供了AI會話時的攔截、增強等功能,也就是Advisor。
?
①修改CommonConfiguration?
package com.itheima.ai.config;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class CommonConfiguration {@Beanpublic ChatClient chatClient(OllamaChatModel model) {return ChatClient.builder(model).defaultSystem("你是一個熱心、可愛的智能助手,你的名字叫小團團,請以小團團的身份和語氣回答問題。").defaultAdvisors(new SimpleLoggerAdvisor(),).build();}
}
②在application.yaml添加日志級別
logging:level:org.springframework.ai: debugcom.itheima.ai: debug
2025-06-17T10:18:45.596+08:00 DEBUG 32052 --- [heima-ai] [oundedElastic-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor ? ?: request: AdvisedRequest[chatModel=org.springframework.ai.ollama.OllamaChatModel@614e1bca, userText=你是誰, systemText=你是一個熱心、可愛的智能助手,你的名字叫小團團,請以小團團的身份和語氣回答問題。, chatOptions=org.springframework.ai.ollama.api.OllamaOptions@6913c33b, media=[], functionNames=[], functionCallbacks=[], messages=[], userParams={}, systemParams={}, advisors=[org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$1@54c6b5de, org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$2@7a8b4de2, SimpleLoggerAdvisor, org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor@799e357a, org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$1@681ff1c8, org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$2@916c150], advisorParams={}, adviseContext={}, toolContext={}]
1.3 會話記憶
步驟①:把資料中的spring-ai-nginx壓縮包放到不含中文和特殊字符的文件夾下并解壓:
?
②通過cmd運行項目
http://localhost:5173/
?
?
③配置CORS策略
package com.itheima.ai.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class MvcConfiguration implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS").allowedHeaders("*").exposedHeaders("Content-Disposition");}
}
④測試
?
大模型是不具備記憶能力的,要想讓大模型記住之前聊天的內容,唯一的辦法就是把之前聊天的內容與新的提示詞一起發給大模型。
?
步驟①:定義會話存儲方式
- 方式1:使用Spring官方提供的InMemoryChatMemory
- 方式2:自己實現ChatMemory接口,把會話存儲到Redis、MongoDB等中
(1)方式1:InMemoryChatMemory
- ②配置會話記憶Advisor
package com.itheima.ai.config;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;@Configuration
public class CommonConfiguration {@Beanpublic ChatMemory chatMemory() {return new InMemoryChatMemory();}@Beanpublic ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {return ChatClient.builder(model).defaultSystem("你是一個熱心、可愛的智能助手,你的名字叫小團團,請以小團團的身份和語氣回答問題。").defaultAdvisors(new SimpleLoggerAdvisor(),new MessageChatMemoryAdvisor(chatMemory) // 會話記憶).build();}
}
- ③添加會話id
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")public Flux<String> chat(@RequestParam("prompt") String prompt,@RequestParam("chatId") String chatId) {return chatClient.prompt().user(prompt).advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)).stream().content();}
(2)方式2:實現ChatMemory接口,保存到Redis中
- 定義Msg實體類:
package com.itheima.ai.entity.po;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.*;import java.util.List;
import java.util.Map;@NoArgsConstructor
@AllArgsConstructor
@Data
public class Msg {MessageType messageType;String text;Map<String, Object> metadata;public Msg(Message message) {this.messageType = message.getMessageType();this.text = message.getText();this.metadata = message.getMetadata();}public Message toMessage() {return switch (messageType) {case SYSTEM -> new SystemMessage(text);case USER -> new UserMessage(text, List.of(), metadata);case ASSISTANT -> new AssistantMessage(text, metadata, List.of(), List.of());default -> throw new IllegalArgumentException("Unsupported message type: " + messageType);};}
}
- 定義ChatMemory實現類:
package com.itheima.ai.repository;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itheima.ai.entity.po.Msg;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.List;@RequiredArgsConstructor
@Component
public class RedisChatMemory implements ChatMemory {private final StringRedisTemplate redisTemplate;private final ObjectMapper objectMapper;private final static String PREFIX = "chat:";@Overridepublic void add(String conversationId, List<Message> messages) {if (messages == null || messages.isEmpty()) {return;}List<String> list = messages.stream().map(Msg::new).map(msg -> {try {return objectMapper.writeValueAsString(msg);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}).toList();redisTemplate.opsForList().leftPushAll(PREFIX + conversationId, list);}@Overridepublic List<Message> get(String conversationId, int lastN) {List<String> list = redisTemplate.opsForList().range(PREFIX + conversationId, 0, lastN);if (list == null || list.isEmpty()) {return List.of();}return list.stream().map(s -> {try {return objectMapper.readValue(s, Msg.class);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}).map(Msg::toMessage).toList();}@Overridepublic void clear(String conversationId) {redisTemplate.delete(PREFIX + conversationId);}
}
- 引入redis依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
- 配置會話記憶advisor:
package com.itheima.ai.config;import com.fasterxml.jackson.databind.ObjectMapper;
import com.itheima.ai.repository.RedisChatMemory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;@Configuration
public class CommonConfiguration {@Autowiredprivate StringRedisTemplate redisTemplate;private ObjectMapper objectMapper = new ObjectMapper();@Beanpublic ChatMemory chatMemory() {return new RedisChatMemory(redisTemplate, objectMapper);}@Beanpublic ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {return ChatClient.builder(model).defaultSystem("你是一個熱心、可愛的智能助手,你的名字叫小團團,請以小團團的身份和語氣回答問題。").defaultAdvisors(new SimpleLoggerAdvisor(),new MessageChatMemoryAdvisor(chatMemory) // 會話記憶).build();}
}
- 配置redis連接信息:
spring:data:redis:host: localhost
1.4 會話歷史
接口說明
查詢會話記錄列表 | 查詢會話記錄詳情 | |
請求方式 | GET | GET |
請求路徑 | /ai/history/{type} | /ai/history/{type}/{chatId} |
請求參數 | type: 業務類型 | type: 業務類型; chatId: 會話id |
返回值 | ["1234", "1246", "1248"] | [{role: "user", content: ""}] |
- 方式1:保存到JVM內存
- 方式2:保存到Redis
步驟①:定義會話歷史接口ChatHistoryRepository?
package com.itheima.ai.repository;import java.util.List;public interface ChatHistoryRepository {/*** 保存會話記錄* @param type 業務類型,如:chat、service、pdf* @param chatId 會話ID*/void save(String type, String chatId);/*** 獲取會話ID列表* @param type 業務類型,如:chat、service、pdf* @return 會話ID列表*/List<String> getChatIds(String type);
}
②創建實現類
package com.itheima.ai.repository;import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.stereotype.Component;import java.util.*;@Slf4j
@Component
@RequiredArgsConstructor
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {private Map<String, List<String>> chatHistory;private final ObjectMapper objectMapper;private final ChatMemory chatMemory;@Overridepublic void save(String type, String chatId) {/*if (!chatHistory.containsKey(type)) {chatHistory.put(type, new ArrayList<>());}List<String> chatIds = chatHistory.get(type);*/List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList<>());if (chatIds.contains(chatId)) {return;}chatIds.add(0, chatId);}@Overridepublic List<String> getChatIds(String type) {/*List<String> chatIds = chatHistory.get(type);return chatIds == null ? List.of() : chatIds;*/return chatHistory.getOrDefault(type, List.of());}
}
③請求模型前保存會話id
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")public Flux<String> chat(@RequestParam("prompt") String prompt,@RequestParam("chatId") String chatId) {// 1. 保存會話idchatHistoryRepository.save("chat", chatId);// 2. 請求模型return chatClient.prompt().user(prompt).advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)).stream().content();}
④定義MessageVo
package com.itheima.ai.entity.vo;import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.Message;@NoArgsConstructor
@Data
public class MessageVO {private String role;private String content;public MessageVO(Message message) {this.role = switch (message.getMessageType()) {case USER -> "user";case ASSISTANT -> "assistant";case SYSTEM -> "system";default -> "";};this.content = message.getText();}
}
⑤根據業務類型、會話id查詢會話歷史詳情
package com.itheima.ai.controller;import com.itheima.ai.entity.vo.MessageVO;
import com.itheima.ai.repository.ChatHistoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {private final ChatHistoryRepository chatHistoryRepository;private final ChatMemory chatMemory;/*** 查詢會話歷史列表* @param type 業務類型,如:chat,service,pdf* @return chatId列表*/@GetMapping("/{type}")public List<String> getChatIds(@PathVariable("type") String type) {return chatHistoryRepository.getChatIds(type);}/*** 根據業務類型、chatId查詢會話歷史詳情* @param type 業務類型,如:chat,service,pdf* @param chatId 會話id* @return 指定會話的歷史消息*/@GetMapping("/{type}/{chatId}")public List<MessageVO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {List<Message> messages = chatMemory.get(chatId, Integer.MAX_VALUE);if(messages == null) {return List.of();}return messages.stream().map(MessageVO::new).toList();}
}
⑥重啟進行HeimaAiApplication測試
⑦也可以選擇使用Redis來實現
package com.itheima.ai.repository;import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.Collections;
import java.util.List;
import java.util.Set;@RequiredArgsConstructor
@Component
public class RedisChatHistory implements ChatHistoryRepository{private final StringRedisTemplate redisTemplate;private final static String CHAT_HISTORY_KEY_PREFIX = "chat:history:";@Overridepublic void save(String type, String chatId) {redisTemplate.opsForSet().add(CHAT_HISTORY_KEY_PREFIX + type, chatId);}@Overridepublic List<String> getChatIds(String type) {Set<String> chatIds = redisTemplate.opsForSet().members(CHAT_HISTORY_KEY_PREFIX + type);if(chatIds == null || chatIds.isEmpty()) {return Collections.emptyList();}return chatIds.stream().sorted(String::compareTo).toList();}
}