Skip to content

Spring AI Advisors:给模型调用加上可组合的横切能力

Advisor 是 Spring AI 在 ChatClient 调用链上的拦截与增强机制。它适合把记忆、RAG 检索、审计日志、安全校验等“每次调用都要做、但不属于具体业务问题”的逻辑抽离出来。它像 AOP,但拦截的对象不是 Java 方法,而是一轮模型请求与响应。

本文以 Spring AI 2.0.0 的 API 为准。Spring AI 早期曾使用 RequestAdvisorResponseAdvisorCallAroundAdvisor 等名称;跨版本复制代码前,应先核对当前依赖版本的官方文档。

1. 先建立正确的心智模型

一次 ChatClient 调用可以简化为:

text
用户代码
  -> ChatClient:组织 system / user / options / tools
  -> Advisor 1 前置处理
  -> Advisor 2 前置处理
  -> ChatModel:请求模型提供方
  -> Advisor 2 后置处理
  -> Advisor 1 后置处理
  -> 返回内容或 Flux

Advisor 可以在模型调用改写请求、补充上下文,也可以在调用观测或处理响应。多个 Advisor 组成责任链,具有典型的“洋葱模型”特征:进入时按顺序执行,返回时按反序退出。

它解决的是横切问题,而不是所有 AI 问题:

需求更合适的能力原因
给模型一个角色、格式或固定规则.system(...) / defaultSystem(...)这是提示词本身,不需要拦截链
根据模型决定调用订单、天气等能力.tools(...) / @Tool工具是模型主动选择的业务动作
每轮加载会话历史Chat Memory Advisor每次请求都要一致地注入上下文
每轮先检索知识库再回答RAG Advisor将检索结果转换为模型上下文
统一记录耗时、token、租户标识自定义 Advisor + Micrometer与具体提示词和业务接口解耦
拦截不合规的问题或输出安全 Advisor / 网关策略需要统一执行、可审计的策略

一句话区分:Tool 是模型可以调用的能力;Advisor 是应用在模型调用周围的能力。

2. 核心接口与调用场景

Spring AI 2.0 将非流式和流式调用拆为两个接口,避免把“一个完整响应”和“连续响应流”混在同一种处理模型里。

接口关键方法处理对象适用场景
AdvisorgetOrder()Advisor 的共同契约定义扩展参与顺序
CallAdvisoradviseCall(request, chain)一次完整的 ChatClientResponse普通 HTTP 问答、完整结果审计、响应改写
StreamAdvisoradviseStream(request, chain)Flux<ChatClientResponse>SSE 打字机输出、逐段过滤、流式埋点
CallAdvisorChainnextCall(request)后续同步责任链前置增强后继续请求,或在返回后处理结果
StreamAdvisorChainnextStream(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:

text
安全输入检查 -> 会话记忆 -> 问题改写 / RAG -> 日志与指标 -> 模型
模型 -> 日志与指标 -> 输出安全检查 -> 返回客户端

不要依赖注册列表的偶然顺序。相邻 Advisor 之间存在“谁先写入、谁后读取”关系时,必须显式实现 getOrder() 并写测试。

2.2 adviseContext 是链内调用状态,不是长期存储

ChatClientRequestChatClientResponse 带有 adviseContext,用于在本轮责任链内传递元数据,例如检索到的文档 ID、脱敏标记、租户 ID 或一次请求的开始时间。它是不可变的;需要变更时使用请求/响应的 updateContext(...) 产生新对象。

不要把聊天全文、用户画像或大检索结果塞进这里:它们会扩大单次调用对象,也不会跨请求持久化。跨轮聊天历史应使用 ChatMemory,知识库检索应使用 VectorStore / RAG 组件。

3. 在 ChatClient 中注册 Advisor

Advisor 可以注册为某个 ChatClient 的默认能力,也可以只附加到一轮调用。

java
@Configuration
class AiConfig {

    @Bean
    ChatClient supportChatClient(ChatClient.Builder builder,
            MessageChatMemoryAdvisor memoryAdvisor,
            QuestionAnswerAdvisor knowledgeAdvisor) {
        return builder
                .defaultSystem("你是客服助手;无可靠依据时明确说明。")
                .defaultAdvisors(memoryAdvisor, knowledgeAdvisor)
                .build();
    }
}

默认注册适合所有请求都必须拥有的能力,例如会话记忆、统一日志或组织级安全策略。一次性能力则放在调用处:

java
String answer = supportChatClient.prompt()
        .user("解释退款进度")
        .advisors(advisor -> advisor.param(
                ChatMemory.CONVERSATION_ID, conversationId))
        .call()
        .content();

这里 .advisors(...) 不新增 Advisor,而是为当前调用配置 Advisor 参数;MessageChatMemoryAdvisorChatMemory.CONVERSATION_ID 区分会话。这样同一个默认 Advisor 可以服务不同用户、不同会话,避免历史串话。

对于只在一个端点使用的能力,可以传入 Advisor 实例:

java
return chatClient.prompt()
        .user(question)
        .advisors(new SimpleLoggerAdvisor())
        .call()
        .content();

3.1 Advisor 日志如何输出和配置

内置 SimpleLoggerAdvisor 只有被注册到这次调用或 defaultAdvisors(...) 后才会参与链路;它通过 Spring Boot 的日志系统在 DEBUG 级别输出请求与响应摘要。开发环境可在 application.yml 中打开 Advisor 包的日志:

yaml
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 等“用户需要延续上一轮语义”的场景。

java
@Bean
MessageChatMemoryAdvisor memoryAdvisor(ChatMemory chatMemory) {
    return MessageChatMemoryAdvisor.builder(chatMemory).build();
}

它解决“模型无状态”的问题,但不等同于长期用户档案。应配置窗口大小、会话 ID 隔离和持久化仓库,详情见本目录《Spring AI 聊天记忆存储》。

4.2 简单 RAG:QuestionAnswerAdvisor

QuestionAnswerAdvisor 使用 VectorStore 查找与问题相似的文档,把结果放入上下文,再让模型基于上下文回答。适合内部制度问答、产品文档助手和知识库客服。

java
@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 安全与工具:SafeGuardAdvisorToolCallingAdvisor

  • SafeGuardAdvisor:用于阻止模型生成有害或不适当的内容。它是基础护栏,不能替代鉴权、数据脱敏和业务风控。
  • ToolCallingAdvisor:负责处理模型发起的工具调用、回灌工具结果,并在需要时继续迭代。通常由 ChatClient 的工具配置自动参与;不要重复注册多个工具调用 Advisor。

工具调用涉及真实副作用时,仍要把“是否允许执行”放在业务权限与工具实现中,不能因为模型请求了工具就直接执行扣款、删除或发布操作。

5. 自定义 CallAdvisor:统一计时与审计

自定义 Advisor 应尽量只有一个职责。下面的例子只记录一次同步模型调用的耗时,既不修改 prompt,也不承载业务规则:

java
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";
    }
}

