前言
如何你看过我之前的两篇关于工具的创建,如何让大模型使用使用工具的话,你应该会有一个疑问,根据输出看起来模型真的调用了我们的定义的工具。但其实模型给我们的一个 ToolMessage 类型的消息。也就是说虽然模型知道要选择使用那个工具了。但是它是不会去真正调用的。因为模型本身是没有执行的这些工具的环境的。它只会生成这些工具的所需要的参数。如果我们想真正执行工具的话就需要我们手动解析ToolMessage内容。然后调用相应的工具。
这里举个例子给你演示工具是如何调用的:
from langchain_core.tools import tool
from langchain_deepseek import ChatDeepSeek
llm = ChatDeepSeek(
model="deepseek-reasoner",
temperature=0,
max_tokens=None,
timeout=None,
max_retries=2,
api_key="You api key",
# other params...
)
@tool
def add(a: int, b: int) -> int:
"""两个整数相加.
参数:
a: 第一个数 integer
b: 第二个数 integer
"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""两个整数相乘.
参数:
a: 第一个数 integer
b: 第二个数 integer
"""
return a * b
tools = [multiply,add]
llm_with_tools = llm.bind_tools(tools)
result = llm_with_tools.invoke("3乘2等于多少?")
print(result.content)
print(result.tool_calls)
for tool_call in result.tool_calls:
selected_tool = {"add": add, "multiply": multiply}[tool_call["name"].lower()]
tool_msg = selected_tool.invoke(tool_call)
print(tool_msg)
示例输出如下:
我将计算3乘2的结果。
[{'name': 'multiply', 'args': {'a': 3, 'b': 2}, 'id': 'call_0_3061be84-b91c-4b53-96b3-203c17e9a33a', 'type': 'tool_call'}]
content='6' name='multiply' tool_call_id='call_0_3061be84-b91c-4b53-96b3-203c17e9a33a'
从结果我们可以看到,content 输出了模型给的消息。它并没有计算 3X2 这个算式。而是在 tool_calls 中给到了它想调用 multiply
这个工具,也就是第二行的输出结果。要真正调用的我们工具的话我们通过遍历 tool_calls
通过选中的工具调用它的 invoke 方法现真正的调用我们的工具也就是你看到的第三行输出的结果。它是 ToolMessage
类型。 好了到这里恭喜你完成了你的第一个工具从定义,模型决定使用那个工具后在到真正的调用,成功的让模型有了能与外界交互的能力。这是我们光能实现调用还不行。我们还需要需要把工具输出的内容返回给模型。要如何做的呢。原理如下:
我们来对刚才的示例做如下改造,增加 messages 历史消息来存储消息列表。
from langchain_core.tools import tool
from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import HumanMessage
llm = ChatDeepSeek(
model="deepseek-reasoner",
temperature=0,
max_tokens=None,
timeout=None,
max_retries=2,
api_key="Your api key",
# other params...
)
@tool
def add(a: int, b: int) -> int:
"""两个整数相加.
参数:
a: 第一个数 integer
b: 第二个数 integer
"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""两个整数相乘.
参数:
a: 第一个数 integer
b: 第二个数 integer
"""
return a * b
tools = [multiply,add]
llm_with_tools = llm.bind_tools(tools)
messages = [HumanMessage(content="3乘2等于多少?11加4等于多少?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
for tool_call in ai_msg.tool_calls:
selected_tool = {"add": add, "multiply": multiply}[tool_call["name"].lower()]
tool_msg = selected_tool.invoke(tool_call)
messages.append(tool_msg)
res = llm_with_tools.invoke(messages)
print(messages)
print(res.content)
[HumanMessage(content='3乘2等于多少?11加4等于多少?', additional_kwargs={}, response_metadata={}), AIMessage(content='我将分别计算这两个问题:\n\n1. **3乘2等于多少?** \n 调用乘法函数:`multiply(a=3, b=2)` \n2. **11加4等于多少?** \n 调用加法函数:`add(a=11, b=4)` \n\n请稍等,我来计算一下。', additional_kwargs={'tool_calls': [{'id': 'call_0_b27c1951-22a9-4480-889b-ea547096d522', 'function': {'arguments': '{"a": 3, "b": 2}', 'name': 'multiply'}, 'type': 'function', 'index': 0}, {'id': 'call_1_850b9583-930c-488c-a99d-a6c8bc1b287a', 'function': {'arguments': '{"a": 11, "b": 4}', 'name': 'add'}, 'type': 'function', 'index': 1}], 'refusal': None, 'reasoning_content': '我们有两个问题:第一个是3乘2,第二个是11加4。\n 我们可以分别调用函数来解决。\n 首先,对于3乘2,我们可以调用multiply函数,参数a=3, b=2。\n 然后,对于11加4,我们可以调用add函数,参数a=11, b=4。\n 注意:由于问题有两个,我们需要依次调用两个函数。\n 但是,注意函数调用的格式要求,我们需要分别调用,并且返回两个结果。\n 由于我们有两个独立的问题,我们可以分别执行两个函数调用。\n 但是,注意:我们一次只能返回一个函数调用。然而,我们可以通过多次交互来完成。但是,根据问题,我们可能需要在一个回答中完成两个计算。\n 然而,我们的函数调用机制允许我们在一次回答中调用多个函数(通过多个tool_calls)。但是,注意:我们返回的格式是一个数组,可以包含多个工具调用。\n 因此,我们可以返回两个工具调用:一个用于乘法,一个用于加法。\n 但是,注意:问题要求我们回答两个问题,所以我们需要返回两个结果。我们可以通过依次调用两个函数,然后得到两个结果,然后分别展示。\n 因此,我们生成两个函数调用请求。\n 但是,注意:我们返回的格式是一个数组,包含两个工具调用(tool_calls),每个工具调用对应一个函数。\n 然后,系统会执行这两个函数,并返回两个结果。然后我们可以将两个结果组合成答案。\n\n 然而,在当前的函数调用机制中,我们通常一次只调用一个函数。但为了效率,我们可以同时调用两个函数(如果模型支持的话)。但是,根据OpenAI的API,一个请求中可以包含多个工具调用(在同一个消息中,以数组形式)。\n\n 但是,我们现在的设定是:我们作为助手,可以返回多个工具调用吗?根据OpenAI的文档,在一个assistant消息中,可以包含多个tool_calls(在同一个消息中)。\n\n 因此,我们可以这样设计:\n 返回一个消息,其中包含两个tool_calls,分别调用multiply和add。\n\n 但是,注意:这两个函数调用是独立的,没有依赖关系,所以可以同时调用。\n\n 所以,我们返回:\n {\n "tool_calls": [\n {\n "id": "call_1", // 需要生成一个唯一ID,但在这里我们不需要实际生成,因为示例中我们可以用占位符,实际系统会处理\n "type": "function",\n "function": {\n "name": "multiply",\n "arguments": {"a": 3, "b": 2}\n }\n },\n {\n "id": "call_2",\n "type": "function",\n "function": {\n "name": "add",\n "arguments": {"a": 11, "b": 4}\n }\n }\n ]\n }\n\n 然后,系统会执行这两个函数,并返回两个结果。我们再将两个结果组合成答案。\n\n 但是,在实际的代码执行中,我们可能不会在一个步骤中完成两个调用。不过,为了简化,我们可以假设系统支持并行调用。\n\n 然而,在当前的提示中,我们要求以JSON格式返回函数调用。但是,注意我们的函数调用格式要求,我们只能返回一个函数调用?实际上,根据OpenAI的API,一个消息中可以包含多个工具调用。\n\n 但是,在函数调用的定义中,我们通常返回一个函数调用。不过,我们可以扩展为多个。\n\n 由于我们的示例中,我们定义了两个函数,并且问题有两个独立的部分,我们可以返回两个函数调用。\n\n 因此,我们决定返回两个函数调用。\n\n 但是,注意:我们返回的格式必须符合要求。在OpenAI的API中,工具调用的消息中有一个tool_calls数组。\n\n 因此,我们可以这样返回(在模拟环境中,我们可以这样构造):\n [\n {\n "name": "multiply",\n "arguments": {\n "a": 3,\n "b": 2\n }\n },\n {\n "name": "add",\n "arguments": {\n "a": 11,\n "b": 4\n }\n }\n ]\n\n 但是,我们之前定义的函数调用返回格式是单个函数调用。为了支持多个,我们需要调整。\n\n 不过,根据题目要求,我们只需要按照函数调用的格式返回即可。实际上,在OpenAI的API中,我们返回的是包含tool_calls的消息。但是,在我们的模拟中,我们可以返回一个包含多个函数调用的数组。\n\n 但是,请注意:我们之前给出的函数定义中,每个函数调用都是独立的。因此,我们可以返回一个列表,包含两个函数调用。\n\n 因此,我们返回一个列表,包含两个字典,每个字典包含函数名和参数。\n\n 但是,注意:在OpenAI的API中,每个工具调用都有一个唯一的id,但在我们的模拟中,我们可以省略,因为执行顺序不会乱,且结果会按顺序返回。\n\n 所以,我们这样返回:\n\n [\n {\n "name": "multiply",\n "arguments": {\n "a": 3,\n "b": 2\n }\n },\n {\n "name": "add",\n "arguments": {\n "a": 11,\n "b": 4\n }\n }\n ]\n\n 然后,系统会执行这两个函数,并将两个结果返回给我们(两个结果会按顺序放在一个列表中)。\n\n 然后,我们根据两个结果来生成回答。\n\n 但是,我们也可以分两次调用:先调用乘法,得到结果后再调用加法。不过,由于两个问题独立,我们可以同时调用。\n\n 因此,我们选择同时调用两个函数。\n\n 步骤:\n 1. 调用multiply(3,2) -> 得到6\n 2. 调用add(11,4) -> 得到15\n\n 然后,我们回答:\n “3乘2等于6,11加4等于15。”\n\n 所以,我们返回两个函数调用。'}, response_metadata={'token_usage': {'completion_tokens': 1370, 'prompt_tokens': 262, 'total_tokens': 1632, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 1251, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 256}, 'prompt_cache_hit_tokens': 256, 'prompt_cache_miss_tokens': 6}, 'model_name': 'deepseek-reasoner', 'system_fingerprint': 'fp_393bca965e_prod0623_fp8_kvcache', 'id': 'b435e6a8-6074-486b-b916-8499a96520c8', 'service_tier': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run--54138050-557e-47b8-9dd0-aae522808960-0', tool_calls=[{'name': 'multiply', 'args': {'a': 3, 'b': 2}, 'id': 'call_0_b27c1951-22a9-4480-889b-ea547096d522', 'type': 'tool_call'}, {'name': 'add', 'args': {'a': 11, 'b': 4}, 'id': 'call_1_850b9583-930c-488c-a99d-a6c8bc1b287a', 'type': 'tool_call'}], usage_metadata={'input_tokens': 262, 'output_tokens': 1370, 'total_tokens': 1632, 'input_token_details': {'cache_read': 256}, 'output_token_details': {'reasoning': 1251}}), ToolMessage(content='6', name='multiply', tool_call_id='call_0_b27c1951-22a9-4480-889b-ea547096d522'), ToolMessage(content='15', name='add', tool_call_id='call_1_850b9583-930c-488c-a99d-a6c8bc1b287a')]
3乘2等于**6**。
11加4等于**15**。
从消息中分析,我们总共经历了如下步骤: 1、给模型发送第一条消息告诉它 “3乘2等于多少?11加4等于多少?”。
2、模型返回了它决定使用那个工具来实现解决相关问题。
3、根据模型返回的 tool_calls
我调用分别调用本地工具,并得到一个 ToolMessage
类型的消息并放到历史消息中。
4、最后把整个消息通完 res = llm_with_tools.invoke(messages)
,返回给模型。模型最终输出来自然语言的结果:
3乘2等于**6**。
11加4等于**15**。
通过这个例子,我们已经学会了怎么把工具的输出返回给模型。这里有个需要注意的地方就是每个 ToolMessage
必须包含一个 tool_call_id
,该 tool_call_id
需要与模型生成的原始工具调用中的 id
匹配。这有助于模型将工具响应与工具调用进行匹配。