我挖掘了一个巨牛的 人工智能 学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。

一、工具调用核心概念
1.1 工具调用根本作用
赋予大语言模型(LLM)与外部世界交互的能力。
1.2 LLM 原生局限性
LLM 是封闭知识系统,天然存在短板:
- 知识固化在训练数据中,存在时间滞后性,无法获取最新信息
- 仅有文本生成逻辑,无法执行任何外部操作
- 不能独立完成数学计算、实时资讯查询、数据库操作、调用第三方 API
工具调用可以彻底突破以上限制。
1.3 工具调用四大核心价值
二、LangChain 四种工具定义方式(完整对比)
前置核心概念:Schema(工具参数契约)
2.1 Schema 通俗定义
Schema 是工具参数规范说明书,底层基于 Pydantic + JSON Schema。
规定:工具所需参数、参数类型、是否必填、参数含义。
2.2 Schema 解决两个核心问题
- 给大模型看:LLM 知道调用工具该传什么参数,不会乱传参
- 给 LangChain 用:自动参数校验,参数缺失/类型错误直接拦截,不把脏数据传入函数
2.1 方式一:@tool 装饰器 + 文档注释(基础写法)
@tool
def sub(a:int,b:int) -> int:
#这里分号写法,先打两对分号,然后在中间换行即可
"""两数相减
Args:
a:第一个整数
b:第二个整数
"""
return a-b
print(sub.invoke({"a": 100, "b": 2}))
print("\\n")
我这个代码有什么错?(装唐阴你们一手)
- 问题 1:print 写在了 return a-b 后面
函数执行到 return 就直接结束,return 之后的代码永远不会运行,你的两行 print 被函数体挡住了。
- 问题 2:缩进错误
print(sub.invoke(…)) 缩进和 return 对齐,属于函数内部代码,不是外部调用代码。
更改之后的代码:
from langchain_core.tools import tool
@tool
def sub(a:int,b:int) -> int:
#这里分号写法,先打两对分号,然后在中间换行即可
"""两数相减
Args:
a:第一个整数
b:第二个整数
"""
return a-b
print(sub.invoke({"a": 100, "b": 2}))
print("\\n")
更改之后就能正常输出了~

优缺点
✅ 签名简洁、少量工具清爽 ❌ 依赖注释格式,易错、需要手动开解析开关
@tool 装饰器做了什么?
-
它把普通函数包装成了一个Tool 对象(BaseTool 的实例)
-
这个对象有 .invoke() 方法,而不是直接调用
2.2 方式二:@tool + Annotated(生产推荐)
最优日常写法,无需开关、不依赖注释,参数描述直接绑定在参数上。
from langchain_core.tools import tool
from typing import Annotated
@tool
def sub(a : Annotated[int, …, "第一个整数"],
b : Annotated[int, …, "第二个整数"],) -> int:
"""两数相减"""
return a-b
print(sub.invoke({"a": 100, "b": 2}))
print(sub.name)
print(sub.description)
print(sub.args)
Annotated 语法解析
- int:参数类型约束
- …:代表参数必填,无默认值(等价 Field 必填)
- 字符串:参数描述,用于生成 Schema、给大模型识别
所以说还是说明 a 这个参数的性质,第一个 int 解释了 a 的类型,第二个代表 a 这个参数是必须填的,第三个表示 a 的含义,都是为了服务a这个参数!
2.3 方式三:Pydantic BaseModel 自定义 Schema(强校验)
适合复杂参数、参数校验、多工具复用场景。
from langchain_core.tools import tool
from pydantic import BaseModel,Field
class SubInput(BaseModel):
"""两数相减"""
a:int = Field(…, description = "第一个整数")
b:int = Field(…, description = "第二个整数")
@tool(args_schema=SubInput)
def sub(a:int,b:int) -> int:
return a-b
优缺点
✅ 校验能力最强(范围/枚举/正则/嵌套)、完全不依赖注释、可复用 ❌ 代码量最大
生产强制规范:必填参数必须写 Field(…, description=""),不可省略,否则模型易漏传参。
工具三要素:
- 工具名称
- 工具描述
- 工具参数
以我上面的函数为例:
from langchain_core.tools import tool
@tool
def sub(a:int,b:int) -> int:
#这里分号写法,先打两对分号,然后在中间换行即可
"""两数相减
Args:
a:第一个整数
b:第二个整数
"""
return a-b
# print(sub.invoke({"a": 100, "b": 2}))
# print("\\n")
print(sub.name)#工具名
print(sub.description)#工具文档描述
print(sub.args)#工具参数
输出结果:
这些就是工具的属性

