51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring AI Advisor 指南

1、概览 {#1概览}

AI 驱动的应用已成为我们的新现实。我们正在广泛实现各种 RAG 应用和提示 API,并使用 LLM 创建令人印象深刻的项目。借助 Spring AI,我们可以更快、更稳定地完成这些任务。

本文将带你了解 Spring AI Advisor 这一功能,它可以为我们处理各种常规任务。

2、Spring AI Advisor 是什么? {#2spring-ai-advisor-是什么}

Advisors 是在 AI 应用程序中处理请求和响应的拦截器。我们可以使用它们为提示流程设置额外的功能。例如,可以建立聊天历史、排除敏感词或为每个请求添加额外的上下文。

该功能的核心组件是 CallAroundAdvisor 接口。我们通过实现该接口来创建 Advisor 链,从而影响我们的请求或响应。Advisor 流程如下图所示:

Advisor 流程图

我们会将提示(prompt )发送到一个聊天模型,该模型关联了一个 Advisor 链。在发送提示之前,链上的每个 Advisor 都会执行其 before 操作。同样,在收到聊天模型的回复之前,每个 Advisor 都会调用自己的 after 操作。

3、ChatMemoryAdvisor {#3chatmemoryadvisor}

ChatMemoryAdvisor 是一组非常有用的 Advisor 实现。我们可以使用这些 Advisor 提供与聊天提示的交流历史,从而提高聊天回复的准确性。

3.1、MessageChatMemoryAdvisor {#31messagechatmemoryadvisor}

使用 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 {#32promptchatmemoryadvisor}

通过 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 {#33vectorstorechatmemoryadvisor}

通过使用 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 {#4questionansweradvisor}

在 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 {#5safeguardadvisor}

有时,我们必须防止在客户端提示中使用某些敏感词。我们可以使用 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 {#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、总结 {#7总结}

本文介绍了 Spring AI 中 Advisor 这一个强大的功能,还介绍了 Spring AI 中预定义的常用 Advisor 实现,


Ref:https://www.baeldung.com/spring-ai-advisors

赞(2)
未经允许不得转载:工具盒子 » Spring AI Advisor 指南