⼯具调⽤根本作⽤是让⼤语⾔模型(LLM)具备与外部世界交互的能⼒。
LLM本⾝是⼀个封闭的知识系统,其能⼒受限于其训练数据(存在滞后性)和内在的⽂本⽣成逻辑。
如果没有工具调用,它⽆法执⾏直接计算、查询实时信息、操作数据库或调⽤任何外部API。⼯具调⽤打破了这层壁垒,其
作⽤具体体现在:

  1. 扩展能⼒边界:模型可以借助⼯具完成它⾃⾝⽆法完成的任务,如执⾏数学计算、搜索⽹络、查询
    数据库等。
  2. 保证信息实时性:通过调⽤搜索⼯具或数据库查询⼯具,LLM可以获取最新的、训练数据中不存在
    的信息,避免回答过时或“⼀本正经地胡说⼋道”。
  3. 处理复杂任务:将⼀个复杂的⽤⼾请求(如“分析我上个⽉的消费趋势”)分解成多个步骤,并依
    次调⽤不同的⼯具(如“从数据库获取数据”->“⽤Python进⾏数据分析”->“⽣成图表”)
    来协同完成。协调这件事这更体现在Agent智能体上。
  4. 连接现有系统:可以将企业内部已有的系统、API和数据库封装成⼯具,让LLM成为⼀个⽤⾃然语
    ⾔驱动的统⼀接⼝,极⼤地提升了⾃动化和集成能⼒
  • 如,当我们希望获取当前天⽓情况时,由于LLM⽆法获取实时信息,此时我们就可以借助天气搜索⼯具,通过外部服务进⾏搜索完成查询
  • 再如,当我们希望获取数据库表中的数据时,由于LLM⽆法直接获取表数据,此时我们就可以借助数据库查询⼯具,通过与数据库交互完成查询

创建工具

langchain提供了几种创建工具的方式

使用@tool修饰器实现工具

LangChain提供了@tool修饰器from langchain_core.tools import tool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langchain_core.tools import tool

@tool
def multiply(a:int,b :int)->int:
""" 两个整数相乘

Args:
a: 第一个整数
b: 第二个整数
"""
return a * b

# 已经可以对工具进行调用了

print(multiply.invoke({"a": 2, "b": 3})) # 输出:6
print(multiply.name)
print(multiply.description)
print(multiply.args)

可以看出,⼯具通过@tool修饰的Python函数实现,其中:

  • 该装饰器默认使⽤函数名称作为⼯具名称
  • 该装饰器将使⽤函数的⽂档字符串作为⼯具的描述
    因此,函数名、类型提⽰和⽂档字符串都是传递给⼯具Schema的⼀部分,不可缺失。定义好的描述是使模型良好运⾏的重要部分

使用Pydantic类

我们可以先使用Pydantic类对工具的结构化输入参数和描述进行描述

这样我们就可以用结构化的类描述结构化的输入了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pydantic import BaseModel,Field

class AddInput(BaseModel):
"""两个整数相加求和"""
a: int = Field(..., description="First Integer")
b: int = Field(..., description="Second Integer")

class MulInput(BaseModel):
"""两数相乘求积"""
a: int = Field(..., description="First Integer")
b: int = Field(..., description="Second Integer")

from langchain.tools import tool

@tool(args_schema=AddInput)
def add(a:int, b:int)->int:
return a+b

@tool(args_schema=MulInput)
def mul(a:int, b:int)->int:
return a*b

使用Annotated类型

我们使用from typing_extensions import Annotated来指定工具函数的输入参数并添加描述

这样只需要声明一个函数即可

1
2
3
4
5
6
7
8
9
10
from typing_extensions import Annotated
from langchain.tools import tool

@tool
def add(
a:Annotated[int, ..., "First integer"],
b: Annotated[int, ..., "Second integer"]
):
"""两数相加"""
return a+b

借助StructuredTool.from_function函数创建

