Lzh on GitHub

Advisors API

Spring AI 的 Advisors API 提供了一种灵活且强大的方式,用于在 Spring 应用中拦截、修改和增强 AI 驱动的交互。通过利用 Advisors API,开发者可以创建更复杂、可复用且易于维护的 AI 组件。

Spring AI 的 Advisors API 提供了一种灵活且强大的方式,用于在 Spring 应用中拦截、修改和增强 AI 驱动的交互。通过利用 Advisors API,开发者可以创建更复杂、可复用且易于维护的 AI 组件。

其主要优势包括封装常见的生成式 AI 模式、转换发送给和从大型语言模型(LLM)返回的数据,并提供跨不同模型和用例的可移植性。

你可以使用 ChatClient API 配置现有的顾问(advisors),如下示例所示:

ChatMemory chatMemory = ... // 初始化聊天记忆存储
VectorStore vectorStore = ... // 初始化向量存储

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(), // 聊天记忆顾问
        QuestionAnswerAdvisor.builder(vectorStore).build()    // RAG 顾问
    )
    .build();

var conversationId = "678";

String response = this.chatClient.prompt()
    // 在运行时设置顾问参数
    .advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
    .user(userText)
    .call()
    .content();

建议在构建阶段通过 builder 的 defaultAdvisors() 方法注册顾问。

顾问还参与可观测性(Observability)栈,因此你可以查看与其执行相关的指标和追踪信息。

了解更多:

核心组件

该 API 包含用于非流式场景的 CallAdvisorCallAdvisorChain,以及用于流式场景的 StreamAdvisorStreamAdvisorChain。它还包括用于表示未封装 Prompt 请求的 ChatClientRequest 和表示 Chat Completion 响应的 ChatClientResponse。两者都持有一个 advise-context,用于在顾问链中共享状态。

adviseCall()adviseStream() 是顾问的关键方法,通常执行的操作包括:

  • 检查未封装的 Prompt 数据
  • 定制和增强 Prompt 数据
  • 调用顾问链中的下一个实体
  • 可选择阻塞请求
  • 检查聊天完成响应
  • 抛出异常以指示处理错误

此外,getOrder() 方法用于确定顾问在链中的执行顺序,getName() 提供顾问的唯一名称。

Spring AI 框架创建的顾问链允许按 getOrder() 值顺序依次调用多个顾问。值越小的顾问越先执行。链中最后一个顾问由框架自动添加,它会将请求发送给 LLM。

下图展示了顾问链(Advisor Chain)与聊天模型(Chat Model)之间的交互流程:

  1. Spring AI 框架根据用户的 Prompt 创建一个 ChatClientRequest,并附带一个空的顾问上下文对象(advisor context)。
  2. 链中的每个顾问处理请求,可能对请求进行修改;也可以选择阻塞请求,即不调用下一个顾问。在这种情况下,该顾问负责填充响应。
  3. 框架提供的最终顾问将请求发送给 Chat 模型
  4. Chat 模型的响应再通过顾问链传回,并转换为 ChatClientResponse,其中包含共享的顾问上下文(context)实例。
  5. 每个顾问都可以对响应进行处理或修改。
  6. 最终的 ChatClientResponse 通过提取 ChatCompletion 返回给客户端。

顾问(advisor)顺序

顾问链中顾问的执行顺序由 getOrder() 方法决定。关键点如下:

  • 顺序值较低的顾问先执行
  • 顾问链的工作方式类似栈
    • 链中的第一个顾问是最先处理请求的。
    • 它也是处理响应时最后一个执行的。
  • 控制执行顺序的方法
    • 将顺序值设为接近 Ordered.HIGHEST_PRECEDENCE,确保顾问在链中最先执行(请求处理时最先,响应处理时最后)。
    • 将顺序值设为接近 Ordered.LOWEST_PRECEDENCE,确保顾问在链中最后执行(请求处理时最后,响应处理时最先)。
  • 顺序值越高,优先级越低
  • 如果多个顾问顺序值相同,其执行顺序不保证。
顾问链看似顺序与执行顺序的矛盾,是由于其 栈式特性
  • 最高优先级的顾问(最小顺序值)被加入栈顶。
  • 它在请求处理时最先执行(栈展开)。
  • 它在响应处理时最后执行(栈回收)。

提醒一下,以下是 Spring Ordered 接口的语义:

public interface Ordered {

