Advisors(顾问)系统
Spring AI Advisors API 提供了一种灵活而强大的方式来拦截、修改和增强 Spring 应用中的 AI 交互。通过利用 Advisors API,开发者可以创建更复杂、可重用和可维护的 AI 组件。
核心优势
- 封装重复出现的生成式 AI 模式
- 转换发送到大语言模型(LLM)和从 LLM 返回的数据
- 提供跨各种模型和用例的可移植性
基本使用
你可以使用 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() 方法注册顾问。
Advisors 也会参与可观测性栈,所以你 可以看到与它们执行相关的指标和追踪。
核心组件
API 由以下组件组成:
CallAdvisor和CallAdvisorChain用于非流式场景StreamAdvisor和StreamAdvisorChain用于流式场景ChatClientRequest表示未密封的提示请求ChatClientResponse表示聊天完成响应
两者都持有一个 advise-context 来在顾问链中共享状态。
关键方法
adviseCall() 和 adviseStream() 是关键的顾问方法,通常执行以下操作:
- 检查未密封的提示数据
- 自定义和增强提示数据
- 调用链中的下一个实体
- 可选择阻止请求
- 检查聊天完成响应
- 抛出异常表示处理错误
此外,getOrder() 方法确定链中顾问的顺序,而 getName() 提供唯一的顾问名称。
执行顺序
顾问链由 Spring AI 框架创建,允许按 getOrder() 值排序的顺序调用多个顾问。值越小越先执行。最后一个顾问(自动添加)将请求发送给 LLM。
执行顺序规则
- 值越小的顾问越先执行
- 顾问链像栈一样工作:
- 链中的第一个顾问最先处理请求
- 也是最后一个处理响应
- 要控制执行顺序:
- 设置接近
Ordered.HIGHEST_PRECEDENCE的顺序,确保顾问在链中首先执行(请求处理最先,响应处理最后) - 设置接近
Ordered.LOWEST_PRECEDENCE的顺序,确保顾问在链中最后执行(请求处理最后,响应处理最先)
- 设置接近
内置顾问
Spring AI 框架提供了几个内置顾问来增强你的 AI 交互:
聊天记忆顾问
这些顾问在聊天记忆存储中管理对话历史:
MessageChatMemoryAdvisor
检索记忆并将其作为消息集合添加到提示中。这种方法保持了对话历史的结构。请注意,不是所有 AI 模型都支持这种方法。
PromptChatMemoryAdvisor
检索记忆并将其纳入提示的系统文本中。
VectorStoreChatMemoryAdvisor
从 VectorStore 检索记忆并添加到提示的系统文本中。这个顾问对于从大数据集中高效搜索和检索相关信息很有用。
问答顾问
QuestionAnswerAdvisor
此顾问使用向量存储提供问答功能,实现朴素的 RAG(检索增强生成)模式。
RetrievalAugmentationAdvisor
顾问使用 org.springframework.ai.rag 包中定义的构建块实现常见的检索增强生成(RAG)流程,并遵循模块化 RAG 架构。
推理顾问
ReReadingAdvisor
实现 LLM 推理的重新阅读策略,称为 RE2,以增强输入阶段的理解。
内容安全顾问
SafeGuardAdvisor
一个简单的顾问,旨在防止模型生成有害或不适当的内容。
自定义顾问
非流式顾问
public class LoggingAdvisor implements CallAdvisor {
@Override
public String getName() { (1)
return "logging-advisor";
}
@Override
public int getOrder() { (2)
return 0;
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundChain chain) {
// 请求前处理
System.out.println("请求:" + advisedRequest.userText());
// 调用下一个顾问或最终的 ChatModel
AdvisedResponse response = chain.nextAroundCall(advisedRequest);
// 响应后处理
System.out.println("响应:" + response.response().getResult().getOutput().getContent());
return response;
}
}
- 为顾问提供唯一名称
- 通过设置顺序值来控制执行顺序。值越小越先执行
流式顾问
public class StreamingLoggingAdvisor implements StreamAdvisor {
@Override
public String getName() {
return "streaming-logging-advisor";
}
@Override
public int getOrder() {
return 0;
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundChain chain) {
// 请求前处理
System.out.println("流式请求:" + advisedRequest.userText());
return chain.nextAroundStream(advisedRequest)
.doOnNext(response -> {
// 每个响应块的处理
System.out.println("流式响应块:" + response.response().getResult().getOutput().getContent());
})
.doOnComplete(() -> {
// 流完成处理
System.out.println("流式响应完成");
});
}
}
Re-Reading (Re2) 顾问
"Re-Reading Improves Reasoning in Large Language Models" 文章介绍了一种称为 Re-Reading (Re2) 的技术,可以提高大语言模型的推理能力。Re2 技术需要像这样增强输入提示:
{Input_Query}
Read the question again: {Input_Query}
实现应用 Re2 技术的顾问如下:
public class ReReadingAdvisor implements CallAdvisor {
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 AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundChain chain) { (1)
String augmentedUserText = PromptTemplate.builder()
.template(this.re2AdviseTemplate)
.variables(Map.of("re2_input_query", advisedRequest.userText()))
.build()
.render();
AdvisedRequest augmentedRequest = AdvisedRequest.from(advisedRequest)
.withUserText(augmentedUserText)
.build();
return chain.nextAroundCall(augmentedRequest);
}
@Override
public String getName() {
return "re-reading-advisor";
}
@Override
public int getOrder() { (2)
return this.order;
}
public ReReadingAdvisor withOrder(int order) {
this.order = order;
return this;
}
}
- before 方法使用 Re-Reading 技术增强用户的输入查询
- 通过设置顺序值来控制执行顺序。值越小越先执行
流式 vs 非流式
执行流程对比
- 非流式顾问:处理完整的请求和响应
- 流式顾问:处理连续的请求和响应流,使用响应式编程概念(例如,响应用 Flux)
流式顾问实现模式
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundChain chain) {
return Mono.just(advisedRequest)
.publishOn(Schedulers.boundedElastic())
.map(request -> {
// 这里可以使用阻塞和非阻塞线程执行
// 下一个顾问之前的处理
})
.flatMapMany(request -> chain.nextAroundStream(request))
.map(response -> {
// 下一个顾问之后的处理
});
}
最佳实践
- 职责单一:保持顾问专注于特定任务以提高模块化
- 状态共享:必要时使用
adviseContext在顾问间共享状态 - 双重实现:为最大灵活性同时实现流式和非流式版本
- 顺序考虑:仔细考虑链中顾问的顺序以确保正确的数据流
API 变更历史
顾问接口变更
- 1.0 M2 中,有单独的
RequestAdvisor和ResponseAdvisor接口RequestAdvisor在ChatModel.call和ChatModel.stream方法之前调用ResponseAdvisor在这些方法之后调用
- 1.0 M3 中,这些接口被替换为:
CallAroundAdvisorStreamAroundAdvisor
ResponseAdvisor中的StreamResponseMode已被移除- 1.0.0 中这些接口被替换:
CallAroundAdvisor→CallAdvisorStreamAroundAdvisor→StreamAdvisorCallAroundAdvisorChain→CallAdvisorChainStreamAroundAdvisorChain→StreamAdvisorChainAdvisedRequest→ChatClientRequestAdvisedResponse→ChatClientResponse
上下文映射处理
- 1.0 M2 中:
- 上下文映射是单独的方法参数
- 映射是可变的并沿链传递
- 1.0 M3 中:
- 上下文映射现在是
AdvisedRequest和AdvisedResponse记录的一部分 - 映射是不可变的
- 要更新上下文,使用
updateContext方法,它使用更新的内容创建新的不可修改映射
- 上下文映射现在是