3869 字
约 12 分钟
5
Spring AI Advisor 实战:优雅打印大模型请求与响应日志

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

就会顺很多。

Spring AI Advisor 实战:优雅打印大模型请求与响应日志
http://clxhxhhr.top/posts/611/
作者
clxstart
发布于
2026-09-14
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。