Spring AI 学习文档
一、基础概念
1.1 系统提示词(System Prompt)
每次对话都会带上的一段隐藏指令,用户看不到,用来给 AI 定位:"你是智慧能源管理系统(EMS)的智能助手,请用中文简洁、专业的回答用户问题",这就是本项目 AiProperties 里的 systemPrompt。它决定了 AI 的"人设"和回答风格。
1.2 对话记忆
大模型天生没有记忆。这次问"我叫张三",下次问"我叫什么",它答不上来——因为每次调用都是独立的,它不记得上一轮说了啥。
ChatGPT 网页版之所以"记得住",是因为网页程序每次都把之前的对话记录重新发一遍给模型。Spring AI 的对话记忆干的就是这件事。
1.3 温度(Temperature)
模型的"发挥程度"参数,浮点数,一般范围 0~2,数值越低:越保守规矩,数值越高:越有创造性、表现欲强
实战建议0.5~0.8作为日常生产起点。
1.4 向量化(Embedding)
把一段文字变成一串数字(比如 1024 个小数),语义相近的文字,数字串也相近。
比如:"变压器故障" → [0.12, -0.35, 0.88, ...] "变压器维修" → [0.11, -0.33, 0.90, ...](很接近) "今天天气不错" → [-0.5, 0.72, -0.1, ...](差很远)
这样"找语义相近的内容"就变成了数学计算。这是 RAG 检索的核心原理
二、Spring AI 概述
2.1 定义
Spring AI 是 Spring 官方推出的 AI 应用开发框架,目标是把 AI 能力以 Spring 一贯的风格(依赖注入、自动配置、可移植抽象)带给 Java 开发者。它解决的核心问题是:
统一抽象:通过 ChatModel、EmbeddingModel、ImageModel、VectorStore 等接口,屏蔽不同厂商(OpenAI、Anthropic、Ollama、国内大模型等)的 API 差异,切换模型只需改配置、换依赖,几乎不改业务代码。
可移植性:同一个 ChatClient 调用,可跑在本地 Ollama,也可切到云端 GPT,只需改一行配置。
工程化能力:内置 Function Calling、RAG、对话记忆、结构化输出、Advisors 扩展链、MCP、可观测性等,让「能跑」变成「可维护、可观测」。
2.2 特性定义
三、依赖与配置
3.1 版本
3.2 依赖
引入Spring AI 依赖
<properties>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- BOM:Spring AI 全家桶的版本清单,引它之后再引具体模块就不用写版本号 -->
<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>模块配置引入模型依赖
<!-- 智谱大模型-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
</dependency>
<!-- 文档解析(RAG 导入 pdf/docx 用) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>3.3 配置详情
配置智谱大模型
spring:
ai:
zhipuai:
api-key: ${ZHIPUAI_API_KEY:}
chat:
options:
model: glm-4.5-air # 聊天模型
temperature: 0.7 # 发挥程度 0~1
embedding:
options:
model: embedding-2 # 向量模型(RAG 用)3.4 自定义业务配置
@ConfigurationProperties——把 yml 里的一段配置映射成 Java 对象,字段有默认值,yml 里写 ai.memory-window: 50 就能覆盖。
@Data
@ConfigurationProperties(prefix = "ai") // 绑定 yml 里 ai.* 前缀
public class AiProperties {
private String systemPrompt = "你是智慧能源管理系统(EMS)的智能助手..."; // 系统提示词
private String ragSystemPrompt = "...知识库问答助手..."; // RAG 提示词
private Integer memoryWindow = 20; // 记忆窗口:最多记 20 条消息
private Integer ragTopK = 4; // RAG 检索最多取 4 个片段
private String vectorStoreFile = "./data/ai/vector-store.json"; // 向量库落盘位置
}3.5 直连ChatModel测试
直接注入 starter 自动配好的 ChatModel:
@SpringBootTest
public class AiTest {
@Test
public void testChat(@Autowired ZhiPuAiChatModel chatModel) {
String content = chatModel.call("你是谁");
System.out.println(content);
}
@Test
public void testStream(@Autowired ZhiPuAiChatModel chatModel) {
Flux<String> stream = chatModel.stream("你是谁");
stream.toIterable().forEach(System.out::print); // 阻塞逐段输出
}
}3.6 ChatOptions 按次覆盖配置
yml 里的 spring.ai.zhipuai.chat.options.* 是全局默认。某次调用想用不同参数,可以按次覆盖(构造 Prompt 时传入)
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
.temperature(0.2) // 这次要严谨
.build();
ChatResponse res = chatModel.call(new Prompt("请写一句诗描述清晨。", options));
System.out.println(res.getResult().getOutput().getText());三个常用参数:
temperature 温度
maxTokens 限制生成的最大token数
stop 截断序列:输出中出现这些字符串就立刻停止,如下所示
stop序列的四种用法:
spring:
ai:
zhipuai:
chat:
options:
max-tokens: 20
stop:
- "\n" # 只要一行
- "。" # 只要一句话
- "政治" # 敏感词,出现即停
- "最后总结一下" # AI 惯用模板词,截掉让文风更拟人3.7 深度思考模型
思考的内容有个专业名词:Chain of Thought(CoT,思维链)。推理模型(如 DeepSeek 的 deepseek-reasoner)会把思考过程单独放在 reasoningContent 里返回,不过缺点是推理模型更慢更贵,复杂任务(数据分析、逻辑推理)才值得用,日常问答用普通模型。
ZhipuAiChatOptions options = ZhipuAiChatOptions.builder()
.model("glm-4.5")
.build();
ChatResponse res = zhipuAiChatModel.call(new Prompt("请写一句诗描述清晨。", options));
ZhipuAiAssistantMessage msg = (ZhipuAiAssistantMessage) res.getResult().getOutput();
String reasoning = msg.getReasoningContent(); // 思考过程(CoT)
String content = msg.getText(); // 最终回答四、对话开发
4.1 ChatModel与ChatClient的关系
ChatClient 提供了通用的 API,适用于所有大模型——面向它编程,就不需要为每一种模型学一套 API。系统提示词、格式化响应、聊天记忆、Tools 这些能力在 ChatClient 上都更易用、更优雅。ChatModel 更底层,只在做某些模型特有的个性化操作时才直接用它。
4.2 创建ChatClient的两种方式
方式一:注入ChatClient.Builder(自动装配)
ChatClient 自动配置与注入:ChatClient.Builder 由 Spring AI 自动配置并注入(基于 classpath 上的模型 starter),无需手动 new。ChatClient 是线程安全、可复用的,推荐在构造函数里用 Builder 构建一次、作为单例复用,避免每次请求重复创建。
@SpringBootTest
public class ChatClientTest {
@Test
public void testChatClient(ChatClient.Builder builder) {
ChatClient chatClient = builder.build();
String content = chatClient.prompt()
.user("Hello")
.call()
.content();
System.out.println(content);
}
}需要注意:如果同时引入了多个模型的starter,容器里有多条ChatModel,自动注入会失败,必须改用方式二。
方式二:手动指定ChatModel
@Test
public void testChatOptions(@Autowired ZhiPuAiChatModel chatModel) {
ChatClient chatClient = ChatClient.builder(chatModel).build();
String content = chatClient.prompt()
.user("Hello")
.call()
.content();
System.out.println(content);
}4.3 多模型动态切换管理
按请求选择用哪个模型,比如日常问答用便宜的flash模型,复杂分析用推理模型。
@Configuration
public class MultiModelConfig {
// 智谱推理模型,适合复杂分析
@Bean
public ChatClient zhipuReason() {
ZhipuAiApi zhipuAiApi = ZhipuAiApi.builder()
.apiKey(System.getenv("ZHIPUAI_KEY"))
.build();
ZhipuAiChatModel zhipuAiChatModel = ZhipuAiChatModel.builder()
.zhipuAiApi(zhipuAiApi)
.defaultOptions(ZhipuAiChatOptions.builder()
// glm-4.5 支持思维链
.model("glm-4.5")
.build())
.build();
return ChatClient.builder(zhipuAiChatModel).build();
}
// 智谱普通对话模型
@Bean
public ChatClient zhipuChat() {
ZhipuAiApi zhipuAiApi = ZhipuAiApi.builder()
.apiKey(System.getenv("ZHIPUAI_KEY"))
.build();
ZhipuAiChatModel zhipuAiChatModel = ZhipuAiChatModel.builder()
.zhipuAiApi(zhipuAiApi)
.defaultOptions(ZhipuAiChatOptions.builder()
// glm-4-flash 免费高速,日常对话首选
.model("glm-4-flash")
.build())
.build();
return ChatClient.builder(zhipuAiChatModel).build();
}
}请求时按参数路由,Spring会把容器里所有ChatClient Bean按Bean名收进Map
@RestController
public class MultiModelsController {
@Autowired
private Map<String, ChatClient> chatClientMap; // key = Bean 名
@GetMapping("/chat")
String generation(@RequestParam String message,
@RequestParam String model) {
ChatClient chatClient = chatClientMap.get(model);
return chatClient.prompt().user(message).call().content();
}
}五、同步对话与流式对话
5.1 同步对话
特点:
简单:一行拿到完整结果
慢:模型生成500字要好几秒
适合:后台任务,对响应速度不敏感的内部接口
5.2 流式对话
SSE(Server-Sent Events,服务器推送事件)基于 HTTP 协议,是一条单向长连接——由服务端持续向客户端推送数据。特点是:一次 HTTP 连接、服务器分多次往回写数据。与之对比,本例中 /chat/stream 通过 produces = text/event-stream 声明 SSE,配合 Flux<String> 逐条推送生成内容。想完整展示成可阅读的文本,前端需要手动拼接返回的分片内容。
@RestController
public class ChatController {
private final ChatClient chatClient;
// 注入 Builder,构建后可多次复用同一个 client
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
// 普通单轮对话
@GetMapping("/chat")
public String chat(@RequestParam("message") String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
// SSE 流式输出(逐 token 返回)
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam("message") String message) {
return chatClient.prompt()
.user(message)
.stream()
.content()
.doOnNext(chunk -> System.out.println("chunk===[" + chunk + "]"))
.concatWithValues("[DONE]");
}
//使用stream()方法,也可以不指定produces = text/event-stream,这样它就不是SSE流式输出了。后端内部是分片(大模型),HTTP 对外不是分片推送。即Spring Web 会把整个 Flux 收集完毕,把所有 chunk 拼接,HTTP 一次性返回完整字符串给前端。
}六、对话记忆
多轮对话需要把历史上下文回传给模型。Spring AI 把记忆拆成两层:逻辑层 ChatMemory + 存储层 ChatMemoryRepository,通过 MessageChatMemoryAdvisor 自动读写。
记忆的工作原理:所谓「让模型记住对话」,本质是——把全部历史多轮对话(用户提问和模型回答成对保存),加上当前最新提问,整体一起传给大模型;不是只带上一轮的回答。
第 1 轮 用户:「我叫张三」 → 模型回答「你好张三」(记忆存下 user + assistant 两条)
第 2 轮 用户:「我叫什么?」 → 请求携带:第1轮user + 第1轮assistant + 第2轮user
模型回答「你叫张三」 (再存下这一轮 assistant)
第 3 轮 用户:「帮我写邮件」 → 请求携带:前面所有历史 + 第3轮user每一轮交互结束,要把模型输出的 assistant 消息存入记忆,下一次请求就携带这一整套消息。缺点也由此而来:对话越多 token 越大,容易触发上下文超限,因此需要做记忆截断(滑动窗口 maxMessages)或摘要压缩。
6.1 内存记忆
// 逻辑层:滑动窗口记忆,最多保留最近 10 条消息
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(10)
.build();
// 挂载 Advisor,自动「读历史 → 调模型 → 存本轮」
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();补充两点:
① 「取多少」与「存多少」是两回事——MessageWindowChatMemory.maxMessages 控制存多少,MessageChatMemoryAdvisor 的 chatHistoryWindowSize 参数控制每次取多少条历史注入;
② 裁剪历史时,system 消息会被保留,不会随窗口滑动被丢弃。
6.2 conversationId隔离
绝不能把 conversationId 写死在 Bean 里,否则所有用户共享同一份记忆。正确做法是每次请求覆盖,记忆按 conversationId 分格子存:
@GetMapping("/chat")
public String chat(@RequestParam("userId") String userId, @RequestParam("message") String message) {
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, bo.getConversationId()))) // 按conversationId隔离
.call()
.content();
}6.3 持久化记忆
1、数据库存储对话记忆
默认情况, 对话内容会存在jvm内存会导致:
一直存最终会撑爆JVM导致OOM。
重启就丢了, 如果已想存储到第三方存储进行持久化
springAi内置提供了以下几种方式(例如 Cassandra、JDBC 或 Neo4j), 这里演示下JDBC方式
(1)添加依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>(2)添加配置\建表
spring.ai.chat.memory.repository.jdbc.initialize-schema=always
spring.ai.chat.memory.repository.jdbc.schema=classpath:/schema-mysql.sql
//application.yml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/springai?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&
driver-class-name: com.mysql.cj.jdbc.Driver(3)配置类
@Configuration
public class ChatMemoryConfig {
@Bean
ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository) {
return MessageWindowChatMemory
.builder()
.maxMessages(1)
.chatMemoryRepository(chatMemoryRepository).build();
}
}
2、Redis存储
若要用Redis存记忆,则需要自己实现ChatMemoryRepository接口,ChatMemoryRepository可以选择Cassandra、Neo4j、MongoDB、CosmosDB、Redis 等实现,按现有基础设施选型即可,上层 ChatMemory 抽象不变。
@Bean
public RedisChatMemoryRepository redisChatMemoryRepository() {
return RedisChatMemoryRepository._builder_()
.host(redisHost)
.port(redisPort)
.password(redisPassword)
.timeout(redisTimeout)
.build();
}6.4 多层次记忆架构
记忆多 = 更像人,但记忆多 = token 超限。无论换什么存储,发给模型的窗口就那么大。想"记更多"就要模仿人类,分层:
近期记忆:保留在上下文窗口中的最近几轮对话,每轮对话完成后立即存储(可通过ChatMemory); 10 条
中期记忆:通过RAG检索的相关历史对话(每轮对话完成后,异步将对话内容转换为向量并存入向量数据库) 5条
长期记忆:关键信息的固化总结
方式一:定时批处理
通过定时任务(如每天或每周)对积累的对话进行总结和提炼
提取关键信息、用户偏好、重要事实等
批处理方式降低计算成本,适合大规模处理
方式二:关键点实时处理
在对话中识别出关键信息点时立即提取并存储
例如,当用户明确表达偏好、提供个人信息或设置持久性指令时
采用"写入触发器"机制,在特定条件下自动更新长期记忆
6.5 会话管理与手动API
除了 Advisor 自动读写,ChatMemory 接口本身也提供手动操作,用于会话生命周期管理
// 手动预置上下文(对话开始前注入业务背景)
chatMemory.add(conversationId, List.of(new UserMessage("对方是储能用户,请在其储能专业的角度回答")));
// 读取历史
List<Message> history = chatMemory.get(conversationId);
// 清空会话(「开始新对话」按钮)
chatMemory.clear(conversationId);6.6 摘要压缩
maxMessages 截断是「丢弃」策略,会丢掉早期信息。需要保留更长历史时,用摘要压缩:定期把历史对话用模型总结成摘要,用摘要替代原始历史注入,大幅减少 token。
Spring AI 无内置摘要功能,可自定义实现,核心思路:
// 用模型生成摘要
String summary = chatClient.prompt()
.system("把下面的对话历史总结成 200 字以内的摘要,保留关键信息:")
.user(historyText) // historyText = 拼接后的历史对话
.call()
.content();七、提示词
提示词(Prompt)是引导模型行为的输入。Spring AI 把 Prompt 拆成消息(Message)与选项(Options)两部分,支持角色、模板、参数化。
7.1 角色消息
本项目中配置了systemPrompt为system消息,装配时通过.defaultSystem(...)挂载,每次调用自动携带
ChatClient.builder(chatModel)
.defaultSystem("你是智慧能源管理系统(EMS)的智能助手,请用中文简洁、专业地回答用户问题。")7.2 模板占位符
用 {变量} 占位,运行时注入,避免字符串拼接。ST4(StringTemplate v4)模板占位符 {变量名} 的变量标识符只能英文、数字、下划线,不能中文、不能空格
ChatClient 写法——.text() 放模板,.param() 填充占位符:
String answer = ChatClient.create(chatModel).prompt()
.user(u -> u
.text("列出5个{domain}行业常见风险点,并简单说明。")
.param("domain", "电化学储能电站"))
.call()
.content();ChatModel 写法——用 SystemPromptTemplate 手动渲染再组装:
String systemText = """
你是一个友好的 AI 助手,帮助人们寻找信息。
你的名字是 {name}。
你应该以 {voice} 的风格回复用户。
""";
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemText);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "小助手", "voice", "专业"));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
List<Generation> response = chatModel.call(prompt).getResults();7.3 自定义模板分隔符
默认占位符是 {xxx}。如果模板文本本身含大量花括号(比如要嵌 JSON 代码片段),会和占位符冲突,可以换成分隔符 <xxx>
// ChatClient 写法:指定 templateRenderer
String answer = ChatClient.create(chatModel).prompt()
.user(u -> u
.text("告诉我5部<composer>的电影")
.param("composer", "John Williams"))
.templateRenderer(StTemplateRenderer.builder()
.startDelimiterToken('<')
.endDelimiterToken('>')
.build())
.call()
.content();// ChatModel 写法:PromptTemplate.builder() 指定 renderer
PromptTemplate promptTemplate = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.template("告诉我5部<composer>的电影.")
.build();
String prompt = promptTemplate.render(Map.of("composer", "John Williams"));StTemplateRenderer——基于 StringTemplate 的渲染器(模块 spring-ai-template-st),默认 {xxx},可自定义起止分隔符。
八、RAG知识库
RAG = 先从知识库检索相关文档,再把这些文档作为上下文注入提示词,让模型基于真实资料回答,减少幻觉。
8.1 ETL数据摄入
三步走:读取 → 切分 → 写入向量库。其中「读取」用 TikaDocumentReader(底层 Apache Tika)解析 PDF/Word/PPT/HTML 等文件——RAG 知识库导入文件必须,需要先引入依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>TikaDocumentReader 这类普通解析器只提取文本,碰到图片既不报错、也不会识别,而是直接跳过——图片里承载的信息就丢了。框架不会自动帮助判断文档里有没有图片,需要业务层自己处理:图片带业务信息(如架构图、流程图、截图里的关键内容)时,要手动把图片抽出来,调用多模态模型转成文字描述,再参与切块和向量化;纯装饰的配图则无需处理。
// 1. 读取(支持 PDF/Word/HTML 等,底层用 Apache Tika)
var reader = new TikaDocumentReader("file:./docs/handbook.pdf");
// 2. 切分(按 token 分块,避免单块过长)
TokenTextSplitter splitter = new TokenTextSplitter(800, 350, 5, 10000, true);
// 3. 写入向量库(自动 embedding)
List<Document> chunks = splitter.apply(reader.get());
vectorStore.add(chunks);切分策略:长文档必须切成小块再入库,块大小直接决定检索质量。上面 TokenTextSplitter(800, 350, 5, 10000, true) 五个参数:
8.2 问答链路:QuestionAnswerAdvisor
最简单的方式,检索 + 注入 + 问答一气呵成。需要引入 spring-ai-advisors-vector-store 依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.topK(5)
.similarityThreshold(0.45)
.build())
.build();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(qaAdvisor)
.build();
// 提问时会自动检索并注入上下文
String answer = chatClient.prompt()
.user("我们的产品支持哪些支付方式?")
.call()
.content();8.3 RetrievalAugmentationAdvisor(RAG 流水线)
QuestionAnswerAdvisor 是"一步到位"的简化版。想精细控制 RAG 每个环节,用 RetrievalAugmentationAdvisor。
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
// 查(相当于 QuestionAnswerAdvisor 的检索部分)
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.similarityThreshold(0.5)
.build())
// 可选:检索为空时自定义兜底话术(默认直接把问题原样发给模型)
.queryAugmenter(ContextualQueryAugmenter.builder()
.allowEmptyContext(false)
.emptyContextPromptTemplate(PromptTemplate.builder()
.template("用户查询位于知识库之外,请礼貌告知无法回答").build())
.build())
// 可选:查询重写——用户表述模糊时,先让大模型改写成更适合检索的查询
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(chatModel))
.targetSearchSystem("能源管理知识库助手")
.build())
// 可选:检索后处理——拿到文档后还可以过滤、打日志、重排序
.documentPostProcessors((query, documents) -> {
return documents;
})
.build();8.4 元数据
切分后还可以用大模型给每个 chunk"加料",提升检索精度,按文档元数据缩小检索范围
String answer = chatClient.prompt()
.user("策略的政策是什么?")
.advisors(a -> a.param(
QuestionAnswerAdvisor.FILTER_EXPRESSION, "source == 'policy.md'"))
.call()
.content();技术手册(前后章节依赖)、教程、法律条款——单块语义不完整时,靠前后块摘要补上下文,为当前块及前后相邻块生成摘要存入元数据(PREVIOUS/CURRENT/NEXT 三种)
// 示例:给每个块生成前/中/后三段摘要
SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(chatModel,
List.of(SummaryMetadataEnricher.SummaryType.PREVIOUS,
SummaryMetadataEnricher.SummaryType.CURRENT,
SummaryMetadataEnricher.SummaryType.NEXT));
chunks = enricher.apply(chunks);九、Advisor机制
Advisor 是 Spring AI 的统一扩展点:它拦截每一次 ChatClient 调用,在请求发出前、响应返回后做增强。接下来要用的对话记忆、RAG、日志,本质都是 Advisor——理解它之后,这些能力就是「一条可插拔的链」。
9.1 请求/响应拦截链
每次 chatClient.prompt().call() 都会经过一条 Advisor 链:
请求(Prompt) ──► [ Advisor 链 ] ──► 调用大模型 ──► 响应(ChatResponse)
▲ │
└──── 响应再反向经过 Advisor ◄────┘每个 Advisor 可以在两个时机介入:
请求阶段(adviseCall):改写用户输入、注入历史/检索结果、附加系统提示等;
响应阶段(adviseResponse):改写模型输出、记录日志、写入记忆等。
通过 ChatClient 的 .defaultAdvisors(...)(默认)或 .advisors(...)(按请求)挂载。
9.2 Advisor 接口层级
自定义 Advisor 优先用 BaseAdvisor:只实现 before/after 就同时支持同步与流式。若只实现 CallAdvisor,.stream() 流式场景不会生效——这是最容易踩的坑。
9.3 内置Advisor:SimpleLoggerAdvisor
整个对话过程是个"黑盒",不利于调试。SimpleLoggerAdvisor 把实际发给模型的请求和收到的响应全部打进日志,配合日志级别
chatClient = ChatClient
.builder(chatModel)
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();9.4 组合与顺序
典型生产配置:记忆 → RAG → 日志,用 order 明确先后:
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).order(10).build(), // 1. 先注入历史
qaAdvisor, // 2. 再检索注入
new SimpleLoggerAdvisor()) // 3. 最后打印日志
.build();9.5 自定义Advisor
基于 BaseAdvisor 实现:请求阶段追加用户消息后缀,响应阶段可做后处理。
public class AppendSuffixAdvisor implements BaseAdvisor {
@Override
public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
// 给用户消息追加后缀(ChatClientRequest 不可变,用 mutate 生成新对象)
return request.mutate()
.prompt(request.prompt().augmentUserMessage("\n\n请用中文回答。"))
.build();
}
@Override
public ChatClientResponse after(ChatClientResponse response, AdvisorChain chain) {
// 响应阶段可在此记录日志、改写输出等
return response;
}
@Override
public int getOrder() {
return 0;
}
}augmentUserMessage,每一轮都会追加,会造成 prompt 越来越长。这里要特别注意:augmentUserMessage 如果设定不合理会容易改变prompt上下文,干扰大模型的判断,输出与预期结果不符的情况。只要是做分类、抽取、JSON 输出这类强格式约束业务,尽量不要用全局 Advisor 自动修改 prompt,极易破坏格式指令。
ChatClientRequest 是不可变 record,必须用 mutate().xxx().build() 生成新对象再返回;BaseAdvisor 同时覆盖同步与流式,无需再单独写 StreamAdvisor。
十、结语
在整理这份笔记的过程中,我系统学习了 Spring AI 的整套功能。我明白了大模型基础概念、模型配置、ChatClient 使用、会话记忆管理、提示词模板、RAG 检索增强以及 Advisor 扩展链。我发现,接入大模型不只是简单调用接口,上下文控制、幻觉处理、模型选型、token 开销都是生产环境需要重点考虑的。后续我会继续在储能 EMS 场景做验证,持续补充这份笔记。