Spring AI Advisor 到底是什么?为什么记忆和 RAG 都绕不开它

13 阅读10分钟

Spring AI Advisor 到底是什么

Spring AI Advisor 到底是什么?为什么记忆和 RAG 都绕不开它

很多 Java 开发者第一次给 AI 加“对话记忆”时,第一反应都是:

在 Service 层查历史消息,拼到 Prompt 里,然后再调模型。

这样写当然能跑。

比如用户问一句话,你先根据 userId 查最近几轮对话,再把历史消息和当前问题拼在一起,最后调用:

chatClient.prompt()
    .user(fullPrompt)
    .call()
    .content();

问题是,接口一多,这段逻辑很快就会散得到处都是。

今天这个接口要加记忆,明天那个接口要加 RAG,后天又想加日志和 token 统计。写着写着,Service 里就堆满了模型调用前后的处理逻辑。

Spring AI 里的 Advisor,就是为了解决这类问题。

它的价值不是让代码看起来更高级,而是把这些重复的调用增强,收进一条统一的链路里。

所以你会看到这些类:

  • 对话记忆:MessageChatMemoryAdvisor
  • RAG 问答:QuestionAnswerAdvisor
  • 日志记录:SimpleLoggerAdvisor

这篇文章,我们就把 Advisor 单独拆开讲清楚。

它到底是什么?

它什么时候执行?

为什么 Agent 记忆和 RAG,都很适合放在这里?


一、为什么不要在业务代码里手动拼一切

假设你要给一个客服 AI 加上“对话记忆”。

最直接的写法大概是这样:

List<Message> history = chatMemoryService.findRecent(userId);

String fullPrompt = buildPromptWithHistory(history, question);

String answer = chatClient.prompt()
    .user(fullPrompt)
    .call()
    .content();

chatMemoryService.save(userId, question, answer);

能跑。

但接口一多,问题就来了。

第一,每个接口都要重复这段逻辑。

客服助手要记忆,文档问答助手也要记忆,日志分析助手也可能要记住项目上下文。你不可能每个地方都复制一遍。

第二,RAG、日志、token 统计这些能力还会继续往里塞。

最后业务代码很容易变成这样:

List<Message> history = chatMemoryService.findRecent(userId);
List<Document> docs = vectorStore.similaritySearch(
    SearchRequest.builder().query(question).topK(5).build()
);

String fullPrompt = buildPrompt(history, docs, question);

String answer = chatClient.prompt()
    .user(fullPrompt)
    .call()
    .content();

// 保存历史、记录日志、统计 token...

看起来还在 Service 里写业务。

实际已经变成了“上下文处理中心”。

第三,很难统一调整。

比如你想把“最近 5 轮历史”改成“最近 3 轮 + 摘要”,如果逻辑散在多个 Service 里,就要到处改。

这就是为什么需要 Advisor

它把这些和模型调用强相关的增强逻辑,从业务代码里拆出来。

代码可以重新变回这样:

String answer = chatClient.prompt()
    .user(question)
    .call()
    .content();

记忆、RAG、日志这些能力,则交给 Advisor 处理。


二、Advisor 在调用链路里的位置

上一篇我们提到,Spring AI 的一次完整调用链路大概是这样:

Controller -> ChatClient -> Prompt -> Advisor Chain -> ChatModel -> ChatResponse

Advisor 就在这条链路中间。

它不是简单的“装饰功能”,而是 Spring AI 处理上下文增强的重要扩展点。

Advisor 什么时候执行?

简单说:在模型调用前后。

你在业务代码里写:

String answer = chatClient.prompt()
    .user("什么是 Spring AI?")
    .call()
    .content();

表面上看,就是发个请求,拿个结果。

但实际执行时,中间可能会经过一条 Advisor Chain

所以,业务代码里看不到 Advisor,不代表它没有执行。

它通常是在构建 ChatClient 时配置好的:

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

之后每次调用:

chatClient.prompt()
    .user("什么是 Spring AI?")
    .call()
    .content();

这个 Advisor 都会参与这次调用。

日志是最容易理解的例子。

SimpleLoggerAdvisor 可以帮你记录模型调用前后的信息。业务代码不用到处写日志逻辑,但每次模型调用都能被统一观察到。

简单说,Advisor 适合承接那些“每次调用模型都可能要做,但又不属于业务主流程”的事。


三、对话记忆为什么适合放 Advisor

对话记忆的本质,是在调用模型前,把相关历史放进这次上下文里。

这件事有三个特点:

  • 每次调用都可能要做;
  • 逻辑相对独立,和具体业务关系不大;
  • 必须在模型调用前完成,否则模型不知道之前聊过什么。