这个写法有三个要点:

  1. 调用 chain.nextCall(request) 才能继续执行后续 Advisor 与模型。
  2. finally 能覆盖正常返回和异常返回,适合计时与清理;异常转换、重试或降级要有明确的错误分类与幂等边界。
  3. getOrder() 是契约的一部分。这个 Advisor 的位置应由它想观察的请求版本决定。

若要注入系统规则、改写用户问题或更新 adviseContext,先基于 ChatClientRequest 的不可变更新 API 创建新请求,再传给 chain.nextCall(...)。不要修改共享对象,也不要在 Advisor 内偷偷依赖 Web 请求线程上下文;流式与异步调用下这种假设会失效。

6. 自定义 StreamAdvisor:流式链路必须保持响应式

流式接口返回 Flux<ChatClientResponse>。核心原则是组合响应式操作,而不是先 block() 收齐答案再处理,否则会失去首 token 延迟和背压能力。

java
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. 一个客服知识库的组合示例

下面是常见的组装方式。顺序应根据实际依赖、性能和安全策略调整:

text
输入安全检查
  -> MessageChatMemoryAdvisor(按 conversationId 恢复历史)
  -> RetrievalAugmentationAdvisor(按租户权限检索知识)
  -> TimingAdvisor / Micrometer 观测
  -> ChatModel + ToolCallingAdvisor
  -> 输出安全检查、响应日志

这条链清楚分工:Controller 只负责认证后的用户输入与返回格式;业务层负责租户权限、领域操作和工具实现;Advisor 负责每轮模型调用的共性增强;ChatModel 只负责推理。这样既便于替换向量库或模型,也能把审计、RAG、记忆各自测试。

8. 落地检查清单

  • 一个 Advisor 只处理一个横切目标;复杂 RAG 优先使用官方组合器而非巨型自定义类。
  • 同步与流式端点都需要时,实现 CallAdvisorStreamAdvisor,并验证两条链。
  • 明确每个 Advisor 的 getOrder() 和它依赖的上游数据。
  • 只把本轮、少量的调用元数据放入 adviseContext;聊天历史、知识与权限信息各归其位。
  • RAG 必须在检索前施加 ACL / 租户过滤;模型看见过的上下文无法在回答阶段“收回”。
  • 把 token、耗时、错误、取消等写入可观测系统,对 prompt 和响应做脱敏与采样。
  • 任何有副作用的 Tool 都必须在业务实现层二次校验授权、参数与幂等性。

9. 进一步阅读