主题
顾问(Advisor)API
Spring AI 顾问 API 以一种灵活而强大的方式,拦截、修改并增强你的 Spring 应用中由 AI 驱动的交互。 借助顾问 API,开发者可以构建更精巧、可复用且易维护的 AI 组件。
其主要收益包括:封装反复出现的生成式 AI 模式、转换与大语言模型(LLM)之间收发的数据,以及在各种模型与用例之间提供可移植性。
你可以像下面示例所示,使用 ChatClient API 配置现有的顾问:
java
ChatMemory chatMemory = ... // Initialize your chat memory store
VectorStore vectorStore = ... // Initialize your vector store
var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(), // chat-memory advisor
QuestionAnswerAdvisor.builder(vectorStore).build() // RAG advisor
)
.build();
var conversationId = "678";
String response = this.chatClient.prompt()
// Set advisor parameters at runtime
.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
.user(userText)
.call()
.content();建议在构建时使用构建器的 defaultAdvisors() 方法注册顾问。
顾问还会参与到可观测性(Observability)技术栈中,因此你可以查看与其执行相关的指标和链路追踪。
核心组件
该 API 由用于非流式场景的 CallAdvisor 和 CallAdvisorChain,以及用于流式场景的 StreamAdvisor 和 StreamAdvisorChain 组成。 它还包括表示未封装 Prompt 请求的 ChatClientRequest、表示聊天补全响应的 ChatClientResponse。两者都持有一个 advise-context,用于在顾问链中共享状态。
adviseCall() 和 adviseStream() 是关键顾问方法,通常执行诸如检查未封装的 Prompt 数据、定制并增强 Prompt 数据、调用顾问链中的下一个实体、选择性阻塞请求、检查聊天补全响应,以及抛出异常以指示处理错误等操作。
此外,getOrder() 方法决定顾问在链中的顺序,而 getName() 提供一个唯一的顾问名称。
由 Spring AI 框架创建的顾问链,按 getOrder() 值的顺序依次调用多个顾问。 值较小的先执行。最后一个顾问由自动添加,负责把请求发送给 LLM。
下面的流程图展示了顾问链与聊天模型之间的交互:
- Spring AI 框架从用户的
Prompt创建一个ChatClientRequest,并附带一个空的顾问context对象。 - 链中的每个顾问处理该请求,并可能修改它。或者,它可以选择不调用下一个实体从而阻塞请求。在后一种情况下,由该顾问负责填写响应。
- 由框架提供的最后一个顾问把请求发送给
Chat Model。 - Chat Model 的响应随后经由顾问链回传,并转换为
ChatClientResponse。后者包含共享的顾问context实例。 - 每个顾问都可以处理或修改该响应。
- 最后一个
ChatClientResponse通过提取ChatCompletion返回给客户端。
顾问顺序
顾问在链中的执行顺序由 getOrder() 方法决定。需要理解的关键点:
- 顺序值较小的顾问先执行。
- 顾问链像一个栈一样运作:
- 链中的第一个顾问最先处理请求。
- 它也是最后处理响应的一个。
- 要控制执行顺序:
- 把顺序设置为接近
Ordered.HIGHEST_PRECEDENCE,可确保某个顾问在链中最先执行(最先处理请求,最后处理响应)。 - 把顺序设置为接近
Ordered.LOWEST_PRECEDENCE,可确保某个顾问在链中最后执行(最后处理请求,最先处理响应)。
- 把顺序设置为接近
- 较大的值被解释为较低的优先级。
- 如果多个顾问具有相同的顺序值,它们的执行顺序没有保证。
NOTE
顺序与执行先后表面上看似矛盾,原因在于顾问链具有栈一般的性质:
- 优先级最高(顺序值最小)的顾问被添加到栈顶。
- 随着栈展开(unwind),它会最先处理请求。
- 随着栈回卷(rewind),它会最后处理响应。
作为回顾,以下是 Spring Ordered 接口的语义:
java
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();
}TIP
对于在输入侧和输出侧都需要排在最前的用例:
- 为每一侧使用独立的顾问。
- 用不同的顺序值来配置它们。
- 使用顾问上下文在它们之间共享状态。
API 概览
主要的顾问接口位于 org.springframework.ai.chat.client.advisor.api 包中。以下是你创建自己顾问时会遇到的关键接口:
java
public interface Advisor extends Ordered {
String getName();
}用于同步与响应式顾问的两个子接口是
java
public interface CallAdvisor extends Advisor {
ChatClientResponse adviseCall(
ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);
}以及
java
public interface StreamAdvisor extends Advisor {
Flux<ChatClientResponse> adviseStream(
ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);
}要继续建议(Advice)链,请在你的 Advice 实现中使用 CallAdvisorChain 和 StreamAdvisorChain:
这些接口是
java
public interface CallAdvisorChain extends AdvisorChain {
/**
- Invokes the next {@link CallAdvisor} in the {@link CallAdvisorChain} with the given
- request.
*/
ChatClientResponse nextCall(ChatClientRequest chatClientRequest);
/**
- Returns the list of all the {@link CallAdvisor} instances included in this chain at
- the time of its creation.
*/
List<CallAdvisor> getCallAdvisors();
}以及
java
public interface StreamAdvisorChain extends AdvisorChain {
/**
- Invokes the next {@link StreamAdvisor} in the {@link StreamAdvisorChain} with the
- given request.
*/
Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest);
/**
- Returns the list of all the {@link StreamAdvisor} instances included in this chain
- at the time of its creation.
*/
List<StreamAdvisor> getStreamAdvisors();
}实现一个顾问
要创建一个顾问,请实现 CallAdvisor 或 StreamAdvisor(或两者都实现)。需要实现的关键方法是用于非流式的 nextCall(),或用于流式顾问的 nextStream()。
示例
我们将提供几个实操示例,来说明如何为观察与增强用例实现顾问。
日志顾问
我们可以实现一个简单的日志顾问,在调用链中的下一个顾问之前记录 ChatClientRequest、之后记录 ChatClientResponse。 注意,该顾问只观察请求与响应,而不修改它们。 此实现同时支持非流式与流式场景。
java
public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {
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 ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
logRequest(chatClientRequest);
ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
logResponse(chatClientResponse);
return chatClientResponse;
}
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest,
StreamAdvisorChain streamAdvisorChain) {
logRequest(chatClientRequest);
Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest);
return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponses, this::logResponse); // <3>
}
private void logRequest(ChatClientRequest request) {
logger.debug("request: {}", request);
}
private void logResponse(ChatClientResponse chatClientResponse) {
logger.debug("response: {}", chatClientResponse);
}
}<1> 为顾问提供一个唯一的名称。 <2> 你可以通过设置顺序值来控制执行顺序。值较小的先执行。 <3> MessageAggregator 是一个工具类,它把 Flux 响应聚合为单个 ChatClientResponse。 这对于日志记录或其他需要观察整个响应而非流中各个单独条目的处理很有用。 注意,在 MessageAggregator 中你无法改变响应,因为它是只读操作。
复读(Re2)顾问
“Re-Reading Improves Reasoning in Large Language Models” 一文引入了称为复读(Re-Reading,Re2)的技术,用以提升大语言模型的推理能力。 Re2 技术要求像这样增强输入提示词:
{Input_Query}
Read the question again: {Input_Query}应用 Re2 技术到用户输入查询的顾问可以这样实现:
java
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) { // <1>
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() { // <2>
return this.order;
}
public ReReadingAdvisor withOrder(int order) {
this.order = order;
return this;
}
}<1> before 方法应用复读技术来增强用户的输入查询。 <2> 你可以通过设置顺序值来控制执行顺序。值较小的先执行。
Spring AI 内置顾问
Spring AI 框架提供了若干内置顾问来增强你的 AI 交互。以下是可用顾问的概览:
聊天记忆顾问
这些顾问在聊天记忆存储中管理对话历史:
MessageChatMemoryAdvisor
检索记忆并以消息集合的形式添加到提示词中。这种方式保留了对话历史的结构。注意,并非所有 AI 模型都支持此方式。
VectorStoreChatMemoryAdvisor
从 VectorStore 检索记忆并添加到提示词的系统文本中。此顾问擅长从大型数据集中高效地搜索并检索相关信息。
问答顾问
QuestionAnswerAdvisor
该顾问使用向量库提供问答能力,实现朴素 RAG(检索增强生成)模式。
RetrievalAugmentationAdvisor
使用定义在 org.springframework.ai.rag 包中的构件、并遵循模块化 RAG 架构,实现常见检索增强生成(RAG)流程的顾问。
推理顾问
ReReadingAdvisor
为 LLM 推理实现一种复读策略(称为 RE2),以在输入阶段增强理解。 依据文章:Re-Reading Improves Reasoning in LLMs。
工具调用顾问
ToolCallingAdvisor
作为顾问链的一部分处理工具调用循环。此顾问始终由 ChatClient 自动注册(除非显式禁用),因此即使未配置任何静态工具,也能支持由另一个顾问在运行时注入的工具。它执行模型请求的工具调用并把结果发回,反复迭代直到不再需要更多工具调用。实现 ToolAdvisor,这是一个标记接口,可防止第二个 ToolCallingAdvisor 被自动注册。 + 完整文档参见 递归顾问 - ToolCallingAdvisor 和 ChatClient - 工具调用。
内容安全顾问
SafeGuardAdvisor
一个旨在阻止模型生成有害或不当内容的简单顾问。
流式与非流式
- 非流式顾问处理完整的请求与响应。
- 流式顾问将请求与响应作为连续流处理,使用响应式编程概念(例如用于响应的 Flux)。
// TODO - Add a section on how to implement a streaming advisor with blocking and non-blocking code.
java
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain chain) {
return Mono.just(chatClientRequest)
.publishOn(Schedulers.boundedElastic())
.map(request -> {
// This can be executed by blocking and non-blocking Threads.
// Advisor before next section
})
.flatMapMany(request -> chain.nextStream(request))
.map(response -> {
// Advisor after next section
});
}最佳实践
- 让顾问聚焦于特定任务,以提升模块化程度。
- 必要时使用
adviseContext在顾问之间共享状态。 - 为你的顾问同时实现流式与非流式版本,以获得最大灵活性。
- 仔细考虑链中顾问的先后顺序,以确保正确的数据流。
破坏性 API 变更
顾问接口
- 在 1.0 M2 中,曾有独立的
RequestAdvisor和ResponseAdvisor接口。RequestAdvisor在ChatModel.call和ChatModel.stream方法之前被调用。ResponseAdvisor在这些方法之后被调用。
- 在 1.0 M3 中,这些接口被替换为:
CallAroundAdvisorStreamAroundAdvisor
- 原先属于
ResponseAdvisor的StreamResponseMode已被移除。 - 在 1.0.0 中,这些接口又被替换:
CallAroundAdvisor->CallAdvisor、StreamAroundAdvisor->StreamAdvisor、CallAroundAdvisorChain->CallAdvisorChain以及StreamAroundAdvisorChain->StreamAdvisorChain。AdvisedRequest->ChatClientRequest和AdivsedResponse->ChatClientResponse。
上下文映射处理
- 在 1.0 M2 中:
- 上下文映射是一个独立的方法参数。
- 该映射是可变的,并沿链传递。
- 在 1.0 M3 中: