LangChain 工具定义与工具调用全流程

目录

一、什么是工具
工具调用根本作用是让大语言模型(LLM)具备与外部世界交互的能力。
LLM 本身是一个封闭的知识系统,其能力受限于其训练数据(存在滞后性)和内在的文本生成逻辑。它无法执行直接计算、查询实时信息、操作数据库或调用任何外部 API。工具调用打破了这层壁垒,其作用具体体现在:
- 扩展能力边界:模型可以借助工具完成它自身无法完成的任务,如执行数学计算、搜索网络、查询数据库等。
- 保证信息实时性:通过调用搜索工具或数据库查询工具,LLM 可以获取最新的、训练数据中不存在的信息,避免回答过时或 “一本正经地胡说八道”。
- 处理复杂任务:将一个复杂的用户请求(如 “分析我上个月的消费趋势”)分解成多个步骤,并依次调用不同的工具(如 “从数据库获取数据”-> “用 Python 进行数据分析” -> “生成图表”)来协同完成。协调这件事这更体现在 Agent 智能体上。
- 连接现有系统:可以将企业内部已有的系统、API 和数据库封装成工具,让 LLM 成为一个用自然语言驱动的统一接口,极大地提升了自动化和集成能力
总的来说:大模型本身是静态知识,不能联网、不能查数据库、不能调用接口。Tool(工具)就是给大模型扩展外部能力的组件,让模型可以发出调用请求,由本地代码执行真实逻辑,再把结果返回给大模型继续回答。
在 LangChain 中,聊天模型提供了额外的功能:工具调用。它能使 LLM 与外部服务、API 和数据库进行交互。工具调用还可用于从非结构化数据中提取结构化信息并执行各种其他任务。
例如,当我们希望获取当前天气情况时,由于 LLM 无法获取实时信息,此时我们就可以借助工具,通过外部服务进行搜索完成查询:

再例如,当我们希望获取数据库表中的数据时,由于 LLM 无法直接获取表数据,此时我们就可以借助工具,通过与数据库交互完成查询:

