AI 實戰篇:Spring-AI再更新!細細講下Advisors

努力的小雨發表於2024-11-19

在2024年10月8日,Spring AI再次進行了更新,儘管當前版本仍為非穩定版本(1.0.0-M3),但博主將持續關注這些動態,並從流行的智慧體視角深入解析其技術底層。目前,Spring AI仍處於小眾狀態,尚未經過開源社群多年的維護和穩定化過程,這與已經較為成熟的Spring框架形成鮮明對比。即便是Spring AI的穩定版本(1.0.0-SNAPSHOT),在常見的maven倉庫中也難以找到,仍需透過Spring的jfrog倉庫進行訪問。

好的,我們不再繞圈子,直接進入主題。在1.0.0-M3版本中,進行了許多重要的更新,我將逐一詳細講解這些特性。今天的重點是深入解析Advisors的概念,因為它與我們當前工作中所使用的一些技術有很多相似之處,能夠幫助大家更容易地理解相關內容。因此,我相信透過這部分的講解,大家將能更好地掌握Spring AI的核心功能。現在,就讓我們開始吧!

什麼是 Spring AI Advisors?

Spring AI Advisor的核心功能在於攔截並可能修改AI應用程式中聊天請求和響應流的元件。在這個系統中,AroundAdvisor是關鍵參與者,它允許開發人員在這些互動過程中動態地轉換或利用資訊。

使用Advisor的主要優勢包括:

  1. 重複任務的封裝:能夠將常見的生成式AI模式打包成可重用的單元,簡化開發過程。
  2. 資料轉換:增強傳送給語言模型(LLM)的資料,並最佳化返回給客戶端的響應格式,以提高互動質量。
  3. 可移植性:建立可跨不同模型和用例工作的可重用轉換元件,提升程式碼的靈活性和適應性。

或許你會覺得,這與我們在Spring中使用AspectJ的方式頗為相似。實際上,當你閱讀完今天的文章後,會發現這不過是換了個名稱而已,主要功能其實是一致的,都是為了增強應用程式的能力。

Advisors VS Advised

這裡我們簡單澄清一下“Advisors”和“Advised”這兩個術語。實際上,它們之間並沒有直接關係,只是因為在檢視原始碼時,我們常常會遇到這兩個詞。Advisors是指我們建立的各種增強功能類,它們負責對請求鏈路進行不同的處理。而“Advised”則是一個形容詞,用來描述某個類已不再是普通類,它經過增強後具備了新的特性。儘管它們也對請求類進行了增強,但這種增強主要是透過屬性遷移的方式實現的。

下面的圖示將幫助進一步理解這一點。

image

Advisors如何運作

如果你以前編寫過AspectJ的註解類,那麼你應該能夠很容易地推測出Advisors是如何運作的。在Advisor系統中,各個Advisor以鏈式結構執行,序列中的每個Advisor都有機會對傳入的請求和傳出的響應進行處理。這種鏈式處理機制確保了每個Advisor可以在請求和響應流中新增自己的邏輯,從而實現更靈活和可定製的功能。

為了幫助理解這一流程,下面我們先來看一下官方提供的流程圖。這張圖詳細展示了各個Advisor如何在請求鏈中進行互動,以及它們如何協同工作以增強整體功能。

image

我來大致講解一下整個流程:

首先,我們會封裝各種請求引數配置,如前面AdvisedRequest的截圖所示,這裡就不再詳細說明。接下來,鏈中的每個Advisor都會處理請求,可能對其進行修改,並將執行流程轉發給鏈中的下一個Advisor。值得注意的是,某些Advisor也可以選擇不呼叫下一個實體,從而阻止請求繼續傳遞。

最終,Advisor將請求傳送到Chat Model。聊天模型的響應將透過Advisor鏈傳遞迴原請求路徑,形成原始上下文和建議上下文的組合。每個Advisor都有機會處理或修改這個響應,確保其符合預期。最後,系統將返回一個AdvisedResponse給客戶端。

接下來,我們將深入探討如何實際使用Advisor。

使用 Advisor

內嵌的Advisor

