夜雨聆风学习资料网

ARTICLE · 1278785

Spring AI 2.0 Advisor 实战:鉴权、日志、RAG 注入一个类搞定

Spring AI 2.0 Advisor 实战:鉴权、日志、RAG 注入一个类搞定

用 Spring AI 写 AI 应用的团队,代码很容易长成一个样子:每个业务方法开头都塞一段"拼系统提示词",中间插一段"查知识库再把结果拼到问题后面",最后还要记日志、算 token、查用户有没有权限。改一次规则,要把所有方法翻一遍。Spring AI 提供了 Advisor 机制,专门用来收掉这些横切逻辑:写一个类,挂到 ChatClient 上,之后所有调用自动生效。这篇文章从原理讲到三个能直接跑的 Advisor。

一、这个问题到底是什么

Advisor 要解决的问题,是 AI 应用里那些"每次调用都要做一遍、但和业务逻辑无关"的动作。

先看一段没有 Advisor 的典型代码。假设你在做一个企业内部的知识库问答接口:

public String ask(String userId, String question) {    // 1. 查权限    if (!permissionService.canUseAi(userId)) {        return "你没有权限使用该功能";    }    // 2. 脱敏,避免把手机号喂给大模型    String safe = question.replaceAll("(1[3-9]\\d)\\d{4}(\\d{4})", "$1****$2");    // 3. 查知识库    List<Document> docs = vectorStore.similaritySearch(            SearchRequest.builder().query(safe).topK(4).build());    String context = docs.stream().map(Document::getText)            .collect(Collectors.joining("\n---\n"));    // 4. 拼提示词    String prompt = """            你是企业知识库助手,只根据下面资料回答,资料里没有就说不知道。            资料:            %s            问题:%s            """.formatted(context, safe);    // 5. 调用模型    long start = System.currentTimeMillis();    String answer = chatClient.prompt().user(prompt).call().content();    // 6. 记日志和耗时    log.info("cost={}ms, user={}", System.currentTimeMillis() - start, userId);    return answer;}

六个步骤里,只有第 3 步的"取哪些资料"和第 6 步的"记什么"算业务,其余全是套路。麻烦的地方在于,一旦这个套路变了——比如公司要求所有 AI 调用都加敏感词过滤,或者日志要加上 token 消耗——你得把所有类似的 ask 方法找出来改一遍。十个接口就是十次改动,漏一个就出事。

Advisor 的思路是把这些动作从业务方法里抽出来,变成一个个独立的"拦截器"。 你只管写"用户问了什么、我要什么答案",鉴权、脱敏、检索、日志这些由 Advisor 挂在调用链上自动执行。写法上从"每次手动做六件事"变成"调用一次,链上自动跑六件事"。

如果你写过后端,这个概念不难理解:它就是 Servlet 的 Filter,或者 Spring MVC 的 HandlerInterceptor。请求进来先过一串 Filter,响应回来再过一遍,谁都不用改业务代码。Advisor 是同一套模型,只不过拦截的对象从 HTTP 请求变成了大模型的调用请求(提示词 + 上下文)。

区别在于,Servlet Filter 只能看到 HTTP 报文,而 Advisor 能看到结构化的 Prompt 对象,能改写系统提示词、能读 token 用量、能拿到调用方传进来的自定义参数。这一点决定了它能干的事情比普通过滤器多得多。

二、底层原理到底怎么回事

一次 chatClient.prompt().call() 的调用,实际是一个从 Advisor 链到模型的洋葱式穿透过程。

调用链长什么样

ChatClient.call()   └─ AdvisorChain(按 order 升序排列)        ├─ PermissionGuardAdvisor   (order = -100)        ├─ AiCallLogAdvisor         (order = 0)        ├─ RagInjectAdvisor         (order = 10)        ├─ ToolCallingAdvisor       (order = 300,内置,负责工具循环)        └─ ChatModelCallAdvisor     (链尾,真正调用大模型)                 └─ ChatModel(OpenAI / DeepSeek / 通义千问…)

链尾那个 ChatModelCallAdvisor 是框架内置的,它才是真正把请求发给模型的地方。你的自定义 Advisor 都排在它前面。请求一路"往下钻",响应再一路"往上传",跟 Web 过滤器完全一致。

三个要实现的接口

Spring AI 把 Advisor 拆成三个接口,按你需要拦截的调用类型来挑:

接口
拦截目标
核心方法
CallAdvisor
同步调用 .call()
adviseCall(request, chain)
 返回 ChatClientResponse
StreamAdvisor
流式调用 .stream()
adviseStream(request, chain)
 返回 Flux<ChatClientResponse>
BaseAdvisor
上面两种都要
你只写 before() / after(),框架帮你拼装

同步和流式的处理方式差别很大,这是新手最容易踩的坑:同步调用拿到了完整响应才能做后置处理,而流式响应是一段一段推过来的,必须在 Flux 上做文章。所以框架故意把两个接口分开,免得你写出"等流式响应全部收完再记日志"这种把流式优势抹掉的代码。

Advisor 接口本身还要求两个方法:getName() 返回名字(日志里标识用),getOrder() 返回顺序(数字越小越靠外)。

为什么 order 这么关键

order 决定了请求和响应分别经过 Advisor 的先后顺序。把链想象成洋葱:order 小的在最外层,请求最早进它、响应最后出它。

请求方向的执行顺序:

PermissionGuard(-100) → Log(0) → RagInject(10) → ToolCalling(300) → ChatModelCall

响应方向的执行顺序,正好反过来:

ChatModelCall → ToolCalling(300) → RagInject(10) → Log(0) → PermissionGuard(-100)

理解了这个,order 怎么定就清楚了:

  • 鉴权要最先做
    ,因为它可能直接拒绝,后面的检索、日志都没必要执行 → 用负数,排最外。
  • 日志要包住所有 Advisor
    ,这样统计的耗时才包含检索时间 → 用 0 或很小的数。
  • RAG 注入要在模型调用之前、在鉴权之后
     → 用正数。
  • 内置 Advisor 的 order 在 300 左右
    ,所以你的自定义 Advisor 只要用小于 300 的数,就一定排在模型调用前面。

context:Advisor 之间的"背包"

ChatClientRequest 和 ChatClientResponse 除了装 Prompt 和 ChatResponse,还各带一个 Map<String, Object> context。这个 Map 可以在调用时塞值:

chatClient.prompt()        .user("公司年假怎么算")        .advisors(a -> a.param("userId", "u-1024").param("role", "employee"))        .call()        .content();

链上每个 Advisor 都能在 request.context() 里读到这些值,也能往下传自己的中间结果。这是把"调用方信息"和"拦截逻辑"解耦的关键——Advisor 不需要认识你的 UserContext 类,只要能读到 userId 就够了。

框架自带的 Advisor 别浪费

很多需求不用自己写,Spring AI 已经内置了一批:

内置 Advisor
做什么
SimpleLoggerAdvisor
打印每次调用的请求和响应,支持自定义格式化
SafeGuardAdvisor
命中敏感词直接返回固定话术,不请求模型
MessageChatMemoryAdvisor
多轮对话记忆,自动把历史消息塞进请求
StructuredOutputValidationAdvisor
结构化输出校验失败时自动重试
ToolCallingAdvisor
工具调用的循环执行,支持最大迭代次数
LastMaxTokenSizeContentPurger
上下文超出窗口时裁掉最早的消息
ChatModelCallAdvisor
链尾,实际发起模型调用

自己写 Advisor 之前先看一眼这张表。 日志用 SimpleLoggerAdvisor、敏感词用 SafeGuardAdvisor,都比你手写靠谱。真正需要手写的场景是:鉴权短路、动态检索注入、把 token 消耗写进业务监控这类框架没有的逻辑。

三、实战:手把手写代码

下面搭一个能跑的工程,包含三个自定义 Advisor:鉴权、日志、RAG 注入。

3.1 pom.xml

<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>4.1.1</version>        <relativePath/>    </parent>    <groupId>com.example</groupId>    <artifactId>ai-advisor-demo</artifactId>    <version>1.0.0</version>    <properties>        <java.version>21</java.version>        <spring-ai.version>2.0.1</spring-ai.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>            <version>4.1.1</version>        </dependency>        <dependency>            <groupId>org.springframework.ai</groupId>            <artifactId>spring-ai-starter-model-openai</artifactId>            <version>2.0.1</version>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <version>4.1.1</version>            <scope>test</scope>        </dependency>    </dependencies>    <dependencyManagement>        <dependencies>            <dependency>                <groupId>org.springframework.ai</groupId>                <artifactId>spring-ai-bom</artifactId>                <version>2.0.1</version>                <type>pom</type>                <scope>import</scope>            </dependency>        </dependencies>    </dependencyManagement>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>                <version>4.1.1</version>            </plugin>        </plugins>    </build></project>

spring-ai-bom 用来统一管理 Spring AI 各模块的版本,spring-ai-starter-model-openai 是 OpenAI 兼容协议的 starter。用国内模型时不用换依赖,只改 base-url 就行。

3.2 application.yml

server:  port: 8080spring:  ai:    openai:      # 换成你的 key,或者用环境变量传入      api-key: {AI_BASE_URL:https://api.openai.com}      chat:        options:          model: {AI_EMBEDDING_MODEL:text-embedding-3-small}logging:  level:    com.example.aiadvisor: DEBUG

3.3 第一个 Advisor:日志、耗时和 token

先写最基础的日志 Advisor。它要干三件事:调用前记下开始时间,调用后算耗时,再从响应里读出 token 用量。

package com.example.aiadvisor.advisor;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.ai.chat.client.ChatClientRequest;import org.springframework.ai.chat.client.ChatClientResponse;import org.springframework.ai.chat.client.advisor.api.CallAdvisor;import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;import org.springframework.ai.chat.messages.Message;import org.springframework.ai.chat.metadata.Usage;import java.util.regex.Pattern;import java.util.stream.Collectors;/** * 记录每次模型调用的耗时、token 消耗,并对手机号做脱敏。 * order = 0:处于链路外层,统计的耗时包含后面 RAG 检索的时间。 */public class AiCallLogAdvisor implements CallAdvisor {    private static final Logger log = LoggerFactory.getLogger(AiCallLogAdvisor.class);    /** 手机号中间四位打码,避免明文进日志 */    private static final Pattern PHONE = Pattern.compile("(1[3-9]\\d)\\d{4}(\\d{4})");    @Override    public String getName() {        return "AiCallLogAdvisor";    }    @Override    public int getOrder() {        return 0;    }    @Override    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain chain) {        String userText = chatClientRequest.prompt().getInstructions().stream()                .map(Message::getText)                .collect(Collectors.joining(" | "));        String masked = PHONE.matcher(userText).replaceAll("$1****$2");        String userId = String.valueOf(chatClientRequest.context().getOrDefault("userId", "anonymous"));        log.debug("开始调用 userId={} prompt={}", userId, masked);        long start = System.currentTimeMillis();        ChatClientResponse response = chain.nextCall(chatClientRequest);        long cost = System.currentTimeMillis() - start;        Usage usage = response.chatResponse() == null ? null : response.chatResponse().getMetadata().getUsage();        if (usage == null) {            log.info("调用完成 userId={} cost={}ms tokens=未知", userId, cost);        } else {            log.info("调用完成 userId={} cost={}ms promptTokens={} completionTokens={} totalTokens={}",                    userId, cost, usage.getPromptTokens(), usage.getCompletionTokens(), usage.getTotalTokens());        }        return response;    }}

