引言
好的,今天我們繼續聊一下Spring AI的相關內容。在10月的時候,我使用Spring AI搭建了一個簡易版的個人助理系統,整體來說效果還是非常不錯的。透過這次嘗試,我對業務系統與AI結合的探索有了更為明確的理解和實踐。雖然目前功能上還相對簡單,整體系統也缺乏較多可操作的互動方式,特別是在資料庫操作方面,功能較為基礎,目前主要實現了一個簡單的查詢功能。
但就在10月末,Spring AI迎來了一個重要的更新,更新後不僅增強了函式呼叫的能力,還引入了全域性引數的概念。這兩個新特性為系統的擴充套件性和可玩性帶來了極大的提升,開啟了更多可能性。
那麼,今天我們就利用這個全域性引數的特性,來實現一個資料庫外掛。具體來說,我們將實現一個完整的增刪改查(CRUD)操作。對於目前的智慧體系統來說,資料庫操作已經是一個至關重要的功能,尤其是在業務系統中,智慧體能夠與資料庫進行互動,不僅提升了系統的靈活性和智慧化程度,也大大增強了業務處理的效率。因此,我們今天的目標就是透過Spring AI的強大功能,實現一套基礎的資料庫操作框架,完成增、刪、改、查四個功能模組。
需要特別注意的是,這裡我們僅僅是透過一個簡單的使用案例來進行分析和講解,當然,這並不代表只能侷限於此,實際上對於大部分業務場景來說,這樣的資料庫操作已經足夠滿足需求,並且可以根據具體的業務需求進一步擴充套件和最佳化功能。
如果有小夥伴還不太清楚如何使用Spring AI搭建自己的智慧體系統,或者對於Spring AI的基本功能還不太瞭解,歡迎檢視我們之前分享的相關文章,瞭解更多相關內容:https://www.cnblogs.com/guoxiaoyu/p/18453559
個人助理大最佳化
首先,讓我們來看一下我們計劃實現的個人助理功能。目前,我們已經實現了旅遊攻略查詢和天氣查詢功能。今天,我們將在此基礎上新增一個“個人待辦”功能。由於資料庫模組較為龐大且複雜,針對這個部分我們將單獨進行詳細講解。以下是該功能的大致實現效果及相關流程示意圖:
效果演示
首先,讓我們來看看經過半天調整後的效果演示。經過一段時間的最佳化和除錯,最終呈現的效果基本符合我的預期。
這裡只演示了下待辦的增刪改查,並沒有演示天氣查詢和旅遊攻略,可以看上一章節的演示。
開始最佳化
提示詞
當然,我們的最佳化工作從入口部分開始,首先,提示詞的設計是不可或缺的。回顧上一章節,由於當時功能較少,我們並沒有對提示詞做過多的修飾,因此整體的互動和模型的響應相對簡單。然而,隨著本次新增功能的增多,模型的回答可能會變得較為雜亂無序。
因此,為了確保模型的輸出能夠更精準、有序,我們在本次最佳化中提前準備並生成了詳細的提示詞。
String conversation_id = "123";
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
.withModel("hunyuan-pro").withTemperature(0.5)
.build();
String systemPrompt = """
- Role: 個人助理小助手
- Background: 使用者需要一個多功能的AI助手,可以提供實時的天氣資訊、詳盡的旅遊攻略以及幫助記錄待辦事項。
- Profile: 你是一個專業的旅行天氣小助手,具備強大的資訊檢索能力和資料處理能力,能夠為使用者提供精確的天氣資訊、詳盡的旅遊攻略,並幫助管理日常待辦事項。
- Skills: 你擁有強大的網路搜尋能力、資料處理能力以及使用者互動能力,能夠快速準確地為使用者提供所需資訊。
- Goals: 提供準確的天氣資訊,制定包含航班、酒店、火車資訊的詳盡旅遊攻略,並幫助使用者記錄和管理待辦事項。
- Constrains: 提供的資訊必須準確無誤,旅遊攻略應詳盡實用,待辦事項管理應簡潔高效。
- OutputFormat: 友好的對話式回覆,包含必要的詳細資訊和格式化的資料。
- Workflow:
1. 接收使用者的天氣查詢請求,並提供準確的天氣資訊。
2. 根據使用者的旅遊目的地,搜尋並提供包括航班、酒店、火車在內的旅遊攻略。
3. 接收使用者的待辦事項,並提供簡潔的記錄和提醒服務。
""";
ChatMemory chatMemory1 = messageChatMemoryAdvisor.getChatMemory();
String content = this.myChatClientWithSystem
.prompt()
.system(systemPrompt)
.user(userInput)
.options(openAiChatOptions)
.advisors(messageChatMemoryAdvisor,myLoggerAdvisor,promptChatKnowledageAdvisor,promptChatDateAdvisor)
.advisors(advisor -> advisor.param("chat_memory_conversation_id", conversation_id)
.param("chat_memory_response_size", 100))
.functions("CurrentWeather","TravelPlanning","toDoListFunctionWithContext")
.toolContext(Map.of("sessionId", conversation_id, "userMemory", chatMemory1,"client",chatClient))
.call()
.content();
log.info("content: {}", content);
ChatDataPO chatDataPO = ChatDataPO.builder().code("text").data(ChildData.builder().text(content).build()).build();
return chatDataPO;
好的,剛才我們新增了一些引數,我現在來詳細解釋一下每個引數的作用和使用場景:
- advisor.param:這是用於單獨修改我們預設增強器(增強型顧問)的引數。它與上面提到的
Advisor
類相關聯,允許使用者在不修改核心程式碼的情況下,自定義和調整增強器的行為和配置。 - toolContent:這個引數是我們在新增函式回撥功能時引入的全域性引數,主要用於處理回撥時的各種工具內容。在函式呼叫中,
toolContent
可以傳遞不同的工具資料,確保回撥過程的正確執行。 - sessionId:這個引數用於標識每個獨立的會話,它幫助我們控制每個使用者的會話狀態。透過給每個會話分配一個唯一的
sessionId
,我們可以確保每個函式呼叫是針對特定使用者的,而非共享的全域性資料。例如,我們的待辦事項是個人化的,只有對應使用者可以看到和操作自己的待辦列表。這裡為了演示的方便,我們將sessionId
設定為固定值,並未接入登入介面,實際應用中應根據使用者身份動態生成。 - userMemory:此引數用於傳遞使用者的歷史上下文,使得回撥函式能夠使用到之前的對話或操作記錄。例如,在待辦事項管理中,我們可能需要根據歷史資料判斷某個任務是否已經完成。
- chatClient:該引數將待辦函式與一個大模型連線,藉助大模型生成SQL查詢或其他複雜操作。
chatClient
負責與大模型進行互動,生成所需的SQL,而外層的思考模型則專注於呼叫介面並處理業務邏輯。
好的,再次提醒一下,如果有些小夥伴之前沒有接觸過智慧體的相關內容,建議你們先去瀏覽一下我之前的第一篇文章,瞭解一下基礎知識,補充相關的背景資訊,這樣對接下來的內容會更加容易理解。
那麼,接下來我們繼續探討與待辦事項相關的內容。
資料表
個人待辦事項的目的非常明確,主要是針對待辦事項表進行增、刪、改、查等基本操作。為了保證系統的高效性與簡潔性,我們在設計資料庫時,所需的資料欄位也非常簡潔直觀。這是我們的建表語句:
create table todo_info(
id int(11) auto_increment primary key,
todo_info varchar(1000) not null,
todo_Date date not null,
done boolean not null default false
)
在生成完這個建表語句之後,一定要妥善儲存,以便後續給大模型提供參考。
函式回撥
首先,我們需要明確待辦函式的回撥必須能夠支援四種基本操作:增刪改查,。此外,回撥函式還需要具備生成SQL語句的能力,並能執行這些SQL語句,以便與資料庫進行互動。只有在這些操作順利完成之後,我們才能將最終的結果資料返回給外層的思考模型,以供進一步的處理和分析。
基於這些要求,我們現在可以開始具體操作,逐步實現所需功能。
好的,我們一步一步來。
待辦函式
根據上述資訊,我們需要設計一個函式,該函式需要包含一個入參和一個回參。入參的主要作用是傳遞一個識別符號,用於唯一標識某一操作,而回參則是一個字串型別的返回值,用於向呼叫方反饋相應的結果或狀態資訊。
public class ToDoListInfoService implements BiFunction<ToDoListInfoService.ToDoRequest, ToolContext, ToDoListInfoService.ToDoResponse> {
private JdbcTemplate jdbcTemplate;
public ToDoListInfoService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@JsonClassDescription("crud:c 代表增加;r:代表查詢,u:代表更新,d:代表刪除")
public record ToDoRequest(String crud) {}
public record ToDoResponse(String message) {}
@Override
public ToDoResponse apply(ToDoListInfoService.ToDoRequest request, ToolContext toolContext) {}
}
可以觀察到,在這裡我們使用的是 BiFunction
介面,而不是之前使用的 Function
介面,因為我們需要使用ToolContext。
需要特別注意的是,儘管在這裡我們使用了
JsonClassDescription
,但它的主要目的是為了提高程式碼的可讀性和可維護性,方便開發人員理解和檢視結構。實際上,大模型並不會依賴於JsonClassDescription
來判斷或解析傳遞的具體引數。
Bean裝配
由於我們在這裡使用了 JdbcTemplate
進行資料庫操作,這就需要在專案中引入相應的 Maven 依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
我這裡仍然選擇使用 Bean 裝配的方式,主要是因為它能夠提供更高的靈活性與可維護性。此外,維護好入參的定義和配置,也能夠在後續的開發中為大模型提供有效的參考和支援。
@Bean
public FunctionCallback toDoListFunctionWithContext(JdbcTemplate jdbcTemplate) {
return FunctionCallbackWrapper.builder(new ToDoListInfoService(jdbcTemplate))
.withName("toDoListFunctionWithContext") // (1) function name
.withDescription("新增待辦,crud:c 代表增加;r:代表查詢,u:代表更新,d:代表刪除") // (2) function description
.build();
}
接下來,我們將著手實現函式內部的具體方法。
上下文資訊
預設提供的 MessageChatMemoryAdvisor
類並不支援直接獲取歷史聊天記錄。因此,我們需要自定義一個類來實現這一功能。為了方便使用,下面的程式碼將整合該功能並暴露一個介面供呼叫。
@Slf4j
public class MyMessageChatMemoryAdvisor extends MessageChatMemoryAdvisor {
private ChatMemory chatMemory;
public MyMessageChatMemoryAdvisor(ChatMemory chatMemory) {
super(chatMemory);
this.chatMemory = chatMemory;
}
public MyMessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int chatHistoryWindowSize) {
super(chatMemory, defaultConversationId, chatHistoryWindowSize);
this.chatMemory = chatMemory;
}
public MyMessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int chatHistoryWindowSize, int order) {
super(chatMemory, defaultConversationId, chatHistoryWindowSize, order);
this.chatMemory = chatMemory;
}
public ChatMemory getChatMemory() {
return chatMemory;
}
}
為了能夠順利訪問歷史聊天記錄,我們專門編寫了一個方法,該方法會將歷史上下文物件返回。透過這種方式,我們可以在後續的操作中方便地獲取到完整的聊天資訊,從而實現對歷史對話內容的正常訪問和處理。
資料庫操作
接下來,我們將進行資料庫操作,但在此過程中,必須依賴大模型的幫助來生成SQL語句。原因在於,外層的大模型具備強大的能力,可以準確地分析並理解需求,從而判斷出具體的操作型別是增、刪、改還是查。接下來,我們將詳細介紹如何實現這一過程。
public ToDoResponse apply(ToDoListInfoService.ToDoRequest request, ToolContext toolContext) {
String tableinfo = """
- Role: SQL語句生成專家
- Background: 使用者需要根據特定的表結構和引數資訊生成精準的MySQL查詢或修改語句,以實現資料庫操作的自動化和效率化。
- Profile: 你是一位經驗豐富的資料庫管理員和SQL專家,精通MySQL資料庫的各種查詢和修改語句,能夠根據使用者提供的表結構和引數資訊快速生成正確的SQL語句。
- Skills: 你具備深厚的資料庫理論知識和豐富的實踐經驗,能夠理解複雜的表結構,準確把握使用者需求,並據此生成高效、準確的SQL語句。
- Goals: 根據使用者提供的表結構和引數資訊,生成可以直接執行的MySQL查詢或修改語句,確保語句的正確性和執行的成功率。
- Constrains: 生成的SQL語句必須符合MySQL的語法規則,能夠直接在MySQL資料庫中執行,且不包含任何額外的資訊或提示。
- OutputFormat: 純SQL文字語句,格式規範,無多餘資訊。禁止使用markdown格式。
- Workflow:
1. 分析使用者提供的表結構資訊和引數資訊。
2. 根據分析結果,確定需要執行的資料庫操作型別(查詢、插入、更新或刪除)。
3. 結合操作型別和使用者提供的資訊,生成符合MySQL語法的SQL語句。
-example:
q.幫我建立一個待辦:明天9點提醒我讀書。今天日期是2024-11-07 20:20:11
a:INSERT INTO todo_info (todo_info, todo_date) VALUES ('明天8點讀書', '2024-11-08 08:00:00');
- tableinfo:
create table todo_info(
id int(11) auto_increment primary key,
todo_info varchar(1000) not null,
todo_Date date not null,
done boolean not null default false
)
""";
String crud = request.crud;
ChatMemory chatMemory = (ChatMemory)toolContext.getContext().get("userMemory");
String conversation_id = (String)toolContext.getContext().get("sessionId");
ChatClient client = (ChatClient)toolContext.getContext().get("client");
List<Message> messages = chatMemory.get(conversation_id, 1);
var userqa = messages.get(0).getContent();
String jsonString = "執行成功";
String content = client.prompt()
.system(tableinfo)
.user("請根據當前問題生成相應SQL文字即可,禁止生成SQL以外的內容:"+userqa+",今天日期是:"+ DateUtil.now())
.call().content();
try {
if (crud.equals("r")) {
jsonString = "查詢到待辦內容如下:" + JSONObject.toJSONString(jdbcTemplate.queryForList(content));
} else {
jdbcTemplate.execute(content);
}
}catch (Exception e){
log.info("ToDoListInfoService:{}", e.getMessage());
jsonString = "執行失敗了";
}
log.info("ToDoListInfoService:{}", content);
return new ToDoResponse(jsonString);
這段程式碼的實現完全依賴於透過全域性引數傳遞過來的資訊,以便更好地處理歷史上下文的問題。傳統的回撥函式方法在處理多輪對話的歷史上下文時存在很大的侷限性,無法有效地追蹤會話中的上下文,因此難以解決這類問題。在這種情況下,我們透過全域性引數傳遞的方式,能夠跨越多次互動,確保在每個步驟中都能訪問到最新的上下文資訊。
讓我們簡單解釋一下這段程式碼的流程:
- 獲取當前會話中的使用者提問:我們從當前會話中獲取最近一次使用者提出的問題,確保我們不會誤取到其他會話的上下文。
- 將問題提交給大模型生成SQL:我們將使用者的提問傳遞給大模型,利用其能力幫助我們生成適當的SQL查詢語句。
- 判斷是否為查詢請求:我們檢查生成的SQL語句是否屬於查詢操作。如果是查詢,則執行查詢並將查詢結果返回給使用者。
- 其他操作的處理:如果不是查詢請求,則說明使用者發出的指令可能是更新或執行類的操作,在這種情況下,我們返回一個"執行成功"的響應。
可以看到,提示詞中的 tableinfo
是我硬編碼寫死的。實際上,我們完全可以將其設計成一個可傳入的引數,這樣不僅提升了外掛的靈活性和可複用性,而且使得該外掛不再僅僅侷限於待辦事項的使用場景,而能夠作為一個通用的資料庫操作外掛,適應不同的需求和應用場景。
為了能夠在演示過程中展示效果,目前我將某些部分做了臨時的硬編碼處理。這只是為了給大家提供一個初步的思路和參考框架,後續我會逐步完善這些功能。
接下來,讓我們一起來看看除錯過程中得到的效果。以下是我在除錯時的截圖:
接下來,我們將檢查資料庫是否已經成功儲存並正常更新了資料。
接下來,我將展示查詢的實際效果,同時生成的 SQL 語句也相當優秀,能夠高效地滿足查詢需求。
可以看到,此處已成功將資料或結果正常返回給前端,系統執行狀態良好。
總結
在本文中,我們深入探討了如何利用 Spring AI 的新功能,特別是全域性引數和增強函式呼叫能力,來構建一個智慧化的個人助理系統。透過這個系統,我們實現了基礎的增、刪、改、查(CRUD)功能,特別聚焦在資料庫互動與待辦事項管理上。我們展示瞭如何將 Spring AI 整合到實際業務流程中,透過模型生成 SQL 查詢語句,提升資料庫操作的自動化程度和靈活性。
首先,我們介紹了 Spring AI 在功能更新後如何簡化和擴充套件業務邏輯處理,特別是在處理多輪對話、使用者歷史資料以及複雜資料庫操作時的優勢。透過全域性引數,系統能夠更精準地捕捉使用者需求,並在資料庫層面執行相關操作,真正實現了智慧化的互動和自動化的業務流程。
我們還設計了一個待辦事項管理功能,其中透過精心設計的提示詞和模型最佳化,使得待辦功能的增刪改查操作更加高效與準確。
總結來說,這次基於 Spring AI 的系統最佳化,不僅為我們提供了一個強大的智慧助手框架,也為實際業務中的智慧化系統提供了可借鑑的方案。未來,我們可以基於此進一步擴充套件功能,打造更加智慧化和個性化的業務解決方案。
我是努力的小雨,一名 Java 服務端碼農,潛心研究著 AI 技術的奧秘。我熱愛技術交流與分享,對開源社群充滿熱情。同時也是一位騰訊雲創作之星、阿里雲專家博主、華為云云享專家、掘金優秀作者。
💡 我將不吝分享我在技術道路上的個人探索與經驗,希望能為你的學習與成長帶來一些啟發與幫助。
🌟 歡迎關注努力的小雨!🌟