三、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();}
}