Spring AI 参考文档 - Chat Client API 与 Advisors API

Spring AI Advisors API 概述

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

主要优势包括:封装常见的生成式AI模式、转换发送至大语言模型(LLM)和从LLM接收的数据,以及提供跨各种模型和用例的可移植性。

您可以使用ChatClient API配置现有的advisor,如下例所示:

ChatMemory chatMemory = ... // 初始化您的聊天内存存储
VectorStore vectorStore = ... // 初始化您的向量存储

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(), // 聊天内存advisor
        QuestionAnswerAdvisor.builder(vectorStore).build()    // RAG advisor
    )
    .build();

var conversationId = "678";

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

建议在构建时使用builder的defaultAdvisors()方法注册advisor。

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


核心组件

该API包含用于非流式场景的CallAdvisorCallAdvisorChain,以及用于流式场景的StreamAdvisorStreamAdvisorChain。它还包括表示未封装的Prompt请求的ChatClientRequest,以及表示Chat Completion响应的ChatClientResponse。两者都持有一个advise-context,用于在advisor链中共享状态。
在这里插入图片描述
Advisors API 类图

adviseCall()adviseStream()是关键的advisor方法,通常执行以下操作:

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

此外,getOrder()方法决定advisor在链中的顺序,而getName()提供唯一的advisor名称。

Advisor链由Spring AI框架创建,允许按getOrder()值顺序依次调用多个advisor。值较低的优先执行。最后一个advisor是自动添加的,负责将请求发送给LLM。

以下流程图说明了advisor链与Chat Model之间的交互:
在这里插入图片描述
Advisors API 流程图

  1. Spring AI框架根据用户的Prompt创建一个ChatClientRequest,并附带一个空的advisor上下文对象。
  2. 链中的每个advisor处理请求,可能会修改它。或者,它可以选择通过不调用下一个实体来阻止请求。在后一种情况下,advisor负责填充响应。
  3. 由框架提供的最后一个advisor将请求发送给Chat Model。
  4. Chat Model的响应随后通过advisor链传回,并转换为ChatClientResponse。后者包含共享的advisor上下文实例。
  5. 每个advisor可以处理或修改响应。
  6. 最终的ChatClientResponse通过提取ChatCompletion返回给客户端。

Advisor 顺序

advisor在链中的执行顺序由getOrder()方法决定。需要理解的关键点:

  • order值较低的advisor优先执行。
  • advisor链以栈(stack)的方式运作:
    • 链中的第一个advisor是第一个处理请求的。
    • 它也是最后一个处理响应的。

控制执行顺序:

  • 将order设置为接近Ordered.HIGHEST_PRECEDENCE,确保advisor在链中首先执行(请求处理优先,响应处理最后)。
  • 将order设置为接近Ordered.LOWEST_PRECEDENCE,确保advisor在链中最后执行(请求处理最后,响应处理优先)。
  • 较高的值被解释为较低的优先级。
  • 如果多个advisor具有相同的order值,则它们的执行顺序无法保证

order与执行序列之间的看似矛盾是由于advisor链的栈式特性:

  • 具有最高优先级(最低order值)的advisor被添加到栈顶。
  • 当栈展开时,它将首先处理请求。
  • 当栈回卷时,它将最后处理响应。

以下是Spring Ordered接口的语义:

public interface Ordered {

    /**
     * 最高优先级值的常量。
     * @see java.lang.Integer#MIN_VALUE
     */
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

    /**
     * 最低优先级值的常量。
     * @see java.lang.Integer#MAX_VALUE
     */
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

    /**
     * 获取此对象的顺序值。
     * <p>较高的值被解释为较低的优先级。因此,
     * 具有最低值的对象具有最高优先级(有点类似于Servlet的{@code load-on-startup}值)。
     * <p>相同的order值将导致受影响对象的排序位置不确定。
     * @return order值
     * @see #HIGHEST_PRECEDENCE
     * @see #LOWEST_PRECEDENCE
     */
    int getOrder();
}

对于需要在输入和输出两侧都位于链首的用例:

  • 为每一侧使用单独的advisor。
  • 为它们配置不同的order值。
  • 使用advisor上下文在它们之间共享状态。

API 概览

主要的Advisor接口位于org.springframework.ai.chat.client.advisor.api包中。以下是您在创建自己的advisor时将遇到的关键接口:

public interface Advisor extends Ordered {

    String getName();

}

同步和响应式Advisor的两个子接口是:

public interface CallAdvisor extends Advisor {

    ChatClientResponse adviseCall(
        ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);

}

public interface StreamAdvisor extends Advisor {

    Flux<ChatClientResponse> adviseStream(
        ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);

}

要在您的Advice实现中继续执行Advice链,请使用CallAdvisorChainStreamAdvisorChain

接口定义如下:

public interface CallAdvisorChain extends AdvisorChain {

    /**
     * 使用给定的请求调用{@link CallAdvisorChain}中的下一个{@link CallAdvisor}。
     */
    ChatClientResponse nextCall(ChatClientRequest chatClientRequest);

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

}

public interface StreamAdvisorChain extends AdvisorChain {

    /**
     * 使用给定的请求调用{@link StreamAdvisorChain}中的下一个{@link StreamAdvisor}。
     */
    Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest);

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

}

实现 Advisor

