背景
經過前面三篇的內容,我想大家對于大模型的構建、Langchain的優勢、Chain的構建有了相當程度的理解(雖然只是最簡單的示例,但是足夠有代表性)。
后續Chain的使用將會更加豐富多彩,您會了解Langchain開發的大模型會有多么逆天的可擴展性。但今天我們先暫緩此部分,我們來講講Langchain里面最最最重要的功能:工具調用!
我們會先從Langchain規定的官方API開始構建,帶著大家跑通一系列工具之后,從底層原理出發,為大家講解大模型是怎么調用工具的(不必擔心,非常淺顯易懂,本欄目始終是新手向的)。
工具說明
在Langchain眼中,所謂的工具都只是函數而已,我們要做的就是把函數寫好,并交給大模型去自主的調用。
Langchain給大模型調用的函數專門設定了一個函數裝飾器: @tool
有關于函數裝飾器,作為新手其實沒必要理解太深刻,只需要理解裝飾器給函數添加了一些功能和變量即可。而這個tool裝飾器僅僅是給函數增加了幾個變量而已,如下:
- 工具名稱:(tool.name)
- 該工具是什么的描述(tool.description)
- 輸入內容的 JSON 格式 (tool.args)
- 工具的結果是否應直接返回給用戶(tool.return_direct)
@tool def func(input:int):'''沒用的函數'''return input print(func.name) # 輸出:funcprint(func.description) # 輸出:沒用的函數print(func.args) # 輸出: {'input': {'title': 'Input', 'type': 'int'}}print(func.return_direct) # 輸出:false
在這個裝飾器中最主要的必須知曉的就是tool.name和tool.?description。這個是后續工具調用的基礎。
-
tool.name
若無特殊設定,默認為函數名。作為新手向,該變量就不要去有額外的操作。只需要知道 tool.name == 函數名 即可。(操作更多也不會有額外的效果,還增加理解難度)
-
tool.description
若無特殊設定,默認為函數的文檔字符串(即函數下方的函數說明),有關文檔字符串的內容可以參考下面的博客,簡單清晰。該部分作為小白直接利用該部分特性即可。有更高要求的看客可以查閱有關BaseTool類的相關知識。
Python 文檔字符串(DocStrings)是個啥??-CSDN博客
大模型的工具調用(直接使用API)
在之前的項目中我們編寫了有關基礎大模型的相關內容,開發了第一個問答大模型以及嘗試了LLMchain的相關內容,我們將在此基礎上繼續往前!
LangChain(二)基礎問答大模型,純新手向-CSDN博客
LangChain(三)基礎問答大模型,從LLMchain開始了解chain!純新手向-CSDN博客
其實不看也可以啦,看了理解起來會更快而已……
step1:工具定義!
該部分我們先定義需要的工具,代碼如下。@tool裝飾器說明這是一個工具。工具名稱為“multiply”,工具描述為“Multiply two integers together.”。
from langchain_core.tools import tool@tool
def multiply(first_int: int, second_int: int) -> int:"""Multiply two integers together."""return first_int * second_int
?這部分沒啥難度,其實就是你自己設定一個函數,然后前面加上@tool,函數內部首行用""" """ 定義一下函數功能描述即可。
step2:大模型定義
該部分我們定義大模型,詳細內容可以參考我之前的博客內容哦。使用百度的千帆大模型
import os
from langchain_community.chat_models import QianfanChatEndpoint# 設定百度千帆大模型的AK和SK-去百度千帆官網的控制臺新建一個應用即可
os.environ["QIANFAN_AK"] = "your AK“"
os.environ["QIANFAN_SK"] = "your SK"#創建千帆LLM模型
qianfan_chat = QianfanChatEndpoint(model="ERNIE-3.5-8K",temperature=0.2,timeout=30,
)
這部分依舊沒啥難度,按部就班走即可。?
?step3:工具設定與綁定!
該部分我們進行工具的設定和與大模型進行綁定!
tools = [multiply]
llm_with_tools = qianfan_chat.bind_tools(tools)
tool_map = {tool.name: tool for tool in tools}
該部分必須要好好解釋一下。不然大家初看之下可能會一頭霧水。
tools = [multiply]
????????由于在實際開發過程中,不可能只有一個工具,我們常常會調用多個工具,那么和大模型進行綁定難道要每個工具函數都綁定一次嗎?咋可能對不對。這部分就是把所有需要調用的函數打造成一個列表,列表內保存的是各個函數(不是函數名!函數名是string,函數就是函數,本質上是個對象,這里理解不了跳過即可,我還記得這是個新手向的博客~~~)。
llm_with_tools = qianfan_chat.bind_tools(tools)
? ? ? ? 這一行是把大模型和工具進行一個綁定,構建一個工具選擇模塊(一個 agent)。大模型就是通過該模塊進行的工具選擇,具體的原理在下一篇博客會詳細講解,此部分我們先暫緩跳過~。
tool_map = {tool.name: tool for tool in tools}
? ? ? ? 這一行是把函數名稱(string)和函數(對象)作為一個字典保存。
????????key:函數名稱,value:函數
? ? ? ? 這個變量大家先留意一下,現在可能看不出用途,后面就有用了。
step4:實際運行!
接下來我們把后面的代碼一次性和盤托出!?
def call_tools(msg: AIMessage) -> Runnable:"""Simple sequential tool calling helper."""tool_calls = msg.tool_calls.copy()for tool_call in tool_calls:tool_call["output"] = tool_map[tool_call["name"]].invoke(tool_call["args"])return tool_callschain = llm_with_tools | call_toolschain.invoke("What's 25 times 11?"
)
I know,I know,突然信息量就上來了對不對。沒事,我們一個一個來! 我們先跳過call_tools的函數定義,我們先看下面:
- chain = llm_with_tools | call_tools
????????對于chain還不理解的同學可以先看我之前的博客,鏈接在上面!看了我之前博客的同學想必依舊有疑惑,我們只是使用過LLMchain,怎么就變成這樣了?
????????實際上Langchain確實有很多已經定義好的chain,只需要調用即可,但是在實際開發中,最實用的依舊是自己定義的chain,個性化的定義才能滿足個性化的需求嘛。
????????Langchain官方自然有可以讓我們自己個性化定義chain的方式。該處就是一個典型。
????????該處的chain是如何工作的呢?作為小白我們不需要去理解源碼。從高維去俯瞰它。步驟如下:
- 用戶輸入給到?llm_with_tools(該部分有大模型)
- llm_with_tools?獲取用戶輸入和函數名稱與描述,大模型進行處理并返回需要的函數名和對應的輸入變量,記為“AIMessage”(這就是上面call_tools的參數哦~)。
- call_tools獲取上一個步驟輸出的參數,并幫助大模型調用對應的函數,并返回結果。
llm_with_tools 的實際輸出!
我們運行下面的代碼:
query = "25 * 11 = ?"messages = [HumanMessage(query)]print("messages1 = ", messages)ai_msg = llm_with_tools.invoke(messages)print("ai_msg = ", ai_msg)
可得輸出如下(手動標準格式了下):
messages1 = [HumanMessage(content='25 * 11 = ?')]
ai_msg =
content=''
additional_kwargs={'finish_reason': 'function_call', 'request_id': 'as-y3xqr3j5b5', 'object': 'chat.completion', 'search_info': [], 'function_call': {'name': 'Multiply', 'arguments': '{"a":25,"b":11}'}, 'tool_calls': [{'type': 'function', 'function': {'name': 'Multiply', 'arguments': '{"a":25,"b":11}'}, 'id': '07eeb8f7-56b0-42f0-b828-4c7d4b17c850'}]}
response_metadata={'token_usage': {'prompt_tokens': 51, 'completion_tokens': 24, 'total_tokens': 75}, 'model_name': 'ERNIE-3.5-8K', 'finish_reason': 'function_call', 'id': 'as-y3xqr3j5b5', 'object': 'chat.completion', 'created': 1720421110, 'result': '', 'is_truncated': False, 'need_clear_history': False, 'function_call': {'name': 'Multiply', 'thoughts': '用戶需要進行乘法運算,我可以使用工具Multiply來完成這個任務。', 'arguments': '{"a":25,"b":11}'}, 'usage': {'prompt_tokens': 51, 'completion_tokens': 24, 'total_tokens': 75}}
id='run-082b9676-4902-4bf6-af1b-545f4a095001-0'
tool_calls=[{'name': 'Multiply', 'args': {'a': 25, 'b': 11}, 'id': '07eeb8f7-56b0-42f0-b828-4c7d4b17c850'}]
大家先別慌!別著急!重點其實很少~。聽我細細說來。
第一行的 messages1中的HumanMessage,僅僅只是告訴大模型這是用戶發出的信息而已,至少在現在這個階段不是重點,不用管他!
最主要的是下面的ai_msg,有三個重要模塊
- “additional_kwargs”:額外信息,對新手沒啥用
- “response_metadata”:正式的響應信息,一堆沒啥用的信息之外,thoughts該字段反映了大模型是如何思考的。并且格式化返回了需要調用的相關函數名稱(string)和函數的參數。
- “tool_calls”:最重要的信息,單獨提出來單純只是降低層級而已,你可以看到上面幾個字段都有一樣的信息。
總而言之,在本篇工具調用欄目看來,最重要的就是tool_calls字段,其他直接忽略。函數選擇器的詳細原理將會放置下一篇博客詳細講解,本文僅說Langchain的API調用的步驟和思路。畢竟是新手向嘛~
綜上 函數選擇器 的功能輸出正式講解完畢,其實大家只需要知道函數選擇器就是用來選擇函數的,最重要的功能就是輸出的tool_calls字段,其中保存大模型想要調用的函數名稱(string)和對應的參數。
call_tools函數的原理和操作!
以防大家往上翻太煩,再粘貼一次。這個函數的主要作用就是獲取AI想要調用的工具,并幫AI調用該工具。
def call_tools(msg: AIMessage) -> Runnable:"""Simple sequential tool calling helper."""tool_calls = msg.tool_calls.copy()for tool_call in tool_calls:tool_call["output"] = tool_map[tool_call["name"]].invoke(tool_call["args"])return tool_calls
該部分的輸入參數就是上一步函數選擇器的輸出:AImessage(不知道大家有沒有注意到,這和HumanMessage正好是對應關系,其實就是一個是用戶的信息,一個是AI的信息而已,僅僅是對信息做一個標識,其實沒啥用)?
后面的 --> Runnable 請忽略,新手直接跳過即可,想了解可以自行了解。?接下來讓我們分行說明!
-
tool_calls = msg.tool_calls.copy()
????????養成好習慣,直接copy,解耦互不影響,尤其在流式場景下。
-
for tool_call in tool_calls
? ? ? ? 因為AI可能需要調用多個函數,所以對每一個AI想要調用的函數都需要處理。我不知道為什么我要解釋這個……
-
tool_call["output"] = tool_map[tool_call["name"]].invoke(tool_call["args"])
????????最重要的是這一行,不知道大家還記不記得?tool_map?這個變量,在上文提過,截圖如下。這一行我們慢慢來,對于當前需要調用的tool_call,有一個字段“name”保存著需要調用函數的函數名稱。用tool_map訪問該名稱,返回該函數名稱(string)對應的函數(對象)!這下大家終于理解為什么我需要強調這多次了吧~。即:
- tool_map[tool_call["name"]] == multiply
- tool_call["args"]?==?{'a': 25, 'b': 11}
這一行 == multiply.invoke({'a': 25, 'b': 11}) ==?multiply('a': 25, 'b': 11) == 275,此時tool_call多了一個字段“output”,value = 275
到此就結束了,我們終于實現了大模型調用工具的基礎操作。大家安心,上面代碼好像很多,好像很復雜,我們最后復習一下,看一下全部的代碼,你會發現沒什么難的其實。
import os
from langchain_community.chat_models import QianfanChatEndpoint
from langchain_core.tools import tool
from langchain_core.messages import AIMessage
from langchain_core.runnables import Runnable# 設定百度千帆大模型的AK和SK
os.environ["QIANFAN_AK"] = " your AK"
os.environ["QIANFAN_SK"] = " your SK"# 定義千帆大模型
qianfan_chat = QianfanChatEndpoint(model="ERNIE-3.5-8K",temperature=0.2,timeout=30,
)# 設定兩個函數,記得描述函數功能,這很重要
@tool
def func1():''' useless function '''return 0@tool
def Multiply(a: int, b: int) -> int:"""Multiplies a and b."""return a * b# 工具集合
tools = [Multiply, func1]
# 工具與大模型綁定,構建函數選擇模塊
llm_with_tools = qianfan_chat.bind_tools(tools)
# 構建一一對應的map
tool_map = {tool.name: tool for tool in tools}# 工具函數執行
def call_tools(msg: AIMessage) -> Runnable:"""Simple sequential tool calling helper."""tool_calls = msg.tool_calls.copy()for tool_call in tool_calls:tool_call["output"] = tool_map[tool_call["name"]].invoke(tool_call["args"])return tool_calls# 構建鏈
chain = llm_with_tools | call_toolsprint(chain.invoke("What's 25 times 11?")[0]["output"])
?輸出:275
總結
有一說一,工具調用會了,世界上還有什么功能實現不了?
但是本篇博客是從API的角度出發為大家構建一個工具調用的操作,下一篇博客我們將從原理出發,直接手擼工具調用!放寬心,依舊是新手向~
由于小博主依舊是個卑微的打工人,只能上班摸魚的時候寫寫博客,后續的博客將保持一周一篇的頻率~ 敬請期待~