欢迎光临
我们一直在努力

LangChain 工具调用从入门到闭环:让大模型真正“动手“干活

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

一、工具调用核心概念

1.1 工具调用根本作用

赋予大语言模型(LLM)与外部世界交互的能力。

1.2 LLM 原生局限性

LLM 是封闭知识系统,天然存在短板:

  • 知识固化在训练数据中,存在时间滞后性,无法获取最新信息
  • 仅有文本生成逻辑,无法执行任何外部操作
  • 不能独立完成数学计算、实时资讯查询、数据库操作、调用第三方 API

工具调用可以彻底突破以上限制。

1.3 工具调用四大核心价值

  • 扩展模型能力边界:借助外部工具,完成模型原生无法实现的任务(数学运算、联网检索、数据库查询等)。
  • 解决信息滞后、缓解模型幻觉:通过工具获取训练集之外的实时真实数据,避免回答过时、凭空编造内容。
  • 拆解复杂任务,支撑 Agent 智能体:可将复杂需求拆分为多步骤,分步调用多工具协同完成。 示例:分析上月消费趋势 → 查数据库数据 → 代码数据分析 → 生成可视化图表。
  • 打通企业现有业务系统:将企业数据库、内部 API、业务系统封装为工具,让自然语言成为统一操作入口,提升自动化与系统集成能力。
  • 二、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 一般用列表、字典这类结构化容器存储,方便存取复杂多层数据。

    拿天气查询工具举例:当用户提问 “北京今日天气”,调用搜索接口后会产出两类数据:

  • content:精简总结文本,专门供给大模型阅读,示例:“最新查询结果显示北京今日晴天,气温区间 25 至 32 摄氏度,适合短袖出行”;
  • artifact:搜索引擎 API 返回的完整原始 JSON 数据,包含全部检索条目、网页标题、资源链接、内容摘要、排序权重、检索参数、附加统计信息等完整元数据,结构示例如下:
  • {
    "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 生成效果不理想,可通过原始 artifact 定位故障根源,区分是接口返回数据异常,还是工具内部文本解析逻辑出错;
  • 业务有数据统计、全量调用记录留存需求,artifact 可以完整保存每一次工具调用的原始返回,便于后期数据分析;
  • 支持基于原始完整数据拓展自定义后续业务逻辑,灵活拓展更多衍生功能。
  • 如果仅单一返回 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?(不可替代优势)

  • 保留原生函数:@tool 会覆盖原函数为 Tool 对象;StructuredTool 不污染原函数,方便单元测试、单独调用
  • 一个函数生成多套工具:同一底层逻辑,可定制不同 name/description 多工具,装饰器做不到
  • 完全自定义配置:不受函数名、注释限制,手动强制指定工具三要素
  • 支持动态批量生成:可读取配置文件、循环批量注册工具,适合大型工程
  • 场景选择

    • 测试、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 三类消息完整闭环

  • HumanMessage:用户原始提问(需求入口)
  • AIMessage:模型返回的工具调用指令(调用哪个工具、传什么参数)
  • ToolMessage:代码执行工具后,带回的结果回执,回填上下文给模型二次推理
  • 没有 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**。

    完美!

    七、核心终极总结

  • 工具调用本质:让封闭 LLM 具备外部交互能力,解决滞后、幻觉、能力不足问题
  • 工具三要素:工具名、工具描述、参数Schema,缺一无法被LLM正常调用
  • 四种工具定义:注释版、Annotated、Pydantic、StructuredTool,按需场景选用
  • content/artifact 分离:兼顾模型推理文本 + 业务原始数据留存
  • 完整工具调用闭环:HumanMessage → AIMessage(工具指令) → ToolMessage(结果回执) → 最终答案
  • 赞(0)
    未经允许不得转载:171主机测评 » LangChain 工具调用从入门到闭环:让大模型真正“动手“干活
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址