工具调用
注意:我们把“工具调用(tool calling)”与“函数调用(function calling)”互换使用。虽然函数调用有时是指调用单个函数,但我们把所有模型都视为在每条消息中都可以返回多个工具调用或函数调用。
提示:在 此处 (opens in a new tab) 查看所有支持工具调用的模型列表。
工具调用允许模型通过生成匹配用户定义 schema 的输出来响应给定提示。虽然名字暗示模型在执行某种动作,但实际上并非如此!模型是在给出工具的 参数,而实际运行工具(或不运行)由用户决定——例如,如果你想从非结构化文本中 抽取匹配某个 schema 的输出,你可以给模型一个“抽取”工具,其参数匹配期望的 schema,然后把生成的输出当作你的最终结果。
一次工具调用包含名称、参数字典和可选标识符。参数字典的结构为 {argument_name: argument_value}。
许多 LLM 提供方,包括 Anthropic (opens in a new tab)、Cohere (opens in a new tab)、Google (opens in a new tab)、Mistral (opens in a new tab)、OpenAI (opens in a new tab) 等,都支持工具调用功能的变体。这些功能通常允许请求 LLM 时包含可用工具及其 schema,并允许响应中包含对这些工具的调用。例如,给定一个搜索引擎工具,LLM 可能先发起对搜索引擎的调用来处理查询。调用 LLM 的系统可以接收该工具调用、执行它,并把输出返回给 LLM 以影响其响应。LangChain 包含一套 内置工具 (opens in a new tab),并支持多种定义你自己 自定义工具 的方法。工具调用对于构建 使用工具的链和代理,以及更广泛地从模型获得结构化输出,极其有用。
各提供方对工具 schema 和工具调用的格式化采用不同的约定。例如,Anthropic 把工具调用作为解析后的结构放在更大的内容块中返回:
[
{
"text": "<thinking>\nI should use a tool.\n</thinking>",
"type": "text"
},
{
"id": "id_value",
"input": {"arg_name": "arg_value"},
"name": "tool_name",
"type": "tool_use"
}
]而 OpenAI 把工具调用分离到一个独立的参数中,参数是 JSON 字符串:
{
"tool_calls": [
{
"id": "id_value",
"function": {
"arguments": '{"arg_name": "arg_value"}',
"name": "tool_name"
},
"type": "function"
}
]
}LangChain 实现了用于定义工具、把它们传给 LLM 以及表示工具调用的标准接口。
请求:把工具传给模型
为了让模型能够调用工具,你需要在发起聊天请求时把工具 schema 传给它。
支持工具调用功能的 LangChain ChatModel 实现了一个 .bind_tools 方法,它接收一组 LangChain 工具对象 (opens in a new tab)、Pydantic 类或 JSON Schema,并以提供方期望的格式把它们绑定到聊天模型。之后对绑定后聊天模型的调用都会在每次模型 API 调用中包含工具 schema。
定义工具 schema:LangChain Tool
例如,我们可以用 @tool 装饰器作用在 Python 函数上,来定义自定义工具的 schema:
from langchain_core.tools import tool
@tool
def add(a: int, b: int) -> int:
"""Adds a and b.
Args:
a: first int
b: second int
"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiplies a and b.
Args:
a: first int
b: second int
"""
return a * b
tools = [add, multiply]定义工具 schema:Pydantic 类
我们也可以用 Pydantic 等价地定义 schema。当你的工具输入更复杂时,Pydantic 很有用:
from langchain_core.pydantic_v1 import BaseModel, Field
# Note that the docstrings here are crucial, as they will be passed along
# to the model along with the class name.
class add(BaseModel):
"""Add two integers together."""
a: int = Field(..., description="First integer")
b: int = Field(..., description="Second integer")
class multiply(BaseModel):
"""Multiply two integers together."""
a: int = Field(..., description="First integer")
b: int = Field(..., description="Second integer")
tools = [add, multiply]我们可以把它们绑定到聊天模型,如下所示(这里以 OpenAI 为例,其它提供方也可用):
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)绑定工具 schema
我们可以使用 bind_tools() 方法来处理把 multiply 转换为一个“工具”并绑定到模型(即,在每次调用模型时传入它)。
# | echo: false
# | output: false
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)llm_with_tools = llm.bind_tools(tools)请求:强制工具调用
当你只是使用 bind_tools(tools) 时,模型可以选择返回一个工具调用、多个工具调用,或者完全不返回工具调用。有些模型支持 tool_choice 参数,让你有一定的能力强制模型调用工具。对于支持这一点的模型,你可以传入你希望模型始终调用的工具名称 tool_choice="xyz_tool_name"。或者你可以传入 tool_choice="any" 来强制模型调用至少一个工具,而不指定具体是哪个工具。
注意:目前
tool_choice="any"功能由 OpenAI、MistralAI、FireworksAI 和 Groq 支持。目前 Anthropic 完全不支持tool_choice。
如果我们想让模型始终调用 multiply 工具,可以这样做:
always_multiply_llm = llm.bind_tools([multiply], tool_choice="multiply")如果我们想让它始终调用 add 或 multiply 中至少一个,可以这样做:
always_call_tool_llm = llm.bind_tools([add, multiply], tool_choice="any")响应:从模型输出中读取工具调用
如果 LLM 响应中包含工具调用,它们会作为 ToolCall (opens in a new tab) 对象的列表,附加到相应的 AIMessage (opens in a new tab) 或 AIMessageChunk (opens in a new tab)(流式输出时)的 .tool_calls 属性上。一个 ToolCall 是一个带类型的字典,包含工具名称、参数值字典以及(可选的)标识符。没有工具调用的消息,该属性默认为空列表。
示例:
query = "What is 3 * 12? Also, what is 11 + 49?"
llm_with_tools.invoke(query).tool_calls[{'name': 'multiply',
'args': {'a': 3, 'b': 12},
'id': 'call_UL7E2232GfDHIQGOM4gJfEDD'},
{'name': 'add',
'args': {'a': 11, 'b': 49},
'id': 'call_VKw8t5tpAuzvbHgdAXe9mjUx'}].tool_calls 属性应包含有效的工具调用。注意,模型提供方有时可能会输出格式错误的工具调用(例如,参数不是有效的 JSON)。在这些解析失败的情况下,InvalidToolCall (opens in a new tab) 实例会被填充到 .invalid_tool_calls 属性中。一个 InvalidToolCall 可以有名称、字符串参数、标识符和错误消息。
如果需要,输出解析器 可以进一步处理输出。例如,我们可以转换回最初的 Pydantic 类:
from langchain_core.output_parsers.openai_tools import PydanticToolsParser
chain = llm_with_tools | PydanticToolsParser(tools=[multiply, add])
chain.invoke(query)[multiply(a=3, b=12), add(a=11, b=49)]响应:流式输出
当在流式上下文中调用工具时,消息块 (opens in a new tab) 会通过 .tool_call_chunks 属性被填充一个由 工具调用块 (opens in a new tab) 对象组成的列表。一个 ToolCallChunk 包含工具 name、args 和 id 的可选字符串字段,并包含一个可选整数字段 index,可用于把多个块连接起来。这些字段是可选的,因为一次工具调用的一部分可能跨不同的块被流式传输(例如,一个包含参数子串的块,其工具名称和 id 可能为 null)。
由于消息块继承自其父消息类,带有工具调用块的 AIMessageChunk (opens in a new tab) 也会包含 .tool_calls 和 .invalid_tool_calls 字段。这些字段是从消息的工具调用块中尽力解析出来的。
注意,目前并非所有提供方都支持工具调用的流式输出。
示例:
async for chunk in llm_with_tools.astream(query):
print(chunk.tool_call_chunks)[]
[{'name': 'multiply', 'args': '', 'id': 'call_5Gdgx3R2z97qIycWKixgD2OU', 'index': 0}]
[{'name': None, 'args': '{"a"', 'id': None, 'index': 0}]
[{'name': None, 'args': ': 3, ', 'id': None, 'index': 0}]
[{'name': None, 'args': '"b": 1', 'id': None, 'index': 0}]
[{'name': None, 'args': '2}', 'id': None, 'index': 0}]
[{'name': 'add', 'args': '', 'id': 'call_DpeKaF8pUCmLP0tkinhdmBgD', 'index': 1}]
[{'name': None, 'args': '{"a"', 'id': None, 'index': 1}]
[{'name': None, 'args': ': 11,', 'id': None, 'index': 1}]
[{'name': None, 'args': ' "b": ', 'id': None, 'index': 1}]
[{'name': None, 'args': '49}', 'id': None, 'index': 1}]
[]
注意,把消息块相加会合并它们对应的工具调用块。这正是 LangChain 各种 工具输出解析器 支持流式输出的原理。
例如,下面我们累积工具调用块:
first = True
async for chunk in llm_with_tools.astream(query):
if first:
gathered = chunk
first = False
else:
gathered = gathered + chunk
print(gathered.tool_call_chunks)[]
[{'name': 'multiply', 'args': '', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}]
[{'name': 'multiply', 'args': '{"a"', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}]
[{'name': 'multiply', 'args': '{"a": 3, ', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 1', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}, {'name': 'add', 'args': '', 'id': 'call_GERgANDUbRqdtmXRbIAS9JTS', 'index': 1}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}, {'name': 'add', 'args': '{"a"', 'id': 'call_GERgANDUbRqdtmXRbIAS9JTS', 'index': 1}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}, {'name': 'add', 'args': '{"a": 11,', 'id': 'call_GERgANDUbRqdtmXRbIAS9JTS', 'index': 1}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}, {'name': 'add', 'args': '{"a": 11, "b": ', 'id': 'call_GERgANDUbRqdtmXRbIAS9JTS', 'index': 1}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}, {'name': 'add', 'args': '{"a": 11, "b": 49}', 'id': 'call_GERgANDUbRqdtmXRbIAS9JTS', 'index': 1}]
[{'name': 'multiply', 'args': '{"a": 3, "b": 12}', 'id': 'call_hXqj6HxzACkpiPG4hFFuIKuP', 'index': 0}, {'name': 'add', 'args': '{"a": 11, "b": 49}', 'id': 'call_GERgANDUbRqdtmXRbIAS9JTS', 'index': 1}]
print(type(gathered.tool_call_chunks[0]["args"]))<class 'str'>
下面我们累积工具调用来演示部分解析:
first = True
async for chunk in llm_with_tools.astream(query):
if first:
gathered = chunk
first = False
else:
gathered = gathered + chunk
print(gathered.tool_calls)[]
[]
[{'name': 'multiply', 'args': {}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}]
[{'name': 'multiply', 'args': {'a': 3}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}]
[{'name': 'multiply', 'args': {'a': 3, 'b': 1}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}]
[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}]
[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}]
[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}, {'name': 'add', 'args': {}, 'id': 'call_P39VunIrq9MQOxHgF30VByuB'}]
[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}, {'name': 'add', 'args': {'a': 11}, 'id': 'call_P39VunIrq9MQOxHgF30VByuB'}]
[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}, {'name': 'add', 'args': {'a': 11}, 'id': 'call_P39VunIrq9MQOxHgF30VByuB'}]
[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}, {'name': 'add', 'args': {'a': 11, 'b': 49}, 'id': 'call_P39VunIrq9MQOxHgF30VByuB'}]
[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_aXQdLhKJpEpUxTNPXIS4l7Mv'}, {'name': 'add', 'args': {'a': 11, 'b': 49}, 'id': 'call_P39VunIrq9MQOxHgF30VByuB'}]
print(type(gathered.tool_calls[0]["args"]))<class 'dict'>
请求:把工具输出传回模型
如果我们使用模型生成的工具调用去实际调用工具,并想把工具结果传回模型,可以使用 ToolMessage 来做到。
from langchain_core.messages import HumanMessage, ToolMessage
@tool
def add(a: int, b: int) -> int:
"""Adds a and b.
Args:
a: first int
b: second int
"""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiplies a and b.
Args:
a: first int
b: second int
"""
return a * b
tools = [add, multiply]
llm_with_tools = llm.bind_tools(tools)
messages = [HumanMessage(query)]
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_output = selected_tool.invoke(tool_call["args"])
messages.append(ToolMessage(tool_output, tool_call_id=tool_call["id"]))
messages[HumanMessage(content='What is 3 * 12? Also, what is 11 + 49?'),
AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Jja7J89XsjrOLA5rAjULqTSL', 'function': {'arguments': '{"a": 3, "b": 12}', 'name': 'multiply'}, 'type': 'function'}, {'id': 'call_K4ArVEUjhl36EcSuxGN1nwvZ', 'function': {'arguments': '{"a": 11, "b": 49}', 'name': 'add'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 49, 'prompt_tokens': 144, 'total_tokens': 193}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': 'fp_a450710239', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-9db7e8e1-86d5-4015-9f43-f1d33abea64d-0', tool_calls=[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_Jja7J89XsjrOLA5rAjULqTSL'}, {'name': 'add', 'args': {'a': 11, 'b': 49}, 'id': 'call_K4ArVEUjhl36EcSuxGN1nwvZ'}]),
ToolMessage(content='36', tool_call_id='call_Jja7J89XsjrOLA5rAjULqTSL'),
ToolMessage(content='60', tool_call_id='call_K4ArVEUjhl36EcSuxGN1nwvZ')]llm_with_tools.invoke(messages)AIMessage(content='3 * 12 = 36\n11 + 49 = 60', response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 209, 'total_tokens': 225}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': 'fp_3b956da36b', 'finish_reason': 'stop', 'logprobs': None}, id='run-a55f8cb5-6d6d-4835-9c6b-7de36b2590c7-0')请求:少样本提示
对于更复杂的工具使用,在提示中加入少样本示例非常有用。我们可以通过把带有 ToolCall 的 AIMessage 以及对应的 ToolMessage 加入提示来做到这一点。
注意:对大多数模型来说,ToolCall 和 ToolMessage 的 id 必须对应,因此每个带有 ToolCall 的 AIMessage 后面都要跟着具有对应 id 的 ToolMessage。
例如,即使有一些特殊指令,我们的模型也可能被运算顺序绊倒:
llm_with_tools.invoke(
"Whats 119 times 8 minus 20. Don't do any math yourself, only use tools for math. Respect order of operations"
).tool_calls[{'name': 'multiply',
'args': {'a': 119, 'b': 8},
'id': 'call_RofMKNQ2qbWAFaMsef4cpTS9'},
{'name': 'add',
'args': {'a': 952, 'b': -20},
'id': 'call_HjOfoF8ceMCHmO3cpwG6oB3X'}]模型不应该现在就尝试做加法,因为它在技术上还无法知道 119 * 8 的结果。
通过加入带有一些示例的提示,我们可以纠正这个行为:
from langchain_core.messages import AIMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
examples = [
HumanMessage(
"What's the product of 317253 and 128472 plus four", name="example_user"
),
AIMessage(
"",
name="example_assistant",
tool_calls=[
{"name": "multiply", "args": {"x": 317253, "y": 128472}, "id": "1"}
],
),
ToolMessage("16505054784", tool_call_id="1"),
AIMessage(
"",
name="example_assistant",
tool_calls=[{"name": "add", "args": {"x": 16505054784, "y": 4}, "id": "2"}],
),
ToolMessage("16505054788", tool_call_id="2"),
AIMessage(
"The product of 317253 and 128472 plus four is 16505054788",
name="example_assistant",
),
]
system = """You are bad at math but are an expert at using a calculator.
Use past tool usage as an example of how to correctly use the tools."""
few_shot_prompt = ChatPromptTemplate.from_messages(
[
("system", system),
*examples,
("human", "{query}"),
]
)
chain = {"query": RunnablePassthrough()} | few_shot_prompt | llm_with_tools
chain.invoke("Whats 119 times 8 minus 20").tool_calls[{'name': 'multiply',
'args': {'a': 119, 'b': 8},
'id': 'call_tWwpzWqqc8dQtN13CyKZCVMe'}]这次我们似乎得到了正确的输出。
下面是 LangSmith trace (opens in a new tab) 的样子。
后续步骤
- 输出解析:参见 OpenAI Tools 输出解析器 和 OpenAI Functions 输出解析器,了解如何把函数调用 API 的响应抽取为各种格式。
- 结构化输出链:有些模型提供了构造函数,可以为你处理创建结构化输出链的工作。
- 工具使用:参见 这些指南,了解如何构造会调用所调用工具的链和代理。