Spring AI Advisors:给模型调用加上可组合的横切能力
Advisor是 Spring AI 在ChatClient调用链上的拦截与增强机制。它适合把记忆、RAG 检索、审计日志、安全校验等“每次调用都要做、但不属于具体业务问题”的逻辑抽离出来。它像 AOP,但拦截的对象不是 Java 方法,而是一轮模型请求与响应。
本文以 Spring AI 2.0.0 的 API 为准。Spring AI 早期曾使用
RequestAdvisor、ResponseAdvisor和CallAroundAdvisor等名称;跨版本复制代码前,应先核对当前依赖版本的官方文档。
1. 先建立正确的心智模型
一次 ChatClient 调用可以简化为:
用户代码
-> ChatClient:组织 system / user / options / tools
-> Advisor 1 前置处理
-> Advisor 2 前置处理
-> ChatModel:请求模型提供方
-> Advisor 2 后置处理
-> Advisor 1 后置处理
-> 返回内容或 FluxAdvisor 可以在模型调用前改写请求、补充上下文,也可以在调用后观测或处理响应。多个 Advisor 组成责任链,具有典型的“洋葱模型”特征:进入时按顺序执行,返回时按反序退出。
它解决的是横切问题,而不是所有 AI 问题:
| 需求 | 更合适的能力 | 原因 |
|---|---|---|
| 给模型一个角色、格式或固定规则 | .system(...) / defaultSystem(...) | 这是提示词本身,不需要拦截链 |
| 根据模型决定调用订单、天气等能力 | .tools(...) / @Tool | 工具是模型主动选择的业务动作 |
| 每轮加载会话历史 | Chat Memory Advisor | 每次请求都要一致地注入上下文 |
| 每轮先检索知识库再回答 | RAG Advisor | 将检索结果转换为模型上下文 |
| 统一记录耗时、token、租户标识 | 自定义 Advisor + Micrometer | 与具体提示词和业务接口解耦 |
| 拦截不合规的问题或输出 | 安全 Advisor / 网关策略 | 需要统一执行、可审计的策略 |
一句话区分:Tool 是模型可以调用的能力;Advisor 是应用在模型调用周围的能力。
2. 核心接口与调用场景
Spring AI 2.0 将非流式和流式调用拆为两个接口,避免把“一个完整响应”和“连续响应流”混在同一种处理模型里。
| 接口 | 关键方法 | 处理对象 | 适用场景 |
|---|---|---|---|
Advisor | getOrder() | Advisor 的共同契约 | 定义扩展参与顺序 |
CallAdvisor | adviseCall(request, chain) | 一次完整的 ChatClientResponse | 普通 HTTP 问答、完整结果审计、响应改写 |
StreamAdvisor | adviseStream(request, chain) | Flux<ChatClientResponse> | SSE 打字机输出、逐段过滤、流式埋点 |
CallAdvisorChain | nextCall(request) | 后续同步责任链 | 前置增强后继续请求,或在返回后处理结果 |
StreamAdvisorChain | nextStream(request) | 后续响应流 | 组合 Flux 操作符继续传递流 |
ChatClientRequest | 请求和 adviseContext | 传给下游 Advisor / 模型的不可变请求 | 读取或构造调用级上下文 |
ChatClientResponse | 响应和 adviseContext | 模型或下游 Advisor 的返回值 | 读取响应、补充结果相关上下文 |
chain 很关键:它代表“调用余下的 Advisor,最终再调用模型”。自定义实现中不调用 chain.nextCall(...) 或 chain.nextStream(...),就等同于短路调用。这只应在明确拒绝请求、命中缓存或实现降级回复时使用。
2.1 getOrder() 决定数据流,而不只是执行先后
低 order 的 Advisor 更靠近请求入口,先看到原始请求,也最后拿到响应。比如一个日志 Advisor 想记录“最终发送给模型的 prompt”,就应排在 RAG / memory 注入之后;安全审查想阻止无效请求耗费 token,则应放在检索和模型调用之前。
建议先按数据依赖排列,再给出 order:
安全输入检查 -> 会话记忆 -> 问题改写 / RAG -> 日志与指标 -> 模型
模型 -> 日志与指标 -> 输出安全检查 -> 返回客户端不要依赖注册列表的偶然顺序。相邻 Advisor 之间存在“谁先写入、谁后读取”关系时,必须显式实现 getOrder() 并写测试。
2.2 adviseContext 是链内调用状态,不是长期存储
ChatClientRequest 与 ChatClientResponse 带有 adviseContext,用于在本轮责任链内传递元数据,例如检索到的文档 ID、脱敏标记、租户 ID 或一次请求的开始时间。它是不可变的;需要变更时使用请求/响应的 updateContext(...) 产生新对象。
不要把聊天全文、用户画像或大检索结果塞进这里:它们会扩大单次调用对象,也不会跨请求持久化。跨轮聊天历史应使用 ChatMemory,知识库检索应使用 VectorStore / RAG 组件。
3. 在 ChatClient 中注册 Advisor
Advisor 可以注册为某个 ChatClient 的默认能力,也可以只附加到一轮调用。
@Configuration
class AiConfig {
@Bean
ChatClient supportChatClient(ChatClient.Builder builder,
MessageChatMemoryAdvisor memoryAdvisor,
QuestionAnswerAdvisor knowledgeAdvisor) {
return builder
.defaultSystem("你是客服助手;无可靠依据时明确说明。")
.defaultAdvisors(memoryAdvisor, knowledgeAdvisor)
.build();
}
}默认注册适合所有请求都必须拥有的能力,例如会话记忆、统一日志或组织级安全策略。一次性能力则放在调用处:
String answer = supportChatClient.prompt()
.user("解释退款进度")
.advisors(advisor -> advisor.param(
ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();这里 .advisors(...) 不新增 Advisor,而是为当前调用配置 Advisor 参数;MessageChatMemoryAdvisor 用 ChatMemory.CONVERSATION_ID 区分会话。这样同一个默认 Advisor 可以服务不同用户、不同会话,避免历史串话。
对于只在一个端点使用的能力,可以传入 Advisor 实例:
return chatClient.prompt()
.user(question)
.advisors(new SimpleLoggerAdvisor())
.call()
.content();3.1 Advisor 日志如何输出和配置
内置 SimpleLoggerAdvisor 只有被注册到这次调用或 defaultAdvisors(...) 后才会参与链路;它通过 Spring Boot 的日志系统在 DEBUG 级别输出请求与响应摘要。开发环境可在 application.yml 中打开 Advisor 包的日志:
logging:
level:
org.springframework.ai.chat.client.advisor: DEBUG日志会按应用现有的 SLF4J / Logback 配置输出到控制台或文件。上面的包级开关用于观察 Spring AI 内置 Advisor;自定义 Advisor 则应使用应用自己的 logger。例如计时日志可以保留在 INFO,无需把整条 Advisor 包都调为 DEBUG。
生产环境不要把原始用户问题、完整提示词、模型答案和凭据全部写入日志。应按数据分级脱敏、限制保留时间,并优先使用 Spring AI 与 Micrometer 的观测能力记录延迟、模型名和 token 等指标。
4. 常用内置 Advisor:按目标选择
4.1 聊天记忆:MessageChatMemoryAdvisor
它在调用前从 ChatMemory 取出当前会话历史并插入 prompt,在调用后保存新的对话消息。适合客服、助手、代码 Copilot 等“用户需要延续上一轮语义”的场景。
@Bean
MessageChatMemoryAdvisor memoryAdvisor(ChatMemory chatMemory) {
return MessageChatMemoryAdvisor.builder(chatMemory).build();
}它解决“模型无状态”的问题,但不等同于长期用户档案。应配置窗口大小、会话 ID 隔离和持久化仓库,详情见本目录《Spring AI 聊天记忆存储》。
4.2 简单 RAG:QuestionAnswerAdvisor
QuestionAnswerAdvisor 使用 VectorStore 查找与问题相似的文档,把结果放入上下文,再让模型基于上下文回答。适合内部制度问答、产品文档助手和知识库客服。
@Bean
QuestionAnswerAdvisor knowledgeAdvisor(VectorStore vectorStore) {
return QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().similarityThreshold(0.75).build())
.build();
}它不是权限系统。检索前仍应由业务层或 Advisor 根据租户、部门和文档 ACL 缩小候选范围;仅依赖向量相似度会造成越权信息进入 prompt。
4.3 完整 RAG:RetrievalAugmentationAdvisor
当需求不止“查相似文档”,而是需要查询改写、多来源检索、文档后处理、引用或空结果策略时,使用 RetrievalAugmentationAdvisor。它基于模块化 RAG 流程组合器,适合可追溯的企业知识问答和复杂检索编排。
选择建议:原型或单知识库场景先用 QuestionAnswerAdvisor;当召回、过滤、重排、引用和权限已成为独立问题,再升级到 RetrievalAugmentationAdvisor。不要一开始就把所有检索逻辑塞进一个自定义 Advisor。
4.4 安全与工具:SafeGuardAdvisor、ToolCallingAdvisor
SafeGuardAdvisor:用于阻止模型生成有害或不适当的内容。它是基础护栏,不能替代鉴权、数据脱敏和业务风控。ToolCallingAdvisor:负责处理模型发起的工具调用、回灌工具结果,并在需要时继续迭代。通常由ChatClient的工具配置自动参与;不要重复注册多个工具调用 Advisor。
工具调用涉及真实副作用时,仍要把“是否允许执行”放在业务权限与工具实现中,不能因为模型请求了工具就直接执行扣款、删除或发布操作。
5. 自定义 CallAdvisor:统一计时与审计
自定义 Advisor 应尽量只有一个职责。下面的例子只记录一次同步模型调用的耗时,既不修改 prompt,也不承载业务规则:
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
@Slf4j
final class TimingAdvisor implements CallAdvisor {
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request, CallAdvisorChain chain) {
long startedAt = System.nanoTime();
try {
return chain.nextCall(request);
}
finally {
long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000;
// 实际项目应写入 MeterRegistry,并避免记录敏感 prompt 内容。
log.info("chat call elapsed={}ms", elapsedMs);
}
}
@Override
public int getOrder() {
return 100;
}
@Override
public String getName() {
return "TimingAdvisor";
}
}这个写法有三个要点:
- 调用
chain.nextCall(request)才能继续执行后续 Advisor 与模型。 finally能覆盖正常返回和异常返回,适合计时与清理;异常转换、重试或降级要有明确的错误分类与幂等边界。getOrder()是契约的一部分。这个 Advisor 的位置应由它想观察的请求版本决定。
若要注入系统规则、改写用户问题或更新 adviseContext,先基于 ChatClientRequest 的不可变更新 API 创建新请求,再传给 chain.nextCall(...)。不要修改共享对象,也不要在 Advisor 内偷偷依赖 Web 请求线程上下文;流式与异步调用下这种假设会失效。
6. 自定义 StreamAdvisor:流式链路必须保持响应式
流式接口返回 Flux<ChatClientResponse>。核心原则是组合响应式操作,而不是先 block() 收齐答案再处理,否则会失去首 token 延迟和背压能力。
import reactor.core.publisher.Flux;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
@Slf4j
final class StreamTimingAdvisor implements StreamAdvisor {
@Override
public Flux<ChatClientResponse> adviseStream(
ChatClientRequest request, StreamAdvisorChain chain) {
long startedAt = System.nanoTime();
return chain.nextStream(request)
.doFinally(signalType -> {
long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000;
// 在这里记录完成、取消或异常等流式终止信号。
log.info("chat stream elapsed={}ms, signal={}", elapsedMs, signalType);
});
}
@Override
public int getOrder() {
return 100;
}
@Override
public String getName() {
return "StreamTimingAdvisor";
}
}实际实现中还要注意:
- 首 token 时间和总耗时是两个不同指标;前者用首次
onNext记录。 - 输出过滤必须支持分块文本,不能假定一个 chunk 就是一句完整话。
- 阻塞式检索、审计写库等工作要移到合适的调度器,不能占用 Reactor 事件线程。
- 客户端断连会触发取消;用
doFinally处理清理和指标,而不是只用doOnComplete。
7. 一个客服知识库的组合示例
下面是常见的组装方式。顺序应根据实际依赖、性能和安全策略调整:
输入安全检查
-> MessageChatMemoryAdvisor(按 conversationId 恢复历史)
-> RetrievalAugmentationAdvisor(按租户权限检索知识)
-> TimingAdvisor / Micrometer 观测
-> ChatModel + ToolCallingAdvisor
-> 输出安全检查、响应日志这条链清楚分工:Controller 只负责认证后的用户输入与返回格式;业务层负责租户权限、领域操作和工具实现;Advisor 负责每轮模型调用的共性增强;ChatModel 只负责推理。这样既便于替换向量库或模型,也能把审计、RAG、记忆各自测试。
8. 落地检查清单
- 一个 Advisor 只处理一个横切目标;复杂 RAG 优先使用官方组合器而非巨型自定义类。
- 同步与流式端点都需要时,实现
CallAdvisor和StreamAdvisor,并验证两条链。 - 明确每个 Advisor 的
getOrder()和它依赖的上游数据。 - 只把本轮、少量的调用元数据放入
adviseContext;聊天历史、知识与权限信息各归其位。 - RAG 必须在检索前施加 ACL / 租户过滤;模型看见过的上下文无法在回答阶段“收回”。
- 把 token、耗时、错误、取消等写入可观测系统,对 prompt 和响应做脱敏与采样。
- 任何有副作用的 Tool 都必须在业务实现层二次校验授权、参数与幂等性。
9. 进一步阅读
- Spring AI Reference - Advisors API
- Spring AI Reference - Recursive Advisors
- Spring AI Reference - Chat Memory
- 本目录:《Spring AI 聊天记忆存储》
- 本目录:《Spring AI · 提示词的使用》
