Spring AI Advisor 拦截器全解:日志 / 敏感词 / 自定义提示词增强

目录

前言

[一、Advisor 简介](#一、Advisor 简介)

两种注册方式

二、实现日志拦截器

三、实现敏感词拦截器

四、实现自定义增强拦截器

五、踩坑:执行顺序问题

问题现象

[✅ 正确写法](#✅ 正确写法)

原因分析

更好的写法

六、完整测试

七、总结


前言

在 Spring AI 开发大模型对话应用时,Advisor 相当于 AI 调用链路的拦截器 / 中间件,类比 Servlet Filter、Spring AOP 切面,可在请求发往大模型前、响应返回后做横切处理:

  1. 打印完整出入参日志(SimpleLoggerAdvisor
  2. 敏感词违规拦截(内置SafeGuardAdvisor
  3. 自定义提示词增强、RAG 知识库注入、Token 限流、权限校验等

一、Advisor 简介

Advisor 类似于 Spring MVC 的拦截器,可以在调用 AI 模型前后进行拦截处理。

两种注册方式

方式 作用范围 执行时机
defaultAdvisors() 全局,对所有请求生效 基础链
advisors() 局部,仅当前请求生效 追加链

二、实现日志拦截器

Spring AI 已内置 SimpleLoggerAdvisor,直接使用即可:

复制代码
@SpringBootTest
public class LogTest {
    @Autowired
    private ChatClient.Builder clientBuilder;
    
    @Test
    public void testLog() {
        // 注册日志顾问和敏感词顾问
        ChatClient client = clientBuilder
            .defaultAdvisors(
                new SimpleLoggerAdvisor(), 
                new SafeGuardAdvisor(List.of("张山"))
            )
            .build();
        
        String content = client.prompt()
            .user("张山帅不帅")
            .call()
            .content();
        System.out.println(content);
    }
}

三、实现敏感词拦截器

复制代码
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;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.messages.AssistantMessage;

import java.util.List;
import java.util.stream.Collectors;

public class SafeGuardAdvisor implements CallAdvisor {
    private final List<String> sensitiveWords;
    private final int order;

    public SafeGuardAdvisor(List<String> sensitiveWords) {
        this(sensitiveWords, 0);
    }

    public SafeGuardAdvisor(List<String> sensitiveWords, int order) {
        this.sensitiveWords = sensitiveWords;
        this.order = order;
    }

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
        // 1. 前置检查:验证用户输入
        String userInput = request.prompt().getContents();
        for (String word : sensitiveWords) {
            if (userInput.contains(word)) {
                return createErrorResponse(request, "您的输入包含敏感词: " + word + ",请修改后重试");
            }
        }

        // 2. 执行后续调用
        ChatClientResponse response = chain.nextCall(request);

        // 3. 后置处理:过滤 AI 回复中的敏感词
        ChatResponse filteredResponse = filterResponse(response.chatResponse());
        return ChatClientResponse.builder()
            .chatResponse(filteredResponse)
            .responseMetadata(response.responseMetadata())
            .build();
    }

    private ChatResponse filterResponse(ChatResponse chatResponse) {
        List<Generation> filtered = chatResponse.getResults().stream()
            .map(gen -> {
                AssistantMessage msg = (AssistantMessage) gen.getOutput();
                String filteredText = filterText(msg.getText());
                AssistantMessage filteredMsg = new AssistantMessage(
                    filteredText,
                    msg.getMetadata(),
                    msg.getToolCalls()
                );
                return new Generation(filteredMsg, gen.getMetadata());
            })
            .collect(Collectors.toList());
        return new ChatResponse(filtered, chatResponse.getMetadata());
    }

    private String filterText(String text) {
        for (String word : sensitiveWords) {
            text = text.replace(word, "***");
        }
        return text;
    }

    private ChatClientResponse createErrorResponse(ChatClientRequest request, String errorMsg) {
        AssistantMessage errorMessage = new AssistantMessage(errorMsg);
        ChatResponse chatResponse = new ChatResponse(List.of(new Generation(errorMessage)));
        return ChatClientResponse.builder()
            .chatResponse(chatResponse)
            .build();
    }

    @Override
    public int getOrder() {
        return order;
    }

    @Override
    public String getName() {
        return getClass().getSimpleName();
    }
}

四、实现自定义增强拦截器

复制代码
import org.jetbrains.annotations.NotNull;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class BeReadingAdvisor implements BaseAdvisor {
    private static final String DEFAULT_USER_TEXT_ADVISE = """
        {re2_input_query}
        Read the question again: {re2_input_query}
        """;

    @Override
    public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
        // 1. 获取原始用户输入
        String contents = request.prompt().getContents();
        
        // 2. 渲染增强后的提示词
        String renderer = PromptTemplate.builder()
            .template(DEFAULT_USER_TEXT_ADVISE)
            .build()
            .render(Map.of("re2_input_query", contents));
        
        // 3. 构建新的请求(让 AI 重新阅读问题)
        return request.mutate()
            .prompt(Prompt.builder().content(renderer).build())
            .build();
    }

    @Override
    public ChatClientResponse after(ChatClientResponse response, AdvisorChain chain) {
        return response;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

五、踩坑:执行顺序问题

问题现象

复制代码
@Test
public void testBeReader() {
    ChatClient client = clientBuilder.build();
    // ❌ 这样可能有问题:两个顾问都在请求级
    String content = client.prompt()
        .advisors(new BeReadingAdvisor(), new SimpleLoggerAdvisor())
        .user("张山帅不帅")
        .call()
        .content();
    System.out.println(content);
}

✅ 正确写法

java

复制代码
@Test
public void testBeReader() {
    ChatClient client = clientBuilder.build();
    // ✅ 显式设置 order,确保执行顺序
    String content = client.prompt()
        .advisors(new BeReadingAdvisor(), new SimpleLoggerAdvisor(1))
        .user("张山帅不帅")
        .call()
        .content();
    System.out.println(content);
}

原因分析

SimpleLoggerAdvisor 需要在最外层执行 ,记录原始请求和最终响应。而 BeReadingAdvisor修改请求内容

当两者都在请求级且 order 相同(默认都是 0)时,执行顺序不确定,可能导致:

  • 日志记录到的是被修改后的请求(失去日志意义)

  • 增强逻辑在日志之后执行(请求已被篡改)

更好的写法

复制代码
@Test
public void testBeReader() {
    // ✅ 将日志顾问放在 default,确保在最外层
    ChatClient client = clientBuilder
        .defaultAdvisors(new SimpleLoggerAdvisor())
        .build();
    
    String content = client.prompt()
        .advisors(new BeReadingAdvisor())
        .user("张山帅不帅")
        .call()
        .content();
    System.out.println(content);
}

六、完整测试

java

复制代码
@SpringBootTest
public class LogTest {
    @Autowired
    private ChatClient.Builder clientBuilder;
    
    @Test
    public void testLog() {
        // 组合使用日志 + 敏感词
        ChatClient client = clientBuilder
            .defaultAdvisors(
                new SimpleLoggerAdvisor(),
                new SafeGuardAdvisor(List.of("张山"))
            )
            .build();
        
        String content = client.prompt()
            .user("张山帅不帅")
            .call()
            .content();
        System.out.println(content);
    }

    @Test
    public void testBeReader() {
        // 日志在 default,增强在请求级
        ChatClient client = clientBuilder
            .defaultAdvisors(new SimpleLoggerAdvisor())
            .build();
        
        String content = client.prompt()
            .advisors(new BeReadingAdvisor())
            .user("张山帅不帅")
            .call()
            .content();
        System.out.println(content);
    }
}

七、总结

  1. 基础设施顾问 (日志、敏感词)→ defaultAdvisors 注册

  2. 业务顾问 (提示词增强)→ advisors() 按需追加

  3. 执行顺序defaultAdvisors 会优先于 advisors() 执行

  4. 日志顾问必须在最外层,记录原始请求和最终响应

  5. 如果都在请求级,通过设置 order 控制顺序