前面大篇幅介绍了关于Spring AI Advisor机制,并介绍了一些常见的内置的advisor。今天我们来自定义有一个Advisor。
要自定义一个属于自己的Advisor,其实很自定义一个AOP一样简单。只需遵循以下步骤:
这里我们实现一个在大模型响应前后,来计算他的耗时时间。在前置增强中统计我们的开始时间,在后置方法中计算耗时。
public class TimingCustomAdvisor implements CallAroundAdvisor {
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
// 1. 前置增强
advisedRequest = this.before(advisedRequest);
// 2. 执行后续的advisor链
AdvisedResponse response = chain.nextAroundCall(advisedRequest);
// 3. 后置增强
this.observeAfter(response);
return response;
}
/**
* 前置增强,记录一个初始时间,并将时间存放到advisorContext
*
* @param advisedRequest
* @return
*/
private AdvisedRequest before(AdvisedRequest advisedRequest){
System.out.println("前置 AdvisorRequest 增强");
// 存储耗时,上文中我们提到AdvisorContext会共享状态。我们将耗时记录在上下文中,当然也可以自己定义一个变量存储
advisedRequest.adviseContext().put("start0", System.currentTimeMillis());
return advisedRequest;
}
/**
* 后置增强,获取初始时间,并计算耗时
* @param advisedResponse
*/
private void observeAfter(AdvisedResponse advisedResponse){
System.out.println("后置 advisedResponse 增强");
Long start0 = (Long) advisedResponse.adviseContext().get("start0");
System.out.println("从 AdvisorContext中获取start0:" + start0);
System.out.println("耗时:" + (System.currentTimeMillis() - start0));
}
/**
* 指定当前advisor名称
* @return
*/
@Override
public String getName() {
return "TimingCustomAdvisor";
}
/**
* 给定执行顺序
* @return
*/
@Override
public int getOrder() {
return Integer.MAX_VALUE;
}
}@RestController
@RequestMapping("/api/advisor")
public class TimingCustomAdvisorController {
@Autowired
private OpenAiChatModel openAiChatModel;
@GetMapping("/timing")
public void timing() {
ChatClient chatClient = ChatClient.builder(openAiChatModel)
.build();
// 将自定义的advisor添加进去
TimingCustomAdvisor advisor = new TimingCustomAdvisor();
ChatClient.CallResponseSpec response = chatClient.prompt()
.advisors(advisor)
.user("李白对美国加关税怎么看?").call();
System.out.println(response.content());
}
}运行结果打印,完全符合我们的预期。

在TimingCustomAdvisor中,我们指定getOrder()为Integer的最大值,按照官网文档说明,order值越小,会越先执行前置before方法,而相应的会越后执行后置after方法。那么作为一个会打酱油的程序员,肯定不是你说啥就是啥。我们来验证下。
我们再来定义一个Advisor,将getorder的值设置的比TimingCustomAdvisor值小:
public class FormatCustomAdvisor implements CallAroundAdvisor {
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
// 1. 前置增强
advisedRequest = this.before(advisedRequest);
// 2. 执行后续的advisor链
AdvisedResponse response = chain.nextAroundCall(advisedRequest);
// 3. 后置增强
this.observeAfter(response);
return response;
}
private AdvisedRequest before(AdvisedRequest advisedRequest){
System.out.println("[FormatCustomAdvisor] - 前置 AdvisorRequest 增强");
return advisedRequest;
}
private void observeAfter(AdvisedResponse advisedResponse){
System.out.println("[FormatCustomAdvisor] - 后置 advisedResponse 增强");
}
@Override
public String getName() {
return "FormatCustomAdvisor";
}
@Override
public int getOrder() {
// 这里order值减一
return Integer.MAX_VALUE - 1;
}
}两个Advisor都添加大模型执行中:
@RestController
@RequestMapping("/api/advisor")
public class TimingCustomAdvisorController {
@Autowired
private OpenAiChatModel openAiChatModel;
@GetMapping("/timing")
public void timing() {
ChatClient chatClient = ChatClient.builder(openAiChatModel)
.build();
// 将自定义的advisor添加进去
TimingCustomAdvisor timingCustomAdvisor = new TimingCustomAdvisor();
// 自定义第二个advisor
FormatCustomAdvisor formatCustomAdvisor = new FormatCustomAdvisor();
ChatClient.CallResponseSpec response = chatClient.prompt()
.advisors(timingCustomAdvisor, formatCustomAdvisor)
.user("李白对美国加关税怎么看?").call();
System.out.println(response.content());
}
}显示打印:

