13. 将工具连接到 LLM
在第 12 章中,我们构建了代理(agent)可以使用的工具(tool)。现在是时候将这些工具连接到 LLM,让代理能够真正使用它们了。
无论工具构建得多么完善,如果 LLM 不知道它的存在,就无法使用它。LLM 需要知道有哪些可用工具,并为每个请求挑选合适的工具。然而,正如我们在第 12 章中学到的,LLM 并不会直接执行工具。相反,它会请求"用这些参数调用这个工具",然后由我们的代理代码来调用工具。这整套机制称为工具调用(tool calling)。
在本章中,我们将把工具绑定到 LLM、执行工具调用请求,并将结果传回 LLM——实现完整的循环。这个循环是我们将在第 14 章中构建的代理循环的基础。
13.1) 绑定工具并检查工具调用请求
要让 LLM 使用工具,它首先需要知道有哪些可用工具。当我们把每个工具的 name、description 和输入模式(input schema)传给 LLM 时,它就能学会何时以及如何使用每个工具。这个过程称为工具绑定(tool binding)。
工具绑定完成后,LLM 在收到问题时会做两件事中的一件。如果不需要工具,它会像往常一样用文本回应。如果需要工具,它会返回一个结构化的请求:"用这些参数调用这个工具。"然后我们的代理会检查这个请求,并调用指定的工具。
让我们从如何绑定工具开始。
13.1.1) 使用 bind_tools() 绑定工具
工具绑定由一个方法处理:bind_tools()。每个支持工具调用的聊天模型都提供这个方法。调用 bind_tools() 会返回一个绑定了指定工具的新模型对象。
让我们绑定在第 12 章中构建的 get_weather 工具。
from langchain_openai import ChatOpenAI
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""获取指定城市的当前天气。"""
return f"It's always sunny in {city}!"
# 将工具绑定到模型——返回一个绑定了该工具的新模型对象
llm_with_tools = llm.bind_tools([get_weather])就这么简单。返回的 llm_with_tools 是一个了解 get_weather 工具的模型。注意,原始的 llm 完全没有改变——它仍然对任何工具一无所知。
bind_tools() 接受一个工具列表作为参数,并返回一个绑定了这些工具的新模型对象。在内部,它会将每个工具的元数据(name、description 和输入模式)转换为 LLM 提供商能够理解的格式,因此工具模式会随着每次模型调用一起发送。
13.1.2) 检查工具调用请求
当我们向绑定了天气工具的模型发送天气问题时,会发生什么?
response = llm_with_tools.invoke("What's the weather in Paris?")
print(f"type: {type(response)}\n")
print(f"tool_calls: {response.tool_calls}\n")
print(f"content: {repr(response.content)}\n")输出:
type: <class 'langchain_core.messages.ai.AIMessage'>
tool_calls: [{'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}]
content: ''由于 llm_with_tools 仍然是一个聊天模型,它返回一个 AIMessage。但这个响应有两点不同。
首先,response.tool_calls 包含一个工具调用请求列表。每个请求都是一个包含四个键的字典:
name——要调用的工具名称。它对应于工具元数据中的name。args——调用工具时要传入的参数。模型查看了输入模式和用户的问题,构造出{'city': 'Paris'}。id——此次调用的唯一标识符。LLM 稍后用它来将工具执行结果与工具调用请求进行匹配。type——始终为'tool_call'。
其次,response.content 是一个空字符串。这个响应不是最终的文本答案——它是一个工具调用请求。
要判断响应是否为工具调用请求,应检查 tool_calls 而不是 content。如果 tool_calls 非空,它就是一个工具调用请求。如果为空,则 LLM 已直接提供了文本答案。
13.2) 执行工具并将结果传回
LLM 已请求调用 get_weather 工具。现在我们的代理代码需要执行该工具,并将结果传回 LLM。
处理一个工具调用请求涉及三个步骤:
- 从
tool_calls中提取工具名称和参数,并执行相应的工具。 - 将执行结果转换为
ToolMessage。 - 将完整的对话(用户问题 + LLM 的工具调用请求 + 工具执行结果)发送回 LLM 以接收响应。
这是该过程的示意图:
13.2.1) 执行工具并创建 ToolMessage
首先,让我们从 tool_calls 中提取工具名称和参数,并运行相应的工具。为了通过名称查找工具,我们创建一个以工具名称为键的字典。
# 一个用于通过名称查找工具的字典
tool_map = {get_weather.name: get_weather}接下来,我们从 tool_calls 中取出一个工具调用请求,在 tool_map 中找到匹配的工具,并执行它。
if response.tool_calls:
tool_call = response.tool_calls[0]
# {'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
print(result) # 输出: It's always sunny in Paris!要将工具执行结果传回 LLM,我们需要将其包装在一个 ToolMessage 中。ToolMessage 有两个必需字段:
content——以字符串形式表示的工具执行结果。tool_call_id——来自tool_calls的id。LLM 用它来识别结果属于哪个请求。
from langchain_core.messages import ToolMessage
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
tool_message = ToolMessage(
content=result,
tool_call_id=tool_call["id"], # 必须与请求的 id 匹配
)
print(tool_message)输出:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'在上面的代码中,我们传入 args 来执行工具,然后将结果与 tool_call_id 组合起来构建 ToolMessage。
有一种更简单的方法可以一步完成。不要只把 args 传给 .invoke(),而是传入整个 tool_call 字典。LangChain 会执行工具并自动返回一个 ToolMessage。
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
print(tool_message)输出:
content="It's always sunny in Paris!" tool_call_id='call_hgXrHGD'结果相同,但代码简单得多,而且不会出现 tool_call_id 不匹配的情况。后续我们将使用这种方法。
13.2.2) 将工具结果发送给 LLM
现在让我们将工具执行结果传给 LLM。我们将完整的对话历史——用户问题、LLM 的工具调用请求和工具执行结果——发送回 LLM,它会基于工具结果生成答案。
让我们将从头到尾的整个过程整合到一段代码中。
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
# 定义工具并绑定
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""获取指定城市的当前天气。"""
return f"It's always sunny in {city}!"
llm_with_tools = llm.bind_tools([get_weather])
tool_map = {get_weather.name: get_weather}
# 步骤 1: 发送用户的问题并接收 LLM 的响应
messages = [HumanMessage("What's the weather in Paris?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
# 步骤 2: 如果请求了工具调用,则执行工具并添加结果
if ai_msg.tool_calls:
tool_call = ai_msg.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
# 步骤 3: 将完整的对话历史发送回 LLM
final_response = llm_with_tools.invoke(messages)
print(final_response.content)输出:
巴黎当前的天气是晴天。祝你今天愉快!LLM 基于工具执行结果生成了最终的文本答案。回到 13.1,content 是一个空字符串——但在将工具结果传回后,我们收到了一个真正的答案。
让我们检查在步骤 3 时 messages 列表中有哪些消息。
for m in messages:
print(f"{type(m).__name__}: {m.content!r}")输出:
HumanMessage: "What's the weather in Paris?"
AIMessage: ''
ToolMessage: "It's always sunny in Paris!"三条消息按顺序存储:用户的问题、LLM 的工具调用请求(内容为空)和工具执行结果。LLM 看到了用户的问题,判断它需要一个工具,于是请求调用 get_weather。代理代码执行了 get_weather 并将结果添加到消息列表中。一旦获得了所需的一切,LLM 就生成了最终响应。
13.3) 为 LLM 提供多个工具
现实世界中的代理通常拥有多个工具。我们目前介绍的所有内容在使用多个工具时完全一样。
让我们在 get_weather 之外加入 calculate 和 search_web,绑定三个工具。
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""获取指定城市的当前天气。"""
return f"It's always sunny in {city}!"
@tool
def calculate(expression: str) -> str:
"""计算一个简单的算术表达式,例如 '123 * 456'。"""
return str(eval(expression)) # 警告: eval() 存在安全风险。请勿在生产环境中使用。
@tool
def search_web(query: str) -> str:
"""在网络上搜索某个主题的最新信息。"""
return f"Top result for '{query}': ..."
tools = [get_weather, calculate, search_web]
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}让我们提一个计算问题。
response = llm_with_tools.invoke("What is 123 multiplied by 456?")
print(response.tool_calls)输出:
[{'name': 'calculate', 'args': {'expression': '123 * 456'}, 'id': 'call_xyz789', 'type': 'tool_call'}]LLM 请求调用 calculate。它将每个工具的 description 与用户的问题进行了比较,判断出 calculate 是正确的工具。
我们可以使用 13.2 中相同的模式来执行工具并获取最终答案。
messages = [HumanMessage("What is 123 multiplied by 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content) # 输出: 123 乘以 456 等于 56,088。在 13.2 中我们只用 tool_calls[0] 处理了第一个请求,但这里我们用 for 循环遍历了所有请求。原因在于 LLM 可以一次请求多个工具调用。
同时发起多个工具请求
让我们看看当同时请求多个工具时会发生什么。代码是一样的——只是问题改变了。
messages = [HumanMessage("What's the weather in Tokyo, and what is 123 * 456?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
print(f"Tool: {tool_call['name']}")
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content)输出:
Tool: get_weather
Tool: calculate
东京当前的天气是晴天,而 123 乘以 456 等于 56,088。LLM 同时请求了 get_weather 和 calculate,for 循环执行了 tool_calls 中的所有请求,并将它们的结果添加到消息列表中。然后 LLM 利用这些结果生成了最终答案。