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
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
@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 能夠始終如一且簡單地實現所有這些功能。