Spring AI Advisors API 提供了一种灵活而强大的方式来拦截、修改和增强 Spring 应用程序中 AI 驱动的交互。通过利用 Advisors API,开发人员可以创建更复杂、可重用且可维护的 AI 组件。
其关键优势包括:封装重复出现的生成式 AI 模式、转换发送到大型语言模型(LLM)及从 LLM 接收的数据,以及提供跨不同模型和用例的可移植性。
您可以使用 ChatClient API 配置现有顾问,如下例所示:
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)堆栈,因此您可以查看与其执行相关的指标和跟踪信息。
了解问题解答顾问 (Question Answer Advisor)
了解聊天记忆顾问 (Chat Memory Advisor)
核心组件
该 API 包含:
用于非流式场景的
CallAroundAdvisor
和CallAroundAdvisorChain
用于流式场景的
StreamAroundAdvisor
和StreamAroundAdvisorChain
代表未密封提示请求的
AdvisedRequest
代表聊天完成响应的
AdvisedResponse
两者都持有一个 advise-context
(建议上下文),用于在顾问链中共享状态。
nextAroundCall()
和nextAroundStream()
是关键顾问方法,通常执行以下操作:检查未密封的提示数据
定制和增强提示数据
调用顾问链中的下一个实体
可选地阻止请求
检查聊天完成响应
抛出异常以指示处理错误
此外,
getOrder()
方法决定顾问在链中的顺序getName()
方法提供唯一的顾问名称
由 Spring AI 框架创建的顾问链 (Advisor Chain),允许按 getOrder()
值排序的顺序调用多个顾问(值较小的先执行)。框架自动添加的最后一个顾问将请求发送给 LLM。
以下流程图说明了顾问链与聊天模型之间的交互:
Advisors API 流程
Spring AI 框架从用户的提示创建
AdvisedRequest
和一个空的AdvisorContext
对象。链中的每个顾问处理请求,可能修改它。或者,它可以选择通过不调用下一个实体来阻止请求(在此情况下,顾问负责填充响应)。
框架提供的最终顾问将请求发送给聊天模型。
聊天模型的响应随后通过顾问链传递回来,并转换为
AdvisedResponse
(包含共享的AdvisorContext
实例)。每个顾问可以处理或修改响应。
通过提取
ChatCompletion
将最终的AdvisedResponse
返回给客户端。
顾问顺序 (Advisor Order)
顾问链中的执行顺序由 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;
/**
* 获取此对象的顺序值。
* <p>较高的值被解释为较低的优先级。因此,
* 具有最低值的对象具有最高优先级(类似于
* Servlet 的 {@code load-on-startup} 值)。
* <p>相同的顺序值将导致受影响对象的任意排序位置。
* @return 顺序值
* @see #HIGHEST_PRECEDENCE
* @see #LOWEST_PRECEDENCE
*/
int getOrder();
}
对于需要在链的输入和输出两侧都处于首位的用例:
为每一侧使用单独的顾问。
为它们配置不同的顺序值。
使用顾问上下文在它们之间共享状态。
API 概览
主要的 Advisor 接口位于包 org.springframework.ai.chat.client.advisor.api
中。创建自己的顾问时会遇到的关键接口:
public interface Advisor extends Ordered {
String getName(); // 获取顾问唯一名称
}
用于同步和响应式顾问的两个子接口是:
public interface CallAroundAdvisor extends Advisor {
/**
* 环绕通知,包装 ChatModel#call(Prompt) 方法。
* @param advisedRequest 被建议的请求
* @param chain 顾问链
* @return 响应
*/
AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain);
}
和
public interface StreamAroundAdvisor extends Advisor {
/**
* 环绕通知,包装被建议请求的调用。
* @param advisedRequest 被建议的请求
* @param chain 要执行的顾问链
* @return 被建议请求的结果
*/
Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain);
}
要继续执行 Advice 链,请在您的 Advice 实现中使用 CallAroundAdvisorChain
和 StreamAroundAdvisorChain
:
接口为:
public interface CallAroundAdvisorChain {
AdvisedResponse nextAroundCall(AdvisedRequest advisedRequest);
}
和
public interface StreamAroundAdvisorChain {
Flux<AdvisedResponse> nextAroundStream(AdvisedRequest advisedRequest);
}
实现顾问
要创建顾问,需实现 CallAroundAdvisor
或 StreamAroundAdvisor
(或两者)。要实现的关键方法是非流式顾问的 nextAroundCall()
或流式顾问的 nextAroundStream()
。
示例
我们将提供几个动手示例来说明如何实现用于观察和增强用例的顾问。
日志记录顾问 (Logging Advisor)
我们可以实现一个简单的日志记录顾问,在调用链中下一个顾问之前记录 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
}
}
为顾问提供唯一名称。
通过设置顺序值控制执行顺序(较低的值先执行)。
MessageAggregator
是一个实用工具类,将Flux
响应聚合成单个AdvisedResponse
。这对于需要观察整个响应(而不是流中的单个项)的日志记录或其他处理非常有用。注意:您不能在MessageAggregator
中更改响应,因为它是只读操作。
重读 (Re-Reading / Re2) 顾问
"重读提高大型语言模型的推理能力" 文章介绍了一种称为 Re-Reading (Re2) 的技术,该技术可提高大型语言模型的推理能力。Re2 技术要求像这样增强输入提示:
{输入问题}
再读一遍问题: {输入问题}
实现一个将 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}
""") // 应用 Re2 格式
.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();
}
}
before
方法应用 Re-Reading 技术增强用户的输入问题。aroundCall
方法拦截非流式请求并应用 Re-Reading 技术。aroundStream
方法拦截流式请求并应用 Re-Reading 技术。通过设置顺序值控制执行顺序(较低的值先执行)。
为顾问提供唯一名称。
Spring AI 内置顾问
Spring AI 框架提供了几个内置顾问来增强您的 AI 交互。以下是可用顾问的概述:
聊天记忆顾问 (Chat Memory Advisors)
这些顾问管理聊天记忆存储中的对话历史:
MessageChatMemoryAdvisor
检索记忆并将其作为消息集合添加到提示中。此方法维护对话历史的结构。注意:并非所有 AI 模型都支持此方法。
PromptChatMemoryAdvisor
检索记忆并将其合并到提示的系统文本中。
VectorStoreChatMemoryAdvisor
从向量存储检索记忆并将其合并到提示的系统文本中。此顾问对于高效搜索和检索大型数据集中的相关信息非常有用。
问题解答顾问 (Question Answering Advisor)
QuestionAnswerAdvisor
此顾问使用向量存储提供问答功能,实现了 RAG(检索增强生成)模式。
内容安全顾问 (Content Safety Advisor)
SafeGuardAdvisor
一个简单的顾问,旨在防止模型生成有害或不适当的内容。
流式与非流式 (Streaming vs Non-Streaming)
顾问流式与非流式流程
非流式顾问处理完整的请求和响应。
流式顾问处理连续流式的请求和响应,使用响应式编程概念(例如,使用
Flux
处理响应)。
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
return Mono.just(advisedRequest)
.publishOn(Schedulers.boundedElastic()) // 指定调度器
.map(request -> {
// 此部分可由阻塞和非阻塞线程执行。
// 顾问在 next 部分之前的逻辑
})
.flatMapMany(request -> chain.nextAroundStream(request)) // 继续链
.map(response -> {
// 顾问在 next 部分之后的逻辑
});
}
最佳实践
让顾问专注于特定任务,以获得更好的模块化。
必要时使用
adviseContext
在顾问之间共享状态。为获得最大灵活性,请同时实现顾问的流式和非流式版本。
仔细考虑顾问链中的顺序以确保正确的数据流。
向后兼容性AdvisedRequest
类已移至新包。
破坏性 API 变更
Spring AI Advisor Chain 从版本 1.0 M2 到 1.0 M3 经历了重大更改。以下是主要修改:
顾问接口
在 1.0 M2 中,有单独的
RequestAdvisor
和ResponseAdvisor
接口。RequestAdvisor
在ChatModel.call
和ChatModel.stream
方法之前调用。ResponseAdvisor
在这些方法之后调用。
在 1.0 M3 中,这些接口已被替换为:
CallAroundAdvisor
StreamAroundAdvisor
以前属于
ResponseAdvisor
的StreamResponseMode
已被移除。
上下文映射处理
在 1.0 M2 中:
上下文映射是一个单独的方法参数。
该映射是可变的,并沿链传递。
在 1.0 M3 中:
上下文映射现在是
AdvisedRequest
和AdvisedResponse
记录(records)的一部分。该映射是不可变的。
要更新上下文,请使用
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;
});
// 方法实现继续...
}
评论区