目录
[一、Advisor 简介](#一、Advisor 简介)
[✅ 正确写法](#✅ 正确写法)
前言
在 Spring AI 开发大模型对话应用时,Advisor 相当于 AI 调用链路的拦截器 / 中间件,类比 Servlet Filter、Spring AOP 切面,可在请求发往大模型前、响应返回后做横切处理:
- 打印完整出入参日志(
SimpleLoggerAdvisor) - 敏感词违规拦截(内置
SafeGuardAdvisor) - 自定义提示词增强、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);
}
}
七、总结
-
基础设施顾问 (日志、敏感词)→
defaultAdvisors注册 -
业务顾问 (提示词增强)→
advisors()按需追加 -
执行顺序 :
defaultAdvisors会优先于advisors()执行 -
日志顾问必须在最外层,记录原始请求和最终响应
-
如果都在请求级,通过设置
order控制顺序