这个函数的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@classmethod
def from_function(
cls,
func: Callable | None = None,
coroutine: Callable[..., Awaitable[Any]] | None = None,
name: str | None = None,
description: str | None = None,
return_direct: bool = False, # noqa: FBT001,FBT002
args_schema: ArgsSchema | None = None,
infer_schema: bool = True, # noqa: FBT001,FBT002
*,
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

常规用法:直接使用函数创建工具

实际上基本上就仅仅是代替了@tool修饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_core.tools.structured import StructuredTool

def multiply(a:int,b :int)->int:
""" 两个整数相乘

Args:
a: 第一个整数
b: 第二个整数
"""
return a * b

mul_tool = StructuredTool.from_function(func=multiply)

print(mul_tool.invoke({"a": 2, "b": 3})) # 输出:6
print(mul_tool.name)
print(mul_tool.description)
print(mul_tool.args)

输出如下

1
2
3
4
5
6
7
8
9
10
11
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\tool.py 
6
multiply
两个整数相乘

Args:
a: 第一个整数
b: 第二个整数
{'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}

Process finished with exit code 0

依赖Pydantic类,并进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pydantic import BaseModel,Field

class MulInput(BaseModel):
"""两数相乘求积"""
a: int = Field(..., description="First Integer")
b: int = Field(..., description="Second Integer")

def mul(a:int, b:int)->int:
return a*b
from langchain_core.tools import StructuredTool

mul_tool = StructuredTool.from_function(
func=mul,
name="Calculator",
description="两数相乘求积",
args_schema=MulInput
)

print(mul_tool.invoke({"a": 2, "b": 3})) # 输出:6
print(mul_tool.name)
print(mul_tool.description)
print(mul_tool.args)

输出如下

1
2
3
4
5
6
7
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\tool.py 
6
Calculator
两数相乘求积
{'a': {'description': 'First Integer', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'Second Integer', 'title': 'B', 'type': 'integer'}}

Process finished with exit code 0

配置额外返回输出原始内容

之前的工具配置都是只会返回用于构建AI消息的content,但是如果我们需要借助工具获取原始数据进行进一步数据加工的话,我们可以在创建工具时配置函数传参response_format"content_and_artifact",即可在原本仅返回content的基础上返回artifact,即其它工件,通常数据类型为python字典或者列表。此时返回类型应该标注为元组Tuple[]

下面是一个简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pydantic import BaseModel,Field
from typing import Tuple,List

class MulInput(BaseModel):
"""两数相乘求积"""
a: int = Field(..., description="First Integer")
b: int = Field(..., description="Second Integer")

def mul(a:int, b:int)->Tuple[int,List[int]]:
nums = [a,b]
content = f"{nums}相乘的结果为{a*b}"
return content,nums
from langchain_core.tools import StructuredTool

mul_tool = StructuredTool.from_function(
func=mul,
name="Calculator",
description="两数相乘求积",
args_schema=MulInput,
response_format="content_and_artifact"
)

print(mul_tool.invoke({"a": 2, "b": 3})) # 输出只有content:6
print(mul_tool.name)
print(mul_tool.description)
print(mul_tool.args)

但是此时直接调用的输出只有content

1
2
3
4
5
6
7
8
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\tool.py 
[2, 3]相乘的结果为6
Calculator
两数相乘求积
{'a': {'description': 'First Integer', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'Second Integer', 'title': 'B', 'type': 'integer'}}

Process finished with exit code 0

若要输出artifact的内容,则需要模拟大模型调用的传参

1
2
3
4
5
6
print(mul_tool.invoke({
"name": "Calculator",
"args": {"a": 2, "b": 3},
"id": "123",
"type": "tool_call"
}))
1
content='[2, 3]相乘的结果为6' name='Calculator' tool_call_id='123' artifact=[2, 3]

绑定工具

创建完工具后就该给大模型示例绑定工具了。使用大模型实例的.bind_tool()方法即可,咱们着重了解下接口参数

1
2
3
4
5
6
7
8
9
10
def bind_tools(
self,
tools: Sequence[dict[str, Any] | type | Callable | BaseTool],
*,
tool_choice: dict | str | bool | None = None,
strict: bool | None = None,
parallel_tool_calls: bool | None = None,
response_format: _DictOrPydanticClass | None = None,
**kwargs: Any,
) -> Runnable[LanguageModelInput, AIMessage]:
  • tools:工具列表
  • tool_choice:默认为None,要求模型要使用哪些工具
    • 形为<<tool_name>>的str,调用<tool_name>工具
    • 'auto':自动选择工具(包括不使用
    • 'none'或者False或者None:不调用工具
    • any或者required或者True:强制至少使用一个工具
  • strict:默认为None
    • True:要求模型输出与工具定义中的Json Schema完全匹配,输入也将根据Schema进行验证
    • False:不验证输入或输出
    • None:不传参
  • parallel_tool_calls :是否允许并行工具的使用,默认允许,值为None
  • kwargs(Any):附加参数

返回值:一个Runnable实例,就跟前面的大模型实例是同一个类型

调用工具

前面代码不变,我们来调用一下大模型

1
2
3
4
5
6
from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="deepseek-chat",openai_api_base="https://api.deepseek.com/v1")
model_with_tool =model.bind_tools([mul_tool])

print(model_with_tool.invoke("3乘5等于多少"))

可以看到大模型返回的参数中有一段工具调用的内容,由于云端的大模型只知道要调用哪些工具,具体的执行只能交给api调用者执行,所以我们只能收到调用工具的指令,具体的调用还得自己完成,但是已经可以自动化了

强制执行

前面绑定工具列表时的tool_choice参数的某些选项能强制调用工具

工具属性

我们知道模型调用的返回值时AIMessage类型,但是如果这次返回的消息调用了工具,那它将具有一个tool_calls属性,可以调用一下并打印出来

1
2
3
4
result = model_with_tool.invoke("3乘5等于多少")
print(result.tool_calls)

# 输出[{'name': 'Calculator', 'args': {'a': 3, 'b': 5}, 'id': 'call_00_hJ7KVoqEQTwhsKGK8oAUCM6U', 'type': 'tool_call'}]

发送ToolMessage

前面可以看到,模型初次调用只告诉了我们要调用哪些工具,所以现在要调用工具实例获取ToolMessage加入到消息列表中,再通知大模型工具调用的结果。

特别的,我们可以借助result.tool_calls[index]["name"]字段来指定要调用的工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage,ToolMessage

model = ChatOpenAI(model="deepseek-chat",openai_api_base="https://api.deepseek.com/v1")
model_with_tool =model.bind_tools([mul_tool])

messages = [
HumanMessage("3乘5等于多少")
]
ai_msg = model_with_tool.invoke(messages)

messages.append(ai_msg)

for tool_call in ai_msg.tool_calls:
selected_tool = {
"Calculator":mul_tool
}
tool_msg = selected_tool[tool_call["name"]].invoke(tool_call)
messages.append(tool_msg)

print(messages)
result = model_with_tool.invoke(messages)
print(result)
print(result.content)
1
2
3
4
5
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\tool.py 
[HumanMessage(content='3乘5等于多少', additional_kwargs={}, response_metadata={}), AIMessage(content='我来帮您计算3乘5等于多少。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 28, 'prompt_tokens': 175, 'total_tokens': 203, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 128}, 'prompt_cache_hit_tokens': 128, 'prompt_cache_miss_tokens': 47}, 'model_provider': 'openai', 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '5a7b936c-ca69-4491-a231-0c34147e5af7', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--58ac78eb-d13a-484f-9f5f-af5024ff82af-0', tool_calls=[{'name': 'Calculator', 'args': {'a': 3, 'b': 5}, 'id': 'call_00_TWuelsiwNZTxJS9ih1sxXRBI', 'type': 'tool_call'}], usage_metadata={'input_tokens': 175, 'output_tokens': 28, 'total_tokens': 203, 'input_token_details': {'cache_read': 128}, 'output_token_details': {}}), ToolMessage(content='[3, 5]相乘的结果为15', name='Calculator', tool_call_id='call_00_TWuelsiwNZTxJS9ih1sxXRBI', artifact=[3, 5])]
content='3乘5等于15。' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 6, 'prompt_tokens': 216, 'total_tokens': 222, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 192}, 'prompt_cache_hit_tokens': 192, 'prompt_cache_miss_tokens': 24}, 'model_provider': 'openai', 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_ffc7281d48_prod0820_fp8_kvcache', 'id': '6027b7d0-5a81-4bff-b5bb-7ac5ad47e405', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--70c07587-8100-40a8-bc4c-b362a74ec83d-0' usage_metadata={'input_tokens': 216, 'output_tokens': 6, 'total_tokens': 222, 'input_token_details': {'cache_read': 192}, 'output_token_details': {}}
3乘5等于15。

