Spring AI 系列之一个很棒的 Spring AI 功能——Advisors

1. 概述

由AI驱动的应用程序已成为我们的现实。我们正在广泛地实现各种RAG应用程序、提示API,并利用大型语言模型(LLM)创建项目。借助 Spring AI,我们可以更快速地完成这些任务。

在本文中,我们将介绍一个非常有价值的功能:Spring AI Advisors

,它可以为我们处理各种日常任务。


2. 什么是 Spring AI Advisor?

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

该功能的核心组件是CallAroundAdvisor接口。我们通过实现这个接口,来创建一系列Advisor,它们会影响我们的请求或响应。下图展示了这些 Advisor 的执行流程:

我们将提示(prompt)发送到一个连接了多个 Advisor 的聊天模型。在提示被发送之前,链中的每个Advisor都会执行它的前置操作(before action) 。同样地,在我们从聊天模型获取响应之前,每个Advisor会调用自己的 后置操作(after action)。


3. 聊天记忆Advisor(Chat Memory Advisors)

聊天记忆Advisor是一组非常有用的Advisor实现。我们可以使用这些 Advisor 为聊天提示提供交流历史,从而提升聊天响应的准确性。


3.1. MessageChatMemoryAdvisor

通过使用MessageChatMemoryAdvisor,我们可以在聊天客户端调用中通过messages属性提供聊天历史。我们将所有消息保存在一个 ChatMemory 实现中,并且可以控制历史记录的大小。

下面我们来实现一个这个 Advisor 的简单示例:

复制代码
@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");
    }
}

在这个测试中,我们创建了一个包含 InMemoryChatMemory 的 MessageChatMemoryAdvisor 实例。然后我们发送了几个提示,要求聊天模型返回包含历史数据的人名。正如我们所看到的,聊天中提到的所有人名都被返回了。


3.2. PromptChatMemoryAdvisor

使用 PromptChatMemoryAdvisor,我们可以实现相同的目标------为聊天模型提供对话历史。不同之处在于,这个Advisor是将聊天记忆直接添加到提示文本(prompt)中。

在底层实现中,我们通过如下方式扩展提示文本:

复制代码
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() 方法。

接下来,让我们测试一下基于相似度匹配的行为表现:

复制代码
@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时,我们会构造一个提示(prompt),请求基于准备好的上下文信息进行回答。

这些上下文信息是通过向量存储中的相似度搜索检索出来的。

让我们来看看它的工作机制:

复制代码
@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");
}

我们将文档中的特定信息填充到了向量存储中。

随后,我们使用QuestionAnswerAdvisor来创建提示,并验证其响应是否与文档内容一致,结果确实如此。


5. SafeGuardAdvisor

有时候我们需要阻止客户端提示中使用某些敏感词。毫无疑问,我们可以使用SafeGuardAdvisor来实现这一目标------只需指定一组禁止词,并将其包含在提示的 Advisor 实例中即可。

如果搜索请求中使用了这些敏感词,Advisor会拒绝该请求,并提示我们重新描述内容:

复制代码
@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() 方法中返回了最大整数值,因此这个 Advisor 会被排在调用链的最后一个执行。

现在,让我们测试这个新的 Advisor:

复制代码
@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并将其附加到了提示(prompt)上。现在让我们看看执行后的日志中发生了什么:

复制代码
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

正如我们所见,我们的 Advisor 成功地记录了提示文本和聊天响应内容。


7. 总结

在本教程中,我们探讨了一个很棒的Spring AI 功能------Advisors。通过 Advisors,我们获得了聊天记忆功能、敏感词控制以及与向量存储的无缝集成。

此外,我们还可以轻松地创建自定义扩展,以添加特定功能。

使用 Advisors,能够让我们以一致且简洁的方式实现上述所有能力。

私信1v1直连大厂总监解决职业发展问题「免.米」

相关推荐
IT_陈寒2 小时前
SpringBoot自动配置差点让我加班到凌晨
前端·人工智能·后端
yumgpkpm2 小时前
Acceldata ODP(Open Data Platform)3.3.6.4(RHEL9)保姆级完整安装手册
大数据·人工智能·hive·hadoop·kafka·hbase·cloudera
程序员cxuan2 小时前
腾讯又来一王炸,开源版 WorkBuddy 太夯了!
人工智能·后端·程序员
邓工说电2 小时前
智慧断路器安全吗?数据加密、离线保护与合规认证全解读
大数据·数据库·人工智能·智能断路器·炜晔科技
阿里云大数据AI技术2 小时前
Lance 数据检索怎么选,当然阿里云 Milvus 向量湖
人工智能
出海客2 小时前
跨境电商多语言客服知识库怎么建:资料结构、检索边界与人工升级
大数据·人工智能
java资料站2 小时前
五、Spring AI Alibaba · Memory · Saver(短期会话)
java·spring·microsoft
adinnet20262 小时前
企业微调大模型(Fine-tuning):先吃透 RAG 还是先训自己的模型
大数据·数据库
xsd202411182 小时前
从自主导航到视觉读表:一台工业巡检机器人的全栈技术链路拆解
人工智能
袁哥大话安全2 小时前
巡隐WEBSHELL扫描软件
人工智能·安全·web