首页 / Spring AI 入门教程 / Advisor 机制:调用链拦截器

Spring AI 入门教程

Advisor 机制:调用链拦截器

本教程共 45 篇 · 第 5 篇 · 更新于 2026-08-16 · 约 8 分钟阅读

Spring AIAdvisor拦截器ChatClient对话记忆日志RAG

本节目标:理解 Advisor 的拦截原理、内置 Advisor 的用法和链的执行顺序。学完你能用 Advisor 给对话加上记忆和日志,也能自己写一个。

5.1 为什么需要 Advisor

先看一个场景。聊天模型 API 是无状态的,它不记得你上轮说了什么。要做多轮对话,就得自己维护历史消息,每次请求都手动拼进去。日志要自己打,业务上下文要自己塞。

假设每个对话接口都要记日志、带历史消息、附加业务上下文。这些代码和”问模型”本身无关,却要重复写在每个请求里。接口一多,全是复制粘贴。

Spring 生态对付这类横切逻辑有成熟套路:拦截器、AOP 切面。Advisor 就是这套思想在 Spring AI 里的落地。它在请求发给模型之前、响应返回之后各插一道,把通用能力从业务代码里抽出来。官方对它的定位是:封装可复用的生成式 AI 模式,转换进出模型的数据,跨模型、跨用例可移植。

5.2 核心接口与数据流

Advisor 的骨架很简单:

public interface Advisor extends Ordered {
    String getName();
}

真正的逻辑在它的两个子接口里,位于 org.springframework.ai.chat.client.advisor.api 包:

public interface CallAroundAdvisor extends Advisor {
    AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain);
}

public interface StreamAroundAdvisor extends Advisor {
    Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain);
}

CallAroundAdvisor 管同步调用,StreamAroundAdvisor 管流式调用。名字里的 Around 和 Spring AOP 的 @Around 一个意思:包住调用,前后都能动手。getName() 给 Advisor 一个唯一标识,日志和监控里能看到是谁在干活。

一次调用的数据流是这样的:

  1. 框架把请求封装成 AdvisedRequest。
  2. 链上第一个 Advisor 处理请求,然后调用 chain.nextAroundCall() 交给下一个。
  3. 依次传递,直到链尾的内置 Advisor,它真正调用 ChatModel。
  4. 响应作为 AdvisedResponse 沿原路返回,每个 Advisor 都能处理它。

AdvisedRequest 和 AdvisedResponse 都带一个 adviseContext,用来在 Advisor 之间共享状态。注意它不可变,想更新要用 updateContext 生成新实例。

5.3 执行顺序:order 决定一切

Advisor 实现 Ordered 接口,getOrder() 的返回值决定顺序。规则和 Spring 一致:数值越小,优先级越高,越先处理请求。Spring 提供了 Ordered.HIGHEST_PRECEDENCE 和 Ordered.LOWEST_PRECEDENCE 两个极端值做参考。

链是栈式结构,这点容易绕。链首的 Advisor 最先处理请求,也最后处理响应。想理清顺序,记住”先进后出”就行。举个例子:A 的 order 是 1,B 的 order 是 2,那么请求按 A、B 顺序处理,响应按 B、A 顺序返回。

链尾由框架内置的终端 Advisor 负责调用 ChatModel,保证它永远最后执行、第一个真正碰模型。这就是为什么你自己加多少 Advisor,调用链都不会乱。同 order 的多个 Advisor,先后顺序不保证,需要排序就显式给值。

5.4 内置 Advisor 一览

Advisor作用
MessageChatMemoryAdvisor把对话历史作为消息加入请求
VectorStoreChatMemoryAdvisor从向量库检索记忆
QuestionAnswerAdvisor基于向量库做 RAG 问答
SafeGuardAdvisor拦截有害或不恰当内容
SimpleLoggerAdvisor打印请求与响应日志

RAG 场景最常用的是 QuestionAnswerAdvisor:根据用户问题去向量库检索相关文档,把结果拼进提示词。用法和记忆 Advisor 一样,builder 构建后挂进链里,RAG 章节会展开。

两个记忆类 Advisor 的区别在”塞到哪”:MessageChatMemoryAdvisor 把历史作为消息列表追加,VectorStoreChatMemoryAdvisor 从向量库检索相关记忆。前者适合短会话,后者适合海量历史。

Note

PromptChatMemoryAdvisor 已在 2.0 移除,它的职责由 MessageChatMemoryAdvisor 接管。网上 1.x 资料里见到它,直接替换即可。

5.5 配置方式:defaultAdvisors 与 advisors

有两种挂载位置:

