前言
在构建基于 Spring AI 的智能应用时,我们经常需要在 AI 请求和响应的处理流程中插入一些额外的逻辑,比如记录日志、优化提示词、对结果进行二次处理等。Spring AI 提供了一套优雅的 Advisor(拦截器) 机制,让你可以像切面编程一样,在 AI 调用的前后加入自定义逻辑。
我将以新手友好的方式,带你理解 Advisor 的核心概念,并手把手教你实现两个实用的自定义 Advisor:一个日志记录器和一个能提升推理能力的 Re-Reading Advisor。
一、什么是 Advisor?
简单来说,Advisor 就像是一个 "拦截器" 。当你的程序调用 AI 模型时,请求会依次经过一系列 Advisor,每个 Advisor 都可以:
- 在请求发送前修改请求(比如改写用户的问题)
- 在响应返回后处理响应(比如记录日志、转换格式)
这种设计让你能够将横切关注点(如日志、监控、提示词优化)与核心业务逻辑分离,代码更加清晰、可维护。
二、Advisor 的两种类型
Spring AI 提供了两种 Advisor 接口,分别对应两种调用场景:
| 接口 | 适用场景 | 核心方法 |
|---|---|---|
CallAroundAdvisor |
非流式请求(一次性返回完整结果) | aroundCall |
StreamAroundAdvisor |
流式请求(数据分块返回) | aroundStream |
最佳实践:如果你的 Advisor 同时支持流式和非流式调用,建议同时实现两个接口。
三、自定义 Advisor 的核心要素
实现一个 Advisor 需要关注以下几点:
- 实现对应接口 :根据场景选择
CallAroundAdvisor和/或StreamAroundAdvisor - 实现核心方法 :在
aroundCall或aroundStream中编写你的逻辑 - 设置执行顺序 :通过
getOrder()指定优先级(值越小越先执行) - 提供唯一名称 :通过
getName()返回一个标识符
四、实战一:自定义日志 Advisor
背景
Spring AI 内置了 SimpleLoggerAdvisor,但它以 Debug 级别输出日志。在默认的 Spring Boot 项目中,Info 级别下看不到日志输出。因此,我们需要一个更精简、可自定义级别的日志记录器。
需求
- 打印 Info 级别 日志
- 只输出 用户提示词 和 AI 回复的文本内容
代码实现
typescript
package com.swl.baoaiagent.advisor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.advisor.api.*;
import org.springframework.ai.chat.model.MessageAggregator;
import reactor.core.publisher.Flux;
@Slf4j
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
// 请求前:打印用户提示词
private void before(AdvisedRequest advisedRequest) {
log.info("Ai Request: {}", advisedRequest.userText());
}
// 响应后:打印 AI 回复
private void observeAfter(AdvisedResponse advisedResponse) {
log.info("Ai Response: {}", advisedResponse.response().getResult().getOutput().getText());
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
before(advisedRequest);
AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);
observeAfter(advisedResponse);
return advisedResponse;
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
before(advisedRequest);
Flux<AdvisedResponse> adviseResponses = chain.nextAroundStream(advisedRequest);
// 流式场景需要聚合消息后才能打印完整内容
return new MessageAggregator().aggregateAdvisedResponse(adviseResponses, this::observeAfter);
}
@Override
public String getName() {
return this.getClass().getSimpleName();
}
@Override
public int getOrder() {
return 0; // 优先级最高,最先执行
}
}
关键点解析
- 流式处理 :流式响应是分块返回的,直接打印会看到多次输出。我们使用
MessageAggregator将分块数据聚合成完整消息后,再打印一次日志。 - 执行顺序 :
getOrder()返回0,表示这个日志拦截器会优先执
五、实战二:Re-Reading Advisor(提升推理能力)
背景
研究发现,让 AI 把问题再读一遍 可以显著提升推理能力。这种技术被称为 Re-Reading(Re2),虽然效果显著,但 成本会翻倍(因为prompt长度翻倍),所以面向 C 端用户时要谨慎使用。
原理
原始请求:
请解释一下量子计算的基本原理
Re2 改写后的请求:
请解释一下量子计算的基本原理
Read the question again: 请解释一下量子计算的基本原理行。
代码实现
typescript
package com.swl.baoaiagent.advisor;
import org.springframework.ai.chat.client.advisor.api.*;
import reactor.core.publisher.Flux;
import java.util.HashMap;
import java.util.Map;
public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
// 在请求前改写 Prompt
private AdvisedRequest before(AdvisedRequest advisedRequest) {
Map<String, Object> advisedUserParams = new HashMap<>(advisedRequest.userParams());
advisedUserParams.put("re2_input_query", advisedRequest.userText());
return AdvisedRequest.from(advisedRequest)
.userText("""
{re2_input_query}
Read the question again: {re2_input_query}
""")
.userParams(advisedUserParams)
.build();
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
advisedRequest = before(advisedRequest);
return chain.nextAroundCall(advisedRequest);
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
advisedRequest = before(advisedRequest);
return chain.nextAroundStream(advisedRequest);
}
@Override
public String getName() {
return this.getClass().getSimpleName();
}
@Override
public int getOrder() {
return 0;
}
}
关键点解析
- Prompt 改写 :我们通过
before方法,将原始问题和"再读一遍"的指令拼接成新的提示词。 - 用户参数传递 :使用
userParams保留了原始输入,方便后续追踪。
六、执行顺序的重要性
当多个 Advisor 同时存在时,getOrder() 决定了它们的执行顺序:
- 值越小,优先级越高,越先执行
- 日志 Advisor 通常放在最前面,以便记录原始请求
- 业务逻辑相关的 Advisor 可以放在后面
scss
请求 → Order=0 (日志) → Order=1 (Re2) → Order=2 (其他) → AI 模型
七、进阶技巧:在 Advisor 之间共享状态
有时我们需要在多个 Advisor 之间传递数据,可以通过 adviseContext 实现:
ini
// 在第一个 Advisor 中存入数据
advisedRequest = advisedRequest.updateContext(context -> {
context.put("startTime", System.currentTimeMillis());
return context;
});
// 在后面的 Advisor 中取出数据
Object startTime = advisedResponse.adviseContext().get("startTime");
这在计算耗时、传递用户身份信息等场景中非常有用。
八、最佳实践总结
- 单一职责:每个 Advisor 只做一件事,避免功能混杂
- 注意顺序 :合理设置
getOrder(),确保依赖关系正确 - 双模式支持 :尽量同时实现
CallAroundAdvisor和StreamAroundAdvisor - 避免耗时操作:Advisor 中不要执行数据库查询、远程调用等耗时操作
- 优雅处理异常:确保 Advisor 不会因为异常导致整个链路中断
- 流式场景用 Reactor :复杂流式处理时,可以使用
Mono/Flux操作符进行灵活编排
结语
通过本文的学习,你应该已经掌握了 Spring AI Advisor 的核心概念和开发方法。自定义 Advisor 让我们能够以非侵入的方式扩展 AI 调用的能力,无论是记录日志、优化提示词,还是实现更复杂的推理增强,都能轻松应对。