一、什么是工具调用?
工具调用根本作用是让LLM具备与外界世界交互的能力。
它能使LLM与外部服务、API和数据库进行交互。⼯具调用还可用于从非结构化数据中提取结构化信息并执行各种其他任务。
1.1创建工具@tool
python
from langchain_core.tools import tool
@tool
def add(a:int, b:int) -> int:
"""两数相加
Args:
a: 第一个数
b: 第二个数
"""
return a+b
print(add.invoke({"a": 2, "b": 3}))
print(add.name)
print(add.description)
result:5
add
两数相加
Args:
a: 第一个数
b: 第二个数
• 该装饰器默认使用函数名称作为工具名称。
• 该装饰器将使用函数的文档字符串作为工具的描述。
结论:函数名、类型提示和文档字符串都是传递给工具 Schema 的一部分,不可缺失。定义好的描述是使模型良好运行的重要部分。
1.1.1 什么是Schema?
类似于一个JSON!
我们看一下JSON串:
json
{
"surname": "王",
"given_name": "刚",
"birthday": "1732-02-22",
"address": {
"district": "萧⼭区",
"city": "杭州市",
"province": "浙江省"
"country": "中国"
}
Json Schema就是一个帮助校对的,用来落实校对左边字段和右边字段的正确性。JSON Schema 是数据本⾝,而不是计算机程序,它只是⼀种"描述其他数据结构"的声明格式。简明扼要地描述数据的表面结构,并根据数据自动验证数据很容易。但是,由于 JSON Schema 不能包含任意代码,因此无法表达数据元素之间的关系存在某些约束。
1.1.2 不写描述1(依赖Pydantic类)
python
from langchain_core.tools import tool
from openai import BaseModel
from openai.types.chat.chat_completion_message import Annotation
from pydantic import Field
class AddInput(BaseModel):
"""两数相加"""
a: int = Field(..., description="the first number")
b: int = Field(..., description="the second number")
@tool(args_schema=AddInput)
def add(a:int, b:int)->int:
return a+b
注意是代码中 @tool 的 args_schema 参数,它表示工具函数在未提供描述、文档字符串等需要传递给⼯具 Schema 的内容时,依赖Pydantic类使用 args_schema 参数,定义并提供工具输⼊参数的schema。默认为None。
我的理解是:在class类中使用BaseModel(BaseModel 是 Python 的 pydantic 库中的一个基类,用于定义数据模型。它提供了数据验证、类型注解和序列化等功能,常用于 API 请求/响应数据的结构化处理。)把每个 参数使用Field函数接口进行描述出这个参数是干什么的,后续使用工具仅需使用args_shema调用这个函数即可。
1.1.3 不写描述2(依赖Annotated)
在LangChain中,可以依赖 Annotated 和⽂档字符串传递给工具 Schema 。
python
from langchain_core.tools import tool
from typing import Annotated
@tool
def add(
a: Annotated[int,"第一个数"],
b: Annotated[int,"第二个数"]
)->int:
"""两数相加"""
return a+b
@tool
def multi(
a: Annotated[int,"第一个数"],
b: Annotated[int,"第二个数"]
)->int:
"""两数相乘"""
return a*b
1.2 使⽤StructuredTool类提供的函数创建⼯具@tool
1.2.1 StructuredTool类参数有什么?
python
lassmethod from_function(
func: Callable | None = None,
coroutine: Callable[[...], Awaitable[Any]] | None = None,
name: str | None = None,
description: str | None = None,
return_direct: bool = False,
args_schema: type[BaseModel] | dict[str, Any] | None = None,
infer_schema: bool = True,
*,
response_format: Literal['content', 'content_and_artifact'] = 'content',
parse_docstring: bool = False,
error_on_invalid_docstring: bool = False,
**kwargs: Any,
) → StructuredTool
• func:要设置的⼯具函数
• coroutine:协程函数,要设置的异步⼯具函数
• name:⼯具名称。默认为函数名称。
• description:⼯具描述。默认为函数⽂档字符串。
• args_schema:⼯具输⼊参数的schema。默认为None。
• response_format:⼯具响应格式。默认为"content"。
◦ 如果配置为 "content" ,则⼯具的输出为ToolMessage的 content 属性。
▪ 对于 HumanMessage 、 AIMessage 已经⻅过,分别表⽰ ⽤⼾消息 和 AI消息响应 ,对于 ToolMessage ,它表⽰对应⼯具⻆⾊所发出的消息。
◦ 如果配置为 "content_and_artifact" ,则输出应是与ToolMessage的 content 属性与 artifact 属性相对应的⼆元组。
1.2.2 写法一:常规写法
python
from langchain_core.tools import tool, StructuredTool
def add(a: int, b: int) -> int:
"""两数相加"""
return a+b
calculator_tool = StructuredTool.from_function(func=add)
print(calculator_tool.invoke({"a": 1, "b": 2}))
简单使用from_function中的func参数即可。
1.2.3 写法二:依赖Pydantic类
python
from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool
class AddTool(BaseModel):
a:int = Field(..., description="第一个数")
b:int = Field(..., description="第二个数")
def add(a:int,b:int)->int:
return a+b
calculator_tool = StructuredTool.from_function(
func=add,
name="calculator",
description="两数相加",
args_schema=AddTool
)
print(calculator_tool.invoke({"a": 2, "b": 3}))
print(calculator_tool.name)
print(calculator_tool.description)
print(calculator_tool.args)
和前面直接使用tool工具一样的,这个args_shema就是直接跑到class类中找这两个参数具体是干什么用的。
1.2.4 写法三:加入response_format 配置
如果希望我们的⼯具区分消息内容(content)和其他⼯件(artifact),让⼤模型读取 content ,⽽⼀些⽤来构造 content 的原始数据保存下来,若后续有⼀些记录、分析的步骤,就可以派上⽤场了,这就是 artifact 。 artifact 通常需要使⽤字典 Dict 或列表 List 保存。
(说白了就是把正文内容和其他内容区分开)
python
from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool
class AddTool(BaseModel):
a:int = Field(..., description="第一个数")
b:int = Field(..., description="第二个数")
def add(a:int,b:int)->Tuple[str,List[int]]:
nums=[a,b]
content=f"{nums}相加的结果是{a+b}"
return content,nums
calculator_tool = StructuredTool.from_function(
func=add,
name="calculator",
description="两数相加",
args_schema=AddTool,
response_format="content_and_artifact"
)
print(calculator_tool.invoke({"a": 2, "b": 3}))
print(calculator_tool.invoke(
{
"name": "calculator",
"args": {"a": 2, "b": 3},
"id": "1111",
"type": "tool_call"
}
))
1.3 绑定工具bind
我们从前面创建完工具以后,需要将工具绑定到大模型中才行,正如我们上面结尾说的,为什么要把content和其他工具内容分开呢,就是为了以后给大模型content是人话,让大模型能够更好的知道我们需要的是什么,所以我们此时需要做绑定大模型和工具的操作。
python
from langchain_core.tools import tool
from typing import Annotated
from langchain_openai import ChatOpenAI
@tool
def add(
a: Annotated[int,"第一个数"],
b: Annotated[int,"第二个数"]
)->int:
"""两数相加"""
return a+b
@tool
def multi(
a: Annotated[int,"第一个数"],
b: Annotated[int,"第二个数"]
)->int:
"""两数相乘"""
return a*b
# 定义大模型
model = ChatOpenAI(
model="qwen-plus", # 可选: qwen-turbo, qwen-max, qwen-plus 等
api_key="sk-xxx", # 替换成刚才复制的API Key
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" # 固定地址[reference:30][reference:31]
)
# 绑定工具
model_with_tools = model.bind_tools(tools = [add,multi])
1.4 工具调用
通过 .bind_tools() ⽅法我们可知,它返回了⼀个Runnable实例,因此我们可以使⽤该Runnable实例,调⽤ .invoke() ⽅法,完成⼯具调⽤。
python
# 从上面代码往后延伸的下面代码
# 绑定工具
model_with_tools = model.bind_tools(tools = [add,multi])
# 调用工具
print(model_with_tools.invoke("4+5等于多少?"))
我们看输出结果是什么?
python
content=''
additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 24, 'prompt_tokens': 254, 'total_tokens': 278, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'qwen-plus',
AIMessage :来⾃AI的消息。从聊天模型返回,作为对提⽰(输⼊)的响应。
◦ content :消息的内容。
◦ additional_kwargs :与消息关联的其他有效负载数据。对于来⾃AI的消息,可能包括模型提供程序编码的⼯具调⽤。
◦ response_metadata :响应元数据。例如:响应标头、logprobs、令牌计数、模型名称。从输出结果看来,AI给出的响应是进⾏⼯具的调⽤
1.4.1 工具属性
输出结果是⼀个AIMessage 。但是,如果调⽤了⼯具,则result 将具有⼀个tool_calls 属性。此属性包括执⾏该⼯具所需的⼀切,包括⼯具名称和输⼊参数。

1.4.2 将工具输出传递给聊天模型
总共有三种Message类型,下面是三种模型的形象表示。
那为什么需要ToolMessage?
聊天模型通常不是接受单个字符串作为输⼊,⽽是接受 聊天消息(XxxMessage)列表,因此在这⾥我们需要将⼯具的返回,构造成 ToolMessage ,再传输给聊天模型!!!
⽅便的是,如果我们使⽤@tool 装饰器创建的⼯具,使⽤ tool.invoke(tool_calls) ,将⾃动返回⼀个 ToolMessage 。
python
# 1.先写HumanMessage
human_msg = [
HumanMessage("2乘3等于几?6乘6等于几?")
]
# 2.再转化为AiMessage
ai_msg = model_with_tools.invoke(human_msg)
human_msg.append(ai_msg)
# 3.构造ToolMessage并添加到消息列表中去
for tool_call in ai_msg.tool_calls:
selected_tool = {"add":add,"multi":multi}[tool_call["name"].lower()]
tool_msg = selected_tool.invoke(tool_call)
human_msg.append(tool_msg)
print(model.invoke(human_msg).content)
print(human_msg)
结果是:
2 × 3 = 6
6 × 6 = 36
[HumanMessage (content='2乘3等于几?6乘6等于几?', additional_kwargs={}, response_metadata={}),
AIMessage (content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 49, 'prompt_tokens': 260, 'total_tokens': 309, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': 192}}, 'model_provider': 'openai', 'model_name': 'qwen-plus', 'system_fingerprint': None, 'id': 'chatcmpl-adcf0c4d-eba0-9f82-99a7-7b9f082eafdf', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019f6a9f-2ddb-7ab1-ade9-56c570ccce71-0', tool_calls={'name': 'multi', 'args': {'a': 2, 'b': 3}, 'id': 'call_3cdf1af27486475d91ced5', 'type': 'tool_call'}, {'name': 'multi', 'args': {'a': 6, 'b': 6}, 'id': 'call_4683705479fa4f2bb94cad', 'type': 'tool_call'}, invalid_tool_calls=\[\], usage_metadata={'input_tokens': 260, 'output_tokens': 49, 'total_tokens': 309, 'input_token_details': {'cache_read': 192}, 'output_token_details': {}}),
ToolMessage (content='6', name='multi', tool_call_id='call_3cdf1af27486475d91ced5'), ToolMessage(content='36', name='multi', tool_call_id='call_4683705479fa4f2bb94cad')]
从流程与代码中可以看到,实际上我们调⽤了两次聊天模型:
• 第⼀次:仅将【 HumanMessage 】发送给聊天模型进⾏处理,结果返回了【包含⼯具调⽤的
AIMessage 】,并没有返回我们想要的结果。然后我们执⾏⼯具,得到 ToolMessage 。
• 第⼆次:将【 HumanMessage + AIMessage + ToolMessage 】消息记录发送给聊天模型进⾏处理,结果返回了【包含结果的 AIMessage 】