这就很适合放在 Advisor 里。

不用 Advisor 的写法

不用 Advisor,就还是前面那套思路:

查历史 -> 转成 Message -> 拼进 Prompt -> 调模型 -> 保存本轮对话

这里说的历史记录,一般是你业务自己维护的聊天表,不是 Spring AI 的 ChatMemoryRepository

问题也很直接:Service 会被“查历史、拼消息、存回复”占满。以后想把“最近 5 轮”改成“最近 3 轮 + 摘要”,也要到处找代码。

用 Advisor 的写法

Spring AI 提供了 MessageChatMemoryAdvisor

@Bean
public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
    return ChatClient.builder(chatModel)
        .defaultAdvisors(
            MessageChatMemoryAdvisor.builder(chatMemory).build()
        )
        .build();
}

配置好以后,业务代码会干净很多:

public String chat(String conversationId, String question) {
    return chatClient.prompt()
        .user(question)
        .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
        .call()
        .content();
}

这里有个关键点:要把当前会话 ID 传进去。

MessageChatMemoryAdvisor 会基于 ChatMemory.CONVERSATION_ID 找到对应会话,再配合 ChatMemory 读取和维护消息。

你的业务代码不用关心“怎么查历史、怎么拼消息、怎么保存”,但要告诉它这是哪一段会话。

它大概做了这些事:

调用前:
  根据 conversationId 读取历史消息
  把历史消息注入本次请求
  把当前用户消息写入 memory

调用后:
  拿到模型响应
  把模型回复写回 memory

只要 conversationId 对得上,Advisor 就能从对应会话里取回历史,帮模型接上“刚才的话题”。

所以对话记忆适合放 Advisor:它围绕模型调用展开,但不应该散落在每个 Service 里。


四、RAG 为什么适合放 Advisor

RAG 的本质,也是在模型调用前做一件事:

先根据用户问题检索相关资料,再把资料放进本次上下文里。

你看,这和对话记忆很像。

对话记忆是从 ChatMemory 里取历史。

RAG 是从 VectorStore 里取文档。

它们都不是模型本身的能力,而是应用在调用模型前做的准备工作。

不用 Advisor 的写法

手动写大概是这样:

List<Document> docs = vectorStore.similaritySearch(
    SearchRequest.builder()
        .query(question)
        .topK(5)
        .build()
);

String prompt = buildPromptWithDocs(docs, question);

String answer = chatClient.prompt()
    .user(prompt)
    .call()
    .content();

这当然也能跑。

但问题和记忆一样:

  • 每个需要 RAG 的接口都要查向量库;
  • 每个地方都要拼文档上下文;
  • 检索参数、过滤条件、提示词模板很难统一管理;
  • 后面要加 rerank、query rewrite,也会继续往业务代码里塞。

最后 Service 又变成了“RAG 处理中心”。

用 QuestionAnswerAdvisor 的写法

Spring AI 提供了 QuestionAnswerAdvisor,它是一个基础 RAG Advisor,可以把常见的 RAG 流程放进增强链里。

@Bean
public ChatClient chatClient(ChatModel chatModel, VectorStore vectorStore) {
    QuestionAnswerAdvisor ragAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
        .searchRequest(SearchRequest.builder()
            .similarityThreshold(0.8)
            .topK(5)
            .build())
        .build();

    return ChatClient.builder(chatModel)
        .defaultAdvisors(ragAdvisor)
        .build();
}

业务代码还是这一段:

String answer = chatClient.prompt()
    .user("公司的请假制度是什么?")
    .call()
    .content();

真正调用时,QuestionAnswerAdvisor 会根据用户问题去 VectorStore 检索相关文档,再把这些文档作为上下文补进本次请求。

模型看到的就不只是用户问题,而是:

用户问题
+ 检索到的相关文档片段

这就是一个基础 RAG 流程。

如果后面要做更复杂的 RAG,比如 query transformation、rerank、复杂检索流程,再去看 RetrievalAugmentationAdvisor 这类更完整的方案。

这一篇先把基础逻辑讲清楚。


五、多个 Advisor 怎么组合

实际项目里,你通常不会只用一个 Advisor

比如一个企业知识库助手,可能同时需要:

  • 记录调用日志;
  • 带上最近几轮对话;
  • 从知识库检索相关文档;
  • 统计 token 消耗。

这些能力如果都写在业务代码里,Service 很快就会乱。

