七、SpringAl 拦截(Advisor )

版本: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 越后执行

六、实践要点 / 踩坑

  1. 不要重建整个 Prompt :用 mutate() / augmentUserMessage() / augmentSystemMessage(),避免丢失 system message 与 ChatOptions。
  2. call 与 stream 要分别考虑 :2.0 只实现 CallAdvisor 时 .stream() 不会被拦截,两个都实现或用 BaseAdvisor。
  3. 多 Advisor 排序:日志 Advisor 想看到最终内容就放最外层(order 更小);改写 Prompt 的 Advisor 一般放内层。
  4. 上下文传递 :ChatClientRequest.context() 是 Map<String, Object>,可跨 Advisor 传数据(如 conversationId),after 里同样能读。
  5. 改响应 :after 里要返回新的 ChatClientResponse,用 ChatClientResponse.builder().from(resp)...build()。
  6. 递归调用 :工具调用循环用 chain.copy(this).nextCall(request),避免自己写 while 循环导致上下文丢失。
  7. 性能 :before 里不要做慢 IO;RAG 检索结果建议放进 context 供后续 Advisor 复用。
相关推荐
undsky_1 小时前
【n8n教程】:Set 节点,实现数据转换魔法!
人工智能·ai·aigc·ai编程
ofoxcoding2 小时前
CC Switch 与 AI API 聚合桌面端对比:Claude Code 和 Codex 的接入方案解析
ai
林伽一3 小时前
从对比语言模型到智能体治理,AI基础设施迎来新一轮重构 | 2026年09月26日
人工智能·科技·ai
IvorySQL3 小时前
去 IOE 的最后一公里:IvorySQL 5.4 × RISC-V 实测
数据库·人工智能·ai·postgresql·risc-v
SamChan903 小时前
PDF翻译时页眉页脚总在捣乱?跨页重复文本块的检测与过滤实测
人工智能·python·ai·pdf·wpf
张忠琳4 小时前
【hermes-agent】Hermes Agent Prompt 构建流程超深度分析
ai·prompt·agent·hermes
小杉泽4 小时前
MCP Tasks 输入生命周期安全:从 input_required 到可恢复执行的状态机审计
ai·ai安全
LucianaiB5 小时前
华为云码道 AI 编程实战:我用 CodeArts 智能体打造了一款 HarmonyOS 游戏化专注应用「智办 ZhiBan」
人工智能·ai·华为云·harmonyos·codearts·材料
张忠琳5 小时前
【hermes-agent】Hermes Agent Kanban 流程超深度分析之一
ai·agent·hermes