Spring AI Advisor 指南
1、概览
AI 驱动的应用已成为我们的新现实。我们正在广泛实现各种 RAG 应用和提示 API,并使用 LLM 创建令人印象深刻的项目。借助 Spring AI,我们可以更快、更稳定地完成这些任务。
本文将带你了解 Spring AI Advisor 这一功能,它可以为我们处理各种常规任务。
2、Spring AI Advisor 是什么?
Advisors 是在 AI 应用程序中处理请求和响应的拦截器。我们可以使用它们为提示流程设置额外的功能。例如,可以建立聊天历史、排除敏感词或为每个请求添加额外的上下文。
该功能的核心组件是 CallAroundAdvisor
接口。我们通过实现该接口来创建 Advisor 链,从而影响我们的请求或响应。Advisor 流程如下图所示:
我们会将提示(prompt)发送到一个聊天模型,该模型关联了一个 Advisor 链。在发送提示之前,链上的每个 Advisor 都会执行其 before
操作。同样,在收到聊天模型的回复之前,每个 Advisor 都会调用自己的 after
操作。
3、ChatMemoryAdvisor
ChatMemoryAdvisor 是一组非常有用的 Advisor 实现。我们可以使用这些 Advisor 提供与聊天提示的交流历史,从而提高聊天回复的准确性。
3.1、MessageChatMemoryAdvisor
使用 MessageChatMemoryAdvisor
,我们可以通过 messages
属性提供聊天客户端调用的聊天历史记录。我们可以将所有消息保存在 ChatMemory
实现中,并控制历史记录的大小。
示例如下:
@SpringBootTest(classes = ChatModel.class)
@EnableAutoConfiguration
@ExtendWith(SpringExtension.class)
public class SpringAILiveTest {
@Autowired
@Qualifier("openAiChatModel")
ChatModel chatModel;
ChatClient chatClient;
@BeforeEach
void setup() {
chatClient = ChatClient.builder(chatModel).build();
}
@Test
void givenMessageChatMemoryAdvisor_whenAskingChatToIncrementTheResponseWithNewName_thenNamesFromTheChatHistoryExistInResponse() {
ChatMemory chatMemory = new InMemoryChatMemory();
MessageChatMemoryAdvisor chatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);
String responseContent = chatClient.prompt()
.user("Add this name to a list and return all the values: Bob")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Bob");
responseContent = chatClient.prompt()
.user("Add this name to a list and return all the values: John")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Bob")
.contains("John");
responseContent = chatClient.prompt()
.user("Add this name to a list and return all the values: Anna")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Bob")
.contains("John")
.contains("Anna");
}
}
在这个测试中,我们创建了一个 MessageChatMemoryAdvisor
实例,其中包含 InMemoryChatMemory
。然后,我们发送了一些提示,要求聊天返回包括历史数据在内的人名。我们可以看到,对话中的所有姓名都已返回。
3.2、PromptChatMemoryAdvisor
通过 PromptChatMemoryAdvisor
,我们可以实现相同的目标 - 为聊天模型提供对话历史记录。不同的是,我们使用此 Advisor 将聊天历史添加到了提示中。在底层,我们通过以下 Advisor 扩展了提示文本:
Use the conversation memory from the MEMORY section to provide accurate answers.
---------------------
MEMORY:
{memory}
---------------------
示例如下:
@Test
void givenPromptChatMemoryAdvisor_whenAskingChatToIncrementTheResponseWithNewName_thenNamesFromTheChatHistoryExistInResponse() {
ChatMemory chatMemory = new InMemoryChatMemory();
PromptChatMemoryAdvisor chatMemoryAdvisor = new PromptChatMemoryAdvisor(chatMemory);
String responseContent = chatClient.prompt()
.user("Add this name to a list and return all the values: Bob")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Bob");
responseContent = chatClient.prompt()
.user("Add this name to a list and return all the values: John")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Bob")
.contains("John");
responseContent = chatClient.prompt()
.user("Add this name to a list and return all the values: Anna")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Bob")
.contains("John")
.contains("Anna");
}
再次尝试创建了一些提示,这次使用 PromptChatMemoryAdvisor
让聊天模型考虑对话历史。果然,所有数据都正确返回给我们。
3.3、VectorStoreChatMemoryAdvisor
通过使用 VectorStoreChatMemoryAdvisor
,我们可以获得更强大的功能。
我们通过向量存储中的相似性匹配搜索消息的上下文。搜索相关文档时,我们会考虑对话 ID。在我们的示例中,我们将使用稍作改动的 SimpleVectorStore
,但也可以替换为任何向量数据库。
首先,创建一个向量存储的 Bean:
@Configuration
public class SimpleVectorStoreConfiguration {
@Bean
public VectorStore vectorStore(@Qualifier("openAiEmbeddingModel")EmbeddingModel embeddingModel) {
return new SimpleVectorStore(embeddingModel) {
@Override
public List<Document> doSimilaritySearch(SearchRequest request) {
float[] userQueryEmbedding = embeddingModel.embed(request.query);
return this.store.values()
.stream()
.map(entry -> Pair.of(entry.getId(),
EmbeddingMath.cosineSimilarity(userQueryEmbedding, entry.getEmbedding())))
.filter(s -> s.getSecond() >= request.getSimilarityThreshold())
.sorted(Comparator.comparing(Pair::getSecond))
.limit(request.getTopK())
.map(s -> this.store.get(s.getFirst()))
.toList();
}
};
}
}
如上,我们创建了一个 SimpleVectorStore
类的 Bean 并重写了其 doSimilaritySearch()
方法。默认的 SimpleVectorStore
不支持元数据过滤,在这里我们将忽略这一点。由于测试中只有一个对话,这种方法非常适合我们。
现在,测试一下历史上下文行为:
@Test
void givenVectorStoreChatMemoryAdvisor_whenAskingChatToIncrementTheResponseWithNewName_thenNamesFromTheChatHistoryExistInResponse() {
VectorStoreChatMemoryAdvisor chatMemoryAdvisor = new VectorStoreChatMemoryAdvisor(vectorStore);
String responseContent = chatClient.prompt()
.user("Find cats from our chat history, add Lion there and return a list")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Lion");
responseContent = chatClient.prompt()
.user("Find cats from our chat history, add Puma there and return a list")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Lion")
.contains("Puma");
responseContent = chatClient.prompt()
.user("Find cats from our chat history, add Leopard there and return a list")
.advisors(chatMemoryAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("Lion")
.contains("Puma")
.contains("Leopard");
}
我们要求聊天在列表中填充一些项目,同时在幕后进行相似性搜索,以获得所有相似文档,然后我们的聊天 LLM 会根据这些文档准备答案。
4、QuestionAnswerAdvisor
在 RAG 应用中,我们广泛使用 QuestionAnswerAdvisor
。
使用该 Advisor,我们可以根据准备好的上下文准备一个请求信息的提示。上下文通过相似性搜索从向量存储中获取。
如下:
@Test
void givenQuestionAnswerAdvisor_whenAskingQuestion_thenAnswerShouldBeProvidedBasedOnVectorStoreInformation() {
Document document = new Document("The sky is green");
List<Document> documents = new TokenTextSplitter().apply(List.of(document));
vectorStore.add(documents);
QuestionAnswerAdvisor questionAnswerAdvisor = new QuestionAnswerAdvisor(vectorStore);
String responseContent = chatClient.prompt()
.user("What is the sky color?")
.advisors(questionAnswerAdvisor)
.call()
.content();
assertThat(responseContent)
.containsIgnoringCase("green");
}
我们用文档中的具体信息填充向量存储(Vector Store)。然后,使用 QuestionAnswerAdvisor
创建一个提示,并验证其响应是否与文档内容一致。
5、SafeGuardAdvisor
有时,我们必须防止在客户端提示中使用某些敏感词。我们可以使用 SafeGuardAdvisor
来实现这一目的,具体做法是指定一个违禁词语列表,并将其包含在提示的顾问实例中。如果在搜索请求中使用了其中任何一个词,该请求将被拒绝,顾问会提示我们重新措辞:
@Test
void givenSafeGuardAdvisor_whenSendPromptWithSensitiveWord_thenExpectedMessageShouldBeReturned() {
List<String> forbiddenWords = List.of("Word2");
SafeGuardAdvisor safeGuardAdvisor = new SafeGuardAdvisor(forbiddenWords);
String responseContent = chatClient.prompt()
.user("Please split the 'Word2' into characters")
.advisors(safeGuardAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("I'm unable to respond to that due to sensitive content");
}
如上例,我们首先创建了一个带有单个违禁词的 SafeGuardAdvisor
。然后,我们尝试在提示符中使用该单词,不出所料,收到了禁用单词验证信息。
6、实现自定义 Advisor
当然,我们可以根据需求实现自定义的 Advisor。
创建一个 CustomLoggingAdvisor
,记录所有聊天请求和回复:
public class CustomLoggingAdvisor implements CallAroundAdvisor {
private final static Logger logger = LoggerFactory.getLogger(CustomLoggingAdvisor.class);
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
advisedRequest = this.before(advisedRequest);
AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);
this.observeAfter(advisedResponse);
return advisedResponse;
}
private void observeAfter(AdvisedResponse advisedResponse) {
logger.info(advisedResponse.response()
.getResult()
.getOutput()
.getContent());
}
private AdvisedRequest before(AdvisedRequest advisedRequest) {
logger.info(advisedRequest.userText());
return advisedRequest;
}
@Override
public String getName() {
return "CustomLoggingAdvisor";
}
@Override
public int getOrder() {
return Integer.MAX_VALUE;
}
}
如上,我们实现了 CallAroundAdvisor
接口,并在调用前后添加了日志逻辑。此外,getOrder()
方法返回了 Integer
最大值,因此该 adviser 是链中的最后一个。
测试如下:
@Test
void givenCustomLoggingAdvisor_whenSendPrompt_thenPromptTextAndResponseShouldBeLogged() {
CustomLoggingAdvisor customLoggingAdvisor = new CustomLoggingAdvisor();
String responseContent = chatClient.prompt()
.user("Count from 1 to 10")
.advisors(customLoggingAdvisor)
.call()
.content();
assertThat(responseContent)
.contains("1")
.contains("10");
}
如上,创建 CustomLoggingAdvisor
并将其附加到提示。
执行日志如下,可以看到,我们的 Advisor 成功记录了提示文本和聊天回复。
c.b.s.advisors.CustomLoggingAdvisor : Count from 1 to 10
c.b.s.advisors.CustomLoggingAdvisor : 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
7、总结
本文介绍了 Spring AI 中 Advisor 这一个强大的功能,还介绍了 Spring AI 中预定义的常用 Advisor 实现,
Ref:https://www.baeldung.com/spring-ai-advisors