6. 聊天模型-工具调用
工具调用(Tool Calling / Function Calling)让大语言模型突破自身知识局限,与外部世界交互。
1. 工具调用概述
1.1 为什么需要工具调用?
大语言模型本身是一个封闭的知识系统,其能力受限于:
- 训练数据滞后性------只能回复截止日期前的知识,无法获取实时信息
- 内在文本生成逻辑------无法执行精确计算、查询数据库或调用外部 API
工具调用打破了这层壁垒,其核心作用:
| 作用 | 说明 | 示例 |
|---|---|---|
| 扩展能力边界 | 让模型完成自身无法完成的任务 | 数学计算、网络搜索、数据库查询 |
| 保证信息实时性 | 获取训练数据中不存在的最新信息 | 实时天气、股票价格、新闻动态 |
| 处理复杂任务 | 将复杂请求分解为多步骤,依次调用不同工具 | "分析上月消费趋势" → 查数据 → 分析 → 生成图表 |
| 连接现有系统 | 将企业 API 和数据库封装成工具 | 自然语言驱动的统一接口 |
1.2 工具调用的三步流程
#mermaid-svg-QIFGmzHVv1CXxvPN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QIFGmzHVv1CXxvPN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QIFGmzHVv1CXxvPN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QIFGmzHVv1CXxvPN .error-icon{fill:#552222;}#mermaid-svg-QIFGmzHVv1CXxvPN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QIFGmzHVv1CXxvPN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QIFGmzHVv1CXxvPN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QIFGmzHVv1CXxvPN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QIFGmzHVv1CXxvPN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QIFGmzHVv1CXxvPN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QIFGmzHVv1CXxvPN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QIFGmzHVv1CXxvPN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QIFGmzHVv1CXxvPN .marker.cross{stroke:#333333;}#mermaid-svg-QIFGmzHVv1CXxvPN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QIFGmzHVv1CXxvPN p{margin:0;}#mermaid-svg-QIFGmzHVv1CXxvPN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QIFGmzHVv1CXxvPN .cluster-label text{fill:#333;}#mermaid-svg-QIFGmzHVv1CXxvPN .cluster-label span{color:#333;}#mermaid-svg-QIFGmzHVv1CXxvPN .cluster-label span p{background-color:transparent;}#mermaid-svg-QIFGmzHVv1CXxvPN .label text,#mermaid-svg-QIFGmzHVv1CXxvPN span{fill:#333;color:#333;}#mermaid-svg-QIFGmzHVv1CXxvPN .node rect,#mermaid-svg-QIFGmzHVv1CXxvPN .node circle,#mermaid-svg-QIFGmzHVv1CXxvPN .node ellipse,#mermaid-svg-QIFGmzHVv1CXxvPN .node polygon,#mermaid-svg-QIFGmzHVv1CXxvPN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QIFGmzHVv1CXxvPN .rough-node .label text,#mermaid-svg-QIFGmzHVv1CXxvPN .node .label text,#mermaid-svg-QIFGmzHVv1CXxvPN .image-shape .label,#mermaid-svg-QIFGmzHVv1CXxvPN .icon-shape .label{text-anchor:middle;}#mermaid-svg-QIFGmzHVv1CXxvPN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QIFGmzHVv1CXxvPN .rough-node .label,#mermaid-svg-QIFGmzHVv1CXxvPN .node .label,#mermaid-svg-QIFGmzHVv1CXxvPN .image-shape .label,#mermaid-svg-QIFGmzHVv1CXxvPN .icon-shape .label{text-align:center;}#mermaid-svg-QIFGmzHVv1CXxvPN .node.clickable{cursor:pointer;}#mermaid-svg-QIFGmzHVv1CXxvPN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QIFGmzHVv1CXxvPN .arrowheadPath{fill:#333333;}#mermaid-svg-QIFGmzHVv1CXxvPN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QIFGmzHVv1CXxvPN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QIFGmzHVv1CXxvPN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QIFGmzHVv1CXxvPN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QIFGmzHVv1CXxvPN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QIFGmzHVv1CXxvPN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QIFGmzHVv1CXxvPN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QIFGmzHVv1CXxvPN .cluster text{fill:#333;}#mermaid-svg-QIFGmzHVv1CXxvPN .cluster span{color:#333;}#mermaid-svg-QIFGmzHVv1CXxvPN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QIFGmzHVv1CXxvPN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QIFGmzHVv1CXxvPN rect.text{fill:none;stroke-width:0;}#mermaid-svg-QIFGmzHVv1CXxvPN .icon-shape,#mermaid-svg-QIFGmzHVv1CXxvPN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QIFGmzHVv1CXxvPN .icon-shape p,#mermaid-svg-QIFGmzHVv1CXxvPN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QIFGmzHVv1CXxvPN .icon-shape .label rect,#mermaid-svg-QIFGmzHVv1CXxvPN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QIFGmzHVv1CXxvPN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QIFGmzHVv1CXxvPN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QIFGmzHVv1CXxvPN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 定义工具
绑定工具到模型
模型调用工具
返回结果
- 定义工具 ------用
@tool装饰器将 Python 函数声明为工具 - 绑定工具------将工具绑定到聊天模型
- 调用工具------模型根据用户输入决定是否调用以及调用哪个工具
2. 使用 @tool 装饰器定义工具
2.1 基础用法
LangChain 提供了 @tool 装饰器,这是自定义工具最简单的方式:
python
from langchain_core.tools import tool
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers.
Args:
a: First integer
b: Second integer
"""
return a * b
@tool 装饰器自动将函数属性映射为工具属性:
| 函数要素 | 工具属性 | 说明 |
|---|---|---|
| 函数名 | tool.name |
工具的名称 |
| 文档字符串 | tool.description |
工具的用途描述 |
| 类型提示 + 文档参数 | tool.args |
工具的输入参数 Schema |
python
print(multiply.name) # 输出: multiply
print(multiply.description) # 输出: Multiply two integers. ...
print(multiply.args) # 输出: {'a': {'title': 'A', 'type': 'integer'}, ...}
# 工具也是 Runnable 接口实例,可直接调用
result = multiply.invoke({"a": 2, "b": 3})
print(result) # 输出: 6
Warning: 文档字符串是必需的
若
@tool装饰的函数没有文档字符串,调用会报错:ValueError: Function must have a docstring if description not provided.
2.2 理解 Schema
Schema 是描述其他数据结构的声明格式,用于自动验证数据。以 JSON Schema 为例:
json
{
"type": "object",
"properties": {
"surname": { "type": "string" },
"given_name": { "type": "string" },
"birthday": { "type": "string", "format": "date" },
"address": {
"type": "object",
"properties": {
"district": { "type": "string" },
"city": { "type": "string" },
"province": { "type": "string" },
"country": { "type": "string" }
}
}
}
}
工具 Schema 同理------从函数名、类型提示和文档字符串中提取属性,声明工具的名称 、描述 和输入参数结构,供大模型理解如何调用该工具。
2.3 工具属性的作用
定义良好的工具属性,本质上是在为 LLM 编写提示词:
- 工具名称 (
name)→ 告诉 LLM 存在哪些可用工具 - 工具描述 (
description)→ 告诉 LLM 每个工具的用途,决定调用哪个 - 工具参数 (
args)→ 告诉 LLM 如何正确传参
文档字符串写得越清晰,LLM 调用工具的准确度越高。
2.4 Google 风格文档字符串
Google 风格是 Python 文档字符串的主流规范,LangChain 工具 Schema 默认解析此格式:
python
def fetch_data(url: str, retries: int = 3) -> dict:
"""从给定的 URL 获取数据。
Args:
url (str): 要从中获取数据的 URL。
retries (int, optional): 失败时重试的次数。默认为 3。
Returns:
dict: 从 URL 解析的 JSON 响应。
"""
# ... 函数实现
3. @tool 的三种定义模式
3.1 模式一:基础模式(依赖文档字符串)
仅依赖 @tool + 函数文档字符串,由装饰器自动提取工具属性:
python
@tool
def add(a: int, b: int) -> int:
"""两数相加
Args:
a: 第一个整数
b: 第二个整数
"""
return a + b
此模式要求:
- 函数必须有文档字符串 (提取为
description) - 参数必须有类型提示 (提取为
args的type) - 建议使用 Google 风格文档提供参数描述
3.2 模式二:Pydantic 模式(依赖 args_schema)
当不希望文档字符串过长,或需要更精确的 Schema 控制时,可使用 Pydantic 类定义参数 Schema:
python
from pydantic import BaseModel, Field
from langchain_core.tools import tool
class AddInput(BaseModel):
"""两数相加"""
a: int = Field(..., description="第一个整数")
b: int = Field(..., description="第二个整数")
@tool(args_schema=AddInput)
def add(a: int, b: int) -> int:
return a + b
简单介绍一下pydantic:Pydantic 是 Python 生态中一个非常流行的数据验证和序列化库 。它的核心思想是:用 Python 类型注解来定义数据结构,然后在运行时自动校验数据是否符合定义。
在这个类中有一些关键的组件:
| 组件 | 说明 |
|---|---|
BaseModel |
Pydantic 的基石类,继承它即可获得自动类型校验、序列化(.model_dump())、JSON Schema 导出(.model_json_schema())等能力 |
Field() |
字段元数据函数,支持 description(字段描述)、default(默认值)、... / Ellipsis(必填标记)、ge/le(数值范围)、pattern(正则)等约束 |
| 类型系统 | 利用 Python 类型注解声明字段类型------str、int、float、bool、list[str]、Optional[T]、Literal[...]、嵌套 BaseModel 等 |
| 类型校验 | 构造实例时自动校验每个字段的类型和约束,不合法数据直接抛出 ValidationError,无需手写 if isinstance(...) |
| 序列化 | .model_dump() → dict;.model_dump_json() → JSON 字符串;最重要的 .model_json_schema() → JSON Schema → LangChain 用它注入提示词,告诉 LLM 工具参数格式 |
在本例中,这些组件协同工作的流程是:
#mermaid-svg-wuK93kGTNyj35Bn2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wuK93kGTNyj35Bn2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wuK93kGTNyj35Bn2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wuK93kGTNyj35Bn2 .error-icon{fill:#552222;}#mermaid-svg-wuK93kGTNyj35Bn2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wuK93kGTNyj35Bn2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wuK93kGTNyj35Bn2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wuK93kGTNyj35Bn2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wuK93kGTNyj35Bn2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wuK93kGTNyj35Bn2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wuK93kGTNyj35Bn2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wuK93kGTNyj35Bn2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wuK93kGTNyj35Bn2 .marker.cross{stroke:#333333;}#mermaid-svg-wuK93kGTNyj35Bn2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wuK93kGTNyj35Bn2 p{margin:0;}#mermaid-svg-wuK93kGTNyj35Bn2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wuK93kGTNyj35Bn2 .cluster-label text{fill:#333;}#mermaid-svg-wuK93kGTNyj35Bn2 .cluster-label span{color:#333;}#mermaid-svg-wuK93kGTNyj35Bn2 .cluster-label span p{background-color:transparent;}#mermaid-svg-wuK93kGTNyj35Bn2 .label text,#mermaid-svg-wuK93kGTNyj35Bn2 span{fill:#333;color:#333;}#mermaid-svg-wuK93kGTNyj35Bn2 .node rect,#mermaid-svg-wuK93kGTNyj35Bn2 .node circle,#mermaid-svg-wuK93kGTNyj35Bn2 .node ellipse,#mermaid-svg-wuK93kGTNyj35Bn2 .node polygon,#mermaid-svg-wuK93kGTNyj35Bn2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wuK93kGTNyj35Bn2 .rough-node .label text,#mermaid-svg-wuK93kGTNyj35Bn2 .node .label text,#mermaid-svg-wuK93kGTNyj35Bn2 .image-shape .label,#mermaid-svg-wuK93kGTNyj35Bn2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-wuK93kGTNyj35Bn2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wuK93kGTNyj35Bn2 .rough-node .label,#mermaid-svg-wuK93kGTNyj35Bn2 .node .label,#mermaid-svg-wuK93kGTNyj35Bn2 .image-shape .label,#mermaid-svg-wuK93kGTNyj35Bn2 .icon-shape .label{text-align:center;}#mermaid-svg-wuK93kGTNyj35Bn2 .node.clickable{cursor:pointer;}#mermaid-svg-wuK93kGTNyj35Bn2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wuK93kGTNyj35Bn2 .arrowheadPath{fill:#333333;}#mermaid-svg-wuK93kGTNyj35Bn2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wuK93kGTNyj35Bn2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wuK93kGTNyj35Bn2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wuK93kGTNyj35Bn2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wuK93kGTNyj35Bn2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wuK93kGTNyj35Bn2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wuK93kGTNyj35Bn2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wuK93kGTNyj35Bn2 .cluster text{fill:#333;}#mermaid-svg-wuK93kGTNyj35Bn2 .cluster span{color:#333;}#mermaid-svg-wuK93kGTNyj35Bn2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wuK93kGTNyj35Bn2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wuK93kGTNyj35Bn2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-wuK93kGTNyj35Bn2 .icon-shape,#mermaid-svg-wuK93kGTNyj35Bn2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wuK93kGTNyj35Bn2 .icon-shape p,#mermaid-svg-wuK93kGTNyj35Bn2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wuK93kGTNyj35Bn2 .icon-shape .label rect,#mermaid-svg-wuK93kGTNyj35Bn2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wuK93kGTNyj35Bn2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wuK93kGTNyj35Bn2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wuK93kGTNyj35Bn2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} BaseModel
定义数据结构
Field
附加描述与约束
类型注解
声明字段类型
model_json_schema()
自动生成 JSON Schema
@tool(args_schema=...)
注入为工具参数定义
LLM 根据 Schema
决定如何调用工具
逐行拆解:
| 代码 | 作用 |
|---|---|
class AddInput(BaseModel) |
继承 Pydantic 的 BaseModel,声明这是一个数据模型类,自动获得类型验证能力 |
"""两数相加""" |
类的文档字符串 → 充当 工具的描述 (tool.description),因为函数体没有文档字符串了 |
a: int |
Pydantic 字段的类型注解 → 声明 a 必须是整数,传字符串会报错 |
= Field(...) |
Pydantic 的 Field() 函数,给字段附加元信息 |
...(三个点) |
表示该字段为必填 (Pydantic 中的 Ellipsis 字面量),没有默认值 |
description="第一个整数" |
字段的描述 → 成为工具参数 Schema 中 a 的 description,LLM 借此理解参数含义 |
python
@tool(args_schema=AddInput) # 告诉 @tool:用 AddInput 代替函数签名来生成 Schema
def add(a: int, b: int) -> int:
return a + b # 函数体可以只关注逻辑,不需要文档字符串
用图示来看数据流动:
#mermaid-svg-SfarXW5q9Z2IryN6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SfarXW5q9Z2IryN6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SfarXW5q9Z2IryN6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SfarXW5q9Z2IryN6 .error-icon{fill:#552222;}#mermaid-svg-SfarXW5q9Z2IryN6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SfarXW5q9Z2IryN6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SfarXW5q9Z2IryN6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SfarXW5q9Z2IryN6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SfarXW5q9Z2IryN6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SfarXW5q9Z2IryN6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SfarXW5q9Z2IryN6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SfarXW5q9Z2IryN6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SfarXW5q9Z2IryN6 .marker.cross{stroke:#333333;}#mermaid-svg-SfarXW5q9Z2IryN6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SfarXW5q9Z2IryN6 p{margin:0;}#mermaid-svg-SfarXW5q9Z2IryN6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SfarXW5q9Z2IryN6 .cluster-label text{fill:#333;}#mermaid-svg-SfarXW5q9Z2IryN6 .cluster-label span{color:#333;}#mermaid-svg-SfarXW5q9Z2IryN6 .cluster-label span p{background-color:transparent;}#mermaid-svg-SfarXW5q9Z2IryN6 .label text,#mermaid-svg-SfarXW5q9Z2IryN6 span{fill:#333;color:#333;}#mermaid-svg-SfarXW5q9Z2IryN6 .node rect,#mermaid-svg-SfarXW5q9Z2IryN6 .node circle,#mermaid-svg-SfarXW5q9Z2IryN6 .node ellipse,#mermaid-svg-SfarXW5q9Z2IryN6 .node polygon,#mermaid-svg-SfarXW5q9Z2IryN6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SfarXW5q9Z2IryN6 .rough-node .label text,#mermaid-svg-SfarXW5q9Z2IryN6 .node .label text,#mermaid-svg-SfarXW5q9Z2IryN6 .image-shape .label,#mermaid-svg-SfarXW5q9Z2IryN6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-SfarXW5q9Z2IryN6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SfarXW5q9Z2IryN6 .rough-node .label,#mermaid-svg-SfarXW5q9Z2IryN6 .node .label,#mermaid-svg-SfarXW5q9Z2IryN6 .image-shape .label,#mermaid-svg-SfarXW5q9Z2IryN6 .icon-shape .label{text-align:center;}#mermaid-svg-SfarXW5q9Z2IryN6 .node.clickable{cursor:pointer;}#mermaid-svg-SfarXW5q9Z2IryN6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SfarXW5q9Z2IryN6 .arrowheadPath{fill:#333333;}#mermaid-svg-SfarXW5q9Z2IryN6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SfarXW5q9Z2IryN6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SfarXW5q9Z2IryN6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SfarXW5q9Z2IryN6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SfarXW5q9Z2IryN6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SfarXW5q9Z2IryN6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SfarXW5q9Z2IryN6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SfarXW5q9Z2IryN6 .cluster text{fill:#333;}#mermaid-svg-SfarXW5q9Z2IryN6 .cluster span{color:#333;}#mermaid-svg-SfarXW5q9Z2IryN6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SfarXW5q9Z2IryN6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SfarXW5q9Z2IryN6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-SfarXW5q9Z2IryN6 .icon-shape,#mermaid-svg-SfarXW5q9Z2IryN6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SfarXW5q9Z2IryN6 .icon-shape p,#mermaid-svg-SfarXW5q9Z2IryN6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SfarXW5q9Z2IryN6 .icon-shape .label rect,#mermaid-svg-SfarXW5q9Z2IryN6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SfarXW5q9Z2IryN6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SfarXW5q9Z2IryN6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SfarXW5q9Z2IryN6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AddInput 类
文档串 : 工具描述
Field() : 各参数描述
@tool 提取 Schema
LLM 看到:
add(a: 第一个整数, b: 第二个整数)
描述:两数相加
LLM 知道何时调用以及怎么传参
关键点:
args_schema参数指定输入参数的 Pydantic 模型- 函数体可以省略文档字符串(通过 Pydantic 类的文档字符串提供工具描述)
Field(..., description=...)中的...表示该字段为必填(无默认值),比如上面的a和b都有...说明a,b必填- Pydantic 在运行时提供数据类型验证
3.3 模式三:Annotated 模式(依赖类型注解)
最简洁的方式------直接在类型注解中嵌入参数描述:
python
from langchain_core.tools import tool
from typing_extensions import Annotated
@tool
def add(
a: Annotated[int, ..., "第一个整数"],
b: Annotated[int, ..., "第二个整数"],
) -> int:
"""两数相加
Args:
a: 第一个整数
b: 第二个整数
"""
return a + b
Annotated 是 Python 的类型注解扩展,格式为 Annotated[类型, 默认值, 描述],LangChain 会自动提取其中的描述信息填充到工具参数的 description 字段。
三种模式生成的工具效果相同,选择哪种取决于编码风格偏好和参数复杂度。
4. 使用 StructuredTool 类定义工具
除了 @tool 装饰器,LangChain 还提供了 StructuredTool 类的 from_function() 类方法来创建工具。这种方式更加灵活,适合在工具需要更多配置时使用。
4.1 from_function() 基础用法
StructuredTool.from_function() 的核心参数:
| 参数 | 说明 |
|---|---|
func |
要包装为工具的 Python 函数 |
name |
工具名称,默认为函数名 |
description |
工具描述,默认为函数文档字符串 |
args_schema |
输入参数的 Pydantic Schema,默认为 None |
response_format |
响应格式,"content" 或 "content_and_artifact" |
parse_docstring |
是否解析文档字符串,默认 False |
最基本的用法是将一个普通 Python 函数包装为工具:
python
from langchain_core.tools import StructuredTool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
calculator_tool = StructuredTool.from_function(func=multiply)
print(calculator_tool.invoke({"a": 2, "b": 3})) # 输出: 6
使用 from_function() 同样要求函数具备:
- 函数名 →
tool.name - 文档字符串 →
tool.description - 类型提示 + 文档参数 →
tool.args
这三个属性是工具 Schema 的核心组成,缺一不可。
4.2 自定义名称、描述与参数
可以通过参数显式指定工具的名称、描述和输入参数 Schema:
python
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
class CalculatorInput(BaseModel):
a: int = Field(description="first number")
b: int = Field(description="second number")
def multiply(a: int, b: int) -> int:
return a * b
calculator_tool = StructuredTool.from_function(
func=multiply,
name="calculator", # 自定义工具名
description="两数相乘", # 自定义工具描述
args_schema=CalculatorInput, # 自定义参数 Schema
)
print(calculator_tool.name) # 输出: calculator
print(calculator_tool.description) # 输出: 两数相乘
print(calculator_tool.args) # 输出参数 Schema(含 description)
Tip:
@toolvsStructuredTool
@tool装饰器本质上是StructuredTool的语法糖。选择哪种方式取决于编码风格------装饰器更简洁,StructuredTool.from_function()在需要动态创建工具或更多配置项时更灵活。
4.3 响应格式:content_and_artifact
默认情况下,工具的输出是一个 ToolMessage 的 content 属性。但在某些场景下,我们希望区分:
- content ------ 给 LLM 阅读的结构化文本(简洁、精炼)
- artifact ------ 给后续组件使用的原始数据(完整、详细)
例如,一个天气查询工具:
- content:"今天北京晴,气温 25°C~32°C"
- artifact:搜索引擎 API 返回的完整 JSON 响应(含多条搜索结果、链接、摘要等)
这样设计的优势:
- 调试与追溯 ------ content 不符合预期时,可通过 artifact 分析是工具解析问题还是 API 问题
- 来源可查 ------ 需要引用具体链接或来源时,artifact 保留了完整数据
- 数据可复用 ------ 后续 Chain 中的组件可读取 artifact 做进一步分析
启用方式是在 from_function() 中设置 response_format="content_and_artifact",并让工具函数返回 (content, artifact) 元组:
python
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from typing import Tuple, List
class CalculatorInput(BaseModel):
a: int = Field(description="first number")
b: int = Field(description="second number")
def multiply(a: int, b: int) -> Tuple[str, List[int]]:
nums = [a, b]
content = f"{nums}相乘的结果是{a * b}"
return content, nums
对于函数multiply定义的核心是 返回一个 (content, artifact) 元组,背后是两个不同的消费对象:
| 返回部分 | 类型 | 给谁看 | 内容特点 |
|---|---|---|---|
content |
str |
LLM 阅读,生成最终回复 | 简洁、自然语言,如 "[2, 3]相乘的结果是6" |
artifact |
List[int] |
开发者/下游组件 使用 | 原始、完整,如 [2, 3],供调试或后续处理 |
再看具体的代码行:
python
nums = [a, b] # 保留原始输入,作为 artifact
content = f"{nums}相乘的结果是{a * b}" # 组装 LLM 易读的文本
return content, nums # 返回 (给LLM看的, 给开发者留的)
函数签名 -> Tuple[str, List[int]] 明确声明了返回的就是这样一个双用途元组 。当 StructuredTool.from_function(response_format="content_and_artifact") 检测到这个模式时,会自动将元组的第一个元素作为 ToolMessage.content,第二个元素放入 ToolMessage.artifact,两者互不干扰。
然后是绑定工具的代码,与普通用法一致:
python
calculator_tool = StructuredTool.from_function(
func=multiply,
name="Calculator",
description="两数相乘",
args_schema=CalculatorInput,
response_format="content_and_artifact"
)
直接调用工具时,只会返回 content 部分:
python
print(calculator_tool.invoke({"a": 2, "b": 3}))
# 输出: [2, 3]相乘的结果是6
若需同时看到 content 和 artifact,需要模拟 LLM 的调用姿态(传入 name、args、type、id):
python
result = calculator_tool.invoke({
"name": "Calculator",
"args": {"a": 2, "b": 3},
"type": "tool_call", # 必填,表示这是一个工具调用请求
"id": "123", # 必填,关联请求与结果
})
print(result)
# ToolMessage(content='[2, 3]相乘的结果是6', artifact=[2, 3], ...)
可以看到,返回的 ToolMessage 中 content 供 LLM 阅读理解,artifact 则保留原始数据供后续组件或人工分析使用。
Note:
@tool也支持response_format
@tool装饰器同样接受response_format参数,用法与StructuredTool.from_function()一致。
5. 绑定工具到聊天模型
工具定义完成后,需要通过 bind_tools() 方法将工具绑定到聊天模型。
5.1 bind_tools() 方法
python
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from typing_extensions import Annotated
@tool
def add(a: Annotated[int, ..., "第一个整数"],
b: Annotated[int, ..., "第二个整数"]) -> int:
"""两数相加"""
return a + b
@tool
def multiply(a: Annotated[int, ..., "第一个整数"],
b: Annotated[int, ..., "第二个整数"]) -> int:
"""两数相乘"""
return a * b
model = ChatOpenAI(model="gpt-4o-mini")
model_with_tools = model.bind_tools(tools=[add, multiply])
bind_tools() 返回一个新的 Runnable 实例(而非修改原模型),支持与 invoke() 链式调用。其重要参数:
| 参数 | 说明 |
|---|---|
tools |
工具定义列表,支持 BaseTool、Pydantic 类、Python 函数、字典 |
tool_choice |
控制工具选择行为 |
strict |
是否保证模型输出与工具 Schema 完全匹配 |
parallel_tool_calls |
是否允许并行工具调用(默认启用) |
5.2 tool_choice 参数
tool_choice 控制模型在何时调用工具:
| 取值 | 行为 |
|---|---|
"auto"(默认) |
模型根据语义自动决定是否调用工具 |
"none" |
不调用任何工具 |
"any" 或 "required" 或 True |
强制调用至少一个工具 |
"<tool_name>" |
强制调用指定名称的工具 |
False 或 None |
无效果,使用模型默认行为 |
自动选择(默认)------模型根据问题语义判断是否需要调用工具:
python
# 与计算相关 → 会调用工具
result = model_with_tools.invoke("2乘3等于多少?")
# AIMessage 中包含 tool_calls 信息
# 与计算无关 → 不会调用工具
result = model_with_tools.invoke("你是谁?")
# AIMessage.content 直接返回文本回答
强制选择------无论是否相关,都要求模型调用工具:
python
model_forced = model.bind_tools(tools=[add, multiply], tool_choice="any")
result = model_forced.invoke("你是谁?")
# 即使问题与工具无关,模型也会强制选择一个工具调用
6. 完整工具调用流程
工具调用的完整流程分为以下步骤:
#mermaid-svg-wBp83afbxdw22fSR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wBp83afbxdw22fSR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wBp83afbxdw22fSR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wBp83afbxdw22fSR .error-icon{fill:#552222;}#mermaid-svg-wBp83afbxdw22fSR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wBp83afbxdw22fSR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wBp83afbxdw22fSR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wBp83afbxdw22fSR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wBp83afbxdw22fSR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wBp83afbxdw22fSR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wBp83afbxdw22fSR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wBp83afbxdw22fSR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wBp83afbxdw22fSR .marker.cross{stroke:#333333;}#mermaid-svg-wBp83afbxdw22fSR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wBp83afbxdw22fSR p{margin:0;}#mermaid-svg-wBp83afbxdw22fSR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wBp83afbxdw22fSR .cluster-label text{fill:#333;}#mermaid-svg-wBp83afbxdw22fSR .cluster-label span{color:#333;}#mermaid-svg-wBp83afbxdw22fSR .cluster-label span p{background-color:transparent;}#mermaid-svg-wBp83afbxdw22fSR .label text,#mermaid-svg-wBp83afbxdw22fSR span{fill:#333;color:#333;}#mermaid-svg-wBp83afbxdw22fSR .node rect,#mermaid-svg-wBp83afbxdw22fSR .node circle,#mermaid-svg-wBp83afbxdw22fSR .node ellipse,#mermaid-svg-wBp83afbxdw22fSR .node polygon,#mermaid-svg-wBp83afbxdw22fSR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wBp83afbxdw22fSR .rough-node .label text,#mermaid-svg-wBp83afbxdw22fSR .node .label text,#mermaid-svg-wBp83afbxdw22fSR .image-shape .label,#mermaid-svg-wBp83afbxdw22fSR .icon-shape .label{text-anchor:middle;}#mermaid-svg-wBp83afbxdw22fSR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wBp83afbxdw22fSR .rough-node .label,#mermaid-svg-wBp83afbxdw22fSR .node .label,#mermaid-svg-wBp83afbxdw22fSR .image-shape .label,#mermaid-svg-wBp83afbxdw22fSR .icon-shape .label{text-align:center;}#mermaid-svg-wBp83afbxdw22fSR .node.clickable{cursor:pointer;}#mermaid-svg-wBp83afbxdw22fSR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wBp83afbxdw22fSR .arrowheadPath{fill:#333333;}#mermaid-svg-wBp83afbxdw22fSR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wBp83afbxdw22fSR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wBp83afbxdw22fSR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wBp83afbxdw22fSR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wBp83afbxdw22fSR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wBp83afbxdw22fSR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wBp83afbxdw22fSR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wBp83afbxdw22fSR .cluster text{fill:#333;}#mermaid-svg-wBp83afbxdw22fSR .cluster span{color:#333;}#mermaid-svg-wBp83afbxdw22fSR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wBp83afbxdw22fSR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wBp83afbxdw22fSR rect.text{fill:none;stroke-width:0;}#mermaid-svg-wBp83afbxdw22fSR .icon-shape,#mermaid-svg-wBp83afbxdw22fSR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wBp83afbxdw22fSR .icon-shape p,#mermaid-svg-wBp83afbxdw22fSR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wBp83afbxdw22fSR .icon-shape .label rect,#mermaid-svg-wBp83afbxdw22fSR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wBp83afbxdw22fSR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wBp83afbxdw22fSR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wBp83afbxdw22fSR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户问题
调用绑定工具的模型
AIMessage 含 tool_calls
解析 tool_calls
根据 name 匹配实际工具
调用工具生成 ToolMessage
组装消息列表
再次调用模型
最终自然语言回答
6.1 获取工具选择结果
当模型决定调用工具时,返回的 AIMessage 中会包含 tool_calls 字段:
python
ai_msg = model_with_tools.invoke("2乘3等于多少?6加6等于多少?")
print(ai_msg.tool_calls)
# [
# {'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': 'call_xxx', 'type': 'tool_call'},
# {'name': 'add', 'args': {'a': 6, 'b': 6}, 'id': 'call_yyy', 'type': 'tool_call'}
# ]
调用 model_with_tools.invoke 之后返回的 AIMessage 中,tool_calls 字段描述了大语言模型调用工具的情况
每个 tool_call 包含:
name------ 要调用的工具名称args------ 工具参数id------ 工具调用标识符,用于关联请求与结果type------ 固定为"tool_call"
6.2 构造 ToolMessage
模型可能一次请求就触发多个工具调用(比如"2乘3等于多少?6加6等于多少?"会同时触发 multiply 和 add),因此需要用 循环 逐个执行:
根据 tool_calls 中的信息,调用对应的实际工具,并将结果包装为 ToolMessage:
python
# 名称到工具的映射字典
tool_map = {"add": add, "multiply": multiply}
for tool_call in ai_msg.tool_calls: # 遍历每个工具调用
selected_tool = tool_map[tool_call["name"].lower()] # 按名称找到实际工具函数
tool_msg = selected_tool.invoke(tool_call) # 执行工具,拿到结果
message.append(tool_msg) # 把结果追加到消息列表
# tool_msg 是 ToolMessage 类型
# - content: 工具执行结果(如 "6")
# - name: 工具名称
# - tool_call_id: 与请求 id 对应
逐条拆解:
| 代码 | 作用 |
|---|---|
for tool_call in ai_msg.tool_calls |
遍历 tool_calls 列表(可能有多条),每次取出一条工具调用 |
tool_map[tool_call["name"].lower()] |
从映射字典中,根据工具名称(如 "multiply")找到对应的实际 Python 函数 |
selected_tool.invoke(tool_call) |
真正执行工具 ------把 tool_call(含 name、args、id)传给函数,得到 ToolMessage 结果 |
message.append(tool_msg) |
将执行结果追加到消息列表,后续模型需要看到所有 ToolMessage 才能给出最终回答 |
6.3 消息列表与模型最终回复
将整个对话过程组装为消息列表,重新发送给模型,让模型给出自然语言回答:
python
from langchain_core.messages import HumanMessage
# 1. 用户问题 -> HumanMessage
message = [HumanMessage("2乘3等于多少?6加6等于多少?")]
# 2. 模型选择 -> AIMessage(含 tool_calls)
ai_msg = model_with_tools.invoke(message)
message.append(ai_msg)
# 3. 执行工具 -> ToolMessage
for tool_call in ai_msg.tool_calls:
selected_tool = tool_map[tool_call["name"].lower()]
tool_msg = selected_tool.invoke(tool_call)
message.append(tool_msg)
# 4. 最终回复 -> 模型总结生成自然语言
final_answer = model_with_tools.invoke(message)
print(final_answer.content)
# 输出: "2乘3等于6,6加6等于12。"
Question: 为什么模型不直接调用工具?
这是 LangChain 的一个核心设计原则 :模型只负责"思考",不负责"执行" 。
原因有三:
- 安全------如果模型能直接执行代码,它就可以删除文件、发送邮件、调用付费 API。把执行权交给你,你就能控制实际发生的操作。
- 模型本质上不能执行 ------LLM 是一个文本预测引擎,它没有权限访问你的文件系统、数据库或网络。它只能告诉你它想调用什么工具、传什么参数,由你的代码去落地执行。
- 可观测性 ------把"模型选择"和"工具执行"拆成两步,你可以在中间插入日志、调试、权限检查。如果模型一步到位,你就完全失控了。
可以理解为:模型是下指令的将军 ,你是执行命令的士兵------将军说"调用 multiply,参数是 (2,3)",你去算出 6,把结果报告给将军,他再根据结果做下一步决策。
6.4 完整代码示例
python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from typing_extensions import Annotated
# 1. 定义工具
@tool
def add(a: Annotated[int, ..., "第一个整数"],
b: Annotated[int, ..., "第二个整数"]) -> int:
"""两数相加"""
return a + b
@tool
def multiply(a: Annotated[int, ..., "第一个整数"],
b: Annotated[int, ..., "第二个整数"]) -> int:
"""两数相乘"""
return a * b
# 2. 绑定工具
model = ChatOpenAI(model="gpt-4o-mini")
model_with_tools = model.bind_tools(tools=[add, multiply])
# 3. 准备消息并调用
message = [HumanMessage("2乘3等于多少?6加6等于多少?")]
ai_msg = model_with_tools.invoke(message)
message.append(ai_msg)
# 4. 执行工具
tool_map = {"add": add, "multiply": multiply}
for tool_call in ai_msg.tool_calls:
selected_tool = tool_map[tool_call["name"].lower()]
tool_msg = selected_tool.invoke(tool_call)
message.append(tool_msg)
# 5. 获取最终回答
final = model_with_tools.invoke(message)
print(final.content)
# 输出: "2乘3等于6,6加6等于12。"
Warning: 注意
tool_choice的影响如果在绑定工具时设置了
tool_choice="any"强制调用工具,在最后一步(将消息列表发给模型生成最终回答)时,模型可能仍会强制调用工具而非给出自然语言回复。因此,最终回答步骤应使用未强制绑定tool_choice的模型实例,或单独创建一个不带tool_choice限制的绑定。
总结
本节完整学习了 LangChain 工具调用的全流程:
- 工具调用的价值------突破 LLM 封闭知识系统,实现实时查询、数据处理和系统集成
- 工具的三要素 ------名称(
name)、描述(description)、参数(args)构成工具 Schema @tool装饰器------三种模式(基础 / Pydantic / Annotated)将 Python 函数转换为工具StructuredTool.from_function()------在不使用装饰器时创建工具的另一种方式,支持自定义配置response_format="content_and_artifact"------分离 LLM 可读的 content 和留给后续组件的 artifactbind_tools()------将工具绑定到聊天模型,通过tool_choice控制调用策略- 完整调用流程------模型选择工具 → 执行工具 → 组装消息列表 → 模型生成最终回答