前言

在构建基于 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 需要关注以下几点:

  1. 实现对应接口:根据场景选择 CallAroundAdvisor 和/或 StreamAroundAdvisor
  2. 实现核心方法:在 aroundCall 或 aroundStream 中编写你的逻辑
  3. 设置执行顺序:通过 getOrder() 指定优先级(值越小越先执行)
  4. 提供唯一名称:通过 getName() 返回一个标识符

四、实战一:自定义日志 Advisor

背景

Spring AI 内置了 SimpleLoggerAdvisor,但它以 Debug 级别输出日志。在默认的 Spring Boot 项目中,Info 级别下看不到日志输出。因此,我们需要一个更精简、可自定义级别的日志记录器。

需求

  • 打印 Info 级别 日志
  • 只输出 用户提示词 和 AI 回复的文本内容

代码实现

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 改写后的请求:

代码实现

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 可以放在后面
请求 → Order=0 (日志) → Order=1 (Re2) → Order=2 (其他) → AI 模型

七、进阶技巧:在 Advisor 之间共享状态

有时我们需要在多个 Advisor 之间传递数据,可以通过 adviseContext 实现:

// 在第一个 Advisor 中存入数据
advisedRequest = advisedRequest.updateContext(context -> {
    context.put("startTime", System.currentTimeMillis());
    return context;
});

// 在后面的 Advisor 中取出数据
Object startTime = advisedResponse.adviseContext().get("startTime");

这在计算耗时、传递用户身份信息等场景中非常有用。


八、最佳实践总结

  1. 单一职责:每个 Advisor 只做一件事,避免功能混杂
  2. 注意顺序:合理设置 getOrder(),确保依赖关系正确
  3. 双模式支持:尽量同时实现 CallAroundAdvisor 和 StreamAroundAdvisor
  4. 避免耗时操作:Advisor 中不要执行数据库查询、远程调用等耗时操作
  5. 优雅处理异常:确保 Advisor 不会因为异常导致整个链路中断
  6. 流式场景用 Reactor:复杂流式处理时,可以使用 Mono/Flux 操作符进行灵活编排

结语

通过本文的学习,你应该已经掌握了 Spring AI Advisor 的核心概念和开发方法。自定义 Advisor 让我们能够以非侵入的方式扩展 AI 调用的能力,无论是记录日志、优化提示词,还是实现更复杂的推理增强,都能轻松应对。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com