LangChain使用之Tools

1、Tools概述

1.1 介绍

  • 要构建更强大的AI工程应用,只有生成文本这样的“纸上谈兵”能力自然是不够的。工具Tools不仅仅是“肢体”的延伸,更是为“大脑”插上了想象力的“翅膀”。借助工具,才能让AI应用的能力真正具备无限的可能,才能从“认识世界”走向“改变世界”。

  • Tools 用于扩展大语言模型(LLM)的能力,使其能够与外部系统、API 或自定义函数交互,从而完成仅靠文本生成无法实现的任务(如搜索、计算、数据库查询等)。

  • 特点:

    • 增强 LLM 的功能:让 LLM 突破纯文本生成的限制,执行实际操作(如调用搜索引擎、查询数据库、运行代码等)。
    • 支持智能决策:在Agent 工作流中,LLM 根据用户输入动态选择最合适的 Tool 完成任务。
    • 模块化设计:每个 Tool 专注一个功能,便于复用和组合(例如:搜索工具 + 计算工具 + 天气查询工具)。
  • LangChain 拥有大量第三方工具。请访问工具集成查看可用工具列表。https://python.langchain.com/v0.2/docs/integrations/tools/

1.2 Tool的要素

  • Tools 本质上是封装了特定功能的可调用模块,是Agent、Chain或LLM可以用来与世界互动的接口。

  • Tool 通常包含如下几个要素:

    • name:工具的名称
    • description:工具的功能描述
    • 该工具输入的JSON模式
    • 要调用的函数
    • return_direct:是否应将工具结果直接返回给用户(仅对Agent相关)
  • 实操步骤:

    • 步骤1:将name、description 和 JSON模式作为上下文提供给LLM。
    • 步骤2:LLM会根据提示词推断出需要调用哪些工具,并提供具体的调用参数信息。
    • 步骤3:用户需要根据返回的工具调用信息,自行触发相关工具的回调。

    注意:

    如果工具具有精心选择的名称、描述和JSON模式,则模型的性能将更好。

    下一章内容我们可以看到工具的调用动作可以通过Agent自主接管。

2、自定义工具

2.1 两种自定义方式

  • 第1种:使用@tool装饰器(自定义工具的最简单方式)
    • 装饰器默认使用函数名称作为工具名称,但可以通过传递字符串作为第一个参数来覆盖此设置。
    • 同时,装饰器将使用函数的文档字符串作为工具的描述,因此必须提供文档字符串。
  • 第2种:使用StructuredTool.from_function类方法
    • 这类似于@tool装饰器,但允许更多配置和同步/异步实现的规范。

2.2 几个常用属性

  • Tool由几个常用属性组成:

    属性 类型 描述
    name str 必选的,在提供给LLM或Agent的工具集中必须是唯一的。
    description str 可选但建议,描述工具的功能。LLM或Agent将使用此描述作为上下文,使用它确定工具的使用
    args_schema Pydantic BaseModel 可选但建议,可用于提供更多信息(例如,few-shot示例)或验证预期参数。
    return_direct boolean 仅对Agent相关。当为True时,在调用给定工具后,Agent将停止并将结果直接返回给用户。

2.3 具体实现

2.3.1 方式1:@tool装饰器

  • 举例1:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    from docutils.nodes import description
    from langchain_core.tools import tool, StructuredTool
    from pydantic import BaseModel


    @tool
    def add_number(a: int, b: int) -> int:
    """计算两个整数的和"""
    return a + b


    print(f"name = {add_number.name}") #默认是函数的名称
    print(f"args = {add_number.args}")
    print(f"description = {add_number.description}") #默认是函数的说明信息
    print(f"return_direct = {add_number.return_direct}") #默认值是False

    # name = add_number
    # args = {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
    # description = 计算两个整数的和
    # return_direct = False
    • 说明:return_direct参数的默认值是False。当return_direct=False时,工具执行结果会返回给Agent,让Agent决定下一步操作;而return_direct=True则会中断这个循环,直接结束流程,返回结果给用户。
  • 举例2:通过@tool的参数设置进行重置。

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


    @tool(name_or_callable="add_two_number", description="add two numbers", return_direct=True)
    def add_number(a: int, b: int) -> int:
    """计算两个整数的和"""
    return a + b


    print(f"name = {add_number.name}") #add_two_number
    print(f"args = {add_number.args}")
    print(f"description = {add_number.description}") #add two numbers
    print(f"return_direct = {add_number.return_direct}") #True

    # name = add_two_number
    # args = {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
    # description = add two numbers
    # return_direct = True

    # 调用工具
    add_number.invoke({"a": 10, "b": 20})
    # 30
  • 举例3:修改args参数的描述

    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
    from pydantic import Field
    from langchain_core.tools import tool
    from pydantic import BaseModel


    class FieldInfo(BaseModel):
    a: int = Field(description="第1个整型参数")
    b: int = Field(description="第2个整型参数")


    @tool(name_or_callable="add_two_number", description="add two numbers", return_direct=True, args_schema=FieldInfo)
    def add_number(a: int, b: int) -> int:
    """计算两个整数的和"""
    return a + b


    print(f"name = {add_number.name}") #add_two_number
    print(f"args = {add_number.args}")
    print(f"description = {add_number.description}") #add two numbers
    print(f"return_direct = {add_number.return_direct}") #True

    # name = add_two_number
    # args = {'a': {'description': '第1个整型参数', 'title': 'A', 'type': 'integer'}, 'b': {'description': '第2个整型参数', 'title': 'B', 'type': 'integer'}}
    # description = add two numbers
    # return_direct = True

