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 控制顺序