第2052篇:Spring AI深度解析——Advisor机制和拦截器链
2026/4/30大约 5 分钟
第2052篇:Spring AI深度解析——Advisor机制和拦截器链
适读人群:使用Spring AI构建应用的工程师 | 阅读时长:约18分钟 | 核心价值:理解Spring AI的Advisor机制,用拦截器链优雅实现日志、安全检查、RAG等横切关注点
用Spring AI的时候,很多功能都可以通过.advisors()方法加进去——RAG的文档检索、对话记忆、日志记录。
但很多人用了Advisor却不太清楚它的工作原理,也不知道怎么自定义。这篇把Advisor机制讲清楚。
Advisor是什么
Advisor是Spring AI的AOP机制,用于拦截ChatClient的请求和响应:
// Advisor的基本接口
public interface CallAroundAdvisor extends Advisor {
/**
* 环绕通知:可以在请求前后做处理
* AdvisedRequest:即将发送给LLM的请求(可以修改)
* AdvisedResponse:LLM的响应(可以修改或替换)
*/
AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain);
/**
* 优先级:数字越小越先执行
*/
int getOrder();
}内置Advisor介绍
Spring AI提供了几个内置的Advisor:
@Configuration
@RequiredArgsConstructor
public class AdvisorConfig {
private final VectorStore vectorStore;
private final ChatMemory chatMemory;
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
// 1. QuestionAnswerAdvisor:RAG检索
.defaultAdvisors(
new QuestionAnswerAdvisor(
vectorStore,
SearchRequest.defaults()
.withTopK(5)
.withSimilarityThreshold(0.7)
)
)
// 2. MessageChatMemoryAdvisor:对话记忆
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory)
)
// 3. SimpleLoggerAdvisor:请求响应日志
.defaultAdvisors(
new SimpleLoggerAdvisor()
)
.build();
}
}
// 使用时Advisor自动生效
@Service
@RequiredArgsConstructor
public class KnowledgeService {
private final ChatClient chatClient;
public String query(String sessionId, String question) {
return chatClient.prompt()
.user(question)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, sessionId))
.call()
.content();
}
}自定义Advisor:日志记录
/**
* 自定义Advisor:详细的LLM调用日志
* 记录请求内容、响应内容、耗时和Token用量
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class DetailedLoggingAdvisor implements CallAroundAdvisor {
private final MeterRegistry meterRegistry;
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest,
CallAroundAdvisorChain chain) {
long startTime = System.currentTimeMillis();
String requestId = UUID.randomUUID().toString().substring(0, 8);
// 记录请求
log.info("[{}] LLM请求开始 | 模型: {} | 用户消息: {}",
requestId,
advisedRequest.chatOptions() != null ?
advisedRequest.chatOptions().getClass().getSimpleName() : "default",
truncate(advisedRequest.userText(), 200)
);
AdvisedResponse response;
try {
response = chain.nextAroundCall(advisedRequest);
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
log.error("[{}] LLM请求失败 | 耗时: {}ms | 错误: {}",
requestId, duration, e.getMessage());
meterRegistry.counter("llm.call.error", "error", e.getClass().getSimpleName())
.increment();
throw e;
}
long duration = System.currentTimeMillis() - startTime;
// 记录响应
ChatResponse chatResponse = response.response();
if (chatResponse != null && chatResponse.getMetadata() != null) {
Usage usage = chatResponse.getMetadata().getUsage();
if (usage != null) {
log.info("[{}] LLM请求完成 | 耗时: {}ms | 输入Token: {} | 输出Token: {}",
requestId, duration, usage.getPromptTokens(), usage.getGenerationTokens());
// 记录指标
meterRegistry.counter("llm.tokens.input")
.increment(usage.getPromptTokens());
meterRegistry.counter("llm.tokens.output")
.increment(usage.getGenerationTokens());
} else {
log.info("[{}] LLM请求完成 | 耗时: {}ms", requestId, duration);
}
}
meterRegistry.timer("llm.call.duration")
.record(duration, TimeUnit.MILLISECONDS);
return response;
}
@Override
public String getName() { return "DetailedLoggingAdvisor"; }
@Override
public int getOrder() { return 0; } // 最先执行(包裹其他Advisor)
private String truncate(String text, int maxLength) {
if (text == null) return "null";
return text.length() > maxLength ? text.substring(0, maxLength) + "..." : text;
}
}自定义Advisor:内容安全检查
/**
* 内容安全检查Advisor
* 在请求发出前检查用户输入,在响应返回后检查LLM输出
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class ContentSafetyAdvisor implements CallAroundAdvisor {
private final ContentSafetyFilter contentFilter;
// 敏感词列表(实际应该从配置或数据库加载)
private static final List<Pattern> BLOCKED_PATTERNS = List.of(
Pattern.compile("(?i)(暴力|色情|违禁|诈骗)"),
Pattern.compile("(?i)(ignore previous|disregard|jailbreak)")
);
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest,
CallAroundAdvisorChain chain) {
// === 请求前检查 ===
String userInput = advisedRequest.userText();
if (userInput != null) {
SafetyCheckResult inputCheck = checkSafety(userInput);
if (!inputCheck.isSafe()) {
log.warn("用户输入安全检查未通过: {}", inputCheck.reason());
// 返回安全拒绝响应,不调用LLM
return buildSafetyDenialResponse(advisedRequest, inputCheck.reason());
}
}
// 调用LLM
AdvisedResponse response = chain.nextAroundCall(advisedRequest);
// === 响应后检查 ===
String outputText = extractText(response);
if (outputText != null) {
SafetyCheckResult outputCheck = checkSafety(outputText);
if (!outputCheck.isSafe()) {
log.warn("LLM输出安全检查未通过: {}", outputCheck.reason());
return buildSafetyDenialResponse(advisedRequest, "响应内容不符合安全规范");
}
}
return response;
}
private SafetyCheckResult checkSafety(String text) {
for (Pattern pattern : BLOCKED_PATTERNS) {
if (pattern.matcher(text).find()) {
return new SafetyCheckResult(false, "包含违规内容: " + pattern.pattern());
}
}
return new SafetyCheckResult(true, null);
}
private AdvisedResponse buildSafetyDenialResponse(
AdvisedRequest request, String reason) {
AssistantMessage refusalMessage = new AssistantMessage(
"很抱歉,您的请求包含不当内容,无法处理。如有疑问请联系客服。");
ChatResponse chatResponse = new ChatResponse(
List.of(new Generation(refusalMessage)));
return new AdvisedResponse(chatResponse, Map.of());
}
private String extractText(AdvisedResponse response) {
if (response.response() == null) return null;
Generation gen = response.response().getResult();
return gen != null ? gen.getOutput().getContent() : null;
}
@Override
public String getName() { return "ContentSafetyAdvisor"; }
@Override
public int getOrder() { return 10; } // 在日志之后执行
public record SafetyCheckResult(boolean isSafe, String reason) {}
}自定义Advisor:响应缓存
/**
* 响应缓存Advisor
* 对于确定性查询(temperature=0),缓存LLM响应
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class ResponseCacheAdvisor implements CallAroundAdvisor {
private final StringRedisTemplate redisTemplate;
private static final Duration CACHE_TTL = Duration.ofHours(24);
private static final String CACHE_PREFIX = "spring-ai:response:";
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest,
CallAroundAdvisorChain chain) {
// 只缓存没有System Prompt(或System Prompt固定)且用户消息明确的请求
String cacheKey = buildCacheKey(advisedRequest);
if (cacheKey == null) {
return chain.nextAroundCall(advisedRequest);
}
// 查缓存
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
log.debug("Spring AI响应缓存命中: {}", cacheKey.substring(0, 20));
return deserializeResponse(cached, advisedRequest);
}
// 调用LLM
AdvisedResponse response = chain.nextAroundCall(advisedRequest);
// 存缓存(只缓存成功的响应)
if (response.response() != null && response.response().getResult() != null) {
String serialized = serializeResponse(response);
redisTemplate.opsForValue().set(cacheKey, serialized, CACHE_TTL);
}
return response;
}
private String buildCacheKey(AdvisedRequest request) {
if (request.userText() == null || request.userText().isEmpty()) return null;
String content = (request.systemText() != null ? request.systemText() : "")
+ "|" + request.userText();
return CACHE_PREFIX + DigestUtils.md5DigestAsHex(
content.getBytes(StandardCharsets.UTF_8));
}
private String serializeResponse(AdvisedResponse response) {
return response.response().getResult().getOutput().getContent();
}
private AdvisedResponse deserializeResponse(String cached, AdvisedRequest request) {
AssistantMessage message = new AssistantMessage(cached);
ChatResponse chatResponse = new ChatResponse(List.of(new Generation(message)));
return new AdvisedResponse(chatResponse, Map.of());
}
@Override
public String getName() { return "ResponseCacheAdvisor"; }
@Override
public int getOrder() { return 20; }
}Advisor的执行顺序
/**
* Advisor执行顺序的配置和优化
*/
@Configuration
public class AdvisorOrderConfig {
@Bean
public ChatClient orderedChatClient(
ChatClient.Builder builder,
DetailedLoggingAdvisor loggingAdvisor, // order=0
ContentSafetyAdvisor safetyAdvisor, // order=10
ResponseCacheAdvisor cacheAdvisor, // order=20
VectorStore vectorStore) {
return builder
.defaultAdvisors(
loggingAdvisor, // 最外层:包裹所有,记录总耗时
safetyAdvisor, // 安全检查:在缓存之前,避免缓存违规内容
cacheAdvisor, // 缓存:命中就不走RAG
new QuestionAnswerAdvisor(vectorStore) // RAG:最后执行
)
.build();
}
}执行顺序(请求方向):日志→安全检查→缓存→RAG→LLM 返回方向(响应方向):LLM→RAG→缓存→安全检查→日志
Advisor机制让横切关注点的管理变得干净。日志、缓存、安全检查都从业务代码中分离出来,每个Advisor单一职责,组合灵活。