但放到 Advisor 里,就可以组合起来。建议显式设置 order,不要只靠参数写在前后:

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        SimpleLoggerAdvisor.builder()
            .order(-100)
            .build(),
        MessageChatMemoryAdvisor.builder(chatMemory)
            .order(0)
            .build(),
        QuestionAnswerAdvisor.builder(vectorStore)
            .searchRequest(SearchRequest.builder().topK(5).build())
            .order(10)
            .build()
    )
    .build();

业务调用还是这一段:

String answer = chatClient.prompt()
    .user(question)
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
    .call()
    .content();

看起来只是问了一个问题。

但背后可能已经做了很多事:

记录请求
-> 读取历史对话
-> 检索知识库文档
-> 组装本次上下文
-> 调用模型
-> 处理响应
-> 更新记忆
-> 记录日志

Advisor Chain 的好处就在这里:每个 Advisor 只关心自己那一段逻辑。

记忆 Advisor 负责记忆。

RAG Advisor 负责检索和补上下文。

日志 Advisor 负责记录调用过程。

它们可以组合在一起,又不会全部挤进 Service。

顺序很重要

多个 Advisor 不是随便放的。

在 Spring AI 里,执行顺序由 Advisor#getOrder() 决定:order 越小,越先处理请求,也越晚处理响应。

如果多个 Advisor 的 order 一样,执行顺序不要依赖。

比如:

order=-100  SimpleLoggerAdvisor
order=0     MessageChatMemoryAdvisor
order=10    QuestionAnswerAdvisor

和:

order=0     MessageChatMemoryAdvisor
order=10    QuestionAnswerAdvisor
order=20    SimpleLoggerAdvisor

看到的请求内容可能就不一样。

日志的 order 更小,看到的更接近原始请求。

日志的 order 更大,看到的更接近最终发给模型的请求。

RAG 放在记忆前还是记忆后,也要看业务目标。

如果希望 RAG 主要根据当前问题检索,可以让它主要读取当前 user message。

如果希望 RAG 结合历史上下文检索,就要确认当前的 Advisor 顺序和请求内容,是否真的把历史信息暴露给了检索逻辑。

没有一个固定答案。

关键是要记住:Advisor 是一条按 order 排出来的链,不是一堆互不相关的配置。


六、Advisor 的边界:什么不该放进来

Advisor 很好用,但不要把它当成万能容器。

它适合放这些东西:

  • 上下文增强;
  • 对话记忆;
  • RAG 检索;
  • 日志记录;
  • token 统计;
  • 轻量的请求改写和响应处理。

它们有一个共同点:

都围绕“这次模型调用”展开。

但下面这些,就别往 Advisor 里塞了。

1. 复杂业务逻辑

比如创建订单、审批退款、修改库存、调用支付系统。

这些应该放在 Service 层,或者通过 Tool Calling 暴露给模型,而不是塞进 Advisor。

Advisor 的职责是增强模型调用,不是替你处理业务流程。

2. 重量级数据处理

比如大文件解析、复杂报表生成、长时间批处理。

这些逻辑如果放进 Advisor,每次模型调用都会被拖慢。

更好的做法是提前处理好,或者放到异步任务里。

Advisor 里只放这次调用真正需要的结果。

3. 和模型调用无关的副作用

比如发短信、发邮件、扣库存、写订单状态。

这些操作一旦放进 Advisor,问题会很难排查。

因为业务代码里看不到它,但每次模型调用时它可能都会执行。

这很危险。

4. 过多隐式逻辑

Advisor 的优点是隐藏复杂度。

但隐藏太多,也会变成问题。

如果一个请求为什么变成这样、为什么多了这些上下文、为什么响应被改了,团队里没人说得清,那就是用过头了。

所以我建议记住一句话:

Advisor 只放和模型调用强相关、可复用、可组合的增强逻辑。

业务流程还是业务流程。

模型增强还是模型增强。

边界要分清。


写在最后

这篇文章,我们把 Advisor 单独拆开讲了一遍。

它解决的不是“怎么把代码写得高级”,而是一个更实际的问题:

不要把记忆、RAG、日志、token 统计,全都塞进 Service。

对话记忆是在调用前补历史,RAG 是在调用前补资料,日志和 token 统计是在调用前后观察这次请求。它们都围绕模型调用展开,所以适合交给 Advisor Chain

业务代码继续写业务。

模型增强交给增强链。

下一步如果要深入,就可以看自定义 Advisor。比如请求日志、token 统计、响应格式检查,本质上都是同一套扩展机制。


我是 Dilee,11 年 Java 老兵,专注 AI 落地应用。

后续会继续更新 Spring AI、RAG、Memory、Tool Calling、MCP 等实战内容。

完整系列也会同步整理在公众号「AI Agent 实战有术」。