Advisors API
Spring AI Advisors API 提供了一种灵活且强大的方式,用于在你的 Spring 应用程序中拦截、修改和增强由 AI 驱动的交互。通过利用 Advisors API,开发者可以创建更复杂、可复用且易于维护的 AI 组件。
其核心优势包括:封装通用的生成式 AI 模式、转换与大语言模型(LLMs)之间传输的数据,以及在各类模型和用例间提供可移植性。
你可以使用 ChatClient API 配置现有的通知器,如下例所示:
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();
建议在构建时使用构建器的 defaultAdvisors() 方法注册通知器。
通知器还会纳入可观测栈中,因此你可以查看与其执行相关的指标和链路追踪信息。
核心组件
该 API 包含用于非流式场景的 CallAdvisor 和 CallAdvisorChain,以及用于流式场景的 StreamAdvisor 和 StreamAdvisorChain。同时还包含 ChatClientRequest(表示未封装的提示词请求)和 ChatClientResponse(表示聊天补全响应)。二者均持有通知上下文,用于在通知器链中共享状态。
Advisors API 类
adviseCall() 和 adviseStream() 是核心的通知器方法,通常执行以下操作:检查未封装的提示词数据、自定义和增强提示词数据、调用通知器链中的下一个组件、可选地拦截请求、检查聊天补全响应,以及抛出异常以标识处理错误。
此外,getOrder() 方法决定通知器在链中的执行顺序,getName() 提供唯一的通知器名称。
由 Spring AI 框架创建的通知器链,支持按照通知器的 getOrder() 值排序,依次调用多个通知器。值越小,执行优先级越高。框架会自动添加最后一个通知器,用于将请求发送至大语言模型。
下图展示了通知器链与聊天模型之间的交互流程:
Advisors API 流程
Spring AI 框架根据用户的提示词创建 ChatClientRequest,并附带一个空的通知器上下文对象。
链中的每个通知器处理请求,可对其进行修改。或者,通知器也可以选择不调用下一个组件,从而拦截请求。在后一种情况下,通知器需要负责填充响应数据。
框架提供的最终通知器会将请求发送至聊天模型。
聊天模型的响应随后通过通知器链回传,并转换为 ChatClientResponse。该响应包含共享的通知器上下文实例。
每个通知器都可以处理或修改响应。
最终的 ChatClientResponse 通过提取聊天补全结果返回给客户端。
通知器执行顺序
通知器在链中的执行顺序由 getOrder() 方法决定。核心要点如下:
顺序值越小的通知器,执行优先级越高。
通知器链以栈的形式运行:
链中的第一个通知器最先处理请求。
同时也是最后一个处理响应的通知器。
控制执行顺序的方式:
将顺序值设置为接近 Ordered.HIGHEST_PRECEDENCE,确保通知器在链中最先执行(请求处理第一,响应处理最后)。
将顺序值设置为接近 Ordered.LOWEST_PRECEDENCE,确保通知器在链中最后执行(请求处理最后,响应处理第一)。
数值越大,优先级越低。
若多个通知器的顺序值相同,其执行顺序不保证。
顺序值与执行序列看似矛盾,是因为通知器链具有栈式特性:
最高优先级(顺序值最小)的通知器会被添加到栈顶。
栈展开时,它会最先处理请求。
栈回卷时,它会最后处理响应。
以下是 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;
/**
* 获取该对象的顺序值。
*数值越大,优先级越低。因此,
* 数值最小的对象优先级最高(类似于
* Servlet 的 {@code load-on-startup} 值)。
*相同顺序值的对象排序位置不固定。
* @return 顺序值
* @see #HIGHEST_PRECEDENCE
* @see #LOWEST_PRECEDENCE
*/
int getOrder();
}对于需要在输入和输出端均处于链首的用例:
为两端分别使用独立的通知器。
为它们配置不同的顺序值。
使用通知器上下文在它们之间共享状态。
API 概览
主要的通知器接口位于 org.springframework.ai.chat.client.advisor.api 包下。创建自定义通知器时,你会用到以下核心接口:
public interface Advisor extends Ordered {
String getName();
}同步和响应式通知器的两个子接口:
public interface CallAdvisor extends Advisor {
ChatClientResponse adviseCall(
ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);
}以及:
public interface StreamAdvisor extends Advisor {
FluxadviseStream(
ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);
}在通知器实现中,使用 CallAdvisorChain 和 StreamAdvisorChain 来延续通知链:
接口定义如下:
public interface CallAdvisorChain extends AdvisorChain {
/**
* 使用给定请求调用 {@link CallAdvisorChain} 中的下一个 {@link CallAdvisor}。
*/
ChatClientResponse nextCall(ChatClientRequest chatClientRequest);
/**
* 返回链创建时包含的所有 {@link CallAdvisor} 实例列表。
*/
ListgetCallAdvisors();
}以及:
public interface StreamAdvisorChain extends AdvisorChain {
/**
* 使用给定请求调用 {@link StreamAdvisorChain} 中的下一个 {@link StreamAdvisor}。
*/
FluxnextStream(ChatClientRequest chatClientRequest);
/**
* 返回链创建时包含的所有 {@link StreamAdvisor} 实例列表。
*/
ListgetStreamAdvisors();
}实现通知器
创建通知器需实现 CallAdvisor 或 StreamAdvisor(或两者都实现)。核心实现方法:非流式使用 nextCall(),流式使用 nextStream()。
示例
我们将提供几个实操示例,演示如何实现用于观测和增强功能的通知器。
日志通知器
我们可以实现一个简单的日志通知器,在调用链中下一个通知器前后,分别记录 ChatClientRequest 和 ChatClientResponse。注意,该通知器仅观测请求和响应,不做修改。此实现同时支持非流式和流式场景。
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 FluxadviseStream(ChatClientRequest chatClientRequest,
StreamAdvisorChain streamAdvisorChain) {
logRequest(chatClientRequest);
FluxchatClientResponses = 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);
}
}为通知器提供唯一名称。
你可以通过设置顺序值控制执行顺序,值越小执行越早。
MessageAggregator 是一个工具类,将 Flux 响应聚合为单个 ChatClientResponse。这对于日志记录或其他需要观测完整响应而非流中单个元素的处理非常有用。注意,你无法在 MessageAggregator 中修改响应,因为它是只读操作。
重读(Re2)通知器
《重读可提升大语言模型的推理能力》一文提出了一种名为重读(Re2)的技术,可提升大语言模型的推理能力。Re2 技术需要按如下方式增强输入提示词:
{Input_Query}
Read the question again: {Input_Query}实现一个将 Re2 技术应用于用户输入查询的通知器,代码如下:
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 方法应用重读技术增强用户输入查询。
你可以通过设置顺序值控制执行顺序,值越小执行越早。
Spring AI 内置通知器
Spring AI 框架提供了多个内置通知器,用于增强 AI 交互。以下是可用通知器概览:
聊天记忆通知器
这类通知器在聊天记忆存储中管理对话历史:
MessageChatMemoryAdvisor:获取记忆并将其作为消息集合添加到提示词中。该方式保留对话历史结构。注意,并非所有 AI 模型都支持此方式。
PromptChatMemoryAdvisor:获取记忆并将其整合到提示词的系统文本中。
VectorStoreChatMemoryAdvisor:从向量存储中获取记忆,并添加到提示词的系统文本中。该通知器适用于从大型数据集中高效搜索和检索相关信息。
问答通知器
QuestionAnswerAdvisor:使用向量存储提供问答能力,实现朴素 RAG(检索增强生成)模式。
RetrievalAugmentationAdvisor:使用 org.springframework.ai.rag 包中定义的构建块,遵循模块化 RAG 架构,实现通用的检索增强生成(RAG)流程。
推理通知器
ReReadingAdvisor:为大语言模型推理实现重读策略(RE2),在输入阶段提升理解能力。基于论文:《重读可提升大语言模型的推理能力》。
内容安全通知器
SafeGuardAdvisor:一个简单的通知器,用于防止模型生成有害或不当内容。
流式与非流式
通知器流式与非流式流程
非流式通知器处理完整的请求和响应。
流式通知器以连续流的形式处理请求和响应,使用响应式编程概念(例如 Flux 处理响应)。
@Override
public FluxadviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain chain) {
return Mono.just(chatClientRequest)
.publishOn(Schedulers.boundedElastic())
.map(request -> {
// 可在阻塞和非阻塞线程中执行
// 通知器调用下一个组件前的逻辑
})
.flatMapMany(request -> chain.nextStream(request))
.map(response -> {
// 通知器调用下一个组件后的逻辑
});
}最佳实践
让通知器专注于特定任务,提升模块化程度。
必要时使用 adviseContext 在通知器之间共享状态。
同时实现通知器的流式和非流式版本,最大化灵活性。
仔细设计通知器在链中的顺序,确保数据流转正常。
破坏性 API 变更
通知器接口
在 1.0 M2 版本中,存在独立的 RequestAdvisor 和 ResponseAdvisor 接口。
RequestAdvisor 在 ChatModel.call 和 ChatModel.stream 方法前调用。
ResponseAdvisor 在这些方法后调用。
在 1.0 M3 版本中,这些接口被替换为:
CallAroundAdvisor
StreamAroundAdvisor
此前属于 ResponseAdvisor 的 StreamResponseMode 已被移除。
在 1.0.0 版本中,接口再次替换:
CallAroundAdvisor → CallAdvisor
StreamAroundAdvisor → StreamAdvisor
CallAroundAdvisorChain → CallAdvisorChain
StreamAroundAdvisorChain → StreamAdvisorChain
数据结构变更:
AdvisedRequest → ChatClientRequest
AdvisedResponse → ChatClientResponse
上下文 Map 处理
在 1.0 M2 版本中:
上下文 Map 是独立的方法参数。
Map 可变,在链中传递。
在 1.0 M3 版本中:
上下文 Map 成为 AdvisedRequest 和 AdvisedResponse 记录的一部分。
Map 不可变。
更新上下文需使用 updateContext 方法,该方法会创建一个包含更新内容的新不可变 Map。