作為Spring AI的一部分,系統內建了多個官方Advisor示例,這些示例不僅數量不多,而且功能各異,能夠很好地展示Advisor的實際應用場景。我們不妨一起來逐一檢視這些內建Advisor的作用和特點,深入瞭解它們如何在請求處理鏈中發揮各自的功能。

  • MessageChatMemoryAdvisor是我們之前提到過的一個常用類,它在請求處理流程中扮演著重要的角色。這個Advisor的主要功能是將使用者提出的問題和模型的回答新增到歷史記錄中,從而形成一個上下文記憶的增強機制。透過這種方式,系統能夠更好地理解使用者的需求,提供更加連貫和相關的響應。

    • 需要注意的是,並非所有的AI模型都支援這種上下文記憶的儲存和管理方式。某些模型可能沒有實現相應的歷史記錄功能,因此在使用MessageChatMemoryAdvisor時,確保所使用的模型具備此支援是至關重要的。
  • PromptChatMemoryAdvisor的功能在MessageChatMemoryAdvisor的基礎上進一步增強,其主要作用在於上下文聊天記錄的處理方式。與MessageChatMemoryAdvisor不同,PromptChatMemoryAdvisor並不將上下文記錄直接傳入messages引數中,而是巧妙地將其封裝到systemPrompt提示詞中。這一設計使得無論所使用的模型是否支援messages引數,系統都能夠有效地增加上下文歷史記憶。

  • QuestionAnswerAdvisor的主要功能是執行RAG(Retrieval-Augmented Generation)檢索,這一過程涉及對知識庫的高效呼叫。當使用者提出問題時,QuestionAnswerAdvisor會首先對知識庫進行檢索,並將匹配到的相關引用文字新增到使用者提問的後面,從而為生成的回答提供更為豐富和準確的上下文。

    • 此外,該Advisor設定了一個預設提示詞,旨在確保回答的質量和相關性。如果在知識庫中無法找到匹配的文字,系統將拒絕回答使用者的問題。
  • SafeGuardAdvisor的核心功能是進行敏感詞校驗,以確保系統在處理使用者輸入時的安全性和合規性。當使用者提交的資訊觸發了敏感詞機制,SafeGuardAdvisor將立即對該請求進行中途攔截,避免繼續呼叫大型模型進行處理。

  • SimpleLoggerAdvisor:這是一個用於日誌列印的工具,我們之前已經對其進行了練習和深入瞭解,因此在這裡不再贅述。

  • VectorStoreChatMemoryAdvisor:該元件實現了長期記憶功能,能夠將每次使用者提出的問題及模型的回答儲存到向量資料庫中。在使用者每次提問時,系統會進行一次檢索,將檢索到的資訊累加到系統提示詞的後面,以便為大模型提供更準確的上下文提示。然而,這裡需要注意的是,如果沒有妥善維護 chat_memory_conversation_id,可能會導致無限制的寫入和檢索,從而引發潛在的災難性bug。因此,確保這一標識的管理和更新至關重要,以避免系統的不穩定性和資料混亂。

在這裡,我們主要討論 chat_memory_conversation_id 引數,它在所有 Advisor 中都是一個關鍵要素。我們必須為每位使用者妥善維護這個引數,避免每次預設生成新 ID,以防在向量資料庫中產生大量垃圾資料。此外,維護好該引數後,可以在後臺利用它清理向量資料庫中的舊資料。需要注意的是,這裡使用的是儲存在 metadata 中的 chat_memory_conversation_id,而不是簡單的 ID,因此在刪除和清理時,需先進行查詢。

自定義Advisor

其實,我們之前已經實現過一個簡單的日誌記錄 Advisor。今天,我們將基於 Re-Reading (Re2)技術,打造一個更高階的 Advisor。實際上,理解這一過程並不複雜,核心在於提前對請求的問題進行包裝。

對於有興趣的同學,我推薦你們檢視 Re2 技術的相關實現及效果,詳細內容可以參考這篇論文:Re2技術實現

此外,關於提示詞的最佳化,如果你對這方面特別感興趣,我建議你瀏覽一下免費開源的部落格文件,這裡有很多有價值的資源可以參考:提示詞指南

接下來,我們不再囉嗦,直接來看一下官方的示例程式碼:

public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
  private static final String DEFAULT_USER_TEXT_ADVISE = """
      {re2_input_query}
      Read the question again: {re2_input_query}
      """;

  @Override
  public String getName() {
      return this.getClass().getSimpleName();
  }

  @Override
  public int getOrder() {
      return 0;
  }

  private AdvisedRequest before(AdvisedRequest advisedRequest) {

      String inputQuery = advisedRequest.userText(); //original user query

      Map<String, Object> params = new HashMap<>(advisedRequest.userParams());        
      params.put("re2_input_query", inputQuery);

      return AdvisedRequest.from(advisedRequest)
              .withUserText(DEFAULT_USER_TEXT_ADVISE)
              .withUserParams(params)
              .build();
  }

  @Override
  public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
      return chain.nextAroundCall(before(advisedRequest));
  }

  @Override
  public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
      return chain.nextAroundStream(before(advisedRequest));
  }
}