2.3.2 方式2:StructuredTool的from_function()

  • StructuredTool.from_function 类方法提供了比@tool装饰器更多的可配置性,而无需太多额外的代码。

  • 举例1:

    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
    27
    from langchain_core.tools.structured import StructuredTool


    # 声明一个函数
    def search_google(query: str):
    return "最后查询的结果"


    # 定义一个工具
    search01 = StructuredTool.from_function(
    func=search_google,
    name="Search",
    description="查询google搜索引擎,并将结果返回"
    )

    print(f"name = {search01.name}")
    print(f"args = {search01.args}")
    print(f"description = {search01.description}")
    print(f"return_direct = {search01.return_direct}")

    # name = Search
    # args = {'query': {'title': 'Query', 'type': 'string'}}
    # description = 查询google搜索引擎,并将结果返回
    # return_direct = False

    search01.invoke({"query":"中美AI的发展现状"})
    # '最后查询的结果'
  • 举例2:

    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
    27
    28
    29
    30
    from langchain_core.tools.structured import StructuredTool
    from pydantic import BaseModel,Field

    class FieldInfo(BaseModel):
    query: str = Field(description="要检索的关键词")


    # 声明一个函数
    def search_google(query: str):
    return "最后查询的结果"


    # 定义一个工具
    search02 = StructuredTool.from_function(
    func=search_google,
    name="Search",
    description="查询google搜索引擎,并将结果返回",
    return_direct=True,
    args_schema=FieldInfo
    )

    print(f"name = {search02.name}")
    print(f"args = {search02.args}")
    print(f"description = {search02.description}")
    print(f"return_direct = {search02.return_direct}")

    # name = Search
    # args = {'query': {'description': '要检索的关键词', 'title': 'Query', 'type': 'string'}}
    # description = 查询google搜索引擎,并将结果返回
    # return_direct = True

2.4 工具调用举例

  • 我们通过大模型分析用户需求,判断是否需要调用指定工具。

2.4.1 举例1:大模型分析调用工具

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
27
28
29
30
31
32
33
34
35
36
# 1、获取大模型
#导入相关依赖
from langchain_community.tools import MoveFileTool
from langchain_core.messages import HumanMessage
from langchain_core.tools import StructuredTool
from langchain_core.utils.function_calling import convert_to_openai_function
import os
import dotenv
from langchain_openai import ChatOpenAI

dotenv.load_dotenv()

os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

# 定义LLM模型
chat_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 2、获取工具的列表
tools = [MoveFileTool()]

# 3、因为大模型invoke调用时,需要传入函数的列表,所以需要将工具转换为函数:convert_to_openai_function()
functions = [convert_to_openai_function(t) for t in tools]

# 4、获取消息列表
messages = [HumanMessage(content="将文件a移动到桌面")]

# 5、调用大模型(传入消息列表、工具的列表)
response = chat_model.invoke(
input=messages,
# tools = tools, #不支持
functions=functions,
)

print(response)
# content='' additional_kwargs={'function_call': {'arguments': '{"source_path":"a","destination_path":"/Users/YourUsername/Desktop/a"}', 'name': 'move_file'}, 'refusal': None} response_metadata={'token_usage': {'completion_tokens': 27, 'prompt_tokens': 76, 'total_tokens': 103, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_efad92c60b', 'id': 'chatcmpl-CDMNkGJTet5BejWsNTnkE9fpJAtxv', 'service_tier': None, 'finish_reason': 'function_call', 'logprobs': None} id='run--587f6b3c-f8a6-4bde-a26e-41d67e1a5b1c-0' usage_metadata={'input_tokens': 76, 'output_tokens': 27, 'total_tokens': 103, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}