可以看到最后它得出了乘法的答案

LangChain提供的工具

LangChain网站也提供了很多现成的工具,点这里跳转🔗

使用示例:TavilySearch

TavilySearch 类可以⽀持我们进⾏搜索,Tavily是⼀个专⻔为AI设计的搜索引擎,专为智能体检索与
推理需求量⾝打造的⼯具。

Tavily 不仅提供了⾼度可编程的API接⼝,还具备显著优于传统搜索引擎的上下⽂相关性理解能⼒。
能够以结构化、可解析的形式返回搜索结果,便于将检索到的信息直接⽤于后续的推理、⽣成或任务
执⾏流程

我们需要挂梯子去官网申请Tavily的API KEY,戳我去官网

申请好后我们将API key的值写入环境变量TAVILY_API_KEY

然后我们安装相关的工具包pip install -U langchain-tavily

然后我们编写代码进行简单的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_tavily import TavilySearch

tavily_tool = TavilySearch(max_results=4)

model = ChatOpenAI(model="deepseek-chat",openai_api_base="https://api.deepseek.com/v1")
model_with_tool =model.bind_tools([tavily_tool])

messages = [
HumanMessage("中国湖南长沙的天气怎么样")
]
ai_msg = model_with_tool.invoke(messages)

messages.append(ai_msg)

for tool_call in ai_msg.tool_calls:
tool_msg = tavily_tool.invoke(tool_call)
messages.append(tool_msg)

result = model_with_tool.invoke(messages)
print(result.content)

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
D:\program_software\AnaConda\envs\langChainP13\python.exe D:\codes\code_pycharm\langChainTool\tool.py 
根据查询结果,我为您提供湖南长沙的天气情况:

**当前天气情况:**
- 温度:6℃(阴天)
- 湿度:49%
- 风向风力:北风2级

**今日天气预报:**
- 白天:10℃,多云
- 夜间:4℃,多云

**未来几天天气趋势:**
- 明天(11月14日):晴,10℃~21℃,北风转西北风<3级
- 后天(11月15日):多云,6℃~23℃,北风<3级转5-6级
- 周日(11月16日):多云,5℃~16℃,北风4-5级转<3级

**生活指数建议:**
- 穿衣指数:较舒适,建议穿着吸湿排汗的夏装,棉麻质地的短打扮
- 感冒指数:少发
- 运动指数:适宜
- 洗车指数:适宜

总体来说,长沙近期天气以多云到晴为主,温度适中,适合外出活动。建议您根据实际体感温度适当增减衣物。

Process finished with exit code 0