大模型能理解自然語言,從而能解決問題,但是就像人類大腦一樣,大腦只能發送指令,實際行動得靠四肢,所以LangChain4j提供的Tools機制就是大模型的四肢。
大模型的不足
大模型在解決問題時,是基于互聯網上很多歷史資料進行預測的,而且答案具有一定的隨機性,那如果我問"今天是幾月幾號?",大模型是大概率答錯的,因為大模型肯定還沒有來得及學習今天所產生的最新資料。
比如:
package com.timi;import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;public class _04_Toos {public static void main(String[] args) {ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://langchain4j.dev/demo/openai/v1").apiKey("demo").build();System.out.println(model.generate("今天是幾月幾號?"));}
}
代碼執行結果為:
今天是十二月十九號。
多執行幾次,每次執行結果很有可能不一樣,所以如果要求大模型處理時間相關的問題,它就無能為力了。
因此,我們擴展一下,出現這種情況的原因是ChatGPT是基于歷史數據來進行預測的,它沒辦法拿到當前最新的數據,比如說時間,從而限制了它的進一步使用,那么LangChain4j的Tools機制就能夠幫助大模型來獲取當前最新的數據,從而解決上述時間相關的問題。
由于LangChain4j提供的"demo"不支持Tools機制,需要大家自行獲取OpenAI的ApiKey,或者找一些OpenAI的代理來間接的調用OpenAI的API。
ToolSpecification
首先需要定義一個工具,其實就是一個方法,用來返回當前日期,并且通過@Tool注解來描述該工具,從而使得大模型在需要獲取當前時間時能夠調用該工具方法得到當前時間:
@Tool("獲取當前日期")
public static String dateUtil(){return LocalDateTime.now().toString();
}
然后將工具方法轉成ToolSpecification對象,并傳遞給大模型:
package com.timi;import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.agent.tool.ToolSpecifications;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.output.Response;import java.time.LocalDateTime;
import java.util.Collections;public class _04_Tools {@Tool("獲取當前日期")public static String dateUtil(){return LocalDateTime.now().toString();}public static void main(String[] args) throws NoSuchMethodException {ChatLanguageModel model = OpenAiChatModel.builder().baseUrl("http://localhost:3000/v1").apiKey("sk-peszVtFXoLnWK45bB15370Df6f344cAa9a088eF50f9c7302").build();ToolSpecification toolSpecification = ToolSpecifications.toolSpecificationFrom(_04_Tools.class.getMethod("dateUtil"));UserMessage userMessage = UserMessage.from("今天是幾月幾號?");Response<AiMessage> response = model.generate(Collections.singletonList(userMessage), toolSpecification);System.out.println(response.content());}
}
所以,一個ToolSpecification對象就代表一個工具,當用戶把要問題UserMessage和工具ToolSpecification一起傳遞給大模型,大模型就知道要結合工具描述來解決用戶的問題,此時大模型響應的AiMessage不再是一串文本,而是:
AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = "call_IPiiRjIM5PmVdDWjpXcUN5c7", name = "dateUtil", arguments = "{}" }] }
一個ToolExecutionRequest,表示一個工具執行請求,表示大模型在解決問題時,需要調用工具來解決用戶的問題,由于我們可能傳了多個工具給大模型,所以toolExecutionRequests是一個List,表示為了解決用戶的問題需要調用哪些工具。
所以,我們在得到了ToolExecutionRequest后,就需要取執行對應的工具方法了,其中ToolExecutionRequest的name屬性就是方法名,arguments就表示要傳遞給方法的參數值:
Response<AiMessage> response = model.generate(Collections.singletonList(userMessage), toolSpecification);AiMessage aiMessage = response.content();
if (aiMessage.hasToolExecutionRequests()) {for (ToolExecutionRequest toolExecutionRequest : aiMessage.toolExecutionRequests()) {String methodName = toolExecutionRequest.name();Method method = _04_Tools.class.getMethod(methodName);// result就是當前時間String result = (String) method.invoke(null);System.out.println(result);}
}
此時的輸出結果為:
2024-03-24T11:37:02.618942
這就是大模型想要的當前時間,相當于是ToolExecutionRequest的響應結果,那我們該如何把這個響應結果告訴給大模型,從而讓大模型告訴我“今天是幾月幾號?”呢?
前面在介紹ChatMessage類型時,除開有UserMessage、AiMessage、SystemMessage之外,還有一種類型就是ToolExecutionResultMessage,因此ToolExecutionResultMessage就表示工具執行結果,所以我們把工具的執行結果封裝為ToolExecutionResultMessage即可:
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest.id(), toolExecutionRequest.name(), result);
然后使用歷史對話的思想,把以上用戶和大模型之間涉及到的ChatMessage按順序添加到List中發送給大模型即可:
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest.id(), toolExecutionRequest.name(), result);AiMessage message = model.generate(Lists.newArrayList(userMessage, aiMessage, toolExecutionResultMessage)).content();
System.out.println(message.text());
這樣大模型就能正確的告訴當前時間了:
今天是2024年3月24日。
AiServices整合Tools
以上使用Tools的方式有點復雜,如果大模型要解決一個復雜問題需要調用多個工具或多輪工具調用,以上代碼就更不合適了,而AiServices能簡化這個過程。
假如有這么一個需求:獲取今天注冊的所有新用戶信息,對于這個需求我們可以這么來實現。
首先定義一個User對象:
static class User {private String username;private Integer age;public User(String username, Integer age) {this.username = username;this.age = age;}
}
然后定義兩個Tools:
static class MyTools {@Tool("獲取當前日期")public static String dateUtil(String onUse) {return LocalDateTime.now().toString();}@Tool("獲取指定日期注冊的用戶信息")public static List<User> getUserInfo(String date) {System.out.println("接收到的date參數的值:" + date);User user1 = new User("司馬懿", 18);User user2 = new User("曹操", 18);return Lists.newArrayList(user1, user2);}
}
一個用來獲取當前時間,一個接收當前時間并返回用戶信息。
再定義一個UserService接口:
interface UserService {@SystemMessage("先獲取具體日期,然后再解決用戶問題")String getUserInfo(String desc);
}
然后利用AiServices創建UserService接口的代理對象:
public static void main(String[] args) {ChatLanguageModel model = ZhipuAiChatModel.builder().apiKey("0f4d2b0e8d95f48e6e1f138b881d0a53.UkIov25cJBSvjFDo").build();UserService userService = AiServices.builder(UserService.class).chatLanguageModel(model).tools(new MyTools()).chatMemory(MessageWindowChatMemory.withMaxMessages(10)).build();String userInfo = userService.getUserInfo("獲取今天的注冊的新用戶信息");System.out.println(userInfo);}
并執行getUserInfo()方法,傳入你的描述信息就可以獲取到User信息了。比如以上代碼的執行結果為:
接收到的date參數的值:2024-04-21
2024年4月20日注冊的用戶有司馬懿和曹操,他們的年齡都是18歲。
源碼分析
在代理對象的invoke()方法中,以下代碼會去調用大模型的底層API:
Response<AiMessage> response = context.toolSpecifications == null? context.chatModel.generate(messages): context.chatModel.generate(messages, context.toolSpecifications);
當指定了Tools時,就會調用context.chatModel.generate(messages, context.toolSpecifications)
,我們debug來看下返回結果:
第一次響應是一個ToolExecutionRequest工具執行請求,name為"now",表示要執行now()方法,也就是獲取當前時間,然后會執行如下代碼:
for (ToolExecutionRequest toolExecutionRequest : aiMessage.toolExecutionRequests()) {// 執行工具ToolExecutor toolExecutor = context.toolExecutors.get(toolExecutionRequest.name());// 工具執行結果String toolExecutionResult = toolExecutor.execute(toolExecutionRequest, memoryId);// 把工具執行請求和結果封裝為ToolExecutionResultMessageToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest,toolExecutionResult);// 添加到ChatMemory中chatMemory.add(toolExecutionResultMessage);
}
然后執行以下代碼,再次請求大模型,此時ChatMemory中包含了第一次工具請求的結果:
response = context.chatModel.generate(chatMemory.messages(), context.toolSpecifications);
這一次得到的響應是:
仍然是一個工具執行請求,只不過方法時getUserInfo()方法,并且入參為上一步工具調用的結果,然后和上面類似,處理該工具執行請求,也就是執行getUserInfo()方法得到工具執行結果,同樣再次把第二次的工具執行請求和結果封裝為ToolExecutionResultMessage,并添加到ChatMemory中,此時ChatMemory中的內容為:
依次為:用戶的問題、第一次工具執行請求和結果、第二次工具執行請求和結果。
把最終的ChatMemory發送給大模型之后,大模型就知道了今天注冊的新用戶信息有哪些了,就會把結果返回給你:
基于此,我們其實打通了大模型和我們系統內部數據之間的橋梁,使得大模型能夠調用我們提供的工具來獲取系統內部的最新數據,而我們可以更進一步讓大模型基于這些數據來做更智能的事情,比如:
需求改為:“獲取今天注冊的新用戶信息,然后基于這些用戶發送一份郵件”,我們只需要再定義一個發送郵件Tool就可以了:
@Tool("給指定用戶發送郵件")
public void email(String user) {System.out.println("發送郵件:" + user);
}
然后:
List<User> users = userService.getUserInfo("獲取今天注冊的新用戶信息,然后基于這些用戶發送一份郵件");
代碼執行結果為:
接收到的date參數的值:2024-04-21
發送郵件:司馬懿,曹操
今天注冊的用戶有司馬懿和曹操,已經給他們發送了一份郵件。
通過這個Demo,我們發現,我們可以利用自然語言來整合各項系統功能,這將是一種新的編程模式:自然語言編程。
本節總結
本節我們學習了LangChain4j中的Tools機制,通過Tools機制可以通過自然語言整合大模型和系統內部功能,使得大模型這個智能大腦擁有了靈活的四肢,從而可以處理更復雜的場景,同時也感受到了自然語言編程離我們越來越近了,下一節我們將學習文本向量化以及向量模型、向量數據庫,這是檢索增強生成(RAG)的基礎。