知識庫 / Spring / Spring AI RSS 訂閱

Spring AI 顧問指南

Artificial Intelligence,Spring AI
HongKong
8
10:54 AM · Dec 06 ,2025

1. 概述

AI 驅動的應用已成為新現實。我們廣泛地實施各種 RAG 應用、提示 API,並利用 LLM 創建令人印象深刻的項目。藉助 Spring AI,我們可以更快、更一致地完成這些任務。

在本文中,我們將回顧一個有價值的功能——Spring AI Advisors,它可以幫助我們處理各種常規任務。

2. Spring AI 顧問是什麼?

顧問是用於處理我們 AI 應用中的請求和響應的攔截器。我們可以利用它們來為我們的提示流程設置額外的功能。例如,我們可以建立聊天曆史記錄、排除敏感詞彙或為每個請求添加額外的上下文。

核心組件是 <em >CallAroundAdvisor</em> 接口。我們通過實現此接口來創建一系列顧問,這些顧問會影響我們的請求或響應。顧問流程如圖所示:

我們將提示發送到連接到顧問鏈的聊天模型。在提示傳遞之前,鏈中的每個顧問都會執行其 <em >before</em> 操作。同樣,在我們收到聊天模型響應之前,每個顧問都會調用其自身的 <em >after</em> 操作。

3. 聊天記憶顧問

聊天記憶顧問是一套非常實用的 顧問 實現。我們可以利用這些顧問為我們的聊天提示提供溝通曆史,從而提高聊天響應的準確性。

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");
    }
}
<div>
 在本次測試中,我們創建了一個 <em >MessageChatMemoryAdvisor</em> 實例,其中包含 <em >InMemoryChatMemory</em>。然後我們發送了一些提示,要求聊天返回對話中人們的名字,包括歷史數據。正如我們所見,所有對話中的名字都已返回。
</div

3.2. PromptChatMemoryAdvisor

使用 PromptChatMemoryAdvisor,我們實現的目標與以往相同,即向聊天模型提供對話歷史。 不同之處在於,通過這個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. <em >VectorStoreChatMemoryAdvisor</em>

使用 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通過使用該顧問,我們準備一個提示,根據已準備好的上下文請求信息。 上下文是從向量存儲中通過相似性搜索檢索到的。
讓我們來檢查一下這種行為:
@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");
}
<div>
 我們使用文檔中的特定信息填充了向量存儲。然後,我們使用 <em >QuestionAnswerAdvisor</em> 創建了一個提示,並驗證了其響應與文檔內容一致。
</div >

5. 安全衞士顧問

有時,我們需要阻止某些敏感詞語出現在客户端提示中。無疑,我們可以使用 安全衞士顧問 通過指定禁詞並將其包含在提示的顧問實例中來實現這一點。如果這些詞語在搜索請求中使用,將會被拒絕,顧問會提示我們重新措辭:

// 解釋:安全衞士顧問用於阻止敏感詞語出現在提示中,
// 通過指定禁詞並將其包含在顧問實例中實現。
// 如果搜索請求中包含這些詞語,則會被拒絕,並提示重新措辭。
@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");
}
<div>
 在這個例子中,我們首先創建了一個 <em >SafeGuardAdvisor</em>,其中包含一個禁止使用的詞。然後,我們嘗試在提示中使用這個詞,正如預期的那樣,我們收到了禁止詞驗證消息。
</div >

6. 實現自定義顧問

當然,我們允許使用任何所需的邏輯來實現自定義顧問。讓我們創建一個名為 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() 方法中返回了最大整數值,因此我們的顧問將成為鏈條的最後一位

現在,讓我們測試我們的新顧問:

@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 並將其附加到提示中。讓我們看看執行後日志中發生的情況:

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。通過 Advisor,我們可以獲得聊天記憶功能、對敏感詞的控制以及與向量存儲的無縫集成。此外,我們還可以輕鬆創建自定義擴展,以添加特定功能。使用 Advisor 能夠始終如一且簡單地實現所有這些功能。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.