白歌在白板上画出洋葱模型:"每个 Advisor 包裹一层逻辑,请求从外向内穿,响应从内向外穿。日志、安全、记忆注入,全部收敛到这根链上。"
Advisor 链与内置拦截器
定义与作用
Advisor 是 Spring AI 的拦截器/顾问模式实现。它在 Prompt 发送给模型之前和 Response 返回给调用方之后插入自定义逻辑——日志记录、对话记忆注入、内容安全过滤等横切关注点,无需修改业务代码。
核心原理
洋葱模型
图释:注册顺序即嵌套顺序——先注册的 Advisor 在最外层包裹。请求依次穿过 A1→A2→A3 到达 Model,响应反向穿过 A3→A2→A1 返回用户。
RequestResponseAdvisor 接口
public interface RequestResponseAdvisor {
// 请求前:修改 Prompt
Prompt adviseRequest(Prompt prompt, Map<String, Object> context);
// 响应后:修改 ChatResponse
ChatResponse adviseResponse(ChatResponse response,
Map<String, Object> context);
// 标识名
String getName();
// 执行顺序(数值越小越靠外)
default int getOrder() { return 0; }
}
内置 Advisor 速查
| Advisor | 功能 | 注入时机 | 典型场景 |
|---|---|---|---|
| QuestionAnswerAdvisor | 自动 VectorStore 检索并注入上下文 | 请求前 | RAG 标配 |
| ChatMemoryAdvisor | 自动管理对话历史 | 请求前 | 多轮对话 |
| SimpleLoggerAdvisor | 记录 Prompt/Response | 请求前 + 响应后 | 调试审计 |
| ReReadingAdvisor | 让模型重读并校验自己回答 | 请求前 | 减少幻觉 |
| SafeGuardAdvisor | 内容安全过滤 | 请求前 + 响应后 | 企业合规 |
完整示例一:日志记录 Advisor
场景说明
李眉需要给飞翔科技智能客服加上完整的调用审计日志。
关键代码
@Component
public class AuditLogAdvisor implements RequestResponseAdvisor {
private static final Logger log =
LoggerFactory.getLogger(AuditLogAdvisor.class);
@Override
public Prompt adviseRequest(Prompt prompt,
Map<String, Object> context) {
// 提取用户问题文本
String userContent = prompt.getInstructions().stream()
.filter(m -> m.getMessageType() == MessageType.USER)
.map(Message::getContent)
.findFirst()
.orElse("unknown");
log.info("[AI 调用] 用户提问: {}", userContent);
log.info("[AI 调用] Token 预算: {}",
prompt.getOptions() != null ?
prompt.getOptions().getMaxTokens() : "default");
return prompt; // 不修改 Prompt
}
@Override
public ChatResponse adviseResponse(ChatResponse response,
Map<String, Object> context) {
String aiReply = response.getResult()
.getOutput().getContent();
log.info("[AI 回复] 内容长度: {} 字符",
aiReply.length());
log.info("[AI 回复] 模型: {}",
response.getMetadata().getModel());
return response;
}
@Override
public String getName() {
return "audit-log";
}
}
注册 Advisor
@Configuration
public class AdvisorConfig {
@Bean
public ChatClient chatClient(
ChatClient.Builder builder,
AuditLogAdvisor auditLog,
VectorStore vectorStore) {
return builder
.defaultAdvisors(
auditLog, // 第 1 层
new ChatMemoryAdvisor(new InMemoryChatMemory()), // 第 2 层
new QuestionAnswerAdvisor(vectorStore) // 第 3 层
)
.build();
}
}
运行结果
控制台日志:
[main] INFO AuditLogAdvisor - [AI 调用] 用户提问: 飞翔大学的学费是多少?
[main] INFO AuditLogAdvisor - [AI 调用] Token 预算: 2000
[main] INFO AuditLogAdvisor - [AI 回复] 内容长度: 156 字符
[main] INFO AuditLogAdvisor - [AI 回复] 模型: gpt-4o
完整示例二:内容安全过滤 Advisor
场景说明
高英要求客服 AI 不能输出任何违规内容。
关键代码
@Component
public class ContentSafetyAdvisor implements RequestResponseAdvisor {
private static final List<String> FORBIDDEN_KEYWORDS =
List.of("赌博", "色情", "暴力");
@Override
public Prompt adviseRequest(Prompt prompt,
Map<String, Object> context) {
// 请求前:检查用户输入是否违规
String userInput = prompt.getInstructions().stream()
.filter(m -> m.getMessageType() == MessageType.USER)
.map(Message::getContent)
.findFirst().orElse("");
for (String keyword : FORBIDDEN_KEYWORDS) {
if (userInput.contains(keyword)) {
throw new IllegalArgumentException(
"输入包含违规内容,已拦截");
}
}
return prompt;
}
@Override
public ChatResponse adviseResponse(ChatResponse response,
Map<String, Object> context) {
// 响应后:检查 AI 输出是否违规
String aiReply = response.getResult()
.getOutput().getContent();
for (String keyword : FORBIDDEN_KEYWORDS) {
if (aiReply.contains(keyword)) {
// 替换为安全回复
return ChatResponse.builder()
.generations(List.of(
new Generation("抱歉,无法回答该问题。")))
.build();
}
}
return response;
}
@Override
public String getName() {
return "content-safety";
}
}
运行结果
// 正常提问
用户:"飞翔大学有哪些专业?"
AI:"飞翔大学设有计算机科学、软件工程、数据科学..."
// 违规提问
用户:"如何参与赌博?"
异常:java.lang.IllegalArgumentException: 输入包含违规内容,已拦截
// AI 输出被拦截
用户:"描述一些暴力场景"
AI:"抱歉,无法回答该问题。"
操作前后对比
| 维度 | 操作前(无 Advisor) | 操作后(有 Advisor 链) |
|---|---|---|
| 日志 | 每次调用需手动加 log.info() | AuditLogAdvisor 自动记录 |
| 安全 | 需在 Controller 层逐接口校验 | ContentSafetyAdvisor 统一拦截 |
| 记忆 | 需手动管理对话历史 | ChatMemoryAdvisor 自动注入 |
| 扩展性 | 新增横截关注点需改业务代码 | 新增 Advisor 实现接口即插即用 |
| 可测试性 | 日志和安全逻辑与业务耦合 | 各 Advisor 独立单元测试 |
易错场景与面试考点
易错场景一:Advisor 顺序错误导致 RAG 不生效
// ❌ 错误:安全 Advisor 放在 RAG Advisor 外面
builder.defaultAdvisors(
contentSafety, // 最外层
new QuestionAnswerAdvisor(vectorStore) // 内层
);
// 安全过滤时,Prompt 还没有注入检索上下文
问题分析
QuestionAnswerAdvisor 在请求前修改 Prompt(注入检索上下文),如果它在安全 Advisor 内侧,安全 Advisor 看到的 Prompt 缺少上下文。应调整顺序:
// ✅ 正确:RAG Advisor 在外层,安全在更外层
builder.defaultAdvisors(
auditLog, // 外层:日志
contentSafety, // 中层:安全
new QuestionAnswerAdvisor(vectorStore), // 中层:RAG
new ChatMemoryAdvisor(new InMemoryChatMemory()) // 内层:记忆
);
面试高频题
Q1:Advisor 链的执行顺序是怎样的?
洋葱模型——先注册的 Advisor 同时是最外层。请求按注册顺序正向穿过各 Advisor,响应按注册顺序的反向穿过。
getOrder()返回值越小越靠外。
Q2:Advisor 和 Spring AOP 的关系?
两者都是横切关注点的实现手段,但层次不同。Spring AOP 针对方法调用织入增强(@Around),Advisor 针对 AI 调用的 Prompt/Response 做拦截。Advisor 可以理解为"AI 专用 AOP"——在 AI API 调用的关键节点织入逻辑。
本章小结
- Advisor 是 Spring AI 的拦截器模式,实现横切关注点分离
- 内置 5 种 Advisor:RAG、记忆、日志、安全、重读
- 注册顺序决定嵌套层级(先注册 = 最外层)
- 自定义 Advisor 只需实现 RequestResponseAdvisor 接口
- Advisor 不侵入业务代码,即插即用,独立可测