Spring AI Advisor 指南
1. 概述
人工智慧驅動的應用程式是我們的新現實。我們正在廣泛實施各種 RAG 應用程式和提示 API,並使用法學碩士創建令人印象深刻的專案。借助 Spring AI,我們可以更快、更一致地完成這些任務。
在本文中,我們將回顧一個名為 Spring AI Advisors 的有價值的功能,它可以為我們處理各種日常任務。
2. Spring AI Advisor是什麼?
顧問是處理人工智慧應用程式中的請求和回應的攔截器。我們可以使用它們為我們的提示流程設定附加功能。例如,我們可能會建立聊天記錄、排除敏感字詞或為每個請求添加額外的上下文。
此功能的核心元件是CallAroundAdvisor
介面。我們實作這個介面來創建一個顧問鏈,它將影響我們的請求或回應。下圖描述了顧問的流程:
我們將提示傳送到附加到一系列顧問的聊天模型。在發出提示之前,鏈中的每個顧問都會執行其before
操作。同樣,在我們從聊天模型獲得回應之前,每個顧問都會呼叫自己的after
操作。
3.聊天記憶顧問
Chat Memory Advisors 是一組非常有用的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 時,我們將聊天記憶體新增至提示。在幕後,我們透過以下建議擴展了提示文字:
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
,但我們也可以將其替換為任何 Vector 資料庫。
首先,讓我們建立一個向量儲存的 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");
}
我們用文件中的特定資訊填充向量儲存。然後,我們使用了一個 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. 實施自訂顧問
當然,我們可以用我們需要的任何邏輯來實現我們的自訂顧問程式。讓我們建立一個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
並將其附加到提示中。我們來看看執行後日誌中發生了什麼事:
cbsadvisors.CustomLoggingAdvisor : Count from 1 to 10
cbsadvisors.CustomLoggingAdvisor : 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
正如我們所看到的,我們的顧問成功記錄了提示文字和聊天回應。
七、結論
在本教程中,我們探索了一個名為 Advisors 的出色 Spring AI 功能。借助 Advisors,我們獲得了聊天記憶功能、對敏感詞的控制以及與向量儲存的無縫整合。此外,我們可以輕鬆建立自訂擴充功能來新增特定功能。使用 Advisors 使我們能夠一致、直接地實現所有這些功能。
與往常一樣,程式碼可以在 GitHub 上取得。