当前位置: 首页 > news >正文

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: DEBUG

pom依赖

<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 AOPSpring 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作用
MessageChatMemoryAdvisorspring-ai-core把历史对话以 Message 形式注入 Prompt
VectorStoreChatMemoryAdvisorspring-ai-core把对话存入向量库(语义检索历史)
QuestionAnswerAdvisorspring-ai-rag经典 RAG:检索 → 拼到 system
RetrievalAugmentationAdvisorspring-ai-rag可拆解的 RAG(QueryTransformer / Retriever /DocumentJoiner / QueryAugmenter)
SafeGuardAdvisorspring-ai-core敏感词/黑名单阻断
SimpleLoggerAdvisorspring-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: DEBUG

pom依赖

<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 支持)或在排序上错开。


作者:筱白爱学习!!

欢迎关注转发评论点赞沟通,您的支持是筱白的动力!

http://www.cnnetsun.cn/news/3273664.html

相关文章:

  • wireshark 如何捕获信息
  • Python 实现基于 TCP 的聊天程序(Socket + threading + Tkinter + 4 节点并行)
  • vi/vim 编辑器使用指南与 Linux 常用文件操作命令
  • AI行业能力指数:基于真实场景的模型评估与选型指南
  • 15分钟速通SQL语法
  • 终极NVIDIA Profile Inspector汉化指南:3小时解锁显卡隐藏功能
  • 无人机视角航拍路面坑洼裂纹检测数据集VOC+YOLO格式4444张1类别
  • Kimi可以生成excel吗?AI导出鸭解锁AI表格导出新路径
  • NVIDIA Profile Inspector 深度解析:如何应对571.96驱动崩溃危机
  • 如何在Blender中实现专业级MMD工作流:MMD Tools插件的终极指南
  • 如何在Blender中实现MMD模型与动画的完美转换:MMD Tools终极指南
  • OBD-II/UDS 渗透测试(一):协议基础 — CAN 总线与 ISO-TP
  • VMware安装CentOS7教程:从下载镜像到网络设置_一步不落_新手零失败
  • 机器人行业面试全攻略:从0到1拿下心仪Offer,看这一篇就够了
  • 医用远程会诊系统:同屏会诊模块的设计与实现
  • Claude 复制内容带乱码太头疼?AI 导出鸭一键解决复制导出难题,程序员高效必备工具
  • 自定义图表:选用Highcharts能否支持动态定制图表
  • 抖音批量下载助手:三步轻松收藏心仪视频内容
  • JetBrains IDE试用期重置完整指南:高效管理开发工具的生命周期
  • Python 3.12 环境迁移:解决 venv 虚拟环境 pip 报错的 2 种可靠方法
  • Dify 工作流编排实战:用可视化 DAG 替代手写 Prompt 链的工程经验
  • AI短剧告别开盲盒!万兴3D导演台让创作者像搭积木一样精准控片
  • 【2013-11-10】Android应用开发笔记:分享至微博及友盟的基本使用
  • 2026 年 EDI 许可证办理服务商筛选指南:评测维度与机构能力分层对比
  • WVP-GB28181-Pro语音对讲功能深度解析与部署优化实践
  • 第一章Netty, NIO基础
  • 给 AI 长出一件新本事——插件不用敲,是聊出来的
  • SpringCloud服务治理实践
  • 拿来即用!C#上位机实现PLC远程控制:Modbus TCP协议+网口通信全解析
  • Istio EnvoyFilter 的高级用法:不该改的地方别手贱