要创建一个advisor,请实现CallAdvisorStreamAdvisor(或两者都实现)。需要实现的关键方法是用于非流式的nextCall()或用于流式的nextStream()


示例

我们将提供几个实践示例,以说明如何实现用于观察和增强用例的advisor。

日志记录 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() { 
        return this.getClass().getSimpleName();
    }

    @Override
    public int getOrder() { 
        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); 
    }

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

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

}
  • 为advisor提供唯一名称。
  • 您可以通过设置order值来控制执行顺序。较低的值优先执行。
  • MessageAggregator是一个工具类,用于将Flux响应聚合成单个ChatClientResponse。这对于需要观察整个响应而非流中单个项目的日志记录或其他处理非常有用。请注意,您不能在MessageAggregator中修改响应,因为它是只读操作。

重读(Re2)Advisor

《Re-Reading Improves Reasoning in Large Language Models》一文介绍了一种称为重读(Re2)的技术,该技术可以提高大语言模型的推理能力。Re2技术需要像这样增强输入提示:

{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) { 
        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() { 
        return this.order;
    }

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

}
  • before方法通过应用重读技术来增强用户的输入查询。
  • 您可以通过设置order值来控制执行顺序。较低的值优先执行。

Spring AI 内置 Advisors

Spring AI框架提供了几个内置advisor,以增强您的AI交互。以下是可用advisor的概述:

聊天内存 Advisors

这些advisor在聊天内存存储中管理对话历史:

  • MessageChatMemoryAdvisor

    • 检索内存并将其作为消息集合添加到提示中。这种方法保持了对话历史的结构。请注意,并非所有AI模型都支持这种方法。
  • VectorStoreChatMemoryAdvisor

    • 从VectorStore检索内存,并将其添加到提示的系统文本中。此advisor对于高效搜索和检索大型数据集中的相关信息非常有用。

问答 Advisor

  • QuestionAnswerAdvisor

    • 此advisor使用向量存储提供问答能力,实现了朴素RAG(检索增强生成)模式。
  • RetrievalAugmentationAdvisor

    • 使用org.springframework.ai.rag包中定义的构建块实现常见的检索增强生成(RAG)流程,遵循模块化RAG架构

推理 Advisor

  • ReReadingAdvisor
    • 实现了LLM推理的重读策略,称为RE2,以增强输入阶段的理解。基于文章:《Re-Reading Improves Reasoning in LLMs》。

工具调用 Advisor

  • ToolCallingAdvisor

    • 处理作为advisor链一部分的工具调用循环。此advisor始终由ChatClient自动注册(除非显式禁用),因此即使在未配置静态工具的情况下,由另一个advisor在运行时注入的工具也能得到支持。它执行模型请求的工具调用,并将结果发送回去,迭代直到不再需要工具调用。实现了ToolAdvisor,这是一个标记接口,用于防止自动注册第二个ToolCallingAdvisor

    完整文档请参见:递归Advisors - ToolCallingAdvisorChatClient - 工具调用

内容安全 Advisor

  • SafeGuardAdvisor
    • 一个简单的advisor,旨在防止模型生成有害或不适当的内容。

流式 vs 非流式

在这里插入图片描述
Advisors 流式与非流式流程图

  • 非流式advisor处理完整的请求和响应。
  • 流式advisor将请求和响应作为连续流处理,使用响应式编程概念(例如,使用Flux处理响应)。
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain chain) {

    return  Mono.just(chatClientRequest)
            .publishOn(Schedulers.boundedElastic())
            .map(request -> {
                // 此处可由阻塞和非阻塞线程执行。
                // Advisor在下一节之前的操作
            })
            .flatMapMany(request -> chain.nextStream(request))
            .map(response -> {
                // Advisor在下一节之后的操作
            });
}

最佳实践

  • 保持advisor专注于特定任务,以提高模块化。
  • 在必要时使用adviseContext在advisor之间共享状态。
  • 同时实现流式和非流式版本的advisor以获得最大的灵活性。
  • 仔细考虑advisor在链中的顺序,以确保正确的数据流。

重大 API 变更

Advisor 接口

  • 在1.0 M2中,有独立的RequestAdvisorResponseAdvisor接口。

    • RequestAdvisorChatModel.callChatModel.stream方法之前调用。
    • ResponseAdvisor在这些方法之后调用。
  • 在1.0 M3中,这些接口已被替换为:

    • CallAroundAdvisor
    • StreamAroundAdvisor
    • 以前属于ResponseAdvisorStreamResponseMode已被移除。
  • 在1.0.0中,这些接口已被替换:

    • CallAroundAdvisorCallAdvisor
    • StreamAroundAdvisorStreamAdvisor
    • CallAroundAdvisorChainCallAdvisorChain
    • StreamAroundAdvisorChainStreamAdvisorChain
    • AdvisedRequestChatClientRequest
    • AdvisedResponseChatClientResponse

上下文映射处理

  • 在1.0 M2中

    • 上下文映射是一个单独的方法参数。
    • 该映射是可变的,并在链中传递。
  • 在1.0 M3中

    • 上下文映射现在是AdvisedRequestAdvisedResponse记录的一部分。
    • 该映射是不可变的
    • 要更新上下文,请使用updateContext方法,该方法会创建一个包含更新内容的新不可修改映射
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