    /**
     * Constant for the highest precedence value.
     * @see java.lang.Integer#MIN_VALUE
     */
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

    /**
     * Constant for the lowest precedence value.
     * @see java.lang.Integer#MAX_VALUE
     */
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

    /**
     * Get the order value of this object.
     * <p>Higher values are interpreted as lower priority. As a consequence,
     * the object with the lowest value has the highest priority (somewhat
     * analogous to Servlet {@code load-on-startup} values).
     * <p>Same order values will result in arbitrary sort positions for the
     * affected objects.
     * @return the order value
     * @see #HIGHEST_PRECEDENCE
     * @see #LOWEST_PRECEDENCE
     */
    int getOrder();
}
如果希望在输入和输出两端都最先执行
  • 为输入和输出分别使用不同的顾问。
  • 设置不同的顺序值。
  • 使用顾问上下文(advisor context)在它们之间共享状态。

API 概览

主要的顾问(Advisor)接口位于包 org.springframework.ai.chat.client.advisor.api 中。以下是创建自定义顾问时会用到的关键接口:

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

用于同步和响应式(Reactive)顾问的两个子接口分别是:

同步顾问接口:

public interface CallAdvisor extends Advisor {
    ChatClientResponse adviseCall(
        ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);
}

响应式顾问接口:

public interface StreamAdvisor extends Advisor {
    Flux<ChatClientResponse> adviseStream(
        ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);
}

在自定义顾问的实现中,如果需要继续调用顾问链,请使用 CallAdvisorChainStreamAdvisorChain

CallAdvisorChain 接口:

public interface CallAdvisorChain extends AdvisorChain {

    /**
     * 调用链中下一个 CallAdvisor 并传入请求。
     */
    ChatClientResponse nextCall(ChatClientRequest chatClientRequest);

    /**
     * 返回在链创建时包含的所有 CallAdvisor 实例列表。
     */
    List<CallAdvisor> getCallAdvisors();
}

StreamAdvisorChain 接口:

public interface StreamAdvisorChain extends AdvisorChain {

    /**
     * 调用链中下一个 StreamAdvisor 并传入请求。
     */
    Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest);

    /**
     * 返回在链创建时包含的所有 StreamAdvisor 实例列表。
     */
    List<StreamAdvisor> getStreamAdvisors();
}

实现顾问

要创建一个顾问(Advisor),你需要实现 CallAdvisorStreamAdvisor(或两者皆实现)。需要重点实现的方法是:

  • 对于 非流式 顾问:实现方法调用 nextCall()
  • 对于 流式 顾问:实现方法调用 nextStream()

示例

我们将通过几个实际示例来展示如何实现用于 “观测与增强” 场景的 Advisor。

日志记录(Logging)Advisor

下面示例实现了一个简单的日志 Advisor,它会在调用下一个 Advisor 之前记录 ChatClientRequest,并在返回后记录 ChatClientResponse。 注意:该 Advisor 仅用于 观察 请求和响应,不会对其进行修改。 此实现同时支持 非流式流式 场景。

public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {

    private static final Logger logger = LoggerFactory.getLogger(SimpleLoggerAdvisor.class);

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

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

    @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> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest);

        return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponses, this::logResponse); // 3
    }

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

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

}

要点说明:

  1. getName():提供顾问的唯一名称。
  2. getOrder():通过设置顺序值控制执行顺序。值越小执行越早。
  3. ChatClientMessageAggregator 是一个工具类,用于将流式响应聚合为单个 ChatClientResponse,主要用于日志记录或只读取不修改响应的场景(注意:响应在聚合器中是只读的,不允许修改)。

Re-Reading(Re2)Advisor

论文 《Re-Reading Improves Reasoning in Large Language Models》 提出一种技术 —— Re-Reading(Re2),可以提升大模型的推理能力。Re2 技术需要将输入 Prompt 增强为如下格式:

{Input_Query}
Read the question again: {Input_Query}

实现一个应用 Re2 技术的 Advisor 示例:

public class ReReadingAdvisor implements BaseAdvisor {

    private static final String DEFAULT_RE2_ADVISE_TEMPLATE = """
            {re2_input_query}
            Read the question again: {re2_input_query}
            """;

    private final String re2AdviseTemplate;

    private int order = 0;

    public ReReadingAdvisor() {
        this(DEFAULT_RE2_ADVISE_TEMPLATE);
    }

    public ReReadingAdvisor(String re2AdviseTemplate) {
        this.re2AdviseTemplate = re2AdviseTemplate;
    }

