SpringAI完整学习指南(三)
目录
为什么重要
接口与实现类
核心接口
内置实现
ChatMemory 工作流程
内存版MessageWindowChatMemory示例
JDBC 持久化示例
自定义Redis 实现示例
多会话隔离
二、ChatClient 与 Advisors 使用
ChatClient 的 3 种装配姿势
直接注入 Builder(最简单,每次请求动态配置)
全局默认 ChatClient Bean(推荐生产用法)
手工构建(不依赖自动配置)
ChatClient Fluent API
Advisor 概念
工作流程
两个核心接口
两个核心数据载体
为什么要 Advisor 而不是直接 @Aspect?
洋葱模型与 order
内置 Advisor
⚠️ 注意
Advisor对应Demo示例
application配置文件
pom依赖
6个Demo示例代码
Advisor 顺序怎么排(重要)
为什么重要
任何"多轮对话"或"用户级会话"都必须有 Memory,而生产环境**绝不能用 InMemory**(重启丢失、不能横向扩展)。Spring AI 提供多种持久化实现,直接决定能否上生产。
ChatMemory 是 Spring AI 提供的对话记忆抽象,用来在多轮对话中保留上下文历史,让模型"记住"之前聊过什么。
接口与实现类
interface ChatMemory ├── InMemoryChatMemory ◄── 默认,演示用 ├── JdbcChatMemory ◄── 关系型数据库 ├── CassandraChatMemory ◄── 高写入吞吐 ├── RedisChatMemory ◄── 配合 Redis Stack ├── JCacheChatMemory ◄── 任意 JCache 实现 └── SpringAIChatMemoryRepository ◄── 自定义仓储抽象生产环境通常不用内存版(重启丢失),而是接入 JDBC / Redis / Cassandra 仓储。
核心接口
public interface ChatMemory { void add(String conversationId, List<Message> messages); // 追加消息 List<Message> get(String conversationId); // 取出该会话的全部历史 void clear(String conversationId); // 清空指定会话 }关键点:按 conversationId 隔离。每个用户/每段会话用不同的 ID(比如 userId、sessionId),互不干扰。
内置实现
| 实现 | 说明 |
|---|---|
| MessageWindowChatMemory | 滑动窗口,只保留最近 N 条消息(默认20),超出自动裁剪,避免 token 爆掉 |
| MessageWindowChatMemory.builder().maxMessages(50)... | 自定义窗口大小 |
ChatMemory 工作流程
用户提问 "继续刚才话题" │ ▼ MessageChatMemoryAdvisor#before │ │ 1. conversationId 取历史 ▼ ChatMemory.get(conversationId) │ │ 2. 注入到 Prompt ▼ ChatModel.call │ │ 3. 拿到响应 ▼ MessageChatMemoryAdvisor#after │ │ 4. 追加 user + assistant 消息 ▼ ChatMemory.add(conversationId, [user, assistant]) │ │ 5. 触发持久化 (JDBC/Redis/...) ▼ 落库内存版MessageWindowChatMemory示例
public GlmController(ChatModel chatModel) { // 1. 创建 ChatMemory(滑动窗口,保留最近 10 条) ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(10) .build(); // 2. 把 ChatMemory 挂到 Advisor MessageChatMemoryAdvisor memoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory) .build(); // 3. 构建 ChatClient this.chatClient = ChatClient.builder(chatModel) .defaultAdvisors(memoryAdvisor) .build(); } @GetMapping("/chatMemory") public String chatMemory(@RequestParam String conversationId,@RequestParam String userMessage) { return chatClient.prompt() .user(userMessage) // ★ 关键:指定本次请求归属哪个会话 .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); }提问1:(使用的会话ID:user-001)
提问2:(使用的会话ID:user-001)
提问3:(使用的会话ID:user-002)
由此可见,使用同一个会话时,再次提问是可以记住历史对话内容的,并且可以看到会话之间是没有任何关联,体现了会话的隔离性
JDBC 持久化示例
POM.xml依赖
<!-- 包含 JdbcChatMemoryRepositoryAutoConfiguration,自动装配 Bean + 解析配置 + 执行 schema初始化 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency><!-- 这个依赖只有实现类,没有 auto-configuration,不注册 Bean,不跑建表脚本 (无法连接数据库使用) --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-model-chat-memory-repository-jdbc</artifactId> </dependency>application.yml 配置文件
spring: sql: init: mode: always # ★ 关键!always=总是执行;embedded=仅内嵌库;never=从不 continue-on-error: false ai: chat: memory: repository: jdbc: initialize-schema: always # 枚举值:always / embedded / never。true 无效! schema: classpath:test.sql retry: max-attempts: 4 zhipuai: api-key: ${GLM_API_KEY} # 在 bigmodel.cn 控制台获取 base-url: https://open.bigmodel.cn/api/paas chat: options: model: glm-5 temperature: 0.7 max-tokens: 2000 datasource: url: jdbc:mysql://localhost:3306/test username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver logging: level: org.springframework.jdbc.core: DEBUG org.springframework.jdbc.datasource.init: DEBUG org.springframework.ai.chat.memory.repository.jdbc: DEBUG com.zaxxer.hikari: INFO接口实现
public GlmController(ChatModel chatModel, ChatMemoryRepository repository) { // 用仓储支撑 ChatMemory,重启后历史还在 ChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(repository) // ★ 注入 JDBC 仓储 .maxMessages(20) .build(); this.chatClient = ChatClient.builder(chatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); } @GetMapping("/chatMemoryJDBC") public String chatMemoryJDBC(@RequestParam String conversationId,@RequestParam String userMessage) { return chatClient.prompt() .user(userMessage) // ★ 关键:指定本次请求归属哪个会话 .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); }自动建表 SQL(MySQL):
CREATE TABLE SPRING_AI_CHAT_MEMORY ( conversation_id VARCHAR(36) NOT NULL, content TEXT NOT NULL, type VARCHAR(10) NOT NULL, -- SYSTEM/USER/ASSISTANT/TOOL "timestamp" TIMESTAMP NOT NULL ); CREATE INDEX idx_conv ON SPRING_AI_CHAT_MEMORY(conversation_id);自定义Redis 实现示例
application.yml 配置文件:
spring: data: redis: host: 127.0.0.1 port: 6379 password: # 没密码留空 database: 0 timeout: 5000ms lettuce: pool: max-active: 8 max-idle: 4 ai: retry: max-attempts: 4 zhipuai: api-key: ${GLM_API_KEY} # 在 bigmodel.cn 控制台获取 base-url: https://open.bigmodel.cn/api/paas chat: options: model: glm-5 temperature: 0.7 max-tokens: 2000 logging: level: org.springframework.data.redis: INFO com.study.chatmemory: DEBUGpom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--Lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency>自定义RedisChatMemory
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.memory.ChatMemoryRepository; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 自定义 Redis ChatMemory 仓储 * * 数据结构: * 会话消息列表:String key = {PREFIX}{conversationId},value = JSON 数组,TTL = TTL_DURATION * 会话 ID 集合:Set key = {IDS_KEY},用于支持 findConversationIds() * * Message 多态处理:序列化时记录 type(SYSTEM/USER/ASSISTANT),反序列化时按 type 还原对应子类 */ @Slf4j @Component @RequiredArgsConstructor public class RedisChatMemoryRepository implements ChatMemoryRepository { private static final String PREFIX = "chat:memory:"; private static final String IDS_KEY = "chat:memory:ids"; private static final Duration TTL_DURATION = Duration.ofDays(1); private final StringRedisTemplate redis; private final ObjectMapper objectMapper; @Override public List<Message> findByConversationId(String conversationId) { String key = PREFIX + conversationId; String json = redis.opsForValue().get(key); if (json == null || json.isEmpty()) { return Collections.emptyList(); } try { List<MessageDTO> dtos = objectMapper.readValue(json, new TypeReference<List<MessageDTO>>() {}); List<Message> messages = new ArrayList<>(dtos.size()); for (MessageDTO dto : dtos) { messages.add(toMessage(dto)); } return messages; } catch (Exception e) { log.warn("反序列化会话消息失败, conversationId={}, err={}", conversationId, e.getMessage()); return Collections.emptyList(); } } @Override public void saveAll(String conversationId, List<Message> messages) { if (messages == null || messages.isEmpty()) { deleteByConversationId(conversationId); return; } List<MessageDTO> dtos = new ArrayList<>(messages.size()); for (Message m : messages) { dtos.add(toDTO(m)); } String key = PREFIX + conversationId; try { String json = objectMapper.writeValueAsString(dtos); redis.opsForValue().set(key, json, TTL_DURATION); redis.opsForSet().add(IDS_KEY, conversationId); } catch (Exception e) { log.error("保存会话消息失败, conversationId={}, err={}", conversationId, e.getMessage()); } } @Override public List<String> findConversationIds() { return new ArrayList<>(redis.opsForSet().members(IDS_KEY)); } @Override public void deleteByConversationId(String conversationId) { redis.delete(PREFIX + conversationId); redis.opsForSet().remove(IDS_KEY, conversationId); } private MessageDTO toDTO(Message m) { MessageDTO dto = new MessageDTO(); if (m instanceof SystemMessage) { dto.setType("SYSTEM"); } else if (m instanceof AssistantMessage) { dto.setType("ASSISTANT"); } else { dto.setType("USER"); } dto.setContent(m.getText()); return dto; } private Message toMessage(MessageDTO dto) { String content = dto.getContent() == null ? "" : dto.getContent(); switch (dto.getType()) { case "SYSTEM": return new SystemMessage(content); case "ASSISTANT": return new AssistantMessage(content); case "USER": default: return new UserMessage(content); } } /** * 消息传输对象:只保留 type + content,避免直接序列化 Message 接口丢类型信息 */ @lombok.Data public static class MessageDTO { private String type; private String content; } }Demo接口示例
public GlmController(ChatModel chatModel, ChatMemoryRepository repository) { // repository 由自定义 RedisChatMemoryRepository 注入(@Component 自动装配) ChatMemory chatMemory = MessageWindowChatMemory.builder() .chatMemoryRepository(repository) // ★ 注入 Redis 仓储 .maxMessages(20) .build(); this.chatClient = ChatClient.builder(chatModel) .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); } @GetMapping("/chatMemoryRedis") public String chatMemoryRedis(@RequestParam String conversationId,@RequestParam String userMessage) { return chatClient.prompt() .user(userMessage) // ★ 关键:指定本次请求归属哪个会话 .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); }多会话隔离
关键代码说明
String resp = chatClient.prompt() .user(msg) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId)) // 动态指定会话 .call() .content();二、ChatClient 与 Advisors 使用
ChatClient 的 3 种装配姿势
Spring Boot 自动配置会注入一个 ChatClient.Builder Bean,你可以直接用,也可以在 @Configuration里自定义一个全局 ChatClient Bean
直接注入 Builder(最简单,每次请求动态配置)
@RestController public class HelloController { private final ChatClient.Builder builder; public HelloController(ChatClient.Builder builder) { this.builder = builder; } @GetMapping("/hi") public String hi(@RequestParam String q) { return builder.build() .prompt() .user(q) .call() .content(); // 直接取文本 } }全局默认 ChatClient Bean(推荐生产用法)
@Configuration public class ChatClientConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory, VectorStore vectorStore) { return builder .defaultSystem("你是一名资深 Java 工程师,回答简洁、可执行") .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory) .conversationId("default") .build(), QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder().topK(4).build()) .build(), new SimpleLoggerAdvisor() ) .build(); } }注入时直接用成品:
@RestController public class AskController { private final ChatClient chatClient; public AskController(ChatClient chatClient) { // 注入成品 this.chatClient = chatClient; } @GetMapping("/ask") public String ask(@RequestParam String q) { return chatClient.prompt().user(q).call().content(); } }手工构建(不依赖自动配置)
ChatModel chatModel = new OpenAiChatModel(apiKey, baseUrl, options); ChatClient chatClient = ChatClient.builder(chatModel).build();ChatClient Fluent API
String result = chatClient.prompt() .system(s -> s.param("lang", "Java")) // 动态系统消息 .user(u -> u.text("写一个 {lang} 单例").param("lang", "Java")) .advisors(a -> a.param("conversationId", "u-001")) // 单次调用追加 advisor 参数 .tools(myToolBean) // 工具 .call() // 同步;流式用 .stream() .content();| API | 作用 |
|---|---|
| .prompt() | 入口 |
| .system(...) | 设置系统消息(可带模板变量) |
| .user(...) | 设置用户消息 |
| .tools(...) | 注册函数(Function Calling) |
| .advisors(Consumer<AdvisorSpec>) | 单次覆盖/追加 advisor 与参数 |
| .call() | 同步调用,返回 CallResponseSpec |
| .stream() | 流式调用,返回 Flux<ChatResponse> |
| .content() | 取出 String |
| .chatResponse() | 取出完整 ChatResponse(含 metadata/usage) |
| .entity(Class<T>) | 直接反序列化为对象(结构化输出) |
Advisor 概念
Advisor 是 AOP 风格拦截器,围绕 ChatClient 请求/响应进行横切增强:日志、记忆、RAG、安全、重试。
它允许你在两个切面上介入:
请求前(before):发给大模型之前,读取 / 改写 Prompt
响应后(after):收到大模型响应之后,读取 / 改写 Response
工作流程
ChatClient.call() │ ▼ ┌──────────────────────────┐ │ Advisor1.before (改写) │ ├──────────────────────────┤ │ Advisor2.before │ ├──────────────────────────┤ │ ChatModel.call │ ├──────────────────────────┤ │ Advisor2.after (加工) │ ├──────────────────────────┤ │ Advisor1.after │ └──────────────────────────┘ │ ▼ 返回两个核心接口
public interface CallAdvisor extends Ordered { String getName(); AdvisedResponse adviseCall(AdvisedRequest request, CallAdvisorChain chain); } public interface StreamAdvisor extends Ordered { String getName(); Flux<AdvisedResponse> adviseStream(AdvisedRequest request, StreamAdvisorChain chain); }两个核心数据载体
AdvisedRequest:拦截到的请求(含 userText、systemText、chatOptions、adviseContext、conversationId 等)。AdvisedRequest.from(req).userText(...).build() 可以改写请求。
AdvisedResponse:包装了 ChatResponse + adviseContext。可以在 after 阶段修改响应。
adviseContext:Map<String, Object>,整个链共享的上下文,可在 advisor 之间传值。
为什么要 Advisor 而不是直接 @Aspect?
| 维度 | Spring AOP | Spring AI Advisor |
|---|---|---|
| 拦截目标 | 任意 Bean 方法 | 仅 LLM 调用链 |
| 类型化上下文 | 难(参数反射) | 强类型 AdvisedRequest |
| 同步/流式统一 | 难 | 一套 API |
| ChatClient Fluent 集成 | 无 | .advisors(a -> ...) 单次覆盖 |
| 与 ChatMemory/VectorStore 联动 | 需手写 | 内置实现 |
洋葱模型与 order
- getOrder() 返回值越小越外层。 - before 逻辑按 order 升序执行,after 逻辑按 order 降序执行。 order=0 Logger before ──► │ order=1 Memory before ──► │ │ order=2 RAG before ──► │ │ │ ChatModel.call order=2 RAG after ◄── │ │ │ order=1 Memory after ◄── │ │ order=0 Logger after ◄── │内置 Advisor
| Advisor | 包 | 作用 |
|---|---|---|
| MessageChatMemoryAdvisor | spring-ai-core | 把历史对话以 Message 形式注入 Prompt |
| VectorStoreChatMemoryAdvisor | spring-ai-core | 把对话存入向量库(语义检索历史) |
| QuestionAnswerAdvisor | spring-ai-rag | 经典 RAG:检索 → 拼到 system |
| RetrievalAugmentationAdvisor | spring-ai-rag | 可拆解的 RAG(QueryTransformer / Retriever /DocumentJoiner / QueryAugmenter) |
| SafeGuardAdvisor | spring-ai-core | 敏感词/黑名单阻断 |
| SimpleLoggerAdvisor | spring-ai-core | 默认 INFO 日志 |
⚠️ 注意
Advisor 只能配合 ChatClient 使用,底层 ChatModel 没有这个能力。
Advisor对应Demo示例
application配置文件
server: port: 8080 spring: # Redis 连接(向量库和缓存共用) data: redis: host: 127.0.0.1 port: 6379 password: # 没密码留空 database: 0 timeout: 5000ms # lettuce: # pool: # max-active: 8 # max-idle: 4 client-type: jedis # ← 关键!强制用 Jedis jedis: pool: max-active: 8 max-idle: 4 sql: init: mode: always # ★ 关键!always=总是执行;embedded=仅内嵌库;never=从不 continue-on-error: false # MySQL 业务库 datasource: url: jdbc:mysql://127.0.0.1:3306/test username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver ai: retry: max-attempts: 4 zhipuai: api-key: {改为自己的key} base-url: https://open.bigmodel.cn/api/paas chat: options: model: glm-5 temperature: 0.7 max-tokens: 2000 embedding: # RAG 需要嵌入 options: model: embedding-3 # ① MySQL 持久化 ChatMemory chat: memory: repository: jdbc: initialize-schema: always # 非 H2 必须改为 always schema: classpath:test.sql # Redis 向量库配置 vectorstore: redis: # Redis Stack 作为向量库 index-name: chat-memory-indexe prefix: "vec:" initialize-schema: false logging: level: org.springframework.data.redis: INFO org.springframework.ai.chat.memory: DEBUG org.springframework.ai.chat.memory.jdbc: DEBUG org.springframework.jdbc: DEBUGpom依赖
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 保留其一即可 --> <!-- 原生 --> <!-- <dependency>--> <!-- <groupId>org.springframework.ai</groupId>--> <!-- <artifactId>spring-ai-starter-model-openai</artifactId>--> <!-- </dependency>--> <!-- 智普的 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-zhipuai</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--Lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!--MySQL 持久化 ChatMemory--> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <!--Spring Data Redis(自定义缓存/限流 Advisor 用)--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Redis Stack 作为向量库(RAG) --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-redis</artifactId> </dependency> <!-- ⑤ QuestionAnswerAdvisor(1.0 GA 后改名,新版叫 spring-ai-vector-store-advisor) --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-advisors-vector-store</artifactId> </dependency> </dependencies>6个Demo示例代码
package com.study.controller; import com.study.config.TimingAdvisor; import com.study.config.TraceAdvisor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SafeGuardAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * Advisors Demo 集合(对应《Advisors使用指南.md》里的各 Demo) * * 设计:每个端点用一个「只挂单个 Advisor」的独立 ChatClient,互不干扰,方便对照学习。 * 所有 ChatClient 都基于已注入的 ChatModel 重新构建(不复用 ChatConfig 里挂全套 Advisor 的那个)。 * * 端点速查: * Demo 0 日志 GET /demo/logger?q=你好 * Demo 1 记忆 GET /demo/memory?sessionId=1&msg=我叫张三 (再用 /demo/memory?sessionId=1&msg=我叫什么 验证记忆) * Demo 2 RAG 先 GET /demo/rag/ingest 灌知识,再 GET /demo/rag?q=退票要扣多少钱 * Demo 3 敏感词 GET /demo/safeguard?q=我的密码是多少 * Demo 5 自定义Base GET /demo/trace?q=你好 * Demo 6 自定义Call GET /demo/timing?q=你好 * * 前置条件: * - Demo 1 需要 MySQL(test 库,spring_ai_chat_memory 表由 jdbc starter 自动建)。 * - Demo 2 需要 Redis Stack(带 RediSearch 模块)+ embedding 模型可用。 */ @RestController @RequestMapping("/demo") public class AdvisorDemoController { private final ChatClient loggerClient; // Demo 0 private final ChatClient memoryClient; // Demo 1 private final ChatClient safeGuardClient; // Demo 3 private final ChatClient traceClient; // Demo 5 private final ChatClient timingClient; // Demo 6 private final ChatClient ragClient; // Demo 2 private final VectorStore vectorStore; public AdvisorDemoController(ChatModel chatModel, ChatMemory chatMemory, VectorStore vectorStore) { this.vectorStore = vectorStore; // Demo 0:SimpleLoggerAdvisor(打印请求/响应) this.loggerClient = ChatClient.builder(chatModel) .defaultAdvisors(new SimpleLoggerAdvisor()) .build(); // Demo 1:MessageChatMemoryAdvisor(复用 JDBC 持久化的 chatMemory Bean) MessageChatMemoryAdvisor memoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory).build(); this.memoryClient = ChatClient.builder(chatModel) .defaultSystem("你是一名资深 Java 工程师,回答简洁") .defaultAdvisors(memoryAdvisor) .build(); // Demo 3:SafeGuardAdvisor(命中敏感词直接拦截) this.safeGuardClient = ChatClient.builder(chatModel) .defaultAdvisors(new SafeGuardAdvisor(List.of("密码", "身份证号"))) .build(); // Demo 5:自定义 BaseAdvisor(before/after 模板) this.traceClient = ChatClient.builder(chatModel) .defaultAdvisors(new TraceAdvisor()) .build(); // Demo 6:自定义 CallAdvisor(计时,复用项目已有的 TimingAdvisor) this.timingClient = ChatClient.builder(chatModel) .defaultAdvisors(new TimingAdvisor()) .build(); // Demo 2:QuestionAnswerAdvisor(RAG) QuestionAnswerAdvisor ragAdvisor = QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(SearchRequest.builder().topK(4).build()) .build(); this.ragClient = ChatClient.builder(chatModel) .defaultSystem("你是航空客服,请依据知识库回答") .defaultAdvisors(ragAdvisor) .build(); } // ===================== Demo 0:SimpleLoggerAdvisor ===================== @GetMapping("/logger") public String logger(@RequestParam String q) { return loggerClient.prompt().user(q).call().content(); } // ===================== Demo 1:MessageChatMemoryAdvisor ===================== // 测试多轮记忆:先用同一个 sessionId 说一句话,再问"我叫什么" @GetMapping("/memory") public String memory(@RequestParam String sessionId, @RequestParam String msg) { return memoryClient.prompt() .user(msg) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId)) // 同 sessionId 才共享历史 .call() .content(); } // ===================== Demo 2:QuestionAnswerAdvisor(RAG) ===================== // 先灌入知识库(内部自动 embedding + 入向量库) @GetMapping("/rag/ingest") public String ingest() { vectorStore.add(List.of( new Document("取消预订:最晚起飞前 48 小时取消;经济舱扣费 75 美元,豪华经济舱 50 美元,商务舱 25 美元。"), new Document("更改预订:起飞前 24 小时内可改签;经济舱 50 美元,商务舱免费。"), new Document("退款将在取消后 7 个工作日内处理。") )); return "知识已写入向量库,可以开始提问了"; } @GetMapping("/rag") public String rag(@RequestParam String q) { return ragClient.prompt().user(q).call().content(); } // ===================== Demo 3:SafeGuardAdvisor ===================== // 试 q=你好(正常);再试 q=我的密码是多少(命中"密码"被拦截) @GetMapping("/safeguard") public String safeGuard(@RequestParam String q) { return safeGuardClient.prompt().user(q).call().content(); } // ===================== Demo 5:自定义 BaseAdvisor ===================== @GetMapping("/trace") public String trace(@RequestParam String q) { return traceClient.prompt().user(q).call().content(); } // ===================== Demo 6:自定义 CallAdvisor(计时) ===================== @GetMapping("/timing") public String timing(@RequestParam String q) { return timingClient.prompt().user(q).call().content(); } }Advisor 顺序怎么排(重要)
执行顺序由getOrder()决定,不是defaultAdvisors(...)列表里的书写顺序。推荐排布:
| 顺序(order 从小到大) | Advisor | 原因 |
|---|---|---|
| 最外(最小) | MessageChatMemoryAdvisor | 最先读历史、最后写响应 |
| 次外 | SimpleLoggerAdvisor/TimingAdvisor | 包住整条链做日志/计时 |
| 中间 | SafeGuardAdvisor | 在记忆之后、模型之前过滤 |
| 靠内 | QuestionAnswerAdvisor/RetrievalAugmentationAdvisor | 紧贴模型,做 RAG 拼接 |
若想强制顺序,给自定义 Advisor 实现
getOrder()返回明确值;内置 Advisor 也可.builder().order(x).build()(若该 Builder 支持)或在排序上错开。
作者:筱白爱学习!!
欢迎关注转发评论点赞沟通,您的支持是筱白的动力!
