Spring AI Advisor 实战:优雅打印大模型请求与响应日志
在开发 AI 应用的时候,我们经常需要知道:
用户到底给大模型发了什么?
System Prompt 是什么?
最终传给模型的完整请求是什么?
大模型最后又返回了什么?
尤其是项目上线以后,一旦出现:
回答不符合预期
提示词没有生效
上下文错乱
模型返回异常
如果没有日志,排查起来会非常痛苦。
所以这一篇我们就来看看:
Spring AI 中如何使用 Advisor,统一记录大模型的请求和响应日志。
一、为什么不能直接在 Controller 里面打印?
最简单的方式当然是:
log.info("用户问题:{}", message);
调用结束后再打印:
log.info("模型回答:{}", response);
普通同步接口这么干问题不大。
但是 AI 应用还有一个非常常见的场景:
流式输出
例如:
Flux<String>
大模型可能不是一次把:
你好,我是一个 AI 助手。
全部返回。
而是分成很多小片段:
你
好
,
我
是
一
个
AI
助
手
。
如果每收到一个片段就打印一次日志,控制台可能变成:
response: 你
response: 好
response: ,
response: 我
response: 是
response: AI
...
非常难看。
而我们真正希望看到的是:
请求:
你是谁?
最终响应:
你好,我是一名 AI 助手……
也就是:
把一次完整的大模型调用,当成一个整体进行拦截和处理。
这时候就可以使用 Spring AI 提供的:
Advisor
二、什么是 Advisor?
可以先把 Advisor 理解成:
Spring AI 专门为 AI 调用提供的一套“拦截器机制”。
如果你学过:
Servlet Filter
Spring MVC Interceptor
Spring AOP
Advisor 的思想其实很像。
正常调用大模型:
用户
↓
ChatClient
↓
大模型
↓
返回结果
加入 Advisor 以后:
用户
↓
ChatClient
↓
Advisor
↓
大模型
↓
Advisor
↓
返回结果
也就是说,Advisor 可以在:
请求大模型之前
做一些事情。
也可以在:
大模型响应之后
再做一些事情。
三、Advisor 能干什么?
日志其实只是 Advisor 最简单的用途之一。
它还可以用来做很多事情,例如:
记录请求日志
记录响应日志
聊天记忆
敏感词过滤
权限判断
Prompt 增强
上下文补充
RAG 检索增强
请求参数修改
响应结果加工
比如用户问:
我的订单什么时候到?
Advisor 可以在真正请求大模型之前,先查询订单系统:
订单号:10086
状态:运输中
预计明天送达
然后自动把这些信息补充到 Prompt 中:
用户问题:
我的订单什么时候到?
系统补充信息:
订单状态:运输中
预计明天送达
最后再交给大模型。
所以 Advisor 的本质并不是:
打印日志组件
而是一套:
AI 请求和响应的增强机制。
四、Advisor 的执行流程
假设我们的 ChatClient 配置了两个 Advisor:
Advisor A
Advisor B
那么请求大概会这样执行:
用户请求
↓
Advisor A
↓
Advisor B
↓
ChatModel
↓
DeepSeek
模型返回以后,则反过来:
DeepSeek
↓
ChatModel
↓
Advisor B
↓
Advisor A
↓
用户
可以把它想象成一层一层套起来:
Advisor A {
Advisor B {
调用大模型
}
}
所以 Advisor 既可以处理:
请求
也可以处理:
响应
这也是它特别适合日志、记忆、RAG 等功能的原因。
五、使用 Spring AI 自带的日志 Advisor
Spring AI 已经内置了一个非常方便的日志组件:
SimpleLoggerAdvisor
所以如果我们只是想查看调用大模型时的请求和响应,甚至不需要自己写 Advisor。
假设项目已经创建了:
ChatClient
那么修改配置类:
package com.quanxiaoha.ai.robot.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(
DeepSeekChatModel chatModel) {
return ChatClient.builder(chatModel)
// 默认系统提示词
.defaultSystem(
"请你扮演一名 Java 项目实战课程的客服人员"
)
// 添加日志 Advisor
.defaultAdvisors(
new SimpleLoggerAdvisor()
)
.build();
}
}
真正关键的只有这一句:
.defaultAdvisors(
new SimpleLoggerAdvisor()
)
意思就是:
给这个 ChatClient 默认添加一个日志 Advisor。
以后通过这个 ChatClient 发起的请求,就会经过 SimpleLoggerAdvisor。
六、为什么配置了以后没有日志?
很多人第一次使用的时候会发现:
SimpleLoggerAdvisor 明明加了
为什么控制台没输出?
原因很简单。
它的日志级别需要开启:
DEBUG
因此编辑:
application.yml
增加:
logging:
level:
org.springframework.ai.chat.client.advisor: debug
配置完成以后重启项目。
七、测试日志效果
例如我们有一个接口:
http://localhost:8080/v2/ai/generate?message=你是谁
调用以后,就可以在控制台看到类似的大模型调用信息。
大致可以理解为:
请求内容:
你是谁
System Prompt:
请你扮演一名 Java 项目实战课程的客服人员
模型响应:
你好,我是一名 Java 项目实战课程的客服人员……
实际日志里面还可能包含:
模型配置
消息列表
AdvisorContext
请求参数
ChatResponse
Token 等信息
这对于开发阶段排查问题非常有帮助。
八、为什么 Advisor 特别适合流式调用?
这是一个很重要的点。
假如我们直接写:
chatClient
.prompt()
.user(message)
.stream()
.content();
底层返回的是一个流。
模型的数据可能一段一段回来。
如果我们自己在流上:
.doOnNext(...)
打印:
.doOnNext(content ->
log.info("response: {}", content)
)
那么控制台就会出现大量碎片:
response: 你
response: 好
response: ,
response: 我
response: 是
response: ...
这种日志实际意义并不大。
而 Advisor 工作的位置更加靠近 Spring AI 的调用链。
因此它可以统一处理 AI 请求生命周期,而不是简单地在 Controller 最外层打印一个字符串。
这也是为什么 AI 应用中:
日志
记忆
RAG
上下文增强
这些通用能力,非常适合通过 Advisor 实现。
九、自定义一个 Advisor
内置的:
SimpleLoggerAdvisor
已经可以满足很多开发场景。
但是实际企业项目中,我们往往希望:
日志格式自己定义
只打印自己关心的数据
记录耗时
保存数据库
上传日志平台
增加 traceId
记录用户 ID
这时候就可以:
自定义 Advisor。
十、添加 Lombok
为了方便打印日志,可以使用 Lombok。
在 pom.xml 中加入:
<properties>
<lombok.version>1.18.30</lombok.version>
</properties>
依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
然后刷新 Maven。
十一、自定义 MyLoggerAdvisor
新建:
advisor
包。
然后创建:
MyLoggerAdvisor
代码:
package com.quanxiaoha.ai.robot.advisor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
@Slf4j
public class MyLoggerAdvisor implements CallAdvisor {
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain) {
// 请求大模型之前
log.info("===== AI 请求开始 =====");
log.info("请求参数:{}", request);
// 继续执行后面的 Advisor
// 最终会真正调用大模型
ChatClientResponse response =
chain.nextCall(request);
// 大模型返回以后
log.info("响应参数:{}", response);
log.info("===== AI 请求结束 =====");
return response;
}
@Override
public int getOrder() {
return 1;
}
@Override
public String getName() {
return this.getClass()
.getSimpleName();
}
}
代码不长,但是里面有几个非常重要的地方。
十二、CallAdvisor 是什么?
我们的类:
public class MyLoggerAdvisor
implements CallAdvisor
实现了:
CallAdvisor
Spring AI 中,可以根据调用方式处理不同场景。
例如:
普通同步调用
流式调用
这里的:
CallAdvisor
主要处理普通调用场景。
例如:
chatClient
.prompt()
.user(message)
.call()
.content();
也就是:
call()
类型的请求。
所以:
CallAdvisor
可以先简单理解成:
同步 AI 调用拦截器。
十三、最重要的一行:nextCall()
整个自定义 Advisor 中最关键的一行是:
ChatClientResponse response =
chain.nextCall(request);
它是什么意思?
可以把 Advisor 链想象成:
MyLoggerAdvisor
↓
其他 Advisor
↓
ChatModel
↓
DeepSeek
当程序执行:
chain.nextCall(request);
意思就是:
当前 Advisor 的事情先做到这里,把请求继续交给后面的 Advisor。
后面的 Advisor 全部执行完成以后,最终会真正调用大模型。
大模型返回结果后:
chain.nextCall(request)
才会返回:
ChatClientResponse
所以:
log.info("请求参数:{}", request);
发生在:
调用大模型之前
而:
log.info("响应参数:{}", response);
发生在:
调用大模型之后
整个结构实际上就是:
// 请求前
log.info("请求");
// 真正执行 AI 调用
ChatClientResponse response =
chain.nextCall(request);
// 请求后
log.info("响应");
return response;
这就是 Advisor 最核心的工作方式。
十四、其实它和 AOP 很像
如果学过 Spring AOP,会发现这个结构非常眼熟。
AOP 里面可能写:
@Before
然后:
proceed()
最后:
@After
Advisor 也是类似的思想:
请求之前
↓
nextCall()
↓
真正调用大模型
↓
获取响应
↓
响应之后
因此我们甚至可以把它简单理解成:
Spring AI 给 AI 调用链准备的一套专用 AOP。
当然它不是传统 Spring AOP 本身,只是设计思想非常类似。
十五、getOrder() 是干什么的?
我们还实现了:
@Override
public int getOrder() {
return 1;
}
因为一个 ChatClient 可以配置很多 Advisor。
例如:
日志 Advisor
聊天记忆 Advisor
RAG Advisor
安全检查 Advisor
于是就会出现一个问题:
到底谁先执行?
这时候就可以通过:
getOrder()
控制顺序。
通常可以理解为:
order 越小
越靠前执行
例如:
Advisor A
order = 0
Advisor B
order = 1
Advisor C
order = 10
请求方向:
A
↓
B
↓
C
↓
大模型
响应回来则反过来:
大模型
↓
C
↓
B
↓
A
这一点非常重要。
以后做:
ChatMemory
RAG
日志
安全校验
时,都可能需要考虑 Advisor 的执行顺序。
十六、getName() 有什么用?
还有一个:
@Override
public String getName() {
return this.getClass()
.getSimpleName();
}
返回当前 Advisor 的名字。
例如:
MyLoggerAdvisor
主要用于:
识别 Advisor
日志
调试
框架内部管理
通常直接返回类名即可。
十七、把自定义 Advisor 加到 ChatClient
Advisor 写好了以后,还没有自动生效。
我们需要修改:
ChatClientConfig
配置:
package com.quanxiaoha.ai.robot.config;
import com.quanxiaoha.ai.robot.advisor.MyLoggerAdvisor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(
DeepSeekChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem(
"请你扮演一名 Java 项目实战课程的客服人员"
)
.defaultAdvisors(
new SimpleLoggerAdvisor(),
new MyLoggerAdvisor()
)
.build();
}
}
现在我们的 ChatClient 中有两个 Advisor:
SimpleLoggerAdvisor
MyLoggerAdvisor
请求就会经过 Advisor 链。
十八、测试自定义 Advisor
例如请求同步接口:
http://localhost:8080/v2/ai/generate?message=你是谁
控制台可能看到:
===== AI 请求开始 =====
请求参数:
ChatClientRequest(...)
响应参数:
ChatClientResponse(...)
===== AI 请求结束 =====
这样,我们自己的 Advisor 就已经生效了。
十九、自己写 Advisor 到底有什么意义?
可能有人会问:
既然 SimpleLoggerAdvisor 都能打印日志了,为什么还要自己写?
因为:
SimpleLoggerAdvisor
适合:
开发
调试
快速查看请求
而真正上线以后,我们往往需要更加复杂的日志。
例如记录:
用户 ID
会话 ID
模型名称
用户 Prompt
System Prompt
响应内容
Token 消耗
响应时间
请求状态
异常信息
比如:
用户:10001
模型:
deepseek-v4-pro
问题:
Spring AI 是什么?
响应耗时:
2380 ms
回答:
Spring AI 是……
状态:
SUCCESS
甚至最终把数据保存到:
MySQL
Elasticsearch
ClickHouse
日志平台
监控系统
这时候自定义 Advisor 的价值就体现出来了。
二十、还可以统计大模型调用耗时
例如:
long startTime =
System.currentTimeMillis();
ChatClientResponse response =
chain.nextCall(request);
long cost =
System.currentTimeMillis()
- startTime;
log.info(
"大模型调用耗时:{} ms",
cost
);
完整一点:
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain) {
long startTime =
System.currentTimeMillis();
log.info("AI 请求:{}", request);
ChatClientResponse response =
chain.nextCall(request);
long cost =
System.currentTimeMillis()
- startTime;
log.info("AI 响应:{}", response);
log.info(
"AI 调用耗时:{} ms",
cost
);
return response;
}
这样以后如果用户反馈:
AI 怎么这么慢?
我们就可以直接通过日志看到:
AI 调用耗时:8350 ms
非常方便。
二十一、Advisor 和普通 AOP 怎么选?
最后总结一个非常实用的问题。
如果只是记录:
Controller 方法参数
Service 方法耗时
普通业务接口日志
使用:
Spring AOP
完全没问题。
但是如果你处理的是:
Prompt
ChatClient
ChatMemory
RAG
AI 请求
AI 响应
模型调用链
优先考虑:
Advisor
因为 Advisor 就是在 Spring AI 的模型调用链里面工作的。
简单理解:
普通业务方法
↓
AOP
AI 调用链
↓
Advisor
二十二、整个执行流程总结
最后把整篇文章串起来。
我们的代码:
chatClient
.prompt()
.user("你是谁")
.call()
.content();
真正执行时,大概经历:
用户发送问题
↓
ChatClient
↓
SimpleLoggerAdvisor
↓
MyLoggerAdvisor
↓
ChatModel
↓
DeepSeek
↓
模型生成响应
↓
MyLoggerAdvisor
↓
SimpleLoggerAdvisor
↓
ChatClient
↓
返回给用户
因此 Advisor 就像守在:
ChatClient
和:
ChatModel
之间的一组拦截器。
二十三、总结
这一篇最重要的其实只有三个知识点。
第一:
SimpleLoggerAdvisor
Spring AI 已经内置了日志 Advisor,可以快速查看请求和响应:
.defaultAdvisors(
new SimpleLoggerAdvisor()
)
同时记得开启:
logging:
level:
org.springframework.ai.chat.client.advisor: debug
第二:
我们也可以自己实现:
CallAdvisor
核心结构:
请求前处理
chain.nextCall(request)
响应后处理
第三:
Advisor 不仅可以打印日志。
以后学习:
聊天记忆
RAG
Prompt 增强
敏感词过滤
权限控制
日志监控
Token 统计
都会再次看到它。
所以可以记住一句话:
Advisor 就是 Spring AI 提供的 AI 调用链增强机制,可以在请求大模型之前和响应返回之后,统一插入我们自己的处理逻辑。
理解了 Advisor,后面继续学习 Spring AI 的:
ChatMemory
↓
RAG
↓
上下文增强
↓
Agent
就会顺很多。