可以看到,在這裡的實現中,實際上並沒有過多的程式碼編寫,僅僅是宣告瞭一個全域性的文字模板,並在請求之前對其進行了簡單的封裝。這種設計思路的核心在於透過模板的預處理,提升請求的有效性和上下文的相關性。根據 Re2 的官方說明,這種方法不僅能夠簡化程式碼結構,還能顯著提升模型的回答效果。

共享引數Advisor

在之前的講解中,例如我們討論的 messageChatMemoryAdvisor,在 Bean 宣告時,實際上是專門為引數配置編寫了預設設定。儘管如此,我們仍然可以透過引數傳入的方式進行動態配置,這種靈活性讓我們能夠根據實際需求調整引數。你可以傳入任何所需的引數,並在重寫的方法中進行讀取和應用,從而使 Advisor 更加靈活和適應不同場景的需求。

在這裡,我們將以官方的 messageChatMemoryAdvisor 為例,展示以前的寫法,以便更好地理解這一配置過程。

ChatDataPO functionGenerationByText(@RequestParam("userInput")  String userInput) { 
    OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
            .withModel("hunyuan-pro").withTemperature(0.5).build();
    String content = this.myChatClientWithSystem
            .prompt()
            .system("請你作為一個小雨的AI小助手,請將工具返回的資料格式化後以友好的方式回覆使用者的問題。制定的旅遊攻略要有航班、酒店、火車資訊")
            .user(userInput)
            .options(openAiChatOptions)
            .advisors(messageChatMemoryAdvisor,myLoggerAdvisor,promptChatKnowledageAdvisor)
//配置類如下:            
@Bean
MessageChatMemoryAdvisor messageChatMemoryAdvisor() {
    InMemoryChatMemory chatMemory = new InMemoryChatMemory();
    return new MessageChatMemoryAdvisor(chatMemory,"123",10);
}            

傳遞引數的效果可以透過以下方式進行改寫,具體實現中省略了一些冗餘的重複程式碼,以便更加清晰地展示主要邏輯:

.advisors(messageChatMemoryAdvisor,myLoggerAdvisor,promptChatKnowledageAdvisor)
.advisors(advisor -> advisor.param("chat_memory_conversation_id", "678")
        .param("chat_memory_response_size", 100))

這樣,我們就能夠實時讀取引數,並在呼叫之前進行恰當的配置,下面是具體的原始碼示例:

image

官方已經為我們封裝了常用的讀取模板,提供了一系列高效且易於使用的功能介面。我們只需直接呼叫這些預定義的模板,便可以快速實現所需的操作。

更新引數

除了在開始呼叫之前設定一些共享引數外,我們還可以在執行期間動態調整這些引數,以便更好地適應實時變化的需求和環境:

@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {

    this.advisedRequest = advisedRequest.updateContext(context -> {
        context.put("aroundCallBefore" + getName(), "AROUND_CALL_BEFORE " + getName());  // Add multiple key-value pairs
        context.put("lastBefore", getName());  // Add a single key-value pair
        return context;
    });

    // Method implementation continues...
}

今天的Advisors介紹就到這裡,希望能為你帶來一些新的啟發和思考。

總結

Spring AI Advisors 提供了一種強大而靈活的方法,旨在顯著增強你的 AI 應用程式的功能和效能。透過充分利用這一 API,你能夠建立出更復雜、可重用且易於維護的 AI 元件,從而提升開發效率和系統的可擴充套件性。

無論你是在實施自定義邏輯以滿足特定業務需求,管理對話歷史記錄以最佳化使用者體驗,還是改進模型推理以獲得更準確的結果,Advisors 都能為你提供簡潔且高效的解決方案。這種靈活性使得開發者能夠快速響應變化,同時保持程式碼的整潔和可讀性,進而為使用者提供更加流暢和智慧的體驗。


我是努力的小雨,一名 Java 服務端碼農,潛心研究著 AI 技術的奧秘。我熱愛技術交流與分享,對開源社群充滿熱情。同時也是一位騰訊雲創作之星、阿里雲專家博主、華為云云享專家、掘金優秀作者。

💡 我將不吝分享我在技術道路上的個人探索與經驗,希望能為你的學習與成長帶來一些啟發與幫助。

🌟 歡迎關注努力的小雨!🌟

相關文章