Advisors API

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

其主要优势包括封装重复出现的生成式 AI 模式,转换发送到大型语言模型 (LLMs) 和从大型语言模型接收的数据,以及在各种模型和用例之间提供可移植性。

您可以使用 ChatClient API 配置现有的建议器,示例如下

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        new MessageChatMemoryAdvisor(chatMemory), // chat-memory advisor
        new QuestionAnswerAdvisor(vectorStore)    // RAG advisor
    )
    .build();

String response = this.chatClient.prompt()
    // Set advisor parameters at runtime
    .advisors(advisor -> advisor.param("chat_memory_conversation_id", "678")
            .param("chat_memory_response_size", 100))
    .user(userText)
    .call()
	.content();

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

建议器也参与到可观测性堆栈中,因此您可以查看与其执行相关的指标和跟踪信息。

核心组件

该 API 包含用于非流式场景的 CallAroundAdvisorCallAroundAdvisorChain,以及用于流式场景的 StreamAroundAdvisorStreamAroundAdvisorChain。它还包括用于表示未密封的 Prompt 请求的 AdvisedRequest,以及用于 Chat Completion 响应的 AdvisedResponse。两者都持有一个 advise-context,用于在建议器链中共享状态。

Advisors API Classes

nextAroundCall()nextAroundStream() 是关键的建议器方法,通常执行诸如检查未密封的 Prompt 数据、自定义和增强 Prompt 数据、调用建议器链中的下一个实体、可选地阻止请求、检查聊天完成响应以及抛出异常以指示处理错误等操作。

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

由 Spring AI 框架创建的建议器链(Advisor Chain)允许按照建议器的 getOrder() 值顺序调用多个建议器。值较低的建议器首先执行。最后一个自动添加的建议器会将请求发送给 LLM。

以下流程图说明了建议器链与聊天模型之间的交互

Advisors API Flow
  1. Spring AI 框架会根据用户的 Prompt 创建一个 AdvisedRequest,同时创建一个空的 AdvisorContext 对象。

  2. 链中的每个建议器都会处理请求,并可能对其进行修改。或者,它也可以选择通过不调用下一个实体来阻止请求。在后一种情况下,建议器负责填充响应。

  3. 由框架提供的最后一个建议器将请求发送给聊天模型。

  4. 然后,聊天模型的响应会通过建议器链传递回来,并转换为 AdvisedResponse。后者包含了共享的 AdvisorContext 实例。

  5. 每个建议器都可以处理或修改响应。

  6. 最终的 AdvisedResponse 通过提取 ChatCompletion 返回给客户端。

建议器顺序

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

  • order 值较低的建议器首先执行。

  • 建议器链作为栈运行

    • 链中的第一个建议器是第一个处理请求的。

    • 它也是最后一个处理响应的。

  • 要控制执行顺序

    • 将 order 值设置为接近 Ordered.HIGHEST_PRECEDENCE,以确保建议器在链中首先执行(先处理请求,后处理响应)。

    • 将 order 值设置为接近 Ordered.LOWEST_PRECEDENCE,以确保建议器在链中最后执行(后处理请求,先处理响应)。

  • 值越高表示优先级越低。

  • 如果多个建议器具有相同的 order 值,其执行顺序不保证。

order 值与执行顺序看似矛盾,原因在于建议器链的栈状特性

  • 优先级最高(order 值最低)的建议器被添加到栈顶。

  • 在栈展开时,它将第一个处理请求。

  • 在栈回卷时,它将最后一个处理响应。

提醒一下,以下是 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();
}

对于需要在链的输入侧和输出侧都优先执行的用例

  1. 为每一侧使用单独的建议器。

  2. 为它们配置不同的 order 值。

  3. 使用建议器上下文在它们之间共享状态。

API 概览

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

public interface Advisor extends Ordered {

	String getName();

}

同步和响应式建议器的两个子接口是

public interface CallAroundAdvisor extends Advisor {

	/**
	 * Around advice that wraps the ChatModel#call(Prompt) method.
	 * @param advisedRequest the advised request
	 * @param chain the advisor chain
	 * @return the response
	 */
	AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain);

}

public interface StreamAroundAdvisor extends Advisor {

	/**
	 * Around advice that wraps the invocation of the advised request.
	 * @param advisedRequest the advised request
	 * @param chain the chain of advisors to execute
	 * @return the result of the advised request
	 */
	Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain);

}

要在您的 Advice 实现中继续 Advice 链,请使用 CallAroundAdvisorChainStreamAroundAdvisorChain

接口是

public interface CallAroundAdvisorChain {

	AdvisedResponse nextAroundCall(AdvisedRequest advisedRequest);

}

public interface StreamAroundAdvisorChain {

	Flux<AdvisedResponse> nextAroundStream(AdvisedRequest advisedRequest);

}

实现一个建议器

要创建一个建议器,请实现 CallAroundAdvisorStreamAroundAdvisor(或两者)。需要实现的关键方法是用于非流式的 nextAroundCall() 或用于流式的 nextAroundStream()

示例

我们将提供一些动手示例,以说明如何实现用于观察和增强用例的建议器。

日志建议器

我们可以实现一个简单的日志建议器,它在调用链中的下一个建议器之前记录 AdvisedRequest,并在之后记录 AdvisedResponse。请注意,该建议器只观察请求和响应,不会修改它们。此实现支持非流式和流式场景。