二、创建工具
2.1 使用 @tool 装饰器创建工具
在 LangChain 中,实现了一个 @tool 装饰器来创建工具,@tool 装饰器是自定义工具的最简单方法。如下所示:
from langchain_core.tools import import tool
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers.
Args:
a: First integer
b: Second integer
"""
return a * b
print(multiply.invoke({"a": 2, "b": 3})) # 输出: 6
print(multiply.name) # 输出: multiply
print(multiply.description) # 输出: Multiply two ...省略...b: Second integer
print(multiply.args) # 输出: {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}

可以看出,工具通过 @tool 加 Python 函数实现,其中:
- 该装饰器默认使用函数名称作为工具名称。
- 该装饰器将使用函数的文档字符串作为工具的描述。
因此,函数名、类型提示和文档字符串都是传递给工具 Schema 的一部分,不可缺失。定义良好的描述是使模型良好运行的重要部分。
对于工具 schema,它将从函数名、类型提示和文档字符串中获取相关属性,以此来声明一个工具,包括其名称、描述、输入参数、输出类型等等。这里需要说明的是,若是简单定义工具,如上述示例,工具 schema 需要解析 Google 风格的文档字符串去获取参数描述。
什么是 Google 风格的文档字符串?Google 风格是 Python 文档字符串的一种写作规范。它并非 Python 语言官方强制要求,而是由 Google 为其内部 Python 项目制定的规范,后来因为其极高的可读性和简洁性而在整个 Python 社区中变得非常流行。它使用 Args:,Returns: 等关键字,参数描述简洁明了,如下所示:
def fetch_data(url, retries=3):
"""从给定的URL获取数据。
Args:
url (str): 要从中获取数据的URL。
retries (int, optional): 失败时重试的次数。默认为3。
Returns:
dict: 从URL解析的JSON响应。
"""
# ... 函数实现 ...
综上所述,工具的属性主要分为三大类:工具名称,工具参数,工具描述。
工具名称告诉大语言模型有哪些工具可以调用,工具参数告诉模型如何来进行调用,工具描述就类似于给大模型写提示词,告诉大模型工具的功能让模型在相应的业务场景选择对应的工具完成业务处理。
所以我们也可以说,当定义工具的时候工具名称,工具参数,工具描述是必须要定义的。除了在定义python函数的时候通过文档字符串的方式写明工具描述。也可以通过其他类与方法依赖 Pydantic 类与Annotated就是其中之一:
2.1.1 依赖 Pydantic 类
若使用 @tool 定义工具时,没有提供文档字符串,则会报错:
from langchain_core.tools import tool
@tool
def add(a: int, b: int) -> int:
return a + b

此时,在 LangChain 中,可以使用 Pydantic 类,提供运行时数据验证和类型检查。通过 Field(description="...") 添加字段描述,LangChain 会自动提取。
from pydantic import BaseModel, Field
class AddInput(BaseModel):
"""Add two integers."""
a: int = Field(..., description="First integer")
b: int = Field(..., description="Second integer")
完整代码:
# pydantic 数据验证
from pydantic import BaseModel, Field
from langchain_core.tools import tool
class AddInput(BaseModel):
"""Add two integers."""
a: int = Field(..., description="First integer")
b: int = Field(..., description="Second integer")
# 定义工具
@tool(args_schema=AddInput)
def add(a: int, b: int) -> int:
# 未提供描述
return a + b
print(add.invoke({"a": 2, "b": 3}))
print(add.name)
print(add.description)
print(add.args)

2.1.2 依赖 Annotated
在 LangChain 中,可以依赖 Annotated 和文档字符串传递给工具 Schema。如下所示:
from langchain_core.tools import import tool
from typing_extensions import Annotated
@tool
def add(
a: Annotated[int, ..., "First integer"],
b: Annotated[int, ..., "Second integer"]
) -> int:
"""Add two integers."""
return a + b
@tool
def multiply(
a: Annotated[int, ..., "First integer"],
b: Annotated[int, ..., "Second integer"]
) -> int:
"""Multiply two integers."""
return a * b
2.2 使用 StructuredTool 创建工具
class langchain_core.tools.structured.StructuredTool 类用来初始化工具,其中 from_function 类方法通过给定的函数来创建并返回一个工具。from_function 类方法定义如下:
@classmethod
def 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"。
实例1:常规用法
对于用该类方法创建的工具,同样函数名、类型提示和文档字符串也都是传递给工具 Schema 的一部分,不可缺失。
from langchain_core.tools import StructuredTool
def add(a:int,b:int)->int:
"""两数相加
Args:
a:第一个参数
b:第二个参数
"""
return a+b
addtool=StructuredTool.from_function(func=add)
print(addtool.name)
print(addtool.invoke({"a": 2, "b": 6}))
print(addtool.func)
print(addtool.description)

示例 2:加入配置,依赖 Pydantic 类
同样的,让工具函数不提供描述、文档字符串等需要传递给工具 Schema 的内容,此时可以:
- 使用
args_schema参数,依赖 Pydantic 类定义并提供工具输入参数的 schema 属性。 - 使用
description参数,替代文档字符串中对于工具描述的 schema 属性。
from langchain_core.tools import StructuredTool
from pydantic import BaseModel,Field
class AddTool(BaseModel):
a:int = Field(...,description="这是第一个参数");
b:int = Field(...,description="这是第二个参数");
def add(a:int,b:int)->int:
return a+b
addtool=StructuredTool.from_function(
func=add,
args_schema=AddTool,
description="这是完成两数相加的工具",
name="ADD"
)
print(addtool.name)
print(addtool.invoke({"a": 2, "b": 6}))
print(addtool.func)
print(addtool.description)
print(addtool.args_schema)

示例 3:加入 response_format 配置
如果希望我们的工具区分消息内容(content)和其他工件(artifact),让大模型读取 content,而一些用来构造 content 的原始数据保存下来,若后续有一些记录、分析的步骤,就可以派上用场了,这就是 artifact。artifact 通常需要使用字典 Dict 或列表 List 保存。
接下来举个例子再来理解下。例如我们定义了一个搜索天气的 tool,若使用搜索引擎工具询问 “今天的天气如何?” 时:
- content 可能是:
“根据最新搜索结果,今天北京晴,气温在25℃到32℃之间。建议穿短袖衣物。” - artifact 可能是某搜索引擎 API 返回的完整 JSON 响应,其中包含多个搜索结果条目、每个条目的标题、链接、摘要、排名等元数据。如下所示:
# Artifact 的示例结构
{
'results': [
{
'title': '北京天气预报 - 中国天气网',
'link': 'https://weather.com.cn/...',
'snippet': '北京今天白天晴,最高气温32℃,夜间晴,最低气温25℃...'
},
{
'title': '北京实时天气 - Weather.com',
'link': 'https://www.weather.com/...',
'snippet': 'Bejing, China Weather. Mostly sunny. High 32C...'
}
# ...更多结果
],
'search_parameters': { ... },
'search_information': { ... }
}
则对于以上原生数据,无论我们今后做日志记录、分析,或自定义后续的处理都很方便。例如存在以下场景:
- 我们不仅仅想要一个总结性的答案,还想要具体的链接、来源或多个备选答案。
- 工具的 content 输出不符合你的预期,我们想查看原始数据来理解问题出在哪里(是工具解析的问题,还是 API 本身返回的问题)。
- 需要记录每次工具调用的完整原始响应,以满足数据分析的要求。
- ……
从这里就可以对比出只返回 content 无法做到这些事情。
如何做到?
我们需要在定义工具时指定 response_format="content_and_artifact" 参数,并确保我们返回一个元组 (content, artifact),代码如下
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from typing import List, Tuple
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
addtool = StructuredTool.from_function(
func=add,
args_schema=AddTool,
description="这是完成两数相加的工具",
name="ADD",
response_format="content_and_artifact"
)
print(addtool.invoke(
{
"name": "ADD",
"args": {"a": 2, "b": 6},
"type": "tool_call",
"id": 123
}
))

需要注意的是在这里调用工具需要模拟大模型调用姿势,如下所示。这将返回一个 ToolMessage:
{
"name": "ADD",
"args": {"a": 2, "b": 6},
"type": "tool_call", #必填
"id": 123 #必填
}
其中type与id是必填字段,typedef表示这次调用是一次工具调用,而id则负责将工具调用请求与工具调用结果关联。
如果我们直接使用工具参数调用工具,将只返回输出的 content 部分:
print(addtool.invoke({"a":2,"b":8}))

三、工具的绑定与调用
3.1 工具的绑定
为了实际将这些工具绑定到聊天模型,可以使用聊天模型的 .bind_tools() 方法。如下所示:
from langchain_openai import ChatOpenAI
# 定义大模型
model = ChatOpenAI(model="gpt-4o-mini")
...
# 绑定工具,返回一个 Runnable 实例
tools = [add, multiply]
model_with_tools = model.bind_tools(tools)
bind_tools () 方法定义
def bind_tools(
tools: Sequence[dict[str, Any] | type | Callable | BaseTool],
*,
tool_choice: dict | str | Literal['auto', 'none', 'required', 'any'] | bool | None = None,
strict: bool | None = None,
parallel_tool_calls: bool | None = None,
**kwargs: Any,
) -> Runnable[PromptValue | str | Sequence[BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]], BaseMessage]
请求参数
tools:绑定到此聊天模型的工具定义列表。支持的类型为:字典、pydantic.BaseModel 类、Python 函数和 BaseTool(如@tool装饰器创建的类)。
tool_choice(默认空):要求模型调用哪个工具。可以设置为:
- 形式为
'<<tool_name>>'的 str:调用<tool_name>工具。 'auto':自动选择工具(包括无工具)。'none':不调用工具。'any'或'required'或True:强制调用至少一个工具。False或None:无效果,默认 OpenAI 的行为。
strict(默认空)如果为 True,则保证模型输出与工具定义中提供的 JSON Schema 完全匹配。输入也将根据提供的 Schema 进行验证。如果为 False,则不会验证输入,也不会验证模型输出。如果为 None,则不会将 strict 参数传递给模型。
parallel_tool_calls:默认为 None,允许并行工具使用。设置为 False 以禁用并行工具。
kwargs(Any):任何附加参数都直接传递给 bind()。
返回值:返回一个 Runnable 实例。
3.2 工具的调用
通过 .bind_tools() 方法我们可知,它返回了一个 Runnable 实例,因此我们可以使用该 Runnable 实例,调用 .invoke() 方法,完成工具调用。示例如下:
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from typing import List, Tuple
class AddTool(BaseModel):
a: int = Field(..., description="这是第一个参数");
b: int = Field(..., description="这是第二个参数");
class MulTool(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
def Mul(a: int, b: int) -> Tuple[str, List[int]]:
nums=[a,b]
content=f"{nums}运算后的结果是{a*b}"
return content, nums
addtool = StructuredTool.from_function(
func=add,
args_schema=AddTool,
description="这是完成两数相加的工具",
name="ADD",
response_format="content_and_artifact"
)
Multool = StructuredTool.from_function(
func=Mul,
args_schema=MulTool,
description="这是完成两数相乘的工具",
name="Mul",
response_format="content_and_artifact"
)
model = ChatDeepSeek(
model="deepseek-chat", # 或 "deepseek-reasoner"
)
tools=[addtool,Multool]
model_with_tools=model.bind_tools(tools=tools)
print(model_with_tools.invoke("2+3等于多少?"))
这里我们定义了两个工具addtool与Multool分别完成了两数相加与两数相乘的运算,并将两个工具绑定到了model聊天模型并返回了一个Runable实例我们用model_with_tools来接受。接着我们传递给大模型一个问题“"2+3等于多少?”并运行model_with_tools实例我们就会得到这样一个运行结果(AIMessage):
content='' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 58, 'prompt_tokens': 380, 'total_tokens': 438, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 380}, 'model_provider': 'deepseek', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'a26a7955944dc5c60445bff77fac9c8e', 'id': '19475149-fa40-403b-a052-872d0b48479f', 'finish_reason': 'tool_calls', 'logprobs': None} id='lc_run--01a033f1-7102-7d90-8bc1-9506449bd099-0' tool_calls=[{'name': 'ADD', 'args': {'a': 2, 'b': 3}, 'id': 'call_00_MIN1pIWaDzy9f0rLoxtY3928', 'type': 'tool_call'}] invalid_tool_calls=[] usage_metadata={'input_tokens': 380, 'output_tokens': 58, 'total_tokens': 438, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}
AIMessage:来自 AI 的消息。从聊天模型返回,作为对提示(输入)的响应。
content:消息的内容。additional_kwargs:与消息关联的其他有效负载数据。对于来自 AI 的消息,可能包括模型提供程序编码的工具调用。response_metadata:响应元数据。例如:响应标头、logprobs、令牌计数、模型名称。
其中的tool_calls字段这样描述:
tool_calls=[{'name': 'ADD', 'args': {'a': 2, 'b': 3}, 'id': 'call_00_MIN1pIWaDzy9f0rLoxtY3928', 'type': 'tool_call'}]
这表明大模型准确识别到了我们提出的计算问题并在AIMessage中添加了一个tool_calls属性,此属性包括执行该工具所需的一切,包括工具名称和输入参数。其中name表示大模型也需要调用的工具名称args则表示需要传递给工具的参数。
同样的如果我们换一个问题,让模型计算两个数字的乘积的时候返回的tool_calls属性就会又有所不同:
print(model_with_tools.invoke("2*3等于多少?"))
tool_calls=[{'name': 'Mul', 'args': {'a': 2, 'b': 3}, 'id': 'call_00_HH8WqZWJIcn6ftg2r7xL0599', 'type': 'tool_call'}]
同样的如果我们提出的问题模型进行识别之后发现没有可以使用的自定义工具来解决时tool_calls属性则不会出现相应的工具调用元素:
print(model_with_tools.invoke("你在干嘛?"))
tool_calls=[]
但是到这里我们发现,当我们绑定工具并调用返回的model_with_tools实例时大模型并没有完全地解决问题而是告诉我们调用工具的姿态与方法并添加到tool_calls属性之中。此时,我们需要完成的是利用tool_calls中调用工具必须的字段自己完成工具的调用:
首先我们需要获取到返回的AIMessage的tool_calls属性,然后取出第0个元素也就是:
{'name': 'Mul', 'args': {'a': 2, 'b': 3}, 'id': 'call_00_HH8WqZWJIcn6ftg2r7xL0599', 'type': 'tool_call'}
其中name字段则表示应该调用哪一个工具,我们可以利用字典进行匹配如果是Mul则说明要进行乘法运算返回Multool;如果是ADD则说明要进行加法运算返回addtool。
tool_call=ai_mess.tool_calls[0]
#tool_call:{'name': 'ADD', 'args': {'a': 2, 'b': 3}, 'id': 'call_00_MIN1pIWaDzy9f0rLoxtY3928', 'type': 'tool_call'}
select_tool={"ADD":addtool,"Mul":Multool}[tool_call["name"]]
#select_tool:addtool
print(select_tool.invoke(tool_call).content)
然后我们将获取到的tool_call也就是一整个工具调用字段传递给工具就能获得到最终的ToolMessage了。

for循环处理多条tool_calls
这里我们只是实现了一条AIMessage的处理,如果用户的问题是多个呢。比如同时让model_with_tools处理两条运算请求分别是结算2和3的和与乘积:
ai_mess=model_with_tools.invoke("2+3的结果是多少?2*3的结果是多少?")
print(ai_mess)
此时打印出来的ai_mess的tool_calls属性会有两个成员一个用来调用加法运算工具另一个用来调用乘法运算工具:
tool_calls=[{'name': 'ADD', 'args': {'a': 2, 'b': 3}, 'id': 'call_00_Z9CSujEDRr7TRwJn0IWL1528', 'type': 'tool_call'}, {'name': 'Mul', 'args': {'a': 2, 'b': 3}, 'id': 'call_01_o4NIUTWQIKbgFhEiRLDO4321', 'type': 'tool_call'}]
那么我们就可以使用for循环的方式逐条获取tool_calls列表中的工具调用条目并挨个比对匹配其中的name字段最后完成工具的调用:
ai_mess=model_with_tools.invoke("2+3的结果是多少?2*3的结果是多少?")
for tool_call in ai_mess.tool_calls:
select_tool={"ADD":addtool,"Mul":Multool}[tool_call["name"]]
print(select_tool.invoke(tool_call).content)

但是这里有个问题:用户提问:"2+3 的结果是多少?2*3 的结果是多少?",我们期望模型最终输出:"2+3 的结果是 5,2*3 的结果是 6" 这类贴合用户原始问题的自然语言回答。模型识别出需要计算,生成 tool_calls 发起工具调用;工具执行完成后返回 toolmessage。但工具返回的仅仅是原始运算数值,格式和内容并不直接匹配用户需要的自然语言答案。
这时必须把工具返回的 toolmessage 再次送入大模型做二次整理。但仅仅把 toolmessage 丢给大模型是不够的:底层 API 调用本身是无状态、没有会话记忆的,大模型只拿到一堆计算结果,丢失了用户最初的原始问题上下文,不知道这组数字要对应回答什么问题,也就无法生成贴合提问场景、通顺合理的最终回答。
要理解这段话我们可以举一个例子,我们可以创建一个聊天模型并告诉大模型自己的名字然后再次提问让其说出我们的名字,此时我们就能发现大模型底层 API 调用是无状态、没有会话记忆的。所以结果就是大模型不知道我们是谁:
model = ChatDeepSeek( model="deepseek-chat", # 或 "deepseek-reasoner" ) print(model.invoke("我叫王小明")) print(model.invoke("我叫什么?"))
所以完整链路必须携带完整上下文:用户原始 query + 模型生成的 tool_call + 工具返回 toolmessage,三者一起送入大模型,模型才能把工具原始输出,映射回用户问题,组装成符合预期的自然语言回复。
代码实现如下:
message=[
HumanMessage("2+3的结果是多少?2*3的结果是多少?")
]
ai_mess=model_with_tools.invoke(message)
message.append(ai_mess)
for tool_call in ai_mess.tool_calls:
select_tool={"ADD":addtool,"Mul":Multool}[tool_call["name"]]
tool_mess=select_tool.invoke(tool_call)
message.append(tool_mess)
print(message)
print(model.invoke(message).content)
message最终内容:
[HumanMessage(content='2+3的结果是多少?2*3的结果是多少?', additional_kwargs={}, response_metadata={}), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 103, 'prompt_tokens': 386, 'total_tokens': 489, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 2}, 'model_provider': 'deepseek', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'a26a7955944dc5c60445bff77fac9c8e', 'id': '3abda4b4-4a79-4c3f-9349-7a2ae106050a', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--01a03726-86f3-79c1-89d4-9ca28a213eb9-0', tool_calls=[{'name': 'ADD', 'args': {'a': 2, 'b': 3}, 'id': 'call_00_ckywMhghy01Wqmqxt1pR8268', 'type': 'tool_call'}, {'name': 'Mul', 'args': {'a': 2, 'b': 3}, 'id': 'call_01_GB8yZFAjELboC1F8vfhS2664', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 386, 'output_tokens': 103, 'total_tokens': 489, 'input_token_details': {'cache_read': 384}, 'output_token_details': {}}), ToolMessage(content='[2, 3]运算后的结果是5', name='ADD', tool_call_id='call_00_ckywMhghy01Wqmqxt1pR8268', artifact=[2, 3]), ToolMessage(content='[2, 3]运算后的结果是6', name='Mul', tool_call_id='call_01_GB8yZFAjELboC1F8vfhS2664', artifact=[2, 3])]
运行结果:

更多推荐





所有评论(0)