首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深入Spring AI:自定义Advisor

深入Spring AI:自定义Advisor

原创
作者头像
有一只柴犬
发布2026-01-12 15:53:25
发布2026-01-12 15:53:25
1610
举报
文章被收录于专栏:人工智能人工智能

1、前言

前面大篇幅介绍了关于Spring AI Advisor机制,并介绍了一些常见的内置的advisor。今天我们来自定义有一个Advisor。

2、快速开始

要自定义一个属于自己的Advisor,其实很自定义一个AOP一样简单。只需遵循以下步骤:

  1. 创建一个Advisor类,实现CallAroundAdvisor或StreamAroundAdvisor接口
  2. 实现接口的aroundCall()、getName()、getOrder()方法
  3. 调用大模型时,将advisor添加进去即可

2.1、定义TimingCustomAdvisor

这里我们实现一个在大模型响应前后,来计算他的耗时时间。在前置增强中统计我们的开始时间,在后置方法中计算耗时。

代码语言:java
复制
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;
    }
}

2.2、使用advisor

代码语言:java
复制
@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());
    }

}

运行结果打印,完全符合我们的预期。

3、验证链式顺序

在TimingCustomAdvisor中,我们指定getOrder()为Integer的最大值,按照官网文档说明,order值越小,会越先执行前置before方法,而相应的会越后执行后置after方法。那么作为一个会打酱油的程序员,肯定不是你说啥就是啥。我们来验证下。

我们再来定义一个Advisor,将getorder的值设置的比TimingCustomAdvisor值小:

代码语言:java
复制
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都添加大模型执行中:

代码语言:java
复制
@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。从结论上来看,也符合这样的执行顺序。而后置增强则与前置增强的顺序是相反的。

4、Spring AI中如何维护Advisor链

不知道大家还记不记得这个图:

图中所有的类我们在前面的例子中都用过了,但是唯独CallAroundAdvisorChain和StreamAroundAdvisorChain还没介绍到。没错,其实他就是用来维持整个Advisor链的核心。上面我们自定义Advisor的时候,使用到了CallAroundAdvisorChain接口。我们就以这个为例来看下他的实现。

4.1、CallAroundAdvisorChain

CallAroundAdvisorChain接口只有一个方法nextAroundCall,我们直接看他的实现DefaultAroundAdvisorChain。从他的成员变量中,我们可以看到两个很显眼的属性:

代码语言:java
复制
private final Deque<CallAroundAdvisor> callAroundAdvisors;

private final Deque<StreamAroundAdvisor> streamAroundAdvisors;

这个Deque是双端队列,而这个就是我们维持advisor链的队列。而在nextAroundCall方法中,会将advisor从队列中取出,然后通过观察者模式接入大模型执行中。核心代码:

代码语言:java
复制
@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));
}

同样的,流式处理中也是一样的处理方式:

代码语言:java
复制
@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:

4.2、DefaultChatClient

那么,Advisor是如何添加到队列中的?我们看下我们添加advisors的代码:

代码语言:java
复制
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推入到队列中了:

接下来便是按照上面说的,会以此从队列中取出执行。

5、小结

通过自定义advisor,可以增强和扩展我们的业务逻辑和AI能力的融合,通过这种方式可以轻松实现 业务规则注入、实时数据处理 等高级功能,而无需侵入核心 AI 模型调用逻辑。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、前言
  • 2、快速开始
    • 2.1、定义TimingCustomAdvisor
    • 2.2、使用advisor
  • 3、验证链式顺序
  • 4、Spring AI中如何维护Advisor链
    • 4.1、CallAroundAdvisorChain
    • 4.2、DefaultChatClient
  • 5、小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档