由于FormatCustomAdvisor的getOrder值为Integer.MAX_VALUE - 1,因此FormatCustomAdvisor的执行顺序优于TimingCustomAdvisor。从结论上来看,也符合这样的执行顺序。而后置增强则与前置增强的顺序是相反的。
不知道大家还记不记得这个图:

图中所有的类我们在前面的例子中都用过了,但是唯独CallAroundAdvisorChain和StreamAroundAdvisorChain还没介绍到。没错,其实他就是用来维持整个Advisor链的核心。上面我们自定义Advisor的时候,使用到了CallAroundAdvisorChain接口。我们就以这个为例来看下他的实现。
CallAroundAdvisorChain接口只有一个方法nextAroundCall,我们直接看他的实现DefaultAroundAdvisorChain。从他的成员变量中,我们可以看到两个很显眼的属性:
private final Deque<CallAroundAdvisor> callAroundAdvisors;
private final Deque<StreamAroundAdvisor> streamAroundAdvisors;这个Deque是双端队列,而这个就是我们维持advisor链的队列。而在nextAroundCall方法中,会将advisor从队列中取出,然后通过观察者模式接入大模型执行中。核心代码:
@Override
public AdvisedResponse nextAroundCall(AdvisedRequest advisedRequest) {
if (this.callAroundAdvisors.isEmpty()) {
throw new IllegalStateException("No AroundAdvisor available to execute");
}
var advisor = this.callAroundAdvisors.pop();
var observationContext = AdvisorObservationContext.builder()
.advisorName(advisor.getName())
.advisorType(AdvisorObservationContext.Type.AROUND)
.advisedRequest(advisedRequest)
.advisorRequestContext(advisedRequest.adviseContext())
.order(advisor.getOrder())
.build();
return AdvisorObservationDocumentation.AI_ADVISOR
.observation(null, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry)
.observe(() -> advisor.aroundCall(advisedRequest, this));
}同样的,流式处理中也是一样的处理方式:
@Override
public Flux<AdvisedResponse> nextAroundStream(AdvisedRequest advisedRequest) {
return Flux.deferContextual(contextView -> {
if (this.streamAroundAdvisors.isEmpty()) {
return Flux.error(new IllegalStateException("No AroundAdvisor available to execute"));
}
var advisor = this.streamAroundAdvisors.pop();
AdvisorObservationContext observationContext = AdvisorObservationContext.builder()
.advisorName(advisor.getName())
.advisorType(AdvisorObservationContext.Type.AROUND)
.advisedRequest(advisedRequest)
.advisorRequestContext(advisedRequest.adviseContext())
.order(advisor.getOrder())
.build();
var observation = AdvisorObservationDocumentation.AI_ADVISOR.observation(null,
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry);
observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();
// @formatter:off
return Flux.defer(() -> advisor.aroundStream(advisedRequest, this))
.doOnError(observation::error)
.doFinally(s -> observation.stop())
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
// @formatter:on
});
}为了验证这个猜想,我们在org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain#nextAroundCall方法处,设置个断点。然后运行我们上面的例子看下:

可以看到队列中包含我们自定义的advisor,而当调用CallAroundAdvisor advisor = (CallAroundAdvisor)this.callAroundAdvisors.pop();方法时,会取出队列第一个advisor,也就是FormatCustomAdvisor。取出后,队列中就剩下TimingCustomAdvisor:

那么,Advisor是如何添加到队列中的?我们看下我们添加advisors的代码:
ChatClient.CallResponseSpec response = chatClient.prompt()
.advisors(timingCustomAdvisor, formatCustomAdvisor)
.user("李白对美国加关税怎么看?").call();点进去advisors方法查看:

他是一个接口方法,那么找一下他的默认实现,就会跳到org.springframework.ai.chat.client.DefaultChatClient.DefaultChatClientRequestSpec#advisors(org.springframework.ai.chat.client.advisor.api.Advisor...)方法。可以看到:

这里的advisors.addAll方法是存储我们当前请求的advisor列表,他并不会推入到我们后续的责任链中。而this.aroundAdvisorChainBuilder.pushAll(Arrays.asList(advisors)); 则会将我们构建的advisor推入到队列中去:

这里框出来的部分,就是将advisor推入到上面我们提到的队列中。
老样子,我们设个断点验证下:
advisor还没压入之前,队列中并不存在我们的advisor:

而当push完成之后,便已经将我们定义的两个advisor推入到队列中了:

接下来便是按照上面说的,会以此从队列中取出执行。
通过自定义advisor,可以增强和扩展我们的业务逻辑和AI能力的融合,通过这种方式可以轻松实现 业务规则注入、实时数据处理 等高级功能,而无需侵入核心 AI 模型调用逻辑。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。