作为对比:

1
2
3
4
5
6
7
8
9
10
11
12
# 获取消息列表
messages = [HumanMessage(content="查询一下明天北京的天气")]

# 调用大模型(传入消息列表、工具的列表)
response = chat_model.invoke(
input=messages,
# tools = tools, #不支持
functions=functions,
)

print(response)
# content='抱歉,我无法提供实时天气信息。建议您查看天气预报网站或使用天气应用程序获取最新的天气信息。' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 29, 'prompt_tokens': 76, 'total_tokens': 105, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_efad92c60b', 'id': 'chatcmpl-CDMPcUVaacw6eKtuHKMVwdKmLkZ9c', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None} id='run--ccf690d0-4b10-4808-91ff-24abfea18ceb-0' usage_metadata={'input_tokens': 76, 'output_tokens': 29, 'total_tokens': 105, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}
  • 通过上面两个测试发现,得到的AIMessage的核心属性如下:
    • 如果分析出需要调用对应的工具:
      • content:信息为空。因为大模型要调用工具,所以就不会直接返回信息给用户
      • additional_kwargs:包含function_call字段,指明具体函数调用的参数和函数名。比如:additional_kwargs={‘function_call’: {‘arguments’: ‘{“source_path”:”a”,”destination_path”:”/Users/YourUsername/Desktop/a”}’, ‘name’: ‘move_file’}, ‘refusal’: None}
    • 如果分析出不需要调用对应的工具:
      • content:信息不为空。
      • additional_kwargs:不包含function_call字段

2.4.2 调用工具说明

  • 说明:

    • 大模型与Agent的核心区别:是否涉及到工具的调用
    • 针对于大模型:仅能分析出要调用的工具,但是此工具(或函数)不能真正的执行
    • 针对于Agent:除了分析出要调用的工具之外,还可以执行具体的工具(或函数)
  • 举例:

    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
    27
    28
    29
    30
    31
    32
    33
    34
    35
    # 1、获取大模型
    #导入相关依赖
    from langchain_community.tools import MoveFileTool
    from langchain_core.messages import HumanMessage
    from langchain_core.utils.function_calling import convert_to_openai_function
    import os
    import dotenv
    from langchain_openai import ChatOpenAI

    dotenv.load_dotenv()

    os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
    os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

    # 定义LLM模型
    chat_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

    # 2、获取工具的列表
    tools = [MoveFileTool()]

    # 3、因为大模型invoke调用时,需要传入函数的列表,所以需要将工具转换为函数:convert_to_openai_function()
    functions = [convert_to_openai_function(t) for t in tools]

    # 4、获取消息列表
    messages = [HumanMessage(content="将当前目录下的文件a.txt移动到/Users/json/Desktop")]

    # 5、调用大模型(传入消息列表、工具的列表)
    response = chat_model.invoke(
    input=messages,
    # tools = tools, #不支持
    functions=functions,
    )

    print(response)
    # content='' additional_kwargs={'function_call': {'arguments': '{"source_path":"a.txt","destination_path":"/Users/json/Desktop/a.txt"}', 'name': 'move_file'}, 'refusal': None} response_metadata={'token_usage': {'completion_tokens': 27, 'prompt_tokens': 83, 'total_tokens': 110, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_efad92c60b', 'id': 'chatcmpl-CSExXXnNjl0nLzyIESGl52ajkX1jK', 'service_tier': None, 'finish_reason': 'function_call', 'logprobs': None} id='run--c94018fb-3218-400b-9b44-b215c4cd3e88-0' usage_metadata={'input_tokens': 83, 'output_tokens': 27, 'total_tokens': 110, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}

    分析下要调用哪个工具或函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import json

    if "function_call" in response.additional_kwargs:
    tool_name = response.additional_kwargs["function_call"]["name"]
    tool_args = json.loads(response.additional_kwargs["function_call"]["arguments"])
    print(f"调用工具:{tool_name} \n 参数:{tool_args}")

    else:
    print(f"模型回复:{response.content}")

    # 调用工具:move_file
    # 参数:{'source_path': 'a.txt', 'destination_path': '/Users/json/Desktop/a.txt'}

    调用对应的工具:

    1
    2
    3
    4
    5
    if "move_file" in response.additional_kwargs["function_call"]["name"]:
    tool = MoveFileTool()
    result = tool.run(tool_args) #调用工具
    print("工具执行的结果", result)
    # 工具执行的结果 File moved successfully from a.txt to /Users/json/Desktop/a.txt.