顾问 API

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

其主要优势包括封装重复出现的生成式人工智能模式、转换与大型语言模型(LLMs)之间发送和接收的数据,以及在各种模型和用例中提供可移植性。

开发者可以使用ChatClient API配置现有的顾问,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()方法注册顾问。顾问也参与可观测性堆栈,因此开发者可以查看与其执行相关的指标和轨迹。

核心组件

Advisors API 包含用于非流式场景的CallAdvisorCallAdvisorChain,以及用于流式场景的StreamAdvisorStreamAdvisorChain。它还包括用于表示未封装的提示词请求的ChatClientRequest,以及用于聊天补全响应的ChatClientResponse。两者都包含一个advise-context,用于在顾问链之间共享状态。

<font style="color:rgb(25, 30, 30);">adviseCall()</font><font style="color:rgb(25, 30, 30);">adviseStream()</font>是关键的顾问方法,通常会执行一些操作,例如检查未密封的提示数据、定制和扩充提示数据、调用顾问链中的下一个实体、选择性地阻止请求、检查聊天完成响应,以及抛出异常以指示处理错误。

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

由Spring AI框架创建的Advisor Chain(顾问链)允许按多个顾问的getOrder()值排序来依次调用它们。值越小,执行得越早。最后一个顾问会被自动添加,它负责将请求发送给大语言模型。

以下流程图展示了顾问链与聊天模型之间的交互:

  1. Spring AI框架根据用户的<font style="color:rgb(25, 30, 30);">Prompt</font>以及一个空的顾问<font style="color:rgb(25, 30, 30);">context</font>对象创建一个<font style="color:rgb(25, 30, 30);">ChatClientRequest</font>
  2. 链中的每个顾问都会处理请求,并可能对其进行修改。或者,顾问也可以选择不调用下一个实体来阻止请求。在后一种情况下,该顾问负责完成响应。
  3. 框架提供的最后一个顾问会将请求发送至<font style="color:rgb(25, 30, 30);">Chat Model</font>
  4. 然后,聊天模型的响应会通过顾问链传回,并转换为<font style="color:rgb(25, 30, 30);">ChatClientResponse</font>。其包含共享的顾问<font style="color:rgb(25, 30, 30);">context</font>实例。
  5. 每个顾问都可以处理或修改响应。
  6. 最终的<font style="color:rgb(25, 30, 30);">ChatClientResponse</font>通过提取<font style="color:rgb(25, 30, 30);">ChatCompletion</font>返回给客户端。

顾问顺序(Advisor Order)

链中顾问的执行顺序由<font style="color:rgb(25, 30, 30);">getOrder()</font>方法决定。需要理解的关键点有如下所示:

  • 顺序值较低的顾问会先执行。
  • 顾问链以栈的方式运作:
    • 链中的第一个顾问会首先处理请求。
    • 它也是最后一个处理响应的。
  • 控制执行顺序:
    • 将顺序设置为接近<font style="color:rgb(25, 30, 30);">Ordered.HIGHEST_PRECEDENCE</font>,以确保顾问在链中首先执行(请求处理时首先执行,响应处理时最后执行)。
    • 将顺序设置为接近<font style="color:rgb(25, 30, 30);">Ordered.LOWEST_PRECEDENCE</font>,以确保顾问在链中最后执行(请求处理时最后执行,响应处理时最先执行)。
  • 值越高,表示优先级越低。
  • 如果多个顾问具有相同的顺序值,它们的执行顺序无法保证。

顺序和执行顺序之间的表面矛盾是由于顾问链的栈式特性: 具有最高优先级(顺序值最低)的顾问被添加到栈的顶部。随着栈的展开,它将是第一个处理请求的。 随着栈的回卷,它将是最后一个处理响应的。

作为提醒,以下是Spring Ordered接口的语义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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. 为每一侧使用单独的顾问。
  2. 为它们配置不同的顺序值。
  3. 使用顾问上下文在它们之间共享状态。

API概览

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

1
2
3
public interface Advisor extends Ordered {
String getName();
}

这是顾问的基本接口,它扩展了 Ordered 接口,意味着顾问具有顺序和名称。

对于同步和响应式顾问的两个子接口分别是:

1
2
3
4
5
6
7
public interface CallAdvisor extends Advisor {

ChatClientResponse adviseCall(
ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain);

}

1
2
3
4
5
6
public interface StreamAdvisor extends Advisor {

Flux<ChatClientResponse> adviseStream(
ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain);

}

为了继续顾问链的执行,开发者可以在顾问实现中使用CallAdvisorChainStreamAdvisorChain

1
2
3
4
5
6
7
public interface CallAdvisorChain extends AdvisorChain {

ChatClientResponse nextCall(ChatClientRequest chatClientRequest);

List<CallAdvisor> getCallAdvisors();

}

1
2
3
4
5
6
7
public interface StreamAdvisorChain extends AdvisorChain {

Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest);

List<StreamAdvisor> getStreamAdvisors();

}

实现顾问

要创建一个顾问,需实现CallAdvisorStreamAdvisor(或两者都实现)。对于非流式顾问,要实现的关键方法是nextCall();对于流式顾问,则是nextStream()

示例

我们将提供几个实际操作示例,以说明如何为观察和增强用例实现顾问功能。

日志顾问

我们可以实现一个简单的日志通知器,在调用链中下一个通知器之前记录ChatClientRequest,之后记录ChatClientResponse。请注意,该通知器仅观察请求和响应,不会对其进行修改。此实现同时支持非流式和流式场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class SimpleLoggerAdvisor implements CallAdvisor, StreamAdvisor {

private static final Logger log = 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) {
logg.debug("request: {}", request);
}