    @Override
    public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) { // 1
        String augmentedUserText = PromptTemplate.builder()
            .template(this.re2AdviseTemplate)
            .variables(Map.of("re2_input_query", chatClientRequest.prompt().getUserMessage().getText()))
            .build()
            .render();

        return chatClientRequest.mutate()
            .prompt(chatClientRequest.prompt().augmentUserMessage(augmentedUserText))
            .build();
    }

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

    @Override
    public int getOrder() { // 2
        return this.order;
    }

    public ReReadingAdvisor withOrder(int order) {
        this.order = order;
        return this;
    }

}

要点说明:

  1. before() 方法增强用户输入,应用 Re2 Prompt 技术;
  2. 可通过 order 设置优先级,值越小执行越早;

Spring AI 内置 Advisors

Spring AI 框架提供多种内置 Advisor,用来增强 AI 的交互体验。

Chat Memory Advisors(聊天记忆顾问)

用于管理对话历史:

  • MessageChatMemoryAdvisor:从 Chat Memory 中读取历史,并以消息集合的形式加入 Prompt,此方法能够保持会话历史的结构。注意,并非所有 AI 模型都支持这种方式。
  • PromptChatMemoryAdvisor:将历史内容写入 Prompt 的 system 文本中
  • VectorStoreChatMemoryAdvisor:从 VectorStore 中检索历史,并写入 Prompt 的 system 文本。该助手对于高效搜索和检索大型数据集中相关的信息非常有用。
问答(RAG)类 Advisors
  • QuestionAnswerAdvisor:该助手使用向量存储来提供问答能力,实现了朴素的 RAG(检索增强生成)模式。
  • RetrievalAugmentationAdvisor:该助手使用 org.springframework.ai.rag 包中定义的构建模块,并遵循模块化 RAG 架构,来实现常见的检索增强生成(RAG)流程。
推理增强 Advisor
内容安全 Advisor
  • SafeGuardAdvisor
    • 用于阻止模型生成有害或不当内容的简单拦截顾问

流式与非流式

  • 非流式(Non-streaming)顾问处理完整的请求与响应。
  • 流式(Streaming)顾问则以连续数据流的方式处理请求与响应,使用响应式编程概念(例如用 Flux 处理响应)。
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain chain) {

    return Mono.just(chatClientRequest)
            .publishOn(Schedulers.boundedElastic())
            .map(request -> {
                // 此处的逻辑可由阻塞线程或非阻塞线程执行。
                // 在进入下一个链路前执行的顾问逻辑(before next)
            })
            .flatMapMany(request -> chain.nextStream(request))
            .map(response -> {
                // 在下一个链路执行完成后处理的顾问逻辑(after next)
            });
}

最佳实践

  1. 将顾问(Advisor)聚焦于明确的单一职责,以获得更好的模块化效果。
  2. 在需要时使用 adviseContext 在多个顾问之间共享状态。
  3. 同时实现顾问的流式与非流式版本,以获得最大的灵活性。
  4. 仔细规划顾问在链中的执行顺序,以确保数据流转正确无误。

重大 API 变更

顾问接口

  • 1.0 M2 中,分别存在 RequestAdvisorResponseAdvisor 接口。
    • RequestAdvisor 会在调用 ChatModel.callChatModel.stream 方法之前执行。
    • ResponseAdvisor 则在这些方法调用之后执行。
  • 1.0 M3 中,这些接口被替换为:
    • CallAroundAdvisor
    • StreamAroundAdvisor
  • 此前属于 ResponseAdvisorStreamResponseMode 已被移除。
  • 1.0.0 版本中,这些接口再次进行了调整:
    • CallAroundAdvisorCallAdvisor,StreamAroundAdvisorStreamAdvisor,CallAroundAdvisorChainCallAdvisorChainStreamAroundAdvisorChainStreamAdvisorChain
    • AdvisedRequestChatClientRequestAdvisedResponseChatClientResponse

上下文 Map 处理

  • 1.0 M2 中:
    • Context Map 是一个独立的方法参数。
    • 该 Map 是可变的,并在链路中传递。
  • 1.0 M3 中:
    • Context Map 现在被合并进了 AdvisedRequestAdvisedResponse 记录中。
    • 该 Map 变为不可变。
    • 若需更新 Context,请使用 updateContext 方法,它会创建一个包含更新内容的全新不可修改 Map。