对于工具来说:
工具名称可以让LLM知道有哪些工具,可以调哪些工具
工具描述实际上就是在写提示词,告诉模型工具的能力,让模型知道调谁(What)
工具参数可以让模型知道怎么调(How)
2.4 方式四:StructuredTool.from_function(工程化专用)
适合正式项目、批量生成工具、保留原生函数场景。
from langchain_core.tools import StructuredTool
def sub(a : int,b : int) -> int:
"""两数相减"""
return a-b
# 手动完整配置工具三要素
sub_tool = StructuredTool.from_function(
func=sub,
name="sub",
description="两数相减",
)
print(sub_tool.invoke({"a": 9, "b": 2}))
由此可见,不管是用哪一种方式去定义函数,工具Schema都不能缺失,工具名,工具参数,工具描述这三件套!
灵魂拷问:
为什么变量a , b后面要加上类型说明,python不是不需要吗?
分两种场景给你说清楚,很好理解:
1. 普通 Python 代码(不用给大模型生成 Schema、不用 Pydantic 校验)
完全可以不写 : int,写了也不影响运行,只是给人 / IDE 看的提示,Python 本身不管。
# 无类型注解,正常运行
def add(a, b):
return a + b
print(add(1,2))
就算你传字符串 add("1","2"),Python 不会拦你。这里不加 :int 完全没问题,代码更简洁。
2. 现在的 LangChain / Pydantic Schema 场景(必须加)
场景 A:@tool Annotated 写法
python
运行
# 必须写 int,不然 LangChain 生成的JSON Schema不知道参数类型
def sub(a: Annotated[int, …, "数字1"], b: Annotated[int, …, "数字2"]):
场景 B:Pydantic BaseModel
python
运行
class SubInput(BaseModel):
a: int = Field(…, "第一个整数")
这里的 :int 不能省略:Pydantic 靠这个注解生成校验规则、生成给 LLM 的工具 Schema;如果不写,它不知道这个参数应该是什么类型,工具调用会失效、大模型乱传参。
方法二:加入配置,依赖Pydantic类 同样的,让工具函数不提供描述、文档字符串等需要传递给工具Schema的内容,如下所示:
class SubInput(BaseModel):
a:int = Field(…,description = "第一个整数")
b:int = Field(…,description = "第三个整数")
#照样先不写工具的描述
def sub(a : int,b : int) -> int:
return a-b
sub_tool = StructuredTool.from_function(
func=sub,
name="SUB",#工具名
description="两数相减",#工具描述
args_schema=SubInput,#工具参数
)
print(sub_tool.invoke({"a": 9, "b": 2}))
print(sub_tool.name)
print(sub_tool.description)
print(sub_tool.args)
三、content 与 artifact 分离(加入 response_format 配置)
3.1 核心作用
在工具开发中,我们可以把输出拆分为两段独立数据:面向大模型的摘要内容 content,以及留存完整原始信息的工件 artifact。大模型仅能读取 content 用于推理对话,而生成摘要时用到的底层原始数据会存入 artifact 保留,后续日志留存、数据复盘、二次加工等流程都能复用这份原始素材。artifact 一般用列表、字典这类结构化容器存储,方便存取复杂多层数据。
拿天气查询工具举例:当用户提问 “北京今日天气”,调用搜索接口后会产出两类数据:
{
"results": [
{
"title": "北京天气预报|中国天气官方平台",
"link": "https://weather.com.cn/xxx",
"snippet": "北京今日白天晴朗,最高温32℃,夜间无降水,最低25℃"
},
{
"title": "北京实时气象数据-Whether全球气象",
"link": "https://www.weather.com/xxx",
"snippet": "北京市区域晴好天气,日间最高气温32摄氏度"
}
# 其余检索条目省略
],
"search_parameters": {},
"search_information": {}
}
这份未经过滤加工的原始数据有大量实用场景,仅单纯返回 content 无法实现:
如果仅单一返回 content 文本,底层原始接口数据会直接丢失,上述溯源、存档、拓展类需求都无法落地实现。
3.2 代码实现(两种写法都支持)
我们需要在定义工具时指定response_format="content_and_artifact" 参数,并确保我们返回一个元组(content,artifact),代码如下:
写法1:@tool 装饰器实现
from typing import Tuple, List
from langchain_core.tools import tool
@tool(response_format="content_and_artifact")
def sub(a: int, b: int) -> Tuple[str, List[int]]:
nums = [a, b]
content = f"{nums}相减的结果是{a-b}"
return content, nums
res = sub.invoke({"a":9, "b":2})
print(res.content)
print(res.artifact)
写法2:StructuredTool 实现
from typing import Tuple, List
from langchain_core.tools import StructuredTool
from pydantic import BaseModel,Field
class SubInput(BaseModel):
a:int = Field(…,description = "第一个整数")
b:int = Field(…,description = "第二个整数")
def sub(a : int,b : int) -> Tuple[str,List[int]]:
nums = [a,b]
content = f"{nums}相减的结果是{a-b}"
return content,nums
sub_tool = StructuredTool.from_function(
func=sub,
name="SUB",
description="两数相减",
args_schema=SubInput,
response_format="content_and_artifact"
)
res = sub_tool.invoke({
"name": "SUB",
"args": {"a": 9, "b": 2},
"type": "tool_call",
"id": "111"
})
print(res)
3.3 工具调用入参字段解析
- name:工具名称,用于多工具场景区分调用对象
- args:工具入参,严格匹配 Schema,自动校验
- type="tool_call":固定协议字段,标识为工具调用请求
- id:工具调用唯一ID,用于关联「调用请求」和「工具返回结果」,解决并发、多轮调用错乱问题
四、@tool 与 StructuredTool 核心取舍(重点)
误区纠正
只有 StructuredTool 支持 content/artifact 是错误的,@tool 装饰器同样完全支持。
为什么要学 StructuredTool?(不可替代优势)
场景选择
- 测试、Demo、少量工具:优先 @tool(Annotated 写法)
- 线上项目、工程化、批量工具:必须用 StructuredTool
五、模型绑定工具 & 工具选择逻辑
5.1 工具绑定代码
from typing import Annotated
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
# 1. 定义工具
@tool
def add(a: Annotated[int, …, "第一个整数"],
b: Annotated[int, …, "第二个整数"]) -> int:
"""两数相加"""
return a+b
@tool
def sub(a: Annotated[int, …, "第一个整数"],
b: Annotated[int, …, "第二个整数"]) -> int:
"""两数相减"""
return a-b
# 2. 初始化模型 + 绑定工具
model = init_chat_model("deepseek-v4-flash", model_provider="deepseek")
tools = [add,sub]
model_with_tools = model.bind_tools(tools=tools)
5.2 模型工具选择逻辑
模型根据输入的相关性决定何时使用工具。
- 普通问答:如「你是谁」→ tool_calls=[]、finish_reason=stop,直接文本回答,不调用工具
- 需要外部能力:如「2-2等于多少」→ finish_reason=tool_calls,模型下发工具调用指令,中断文本输出,等待代码执行工具
六、完整闭环工具调用(三大消息核心机制)
6.1 三类消息完整闭环
没有 ToolMessage = 工具调用未完成,模型永远不知道工具执行结果。所以需要将这3个消息一起交给大模型才算真正的工具调用!
6.2 ToolMessage 核心字段(缺一不可)
- content:工具执行结果文本
- name:对应调用的工具名
- tool_call_id:必须和 AIMessage 工具调用ID完全匹配,防止多工具并发错乱
AIMessage 是模型下发的「工具执行指令」,ToolMessage 就是程序执行完工具后,带回给模型的「工具回执 + 运算结果」
6.3 完整可运行闭环代码
from typing import Annotated
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
#定义工具
@tool
def add(a: Annotated[int, …, "第一个整数"],
b: Annotated[int, …, "第二个整数"]
)->int:
"""
两数相加
"""
return a+b
@tool
def sub(a: Annotated[int, …, "第一个整数"],
b: Annotated[int, …, "第二个整数"]
)->int:
"""
两数相减
"""
return a-b
model = init_chat_model("deepseek-v4-flash", model_provider="deepseek")
tools = [add,sub]
#绑定工具,现在返回的model才是已经绑定工具的model,上面初始化的model并没有绑定
model_with_tools = model.bind_tools(tools=tools)
#第一种仅将[HumanMessage]发送给聊天模型进行处理
AI_MESSAGE = model_with_tools.invoke("2-2的结果是多少")
result = sub.invoke(AI_MESSAGE.tool_calls[0])
# print(model_with_tools.invoke("2-2的结果是多少"))
# print("\\n")
#第二种调用将【HumanMessage+AIMessage+ToolMessage 】消息记录发送给聊天模型
#定义消息列表
message = [
HumanMessage("2-2的结果是多少,2+2的结果是多少")
]
ai_message = model_with_tools.invoke(message)
message.append(ai_message)
#构造ToolMessage,并添加到消息列表当中
for tool_call in ai_message.tool_calls:
#selected_tool = {"add":add, "sub":sub}[tool_call["name"].lower()]#使用匿名字典,可读性太差!!!
#下面是简易的代码,适合新手
tool_map = {"add":add, "sub":sub}
tool_name = tool_call["name"].lower()
selected_tool = tool_map.get(tool_name)
if selected_tool is None:
raise ValueError(f"不存在名为{tool_name}的工具")
tool_message = selected_tool.invoke(tool_call)
message.append(tool_message)
#print(message)#下面是输出打印的内容
#[HumanMessage(content='2-2的结果是多少,2+2的结果是多少', additional_kwargs={}, response_metadata={}),
# AIMessage(content='好的,我来分别计算这两个结果。', additional_kwargs={'refusal': None, 'reasoning_content': '用户问的是2-2和2+2的结果。这两个都是简单的算术运算。我可以使用工具来计算。\\n\\n先计算2-2:使用sub工具,参数a=2, b=2\\n再计算2+2:使用add工具,参数a=2, b=2\\n\\n这两个工具调用没有依赖关系,可以同时进行。'}, response_metadata={'token_usage': {'completion_tokens': 183, 'prompt_tokens': 375, 'total_tokens': 558, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 72, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 256}, 'prompt_cache_hit_tokens': 256, 'prompt_cache_miss_tokens': 119}, 'model_provider': 'deepseek', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '99d6fbe3-2acf-4c86-98f0-686cc1f8bb4d', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run–019f3cee-bf61-7c23-846a-979658d6b28b-0', tool_calls=[{'name': 'sub', 'args': {'a': 2, 'b': 2}, 'id': 'call_00_xMvB9WDDPuqsxsfRDCuz8695', 'type': 'tool_call'}, {'name': 'add', 'args': {'a': 2, 'b': 2}, 'id': 'call_01_sOqH5UHNG85jUjPQCbqo2136', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 375, 'output_tokens': 183, 'total_tokens': 558, 'input_token_details': {'cache_read': 256}, 'output_token_details': {'reasoning': 72}}),
# ToolMessage(content='0', name='sub', tool_call_id='call_00_xMvB9WDDPuqsxsfRDCuz8695'),
# ToolMessage(content='4', name='add', tool_call_id='call_01_sOqH5UHNG85jUjPQCbqo2136')]
print(model.invoke(message).content)
输出结果:
2-2的结果是 **0**,2+2的结果是 **4**。
完美!