public class SimpleLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {

	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 AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {

		logger.debug("BEFORE: {}", advisedRequest);

		AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);

		logger.debug("AFTER: {}", advisedResponse);

		return advisedResponse;
	}

	@Override
	public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {

		logger.debug("BEFORE: {}", advisedRequest);

		Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);

        return new MessageAggregator().aggregateAdvisedResponse(advisedResponses,
                    advisedResponse -> logger.debug("AFTER: {}", advisedResponse)); (3)
	}
}
1 提供建议器的唯一名称。
2 您可以通过设置 order 值来控制执行顺序。值较低的先执行。
3 MessageAggregator 是一个工具类,它将 Flux 响应聚合到一个 AdvisedResponse 中。这对于记录日志或观察整个响应而非流中单个项的其他处理非常有用。请注意,您不能在 MessageAggregator 中更改响应,因为它是一个只读操作。

重复阅读 (Re2) 建议器

重复阅读提高了大型语言模型的推理能力》这篇文章介绍了一种名为重复阅读 (Re2) 的技术,可以提高大型语言模型的推理能力。Re2 技术需要像这样增强输入提示词:

{Input_Query}
Read the question again: {Input_Query}

实现一个将 Re2 技术应用于用户输入查询的建议器可以像这样完成:

public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {


	private AdvisedRequest before(AdvisedRequest advisedRequest) { (1)

		Map<String, Object> advisedUserParams = new HashMap<>(advisedRequest.userParams());
		advisedUserParams.put("re2_input_query", advisedRequest.userText());

		return AdvisedRequest.from(advisedRequest)
			.userText("""
			    {re2_input_query}
			    Read the question again: {re2_input_query}
			    """)
			.userParams(advisedUserParams)
			.build();
	}

	@Override
	public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) { (2)
		return chain.nextAroundCall(this.before(advisedRequest));
	}

	@Override
	public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) { (3)
		return chain.nextAroundStream(this.before(advisedRequest));
	}

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

    @Override
    public String getName() { (5)
		return this.getClass().getSimpleName();
	}
}
1 before 方法应用重复阅读技术来增强用户的输入查询。
2 aroundCall 方法拦截非流式请求并应用重复阅读技术。
3 aroundStream 方法拦截流式请求并应用重复阅读技术。
4 您可以通过设置 order 值来控制执行顺序。值较低的先执行。
5 提供建议器的唯一名称。

Spring AI 内置建议器

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

聊天记忆建议器

这些建议器在聊天记忆存储中管理对话历史记录

  • MessageChatMemoryAdvisor

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

  • PromptChatMemoryAdvisor

    检索记忆并将其整合到提示词的系统文本中。

  • VectorStoreChatMemoryAdvisor

    从 VectorStore 中检索记忆并添加到提示词的系统文本中。此建议器对于高效搜索和检索大型数据集中的相关信息非常有用。

问答建议器
  • QuestionAnswerAdvisor

    此建议器使用向量存储来提供问答功能,实现了 RAG(检索增强生成)模式。

内容安全建议器
  • SafeGuardAdvisor

    一个简单的建议器,旨在防止模型生成有害或不适当的内容。

流式 vs 非流式

Advisors Streaming vs Non-Streaming Flow
  • 非流式建议器处理完整的请求和响应。

  • 流式建议器使用响应式编程概念(例如,用于响应的 Flux)将请求和响应作为连续流进行处理。

@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {

    return  Mono.just(advisedRequest)
            .publishOn(Schedulers.boundedElastic())
            .map(request -> {
                // This can be executed by blocking and non-blocking Threads.
                // Advisor before next section
            })
            .flatMapMany(request -> chain.nextAroundStream(request))
            .map(response -> {
                // Advisor after next section
            });
}

最佳实践

  1. 建议器应专注于特定任务,以提高模块化程度。

  2. 必要时使用 adviseContext 在建议器之间共享状态。

  3. 实现建议器的流式和非流式版本,以获得最大的灵活性。

  4. 仔细考虑链中建议器的顺序,以确保正确的数据流。

向后兼容性

AdvisedRequest 类已移至新包。

API 破坏性变更

Spring AI 建议器链从版本 1.0 M2 到 1.0 M3 发生了重大变化。以下是主要修改内容:

建议器接口

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

    • RequestAdvisorChatModel.callChatModel.stream 方法之前被调用。

    • ResponseAdvisor 在这些方法之后被调用。

  • 在 1.0 M3 中,这些接口已被以下接口取代:

    • CallAroundAdvisor

    • StreamAroundAdvisor

  • StreamResponseMode,之前是 ResponseAdvisor 的一部分,已被移除。

上下文 Map 处理

  • 在 1.0 M2 中

    • 上下文 map 是一个单独的方法参数。

    • 该 map 是可变的,并在链中传递。

  • 在 1.0 M3 中

    • 上下文 map 现在是 AdvisedRequestAdvisedResponse 记录的一部分。

    • 该 map 是不可变的。

    • 要更新上下文,请使用 updateContext 方法,该方法将创建一个包含更新内容的新不可修改 map。

在 1.0 M3 中更新上下文的示例

@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {

    this.advisedRequest = advisedRequest.updateContext(context -> {
        context.put("aroundCallBefore" + getName(), "AROUND_CALL_BEFORE " + getName());  // Add multiple key-value pairs
        context.put("lastBefore", getName());  // Add a single key-value pair
        return context;
    });

    // Method implementation continues...
}