private void logResponse(ChatClientResponse chatClientResponse) {
logg.debug("response: {}", chatClientResponse);
}

}
  1. 提供顾问的唯一名称。
  2. 您可以通过设置顺序值来控制执行顺序。值越低,越早执行。
  3. MessageAggregator 是一个实用工具类,它能将多个Flux 响应聚合成单个ChatClientResponse。这对于记录整个响应而不是流中的单个项目非常有用。请注意,您不能在 MessageAggregator 中更改响应,因为它是只读操作。

重读(Re2)顾问

《“重新阅读提升大型语言模型的推理能力”》一文介绍了一种名为“重新阅读(Re2)”的技术,该技术可增强大型语言模型的推理能力。Re2技术需要对输入提示进行如下增强:

1
{Input_Query}再读一遍问题:{Input_Query}

实现一个将Re2技术应用于用户输入查询的顾问可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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、<font style="color:rgb(25, 30, 30);">before</font>方法通过应用重读技术来增强用户的输入查询。

2、开发者可以通过设置order值来控制执行顺序。值越小,执行越早。

Spring AI 内置顾问

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

聊天记忆顾问

这些顾问在聊天记忆存储中管理对话历史:

  • <font style="color:rgb(25, 30, 30);">MessageChatMemoryAdvisor</font>

检索记忆并将其作为消息集合添加到提示中。这种方法能保持对话历史的结构。请注意,并非所有人工智能模型都支持这种方法。

  • <font style="color:rgb(25, 30, 30);">PromptChatMemoryAdvisor</font>

检索记忆并将其整合到提示词的系统文本中。

  • <font style="color:rgb(25, 30, 30);">VectorStoreChatMemoryAdvisor</font>

从向量存储中检索记忆并将其添加到提示词的系统文本中。该辅助工具有助于高效地从大型数据集中搜索和检索相关信息。

问答顾问
  • <font style="color:rgb(25, 30, 30);">QuestionAnswerAdvisor</font>

该顾问使用向量存储来提供问答功能,采用了简单的检索增强生成(RAG)模式。

<font style="color:rgb(25, 30, 30);">RetrievalAugmentationAdvisor</font>

  • 该顾问使用org.springframework.ai.rag包中定义的构建块并遵循模块化RAG架构,实现了常见的检索增强生成(RAG)流程。
推理顾问
  • <font style="color:rgb(25, 30, 30);">ReReadingAdvisor</font>

实现了一种用于大语言模型推理的重读策略,称为RE2,以增强输入阶段的理解。基于文章:《重读提升大语言模型的推理能力》(arxiv.org/pdf/2309.06275)。

内容安全顾问
  • <font style="color:rgb(25, 30, 30);">SafeGuardAdvisor</font>

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

流式与非流式

  • 非流式顾问处理完整的请求和响应。
  • 流式顾问将请求和响应作为连续流来处理,采用响应式编程概念(例如,使用Flux处理响应)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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
});
}

最佳实践

  1. 让顾问专注于特定任务,以实现更好的模块化。
  2. 必要时,使用<font style="color:rgb(25, 30, 30);">adviseContext</font>在顾问之间共享状态。
  3. 实现顾问的流式和非流式版本,以获得最大的灵活性。
  4. 仔细考虑链中顾问的顺序,以确保数据的正确流转。

API重大变更

顾问接口

  • 在1.0 M2中,存在独立的<font style="color:rgb(25, 30, 30);">RequestAdvisor</font><font style="color:rgb(25, 30, 30);">ResponseAdvisor</font>接口。
    • <font style="color:rgb(25, 30, 30);">RequestAdvisor</font><font style="color:rgb(25, 30, 30);">ChatModel.call</font><font style="color:rgb(25, 30, 30);">ChatModel.stream</font> 方法之前被调用。
    • <font style="color:rgb(25, 30, 30);">ResponseAdvisor</font>是在这些方法之后被调用的。
  • 在1.0 M3中,这些接口已被替换为:
    • <font style="color:rgb(25, 30, 30);">CallAroundAdvisor</font>
    • <font style="color:rgb(25, 30, 30);">StreamAroundAdvisor</font>
  • 此前属于<font style="color:rgb(25, 30, 30);">ResponseAdvisor</font><font style="color:rgb(25, 30, 30);">StreamResponseMode</font>已被移除。
  • 在1.0.0版本中,这些接口已被替换:
    • <font style="color:rgb(25, 30, 30);">CallAroundAdvisor</font><font style="color:rgb(25, 30, 30);">CallAdvisor</font><font style="color:rgb(25, 30, 30);">StreamAroundAdvisor</font><font style="color:rgb(25, 30, 30);">StreamAdvisor</font><font style="color:rgb(25, 30, 30);">CallAroundAdvisorChain</font><font style="color:rgb(25, 30, 30);">CallAdvisorChain</font> 以及 <font style="color:rgb(25, 30, 30);">StreamAroundAdvisorChain</font><font style="color:rgb(25, 30, 30);">StreamAdvisorChain</font>
    • <font style="color:rgb(25, 30, 30);">AdvisedRequest</font><font style="color:rgb(25, 30, 30);">ChatClientRequest</font><font style="color:rgb(25, 30, 30);">AdivsedResponse</font><font style="color:rgb(25, 30, 30);">ChatClientResponse</font>

上下文映射处理

  • 在1.0 M2中:
    • 上下文映射是一个单独的方法参数。
    • 该映射是可变的,并在链中传递。
  • 在1.0 M3中:
    • 上下文映射现在是<font style="color:rgb(25, 30, 30);">AdvisedRequest</font><font style="color:rgb(25, 30, 30);">AdvisedResponse</font>记录的一部分。
    • 该映射是不可变的。
    • 要更新上下文,请使用<font style="color:rgb(25, 30, 30);">updateContext</font>方法,该方法会创建一个包含更新内容的新的不可修改映射。