几个关键点:

  • chain.nextCall(request)
     这一行是分水岭。它之前是"请求还没发出去",之后是"响应已经拿到"。所有前置处理写上面,后置处理写下面。
  • 必须把 response 原样返回,否则上游 Advisor 拿到的是 null。
  • getInstructions()
     返回的是完整消息列表(系统消息、用户消息都在里面),所以这里直接把每段文本拼起来做脱敏,简单够用。

流式调用走的是另一条路。流式响应是 Flux<ChatClientResponse>,一段一段推,不能等收完再处理:

package com.example.aiadvisor.advisor;import org.slf4j.Logger;import org.slf4j.LoggerFactory;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;import reactor.core.publisher.Flux;/** * 流式调用的日志:用 doOnSubscribe / doOnComplete 挂在流上, * 不阻塞、不缓存,收到一段推一段。 */public class StreamLogAdvisor implements StreamAdvisor {    private static final Logger log = LoggerFactory.getLogger(StreamLogAdvisor.class);    @Override    public String getName() {        return "StreamLogAdvisor";    }    @Override    public int getOrder() {        return 0;    }    @Override    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain chain) {        long start = System.currentTimeMillis();        return chain.nextStream(chatClientRequest)                .doOnSubscribe(subscription -> log.debug("流式调用开始"))                .doOnComplete(() -> log.info("流式调用结束 cost={}ms", System.currentTimeMillis() - start))                .doOnError(error -> log.error("流式调用失败 cost={}ms", System.currentTimeMillis() - start, error));    }}

注意这里没有 .collectList() 或者 .block()——一旦收完再处理,流式的体验就没了。

3.4 第二个 Advisor:鉴权,命中就短路

这个 Advisor 演示怎么"不调用模型直接返回"。做法是命中拒绝条件时不执行 chain.nextCall(),自己造一个响应返回。

package com.example.aiadvisor.advisor;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.ai.chat.client.ChatClientRequest;import org.springframework.ai.chat.client.ChatClientResponse;import org.springframework.ai.chat.client.advisor.api.CallAdvisor;import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;import org.springframework.ai.chat.messages.AssistantMessage;import org.springframework.ai.chat.model.ChatResponse;import org.springframework.ai.chat.model.Generation;import java.util.List;import java.util.Map;import java.util.Set;/** * 调用前检查 context 里的角色,没有权限就直接返回固定话术, * 不发起模型调用,省时间也省钱。 * order = -100:排在最外层,最先执行。 */public class PermissionGuardAdvisor implements CallAdvisor {    private static final Logger log = LoggerFactory.getLogger(PermissionGuardAdvisor.class);    /** 允许使用 AI 功能的角色白名单 */    private static final Set<String> ALLOWED_ROLES = Set.of("employee", "admin");    @Override    public String getName() {        return "PermissionGuardAdvisor";    }    @Override    public int getOrder() {        return -100;    }    @Override    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain chain) {        Map<String, Object> context = chatClientRequest.context();        String role = String.valueOf(context.getOrDefault("role", "guest"));        if (!ALLOWED_ROLES.contains(role)) {            log.warn("拒绝调用 role={}", role);            return denied(chatClientRequest, "当前账号没有使用 AI 助手的权限,请联系管理员开通。");        }        return chain.nextCall(chatClientRequest);    }    /** 构造一个"假的"模型响应,让调用方拿到正常结构,不用改业务代码 */    private ChatClientResponse denied(ChatClientRequest request, String message) {        ChatResponse chatResponse = ChatResponse.builder()                .generations(List.of(new Generation(new AssistantMessage(message))))                .build();        return ChatClientResponse.builder()                .chatResponse(chatResponse)                .context(request.context())                .build();    }}