// 构建期:全局默认,所有请求生效
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(chatMemory).build(),
                new SimpleLoggerAdvisor())
        .build();

// 请求期:只影响当前调用,可传参数
String response = chatClient.prompt()
        .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-001"))
        .user("我是谁?")
        .call()
        .content();

AdvisorSpec 提供 param()/params() 传参数、advisors() 临时加 Advisor。官方建议:常用 Advisor 放 defaultAdvisors,按请求变化的参数放调用链。两处可以叠加,default 的 Advisor 始终在链上,请求期的参数只对本次生效。

5.6 示例一:对话记忆

先建一个记忆对象。MessageWindowChatMemory 维护最近 N 条消息,超出的旧消息会被清掉,系统消息除外:

ChatMemory chatMemory = MessageWindowChatMemory.builder()
        .chatMemoryRepository(new InMemoryChatMemoryRepository())
        .maxMessages(20)
        .build();

记忆的存取发生在调用前后:响应回来后把这一轮写进去,下一次请求前把历史读出来拼进消息列表。这些活 MessageChatMemoryAdvisor 全包了。把 Advisor 挂上,指定 conversationId,同一个会话就”记得”上下文了:

ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
        .build();

挂上之后,同一个 conversationId 的会话就”记得”上下文了:先问”我叫张三”,再问”我叫什么名字”,第二次回答会正确说出”张三”。conversationId 是会话的钥匙,不同用户用不同 ID,记忆互不串扰。记忆存内存还是数据库,取决于选哪个 Repository,完整的多轮对话示例见第 21 章。

5.7 示例二:请求日志

SimpleLoggerAdvisor 打印每次请求和响应,调试期特别好用:

ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(new SimpleLoggerAdvisor())
        .build();

它的日志级别是 DEBUG,要在配置里打开:

logging.level.org.springframework.ai.chat.client.advisor=DEBUG

控制台就能看到类似下面的输出:

DEBUG SimpleLoggerAdvisor - request: AdvisedRequest[prompt=Prompt{messages=[UserMessage{content='你好'...}]}]
DEBUG SimpleLoggerAdvisor - response: {"result":{"output":{"text":"你好!有什么我可以帮助你的吗?"...}}}

想自定义打印内容,SimpleLoggerAdvisor 有带两个函数参数的构造器,一个格式化请求,一个格式化响应。请求和响应里可能带敏感信息,生产环境慎开,别让日志变成泄露口。

5.8 示例三:自定义 Advisor

实现 CallAroundAdvisor,在调用前后各打一条日志。这就是简易版 SimpleLoggerAdvisor:

public class MyLogAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {

    private static final Logger log = LoggerFactory.getLogger(MyLogAdvisor.class);

    @Override
    public String getName() {
        return "my-log-advisor";
    }

    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
        log.debug("请求文本: {}", request.userText());
        AdvisedResponse response = chain.nextAroundCall(request);
        log.debug("响应: {}", response);
        return response;
    }

    @Override
    public Flux<AdvisedResponse> aroundStream(AdvisedRequest request, StreamAroundAdvisorChain chain) {
        log.debug("请求文本: {}", request.userText());
        return chain.nextAroundStream(request);
    }
}

要点:aroundCall 里必须调用 chain.nextAroundCall() 把请求传下去,否则链就断了,模型不会被调用。想修改请求,用 AdvisedRequest.from(request) 复制一份再改字段。流式场景要观察完整响应,可用 MessageAggregator 聚合 Flux,但它是只读的,不能改内容。

只拦截不修改的 Advisor 是最简单的形态。想增强提示词,就在 aroundCall 里改 userText 再放行。官方文档有个重读(Re2)技巧:把用户问题在提示词里重复一遍,模型推理更稳。实现上就是在请求前把 userText 拼成”{问题} Read the question again: {问题}“,这就是典型的前置增强。

5.9 最佳实践

  • 一个 Advisor 只做一件事,方便复用和测试。
  • 需要跨 Advisor 共享数据时,用 adviseContext,别用静态变量。
  • 同时实现两个接口,同步和流式都支持,调用方不用区分。
  • 链的顺序要心里有数:记忆类放前面,日志类放最后。顺序错了,前面 Advisor 加的内容可能进不了后面的处理。
  • order 值别都用默认 0,显式给不同数值,排序结果才可控。

5.10 小结

Advisor 是 ChatClient 的扩展点,本质是请求、响应两端的环绕拦截。五个内置 Advisor 覆盖了记忆、RAG、安全和日志。自定义 Advisor 只需实现接口并调用 chain 放行。记住 order 的栈式语义,你就能编排任意复杂的调用链。