版本:Spring AI 1.0.x / 2.0(包名
org.springframework.ai.chat.client.advisor)说明:1.x 只有一个
Advisor接口,流式与非流式混在一起;2.0 做了较大重构,本文按 2.0 为主整理,差异处单独标注。
一、Advisor 是什么
Spring AI 用面向切面 的思想提供 Advisors API:在不改动业务代码的前提下,对「发给大模型的请求」和「大模型返回的响应」做拦截、修改、增强。
一次 chatClient.prompt()...call() 的完整链路:
业务代码 → advisor1.before → advisor2.before → ... → 真正调用 ChatModel
业务代码 ← advisor1.after ← advisor2.after ← ... ← 模型响应


1.1 架构重构:拆分流式与非流式(2.0)
① 拆分为 CallAdvisor 和 StreamAdvisor
1.x 只有一个 Advisor 接口,流式和非流式混在一起容易搞混;2.0 直接拆成两个独立接口:
| 接口 | 适用场景 | 核心方法 |
|---|---|---|
CallAdvisor |
非流式调用 .call() |
adviseCall(request, chain) |
StreamAdvisor |
流式调用 .stream() |
adviseStream(request, chain) |
需要哪种就实现哪种;像日志这种两种都要支持的,两个接口都实现(或用 BaseAdvisor)。
② 执行顺序:getOrder() 升序
2.0 中 Advisor 的执行顺序由 getOrder() 决定,值越小越先执行 before ,与 Spring Ordered 接口语义一致;after 则相反(内层先返回)。
③ 递归 Advisor 模式
2.0 针对工具调用(Tool Calling)这类需要循环的场景提供了递归写法:chain.copy(this).nextCall(request),让请求重新从链头走一遍。
简单说:工具调用拿到结果后不直接返回,而是把结果塞回请求里重新走链,直到模型不再要求调用工具为止。
java
@Override
public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
ChatClientResponse response = chain.nextCall(this.before(request, chain));
// 模型要求调用工具 → 把工具结果塞回请求 → 重新走一遍链
if (hasToolCalls(response)) {
return chain.copy(this).nextCall(withToolResults(request, response));
}
return response;
}
④ 新的请求/响应对象
| 对象 | 说明 |
|---|---|
ChatClientRequest |
未封装的 Prompt 请求,包含用户消息、系统消息、工具定义、上下文 context |
ChatClientResponse |
聊天完成响应,包含模型回复内容、用量信息、上下文 context |
比 1.x 的设计更清晰,也更容易在 Advisor 中修改。
1.2 核心接口 / 类速查
| 类型 | 作用 |
|---|---|
CallAdvisor / CallAdvisorChain |
非流式拦截器与责任链 |
StreamAdvisor / StreamAdvisorChain |
流式拦截器与责任链 |
ChatClientRequest / ChatClientResponse |
请求 / 响应载体(可修改、可携带 context) |
BaseAdvisor |
官方抽象基类,封装样板代码,子类只写 before / after / getOrder |
PromptTemplate |
提示词模板渲染,常配合 before 改写 Prompt |
二、内置 Advisor 清单(2.0)
| Advisor | 说明 | 默认 Order |
|---|---|---|
SafeGuardAdvisor |
安全护栏(敏感词过滤等) | HIGHEST_PRECEDENCE |
QuestionAnswerAdvisor |
Naive RAG(检索增强生成) | HIGHEST_PRECEDENCE + 100 |
RetrievalAugmentationAdvisor |
Modular RAG(模块化 RAG) | HIGHEST_PRECEDENCE + 100 |
MessageChatMemoryAdvisor |
对话记忆(消息窗口方式) | HIGHEST_PRECEDENCE + 200 |
VectorStoreChatMemoryAdvisor |
对话记忆(向量检索方式) | HIGHEST_PRECEDENCE + 200 |
ToolCallingAdvisor |
工具调用循环(自动执行 tool call) | HIGHEST_PRECEDENCE + 300 |
SimpleLoggerAdvisor |
日志记录对话请求和响应 | LOWEST_PRECEDENCE |
StructuredOutputValidationAdvisor |
结构化输出验证 | LOWEST_PRECEDENCE |
1.x 另有
PromptChatMemoryAdvisor(把历史对话作为 system 文本追加),2.0 中推荐使用MessageChatMemoryAdvisor。
三、内置用法:日志拦截 SimpleLoggerAdvisor
对话过程是「黑盒」,用它打印实际发给模型的内容和返回内容,便于调试。
3.1 注册为默认 Advisor(全局生效)
java
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(DeepSeekChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
}
也可以在单次请求里临时加:
java
chatClient.prompt()
.user("中国有多大?")
.advisors(new SimpleLoggerAdvisor())
.call()
.content();
3.2 打开日志级别(必须,否则看不到)
properties
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
日志输出示例:
request: ChatClientRequest[prompt=..., context={...}]
response: ChatClientResponse[chatResponse=..., context={...}]
默认打印
DEBUG级别;敏感场景可用new SimpleLoggerAdvisor(requestToString, responseToString, logLevel)自定义输出做脱敏。
四、自定义 Advisor:重读(Re2)
重读策略:让 LLM 再读一遍问题,类似人类审题,从而更深入理解问题、发现复杂模式,在推理类任务上表现更好。
模板:
{Input_Query}
再次阅读问题:{Input_Query}
4.1 基于 BaseAdvisor(推荐)
java
/**
* 重读(Re2)Advisor:把用户问题重复一遍再发给模型
*/
public class ReReadingAdvisor implements BaseAdvisor {
private static final String DEFAULT_USER_TEXT_ADVISE = """
{re2_input_query}
Read the question again: {re2_input_query}
""";
@Override
public int getOrder() {
return 0;
}
@Override
public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
// 1. 取用户输入文本
String inputQuery = chatClientRequest.prompt().getUserMessage().getText();
// 2. 渲染重读模板
String augmentedUserText = PromptTemplate.builder()
.template(DEFAULT_USER_TEXT_ADVISE)
.build()
.render(Map.of("re2_input_query", inputQuery));
// 3. 在「原请求」基础上只改 user message,保留 system message / options / 历史消息 / context
return chatClientRequest.mutate()
.prompt(chatClientRequest.prompt().augmentUserMessage(augmentedUserText))
.build();
}
@Override
public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
// 响应不做处理,原样返回
return chatClientResponse;
}
}
⚠️ 注意:直接
ChatClientRequest.builder().prompt(Prompt.builder().content(text).build()).build()会丢弃原有的 system message、ChatOptions、多轮历史与上下文 ,只适合最简单的演示。生产代码请用mutate()+augmentUserMessage()。
4.2 使用
java
@SpringBootTest
public class AdvisorTest {
private ChatClient chatClient;
@BeforeEach
public void init(@Autowired DeepSeekChatModel chatModel) {
chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(new SimpleLoggerAdvisor()) // 全局:日志
.build();
}
@Test
public void testRe2() {
String content = chatClient.prompt()
.user("中国有多大?")
.advisors(new ReReadingAdvisor()) // 单次:重读
.call()
.content();
System.out.println(content);
}
}
4.3 流式场景
BaseAdvisor 同时实现了 CallAdvisor 和 StreamAdvisor:默认 adviseStream 只把 before 作用在请求 上,响应流不做处理,所以上面的 Re2 Advisor 在 .stream() 下同样生效。
若需要在流式场景处理响应,重写 adviseStream:
java
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest request, StreamAdvisorChain chain) {
return chain.nextStream(before(request, null))
.map(resp -> after(resp, null)); // 逐块处理,注意不要破坏 SSE 结构
}
五、注册方式与执行顺序
| 方式 | 作用域 | 典型场景 |
|---|---|---|
ChatClient.builder().defaultAdvisors(...) |
该 ChatClient 的所有请求 | 日志、鉴权、会话记忆 |
.advisors(...)(单次 prompt) |
仅当前这一次调用 | 重读、临时 RAG、单次提示词增强 |
两者会合并进同一条责任链,统一按 getOrder() 升序排序。
组合示例(记忆 + RAG + 日志):
java
ChatClient.builder(chatModel)
.defaultAdvisors(
new SimpleLoggerAdvisor(), // 最外层:看最终内容
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore).build()
)
.build();
执行顺序记忆法:
getOrder() 越小 → 越靠外 → before 越先执行、after 越后执行
六、实践要点 / 踩坑
- 不要重建整个 Prompt :用
mutate()/augmentUserMessage()/augmentSystemMessage(),避免丢失 system message 与ChatOptions。 - call 与 stream 要分别考虑 :2.0 只实现
CallAdvisor时.stream()不会被拦截,两个都实现或用BaseAdvisor。 - 多 Advisor 排序:日志 Advisor 想看到最终内容就放最外层(order 更小);改写 Prompt 的 Advisor 一般放内层。
- 上下文传递 :
ChatClientRequest.context()是Map<String, Object>,可跨 Advisor 传数据(如conversationId),after里同样能读。 - 改响应 :
after里要返回新的ChatClientResponse,用ChatClientResponse.builder().from(resp)...build()。 - 递归调用 :工具调用循环用
chain.copy(this).nextCall(request),避免自己写 while 循环导致上下文丢失。 - 性能 :
before里不要做慢 IO;RAG 检索结果建议放进context供后续 Advisor 复用。