背景
经过前面三篇的内容,我想大家对于大模型的构建、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) # 输出:func print(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_calls
chain = llm_with_tools | call_tools
chain.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_tools
print(chain.invoke("What's 25 times 11?")[0]["output"])
输出:275
总结
有一说一,工具调用会了,世界上还有什么功能实现不了?
但是本篇博客是从API的角度出发为大家构建一个工具调用的操作,下一篇博客我们将从原理出发,直接手撸工具调用!放宽心,依旧是新手向~
由于小博主依旧是个卑微的打工人,只能上班摸鱼的时候写写博客,后续的博客将保持一周一篇的频率~ 敬请期待~