这里有几个细节值得说:

  • context 要原样传回去
    。下游如果有 Advisor 依赖 context 里的值,丢了这个 Map 会直接 NPE。
  • 用 context.getOrDefault("role", "guest") 而不是强制取值,是为了让"忘记传参数"这种情况默认落到拒绝分支,安全方向朝保守的一边倒。
  • 拼出来的 ChatResponse 结构上和真实响应一致,业务代码调 .content() 照样能拿到字符串。

3.5 第三个 Advisor:RAG 检索注入

把知识库检索做成 Advisor,业务代码里就只剩下"我问什么",检索和拼提示词全自动。

package com.example.aiadvisor.advisor;import org.springframework.ai.chat.client.ChatClientRequest;import org.springframework.ai.chat.client.ChatClientResponse;import org.springframework.ai.chat.client.advisor.api.CallAdvisor;import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;import org.springframework.ai.chat.messages.Message;import org.springframework.ai.document.Document;import org.springframework.ai.vectorstore.SearchRequest;import org.springframework.ai.vectorstore.VectorStore;import java.util.List;import java.util.stream.Collectors;/** * 在模型调用前检索知识库,把命中的资料追加到用户消息后面。 * order = 10:排在模型调用之前,排在日志 Advisor 里面。 */public class RagInjectAdvisor implements CallAdvisor {    private final VectorStore vectorStore;    private final int topK;    public RagInjectAdvisor(VectorStore vectorStore) {        this(vectorStore, 4);    }    public RagInjectAdvisor(VectorStore vectorStore, int topK) {        this.vectorStore = vectorStore;        this.topK = topK;    }    @Override    public String getName() {        return "RagInjectAdvisor";    }    @Override    public int getOrder() {        return 10;    }    @Override    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain chain) {        String question = chatClientRequest.prompt().getInstructions().stream()                .map(Message::getText)                .collect(Collectors.joining("\n"));        List<Document> docs = vectorStore.similaritySearch(                SearchRequest.builder()                        .query(question)                        .topK(this.topK)                        .similarityThreshold(0.5)                        .build());        // 一条都没命中就别硬塞,让模型自己回答,避免答非所问        if (docs == null || docs.isEmpty()) {            return chain.nextCall(chatClientRequest);        }        String context = docs.stream()                .map(Document::getText)                .collect(Collectors.joining("\n---\n"));        ChatClientRequest mutated = chatClientRequest.mutate()                .prompt(chatClientRequest.prompt().augmentUserMessage("""                        【参考资料】                        %s                        请只根据上面的参考资料回答,资料里没有写的内容,直接回答"资料里没有提到"。                        """.formatted(context)))                .build();        return chain.nextCall(mutated);    }}

mutate() 是关键 API:它基于原请求生成一个副本,你不用自己 new 一个 Prompt。augmentUserMessage(text) 会把文字追加到用户消息末尾,原来的问题还在,两段拼在一起发给模型。

如果知识库要按用户权限过滤(比如销售只能查销售手册),在 SearchRequest 上加 filterExpression("department == 'sales'") 就行,Advisor 里能读到 context.get("department"),拼成对应表达式。

3.6 装配:配置类、知识库和接口

把三个 Advisor 挂到 ChatClient 上:

package com.example.aiadvisor.config;import com.example.aiadvisor.advisor.AiCallLogAdvisor;import com.example.aiadvisor.advisor.PermissionGuardAdvisor;import com.example.aiadvisor.advisor.RagInjectAdvisor;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.embedding.EmbeddingModel;import org.springframework.ai.vectorstore.SimpleVectorStore;import org.springframework.ai.vectorstore.VectorStore;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class AiAdvisorConfig {    @Bean    public VectorStore vectorStore(EmbeddingModel embeddingModel) {        // 示例用内存向量库,生产环境换成 Milvus / Redis 的 starter 即可        return SimpleVectorStore.builder(embeddingModel).build();    }    @Bean    public ChatClient chatClient(ChatClient.Builder builder, VectorStore vectorStore) {        return builder                .defaultSystem("你是企业内部知识库助手,回答简洁、口语化,不要输出 Markdown 表格。")                .defaultAdvisors(                        new PermissionGuardAdvisor(),   // order = -100                        new AiCallLogAdvisor(),         // order = 0                        new RagInjectAdvisor(vectorStore) // order = 10                )                .build();    }}

defaultAdvisors() 注册的 Advisor 对每次调用都生效。如果某个接口想临时加一个,用 chatClient.prompt().advisors(...) 单独挂。

塞几条测试数据进去:

package com.example.aiadvisor.config;import org.springframework.ai.document.Document;import org.springframework.ai.vectorstore.VectorStore;import org.springframework.boot.ApplicationRunner;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.List;import java.util.Map;@Configurationpublic class KnowledgeBaseConfig {    @Bean    public ApplicationRunner loadDocuments(VectorStore vectorStore) {        return args -> vectorStore.add(List.of(                Document.builder()                        .text("公司年假规则:入职满一年 5 天,满三年 10 天,满五年 15 天。当年未休完可结转 3 天到次年 3 月底。")                        .metadata(Map.of("department", "hr"))                        .build(),                Document.builder()                        .text("差旅报销标准:市内交通实报实销,高铁二等座,住宿一线城市每晚不超过 500 元。")                        .metadata(Map.of("department", "finance"))                        .build()        ));    }}

最后是对外接口:

package com.example.aiadvisor.controller;import org.springframework.ai.chat.client.ChatClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class ChatController {    private final ChatClient chatClient;    public ChatController(ChatClient chatClient) {        this.chatClient = chatClient;    }    @GetMapping("/chat")    public String chat(@RequestParam String q,                       @RequestParam(defaultValue = "u-1001") String userId,                       @RequestParam(defaultValue = "employee") String role) {        return chatClient.prompt()                .user(q)                .advisors(a -> a.param("userId", userId).param("role", role))                .call()                .content();    }}

启动后访问 http://localhost:8080/chat?q=年假有几天,日志里会依次出现鉴权通过、开始调用、调用完成(带 token 数和耗时)三行,回答里会带上知识库里的年假规则。把 role 改成 guest,模型不会被调用,直接返回权限不足的提示。

四、踩坑经验和最佳实践

改请求一定要用 mutate(),别自己 new Prompt。 自己构造 Prompt 很容易把 options(温度、模型名这些)丢掉,模型就悄悄变成默认配置了。mutate() 是复制再改,原有字段全保留。

调用 chain.nextCall() 之前抛异常,等于拒绝服务。 Advisor 链没有统一的异常兜底,前置逻辑里一个 NullPointerException 会让整个调用失败并冒泡给上层。做鉴权、限流这类可能失败的逻辑,建议 catch 住然后返回兜底响应,别让异常穿出去。

context 里塞的对象要能跨线程看。 流式调用跑在 Reactor 线程上,如果你在 context 里塞了一个绑定 ThreadLocal 的对象,到了流式处理阶段可能取不到。放字符串、数字这类不可变值最稳。

别在 Advisor 里做重活。 Advisor 是在调用链主线程上同步执行的,一次向量检索通常几十毫秒还能接受,但如果你在里面调远程接口、循环查数据库,每个请求都会多等这么久。RAG 检索想并行化,把它挪到业务侧用 CompletableFuture 先查好,再通过 context 把结果传进 Advisor。

Advisor 数量控制在合理范围。 每个 Advisor 都会走一遍请求和响应,5 个以内基本无感。见过有人写十几个 Advisor,每个都拼一次字符串,日志里都分不清谁改了什么。同类逻辑尽量合并到一个类里。

生产日志别打印完整提示词。 提示词里往往有用户输入的隐私信息,脱敏后再落盘。真要排查问题,把原文写到单独的加密文件里,别混进主日志。

五、性能对比和技术选型

Advisor 链本身的性能开销可以忽略,它省下的是重复代码和漏改风险。

单次 Advisor 的执行就是读一个 Map、拼几个字符串,纯 CPU 开销在微秒级。而一次大模型调用通常在几百毫秒到几秒之间,Advisor 的开销占比不到千分之一。真正影响性能的是 Advisor 里干了什么——向量检索、远程调用这些才是耗时的来源,和 Advisor 机制本身无关。

方案
能拿到 Prompt 对象
能改请求
能短路
适合场景
Spring AOP 环绕通知
否
否
可以
纯耗时统计
业务侧手写工具方法
否
否
可以
一次性逻辑
Spring AI Advisor
是
是
是
鉴权、RAG 注入、日志、限流
LangChain4j ChatModelListener
部分
否
否
只做监听,不改请求

结论很直接:只要需要"看提示词内容"或者"改提示词",就用 Spring AI Advisor。 AOP 切面拿不到 Prompt,只能统计方法耗时;LangChain4j 的 ChatModelListener 是纯监听器,能记录但改不了请求。跨技术栈做 RAG 注入这类需求,Advisor 是 Java 生态里最顺手的方案。

选型上还有一个建议:能用内置就别自己写。敏感词过滤用 SafeGuardAdvisor,日志用 SimpleLoggerAdvisor,多轮记忆用 MessageChatMemoryAdvisor——这三个是框架维护的,字段和行为都比你手写的稳。

六、总结

Advisor 是 Spring AI 用来承载横切逻辑的机制,本质是一条洋葱式的调用链,请求穿进去、响应穿出来,中间每层都能读请求、改请求、短路返回。

三个要点记住就够了:

  • 按调用类型选接口
    :同步实现 CallAdvisor,流式实现 StreamAdvisor,两个都要就实现 BaseAdvisor。
  • order 决定位置
    :越小越靠外。鉴权用负数排最前,日志用 0 包住整条链,RAG 注入用小于 300 的正数排在模型调用之前。
  • context 是传参通道
    :调用方用 .advisors(a -> a.param(...)) 塞值,Advisor 里用 request.context() 读值,业务对象和拦截逻辑就此解耦。

写代码的时候,先看一眼内置 Advisor 表,能复用就复用;真要自己写,从日志 Advisor 开始,跑通再往里加鉴权和检索。这样出问题的时候,一眼就能看出是哪一层的事。

参考资料

  • Spring AI - Chat Client API
  • Spring AI - Advisors API
  • Spring AI - Vector Databases
  • Spring AI - OpenAI Embeddings

文 / 会编程的吕洞宾

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料