《SpringAI 入门教程》 12. 对话拦截 Advisors 使用

129 阅读4分钟

Spring AI Advisors API 为我们的 SpringAI 应用提供了一种灵活且强大的方式,用于拦截、修改和增强基于 AI 的交互。通过利用 Advisors API,我们可以构建更复杂、可重用且易于维护的 AI 组件。

其主要优势包括:封装常见的生成式 AI 模式、对发送给大型语言模型(LLMs)以及从中返回的数据进行转换,并支持在不同模型和使用场景之间的可移植性。

1. Advisors 处理流程图

步骤解释

  1. Spring AI 框架会根据用户的 Prompt 创建一个 ChatClientRequest,同时生成一个空的 Advisor 上下文对象。

  2. 链中的每个 Advisor 会处理该请求,并可能对其进行修改。或者,Advisor 也可以选择阻止该请求 —— 即不调用链中的下一个实体。在这种情况下,Advisor 需要自己填充返回内容。与Filter用法类似。

  3. 由框架提供的最后一个 Advisor 会将请求发送给 ChatModel大模型(LLM)处理。

  4. Chatmodel返回的响应会沿着 Advisor 链反向传递,并被转换为 ChatClientResponse,其中包含共享的 Advisor 上下文实例。

  5. 每个 Advisor 都可以处理或修改该响应。

  6. 最终,通过提取 ChatCompletion,将 ChatClientResponse 返回给调用方。

2. Advisor使用场景

  • 日志请求记录

  • 大模型响应时间记录

  • 敏感词拦截

  • 提示词的拦截、修改、增强

  • ChatMemory、RAG、可视化等等都离不开对话拦截器 Advisor

3. Advisors 涉及的类图

adviseCall()adviseStream() 是关键的 Advisor 方法,通常用于执行以下操作:检查未封装的 Prompt 数据、自定义和增强 Prompt 数据、调用 Advisor 链中的下一个实体、有选择地阻止请求、检查聊天补全响应,以及在处理出错时抛出异常。

另外,getOrder() 方法用于确定 Advisor 在链中的执行顺序,而 getName() 方法则用于提供一个唯一的 Advisor 名称。

由 Spring AI 框架创建的 Advisor Chain 链 支持按顺序调用多个 Advisor,其执行顺序由各自的 getOrder() 值决定,数值越小的 Advisor 越先执行。链中的最后一个 Advisor 是由系统自动添加的,它负责将请求发送到 LLM。

4. 自定义日志拦截器


import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClientMessageAggregator;
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.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import reactor.core.publisher.Flux;

/**
* @author : upfive
* @version : 1.0.0
* @date : 2025/8/20 16:29
*/
@Slf4j
public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
        logRequest(chatClientRequest);
        ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
        logResponse(chatClientResponse);
        return chatClientResponse;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
        logRequest(chatClientRequest);
        Flux<ChatClientResponse> chatClientResponse = streamAdvisorChain.nextStream(chatClientRequest);
        return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponse, this::logResponse );
    }

    private void logRequest(ChatClientRequest request) {
        log.debug("request: {}", request);
    }

    private void logResponse(ChatClientResponse chatClientResponse) {
        log.debug("response: {}", chatClientResponse);
    }

    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
import lombok.AllArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.SafeGuardAdvisor;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.List;

@AllArgsConstructor
@RestController
public class ChatClientAdvisorController {

    private final DeepSeekChatModel chatModel;

    @RequestMapping(value = "/advisor", produces = "text/stream;charset=UTF-8")
    public void advisor() {
        var chatClient = ChatClient.builder(chatModel)
                //.defaultSystem(prompt)
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
        String content = chatClient.prompt().user("你好,你是谁?").call().content();
        System.out.println(content);

        System.out.println("=================");
        Flux<ChatClientResponse> res = chatClient.prompt()
                // SpringAI提供的默认拦截器
                .advisors(new SafeGuardAdvisor(List.of("疯狂")))
                .user("今天星期几?疯狂星期四吗?").stream().chatClientResponse();
        res.toIterable().forEach(System.out::println);

    }

}

5. 自定义拦截器实现重读功能

重读(Re2)

重读策略的核心在于让LLMs重新审视输入的问题,这借鉴了人类解决问题的思维方式。通过这种方式,LLMs能够更深入地理解问题,发现复杂的模式,从而在各种推理任务中表现得更加强大。

{Input_Query}
再次阅读问题:{Input_Query}

可以基于BaseAdvisor来实现自定义Advisor,它实现了重复的代码,提供模版方法让我们可以专注自己业务编写即可。


import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;

import java.util.Map;

/**
* @author : upfive
* @version : 1.0.0
* @date : 2025/8/20 16:29
*/
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 ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
        String contents = chatClientRequest.prompt().getContents();
        String re2InputQuery = PromptTemplate.builder().template(DEFAULT_USER_TEXT_ADVISE).build()
                .render(Map.of("re2_input_query", contents));
        // 复制一个新的ChatClientRequest对象,并修改其中的prompt
        return chatClientRequest.mutate()
                .prompt(Prompt.builder().content(re2InputQuery).build())
                .build();
    }

    @Override
    public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {

        return chatClientResponse;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
@RequestMapping(value = "/reReading", produces = "text/stream;charset=UTF-8")
public void reReading() {
    var chatClient = ChatClient.builder(chatModel)
            //.defaultSystem(prompt)
            .defaultAdvisors(new org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor(), new ReReadingAdvisor())
            .build();
    String content = chatClient.prompt().user("你好,你是谁?").call().content();
    System.out.println(content);
}

6. 最佳实践

  1. 让Advisors专注于特定任务,以实现更好的模块化。
  2. 在需要时使用 adviseContext 在Advisors之间共享状态。
  3. 实现Advisors的流式(streaming)和非流式(non-streaming)版本,以获得最大的灵活性。
  4. 仔细考虑Advisors链中的顺序,以确保数据流的正确性。