Advisors API

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

主要优势包括封装重复的生成式 AI 模式、转换发送到大型语言模型(LLMs)的数据以及提供跨各种模型和用例的可移植性。

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

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(), // chat-memory advisor
        QuestionAnswerAdvisor.builder((vectorStore).builder() // 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() 方法在构建时注册 advisors。

Advisors 也参与可观察性堆栈,因此您可以查看与其执行相关的指标和跟踪。

核心组件

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

Advisors API Classes

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

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

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

以下流程图说明了 advisor 链与 Chat Model 之间的交互:

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

  2. 链中的每个 advisor 处理请求,可能会修改它。或者,它可以选择通过不调用下一个实体来阻止请求。在后一种情况下,advisor 负责填写响应。

  3. 框架提供的最终 advisor 将请求发送到 Chat Model

  4. Chat Model 的响应然后通过 advisor 链传回并转换为 AdvisedResponse。后者包含共享的 AdvisorContext 实例。

  5. 每个 advisor 可以处理或修改响应。

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

Advisor 顺序

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

  • 具有较低顺序值的 advisors 首先执行。

  • advisor 链作为堆栈运行:

    • 链中的第一个 advisor 是第一个处理请求的。

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

  • 要控制执行顺序:

    • 将顺序设置为接近 Ordered.HIGHEST_PRECEDENCE 以确保 advisor 在链中首先执行(请求处理时首先,响应处理时最后)。

    • 将顺序设置为接近 Ordered.LOWEST_PRECEDENCE 以确保 advisor 在链中最后执行(请求处理时最后,响应处理时首先)。

  • 较高的值被解释为较低的优先级。

  • 如果多个 advisors 具有相同的顺序值,它们的执行顺序不能保证。

顺序和执行序列之间的看似矛盾是由于 advisor 链的堆栈性质:

  • 具有最高优先级(最低顺序值)的 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>相同的顺序值将导致受影响对象的任意排序位置。
     * @return 顺序值
     * @see #HIGHEST_PRECEDENCE
     * @see #LOWEST_PRECEDENCE
     */
    int getOrder();
}

对于需要在输入和输出端都是链中第一个的用例:

  1. 为每一端使用单独的 advisors。

  2. 使用不同的顺序值配置它们。

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

API 概述

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

public interface Advisor extends Ordered {

	String getName();

}

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

public interface CallAroundAdvisor extends Advisor {

	/**
	 * 包装 ChatModel#call(Prompt) 方法的环绕通知。
	 * @param advisedRequest 建议的请求
	 * @param chain advisor 链
	 * @return 响应
	 */
	AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain);

}

public interface StreamAroundAdvisor extends Advisor {

	/**
	 * 包装建议请求调用的环绕通知。
	 * @param advisedRequest 建议的请求
	 * @param chain 要执行的 advisors 链
	 * @return 建议请求的结果
	 */
	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);

}

实现 Advisor

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

示例

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

日志记录 Advisor

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

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 为 advisor 提供唯一名称。
2 您可以通过设置顺序值来控制执行顺序。较低的值首先执行。
3 MessageAggregator 是一个实用类,它将 Flux 响应聚合为单个 AdvisedResponse。 这对于记录或观察整个响应而不是流中单个项目的其他处理很有用。 注意,您不能在 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 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 您可以通过设置顺序值来控制执行顺序。较低的值首先执行。
5 为 advisor 提供唯一名称。

Spring AI 内置 Advisors

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

聊天内存 Advisors

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

  • MessageChatMemoryAdvisor

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

  • PromptChatMemoryAdvisor

    检索内存并将其合并到提示的系统文本中。

  • VectorStoreChatMemoryAdvisor

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

问答 Advisor
  • QuestionAnswerAdvisor

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

内容安全 Advisor
  • SafeGuardAdvisor

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

流式与非流式

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

  • 流式 advisors 将请求和响应作为连续流处理,使用响应式编程概念(例如,使用 Flux 处理响应)。

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

    return  Mono.just(advisedRequest)
            .publishOn(Schedulers.boundedElastic())
            .map(request -> {
                // 这可以由阻塞和非阻塞线程执行。
                // Advisor 在 next 部分之前
            })
            .flatMapMany(request -> chain.nextAroundStream(request))
            .map(response -> {
                // Advisor 在 next 部分之后
            });
}

最佳实践

  1. 保持 advisors 专注于特定任务,以实现更好的模块化。

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

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

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

向后兼容性

重要:AdvisedRequest 类已移至新包。

API 重大变更

Spring AI Advisor Chain 从版本 1.0 M2 到 1.0 M3 经历了重大变化。以下是主要修改:

Advisor 接口

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

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

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

  • 在 1.0 M3 中,这些接口已被替换为:

    • CallAroundAdvisor

    • StreamAroundAdvisor

  • StreamResponseMode(以前是 ResponseAdvisor 的一部分)已被删除。

上下文映射处理

  • 在 1.0 M2 中:

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

    • 映射是可变的,并沿链传递。

  • 在 1.0 M3 中:

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

    • 映射是不可变的。

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

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

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

    this.advisedRequest = advisedRequest.updateContext(context -> {
        context.put("aroundCallBefore" + getName(), "AROUND_CALL_BEFORE " + getName());  // 添加多个键值对
        context.put("lastBefore", getName());  // 添加单个键值对
        return context;
    });

    // 方法实现继续...
}