Day 5:工具体系
一、工具基础:
1、什么是工具?
工具 = Agent 的超能力插槽
更完整地说,一个工具是:
- 有明确输入参数(基于类型注解自动生成 schema)
- 有明确输出格式
- 有文档描述(docstring)
- 可以被模型选择、调用、组合
- 通过 LCEL(LangChain Expression Language)作为 runnable 插入链路
在新版架构中:工具本质是一个 Runnable 对象,也就是说工具可以被直接用于:
invoke()ainvoke()batch()astream()
并且可以与 prompt、model 完整拼接:
prompt | model | tool
二、LLM 为什么需要工具?(工程原因 + 原理原因)
1、原理原因:
我们通常用 “ 幻觉 ” 和 “ 无能 ” 来概括 LLM 的原生缺陷,工具正是为了解决这两点:
| 模型能力缺口 (Pain Point) | 工具解决方案 (Solution) | 典型场景 |
|---|---|---|
| 无法获取实时信息 | 外部数据连接 | 查天气、查股价、搜新闻 |
| 逻辑计算不稳定 | 精确计算引擎 | 算术运算、符号推导 |
| 无法与物理世界交互 | 副作用执行 (Side-effects) | 发邮件、写数据库、API 提交 |
| 上下文窗口有限 | 外挂记忆检索 | RAG (VectorStore)、读取本地长文档 |
| 无法执行私有操作 | 权限隔离 | 查询内部 ERP、CRM 系统数据 |
2、工程需求(工业系统里必须这么做)
- 可控性
- 工具调用有明确日志、审计、参数
- 可以可视化模型完整推理轨迹(tracing)
- 可解释性
- 可以清楚地看到“为什么模型调用这个工具”
- 安全性
- 工具可以分级授权
- 用户不能直接让模型执行风险操作
- 稳定性
- 把确定性工作交给工具执行
- 微服务式结构提高可扩展性
- 可测试性
- 工具是函数,天然可以单测
- CI/CD 非常友好
- 模块化与复用
- 工具 = 微服务
- 工作流通过工具组合
三、工具长什么样?
在最新版的LangChain体系中,有以下三种常用的工具构建方式:
- @Tool:快速开发、迭代、验证
- StructuredTool:底层版工具定义(大项目最常用)
- Runnable:把整个业务 pipeline 封成工具
下面我们就围绕着这三点详细展开看看
**最简单:**新版工具 = 用
@tool修饰的普通 Python 函数
1、最简单形式:@Tool:
新版工具 = 用 @tool 修饰的普通 Python 函数
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> dict:
"""这就是工具描述(docstring):查询某个城市的天气。"""
return {"city": city, "temp": 28}
LangChain 会自动生成:
- 工具名:“get_weather”
- 工具描述:“docstring”,docstring 越清晰,工具选择越稳定
- 输入参数 schema(从函数参数的类型注解自动生成)
- 输出 schema(根据返回类型注解生成)
1.1、工具的 input schema 与 自动解析
LLM 工具调用的稳定性,本质上取决于:
- 输入参数的结构化(schema)
- 类型注解(type hints)
- docstring 的指令性
新版 @tool 装饰器 自动负责所有 schema 生成,只要正确书写函数和注解,它就能把工具定义成模型可调用的标准格式,接下来按两个关键点展开:
1.1.1、自动 schema(结构化输入)
不再需要手写 schema,LangChain 自动根据类型注解帮你构建结构化接口,只要写了类型注解,LangChain 就会自动把它变成 JSON schema,让模型知道“应该传哪些字段、什么类型、是否必填”,也就是说:
写了 Python → LangChain 自动生成可供 LLM 调用的接口规范
1.1.2、 自动 schema 长什么样?
假如现在自定义了一个工具:
from langchain_core.tools import tool
@tool
def convert_temp(temp: float, to: str) -> float:
"""docstring:温度转换,将 temp 转为目标单位 to(支持 C 或 F)"""
if to == "F":
return temp * 9/5 + 32
return (temp - 32) * 5/9
新版工具系统会自动生成这样的 schema:
{
"name": "convert_temp",
"description": "温度转换,将 temp 转为目标单位 to(支持 C 或 F)。",
"input_schema": {
"type": "object",
"properties": {
"temp": { "type": "number" },
"to": { "type": "string" }
},
"required": ["temp", "to"]
}
}
当 LLMLLMLLM 想要调用工具时,就会构造:
{
"tool": "convert_temp",
"temp": 36.5,
"to": "F"
}
因为 上面自动生成的 schema 已经告诉模型:
- 参数 temp 类型 → number
- 参数 to 类型 → string
- 哪些字段必填
- 工具描述是什么用途
“想调用,就必须按我的规矩来”,这样,模型完全不会乱传或漏传参数
必须要注意的一点是:工具的 input schema 全自动生成,而它的质量 100% 取决于类型注解与 docstring,如果想在工业环境避免,那就必须正确无误的写清楚:
全部参数类型,精准 docstring,明确返回类型
1.2、工具与模型绑定 & 工具链运行机制
Agent 调用工具并非黑盒,而是一个标准的 Request-Response 闭环。
1. 完整的生命周期(The Loop)
-
准备(Binding / 宣告):
- 在 agent 启动之前,平台把可用工具的 name / description / input_schema / metadata 注入到 LLM 的系统 prompt(或模型的工具注册接口)
- 这一步确保模型“知道”工具是什么、何时该用、怎么传参
- description 用作触发条件
- schema 决定参数格式
- metadata(如
sensitivity、owner)不暴露给 LLM
-
思考(Reasoning/ Plan):
- 模型接收到
HumanMessage(用户请求)后,基于上下文 + prompt + 工具描述决定是否需要调用工具以及选择哪个工具与参数 - 产物:一个或多个
AIMessage(可能包含tool_calls字段) - 注意:
- 如果没有工具调用,直接走常规回复分支(降低延迟)
- 模型可能生成 多步计划(先校验、后查询、再格式化),这时候可能会生成多个 tool_call
- 模型接收到
-
生成指令(Tool Call Generation):模型把调用意图结构化为
tool_call(单个工具调用计划),输出一个结构化消息(AIMessage中包含tool_calls字段),包含:-
name: 工具名 -
args: 参数 JSON, 必须符合工具的 JSON Schema;若结构不合,应由执行层拒绝并返回结构化错误 -
id: 是具体调用时生成的唯一调用 ID,用于把后续ToolMessage与该调用关联 -
{ "type": "ai_message", "content": "I will call tool(s).", "tool_calls": [ { "id": "tc-20251123-0001", "name": "get_weather", "args": {"city": "北京"}, "stream": false } ] }
-
-
执行工具(Execution):
- 执行层(Tool Executor / AgentExecutor / LangGraph 的 ToolNode)收到
tool_call,执行以下流程:- 参数校验(基于 args_schema/Pydantic)
- 权限检查(metadata -> 鉴权策略)
- 幂等检测(若为副作用操作,需要 idempotency key)
- 选择执行模式:
- 同步执行:直接调用
func(**args)。 - 异步执行:
await coroutine(**args)。 - 并行/批量执行(如果
AIMessage中有多个 tool_calls 且可并行)。 - 流式执行:如果工具支持 streaming(如大文件生成),执行层需要把流片段回填给模型(见步骤 4 流式说明)。
- 同步执行:直接调用
- 捕获返回或异常,生成
ToolMessage
- 执行层(Tool Executor / AgentExecutor / LangGraph 的 ToolNode)收到
-
观察回填(Observation):工具执行完后,执行层把结果封装成
ToolMessage并回填给模型。ToolMessage必需包含tool_call_id,以便模型把结果与之前的计划匹配-
ToolMessage 示例:
-
{ "type": "tool_message", "tool_call_id": "tc-20251123-0001", "content": { "result": {"city":"北京","temperature":22,"condition":"多云"}, "meta": {"runtime_ms":123, "node":"weather_service_v2"} } }
-
-
对于需要 streaming 的工具(TTS、图像生成、长文本生成),执行层可以发送一系列按序号带
is_final标记的ToolMessage分片:ToolMessage(part=1, tool_call_id=..., content_chunk=...)- 最后一个分片标
is_final=true - 模型在收到第一个分片时可以开始“增量推理”,但通常会等待
is_final附近才能生成稳定回答(取决于实现)。
-
-
最终响应(Response):模型接收
ToolMessage内容,结合上下文,生成最终回复。
2. 关键协议:Message Role
在 LangChain / OpenAI 协议中,工具交互涉及三种 Message:
HumanMessage: 用户提问。AIMessage: 模型回复(如果包含tool_calls属性,说明它请求调用工具)。ToolMessage(重点): 代表工具的执行结果。必须包含tool_call_id,否则模型无法将结果与请求对应,并且,content必须是字符串,以及tool_call_id必须对应AIMessage.tool_calls[i].id。
3、 工具与模型绑定:model.bind_tools([…])
想让模型“会用工具”,必须告诉它有哪些工具,在最新的langchain中绑定起来非常简单
LLM = ChatOpenAI(model = "")
LLM_with_tools = LLM.bind_tools([add, get_weather])
绑定后:
- 模型就“理解”了每个工具的 schema(来自类型注解)
- 模型知道工具的描述(来自 docstring)
- 模型知道各工具的参数格式
- 模型能判断“什么时候应该调用工具”
模型的行为立即发生改变:
-
绑定前:
-
它会直接回答:“北京天气大概是…”
-
绑定后:
-
它会说:“我要调用 get_weather 工具”
-
绑定什么,模型就拥有什么能力,这就是智能可插拔
4、工具元数据 :
工具元数据主要来源于:
-
name: 工具的名称(函数名)
- 例如
get_weather - 模型在对话中就会输出
"tool": "get_weather"
- 例如
-
函数 docstring
-
模型依赖这里的自然语言描述理解工具用途
-
例如:
-
@tool def get_weather(city: str) -> dict: """查询指定城市的实时天气""" -
模型会认为:
-
这个工具可解决“天气”问题
- 必要参数为
city
- 必要参数为
-
-
类型注解:
- 决定 schema 的结构,决定模型怎么拼接 JSON
-
returns(不强制,但 strongly recommended) -
coroutine / sync信息 -
extra(你可以加自定义 metadata)
6、工具链的运行机制:
这是最重要的部分,下面的五个步骤构成了整个“Agent 工具调用”的完整生命周期
步骤 1:模型识别意图(是否需要工具)
模型会根据输入判断:
- 是否需要调用工具?
- 调用哪个?
凭借:
- 绑定的 tool 列表
- 工具 docstring
- schema 参数结构
模型在脑中进行一次“工具匹配”,类似 AI 自己做 routing
例如:
用户: “帮我查一下北京天气。”
模型内部会得出:
“我这里有一个叫 get_weather 的工具,描述里说可以查城市天气,符合。”于是它决定调用工具
步骤 2:模型生成工具调用格式(JSON)
这是新版工具调用最关键的消息格式:
{
"tool": "get_weather",
"args": {"city": "北京"}
}
代码中不用写任何解析逻辑, 这条消息会被 LangChain 的 ToolExecutor 捕获
步骤 3:工具执行(Python 层面)
LangChain 自动执行:
result = get_weather(city="北京")
不需要自己解析 JSON → 字段 → Python 参数,只处理纯 Python 数据
步骤 4:工具结果回流模型(Observation)
工具执行完毕后,系统会把结果作为“工具输出”发回模型:
Tool(get_weather) returned:
{"temp": 23, "wind": "north", "qh": 46}
这一步叫:observation message(工具观察结果),模型会继续收到这个消息,然后基于它继续推理
步骤 5:模型生成最终回答
模型看到工具结果后,会像人类一样继续推理:
北京当前 23℃
风向北风
空气湿度 46%
适合出门散步
这样我们得到的是:
- 准确性:来自工具
- 表达与推理:来自模型
- 过程自动化:来自工具链机制
2、工业级 Agent 开发核心:StructuredTool 深度解析
在简单的脚本编写中,@tool 装饰器非常方便,但在构建复杂的企业级 Agent 时,我们需要更强的控制力,StructuredTool 是 LangChain 提供的“精密仪器”封装方式
相比于 @tool,它的核心优势在于将 “做什么(逻辑)” 和 “传什么(数据结构)” 彻底解耦
在正式开始讲解 StructuredToolStructuredToolStructuredTool 之前,我们先补充几个概念:
2.1、BaseModel:
BaseModel 是定义“数据结构 + 验证规则”的基类。继承它就能得到自动类型校验、数据解析、错误提示等能力。它能帮你:
- 定义 API 请求/响应体(FastAPI 自动生成 OpenAPI 文档)
- 解析外部数据(JSON、CSV、数据库行)并保证类型正确
- 配置管理(环境变量、配置文件自动加载并校验)
- 代替手动写一堆 if/try 做参数检查
只要数据不符合声明的类型或约束,Pydantic 立刻抛出清晰的 ValidationError,告诉你到底哪错了
2.2、Field:
我们可以理解为 Field 是给 BaseModel 字段“加料”的工具,提供默认值、别名、长度限制、数值范围、描述、是否冻结等几乎所有你想要的额外控制
不写 Field 时,字段只有类型和默认值,写了 Field 后,就可以控制:
- 默认值 / 默认工厂
- 别名(外部数据用 snake_case,内部用 camelCase)
- 约束(gt/lt/max_length/pattern 等)
- 是否必须、是否在输出中隐藏、是否冻结、是否已废弃
- JSON Schema 中的标题、描述、示例(让 Swagger 文档更漂亮)
常用参数:
| 参数 | 含义 | 示例 |
|---|---|---|
| gt | 值必须大于指定数值 | gt=0 (正数) |
| ge | 值必须大于或等于指定数值 | ge=18 |
| lt | 值必须小于指定数值 | lt=100 |
| le | 值必须小于或等于指定数值 | le=100 |
| min_length | 字符串或集合的最小长度 | min_length=6 (密码) |
| max_length | 字符串或集合的最大长度 | max_length=50 |
| pattern | 字符串必须匹配的正则表达式 | pattern=r"^\d+$" |
| examples | 字段的示例值(优化文档) | ["user@example.com"] |
| default | 字段的默认值 | default=10 |
| description | 关键:给 LLM 看的参数说明 | "用户的唯一ID" |
| alias | 序列化时的别名 | alias="user_name" |
2.3、Annotated:
typing.Annotated 是 Python 标准库 typing 模块中的一个核心工具,允许你在类型注解中附加任意元数据,这些元数据可以被第三方工具使用,而不改变类型本身,相当于给“给类型贴上一些你需要的标签“,让第三方工具(如 Pydantic)能读取标签实现验证、序列化、文档生成等功能
语法:
from typing import Annotated
# 基础语法:
Annotated[基础数据类型, 标签1,标签2,···]
# 实际例子
Annotated[str, "必须是邮箱"] # 纯字符串标签(最简单)
Annotated[int, Field(gt=0)] # Pydantic 约束(最常见)
Annotated[int, Gt(0), Le(100)] # annotated-types 约束(推荐,Pydantic 无关)
Annotated[list[str], Len(1, 10)] # 列表长度约束
Annotated[float, AfterValidator(lambda x: round(x, 2))]
代码示例:如何定义复杂的参数结构
# 先导包
from typing import Annotated
from datetime import datetime
from langchain_core.pydantic_v1 import BaseModel, Field, EmailStr, PositiveInt
# 核心组件导入路径
from langchain_core.tools import StructuredTool
# 继承BaseModel,构成传入数据的契约,相当于给LLM定规矩,要求必须按照契约的要求传递参数
class User(BaseModel):
# 1. 基础约束:正整数
id: PositiveInt = Field(description="用户的唯一ID")
# 2. 专用类型:邮箱格式自动校验
email: EmailStr = Field(description="用户的注册邮箱")
# 3. 复杂约束:Annotated 写法(推荐,语义更清晰)
# 加标签,告诉第三方工具,传进来的参数是字符串类型的
# 长度大于3,小于30,经过政策化处理
username: Annotated[
str,
Field(
min_length=3,
max_length=30,
pattern=r"^[a-zA-Z0-9_]+$",
examples=["john_doe"],
description="只能包含字母数字下划线"
)
]
# 4. 可选与别名
age: Annotated[int | None, Field(None, ge=0, le=120, alias="user_age")]
# 5. 默认工厂函数 (动态生成默认值)
created_at: datetime = Field(default_factory=datetime.now, frozen=True)
示例2:
class AddInputSchema(BaseModel):
a: int = Field(..., description="第一个加数")
b: int = Field(..., description="第二个加数")
1、定义:什么是 StructuredTool?
StructuredToolStructuredToolStructuredTool = 明确定义的工具类
它是一个标准化的对象,包含以下四个核心要素:
- namenamename:工具名称
- LLM 识别工具的唯一标识符(ID),必须清晰、无歧义
- descriptiondescriptiondescription:工具功能描述
- 最重要的部分,它决定了 LLM 在什么场景下决定调用该工具(相当于给 LLM 的 Prompt)
- args schemsargs~schemsargs schems: 参数信息
- 使用 Pydantic BaseModel 定义
- 作用:生成 JSON Schema 告诉 LLM 如何传参,并在工具执行前自动校验数据合法性
- func/coroutinefunc / coroutinefunc/coroutine: 函数执行逻辑
- 实际运行的 Python 函数(同步
func或异步coroutine)
- 实际运行的 Python 函数(同步
2、使用步骤详解:
第一步:定结构 (Define Schema):
通俗一点地说,这是给 LLM 立规矩的地方,通过导入和继承 BaseModel,我们告诉系统:如果想调用这个工具,就必须传 符合要求 的数据;
from typing import Annotated
from datetime import datetime
from langchain_core.pydantic_v1 import BaseModel, Field, EmailStr, PositiveInt
class User(BaseModel):
id: PositiveInt = Field(description="用户的唯一ID")
email: EmailStr = Field(description="用户的注册邮箱")
username: Annotated[
str,
Field(
min_length=3,
max_length=30,
pattern=r"^[a-zA-Z0-9_]+$",
examples=["john_doe"],
description="只能包含字母数字下划线"
)
]
age: Annotated[int | None, Field(None, ge=0, le=120, alias="user_age")]
created_at: datetime = Field(default_factory=datetime.now, frozen=True)
第二步:写逻辑(纯python函数)
这是工具的内核。LLM 传递的参数经过“规矩”(Schema)检查后,真正执行业务逻辑的地方。我们只需要构建一个纯粹的 Python 函数,例如:
def add_impl(a: int, b: int) -> int:
return a + b
注意: 函数的参数名必须与 Schema 中的字段名一一对应
第三步:做组装(实例化 StructuredToolStructuredToolStructuredTool )
这里将“规矩”和“内核”打包成 Agent 能用的“插件”
# 2. 实例化工具
add_tool = tructuredTool.from_function(
name="calculator_add", # 1. 名字:LLM 调用的句柄
description="当需要计算两个数字的和时使用此工具。", # 2. 描述:LLM 的决策依据
func=add_logic, # 3. 传内核:实际执行的代码
args_schema=AddInputSchema, # 4. 定契约:输入参数的校验标准
metadata={"owner": "payments", "sensitivity": "low"} # 5. 元数据:给系统看的标签
)
完整示例:
# tools/structured_tools.py
# 本模块演示如何使用 LangChain 的 StructuredTool 构建一个“加法”工具
# 采用现代 Pydantic V2 语法,并遵循官方最佳实践
# ─────────────── 标准库 ───────────────
import logging # 用于记录日志,便于调试与追踪
from typing import Any, Dict # 类型提示,提升代码可读性与 IDE 支持
# ─────────────── 第三方库 ───────────────
# 1. 直接从 pydantic 导入(现代版本的 LangChain 推荐方式)
from pydantic import BaseModel, Field # BaseModel 用于定义参数契约,Field 用于添加字段描述
# 2. 核心组件导入路径
from langchain_core.tools import StructuredTool # LangChain 结构化工具基类
# ─────────────── 日志配置 ───────────────
# 获取当前模块的日志记录器,方便后续打印调试信息
logger = logging.getLogger(__name__)
# ==========================================
# Step 1: 定义契约 (Schema)
# ==========================================
# 使用 Pydantic BaseModel 定义工具输入参数的结构与描述
class AddArgs(BaseModel):
"""
加法工具所需的输入参数模型
每个字段的描述会被自动注入到 Prompt 中,帮助大模型理解如何调用
"""
# 第一个加数,必填,描述会被模型看到
a: int = Field(..., description="第一个整数")
# 第二个加数,必填,描述会被模型看到
b: int = Field(..., description="第二个整数")
# ==========================================
# Step 2: 实现逻辑 (Implementation)
# ==========================================
def add_impl(a: int, b: int) -> int:
"""
执行具体的加法逻辑
该 docstring 仅供开发者阅读,大模型实际看到的是 args_schema 中的描述
"""
# 直接返回两数之和
return a + b
# ==========================================
# Step 3: 实例化工具 (Instantiation)
# ==========================================
# 【最佳实践】使用 .from_function() 工厂方法
# 相比直接使用 StructuredTool() 构造函数,它能自动处理异步协程推断
add_tool = StructuredTool.from_function(
func=add_impl, # 工具真正执行的函数
name="add", # 工具名称,模型调用时使用
description="执行两个整数相加,返回整数结果", # 给模型看的工具功能说明
args_schema=AddArgs, # 参数校验与描述模型
return_direct=False, # False:结果先回传给模型,由模型决定是否展示;True:直接展示给用户
)
# ==========================================
# Step 4: 注入元数据 (Metadata)
# ==========================================
# 在 v1.0 中,工具是 Runnable,metadata 用于链路追踪和鉴权
add_tool.metadata = {
"owner": "payments", # 工具所属团队
"sensitivity": "low" # 数据敏感度级别
}
# ─────────────── 测试入口 ───────────────
# 仅在脚本直接运行时执行,用于教学演示
if __name__ == "__main__":
import json # 用于美化打印 JSON Schema
# 打印工具名称
print(f"Tool Name: {add_tool.name}")
# 使用 Pydantic V2 推荐的方式获取并打印输入参数的 JSON Schema
schema = add_tool.args_schema.model_json_schema()
print(f"Input Schema: {json.dumps(schema, ensure_ascii=False, indent=2)}")
# 实际调用工具并打印结果
result = add_tool.invoke({'a': 10, 'b': 20})
print(f"Run Result: {result}")
StructuredTool.from_function 参数详解手册
一、方法签名
@classmethod
def from_function(
cls,
func: Optional[Callable] = None,
name: Optional[str] = None,
description: Optional[str] = None,
return_direct: bool = False,
args_schema: Optional[Type[BaseModel]] = None,
infer_schema: bool = True,
*,
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
coroutine: Optional[Callable] = None,
**kwargs: Any,
) -> StructuredTool
二、核心参数详解
以下参数决定了工具的基本行为和契约定义。
1. func (必须/推荐)
- 类型:
Callable - 定义: 工具被调用时实际执行的同步函数逻辑。
- 工程细节:
- 如果 Agent 以同步模式 (
agent.invoke) 运行,将直接执行此函数。- 如果 Agent 以异步模式 (
agent.ainvoke) 运行且没有提供coroutine参数,LangChain 会自动在线程池 (ThreadPoolExecutor) 中运行此函数,以防止阻塞异步事件循环。
- 如果 Agent 以异步模式 (
- 注意: 函数必须有类型注解 (Type Hints),否则
infer_schema=True时会报错。
2. args_schema (核心/强烈推荐)
- 类型:
Type[BaseModel](Pydantic v2BaseModel) - 定义: 输入参数的严格定义(契约)。
- 默认值:
None(如果为 None,则根据func的签名自动推断)。 - 工程细节:
- 优先级最高: 如果提供了此参数,LangChain 将忽略
func的签名,完全以args_schema作为工具的输入标准。- 最佳实践: 在生产环境中,永远显式提供此参数。自动推断虽然方便,但无法处理复杂的校验逻辑(如正则表达式、数值范围),也无法提供针对每个字段的详细描述 (
Field(..., description="..."))。
- 最佳实践: 在生产环境中,永远显式提供此参数。自动推断虽然方便,但无法处理复杂的校验逻辑(如正则表达式、数值范围),也无法提供针对每个字段的详细描述 (
3. name (可选)
- 类型:
str - 定义: 工具的唯一标识符。
- 默认值:
func.__name__(函数名)。 - 工程细节:
- 命名规范: 建议使用 蛇形命名法 (snake_case),如
search_user_data。- 模型影响: 某些 LLM(如 Claude)对工具名称有字符限制(通常只能包含字母、数字、下划线,且不能以数字开头)。不要使用空格或特殊字符。
4. description (可选)
- 类型:
str - 定义: 工具功能的自然语言描述。
- 默认值:
func.__doc__(函数的 docstring)。 - 工程细节:
- Prompt 权重: 这是 System Prompt 中工具定义的
description字段。它直接决定了模型**“在什么情况下决定调用这个工具”**。- 截断: 对于 Token 限制较小的模型,过长的 description 可能会被截断,建议精简扼要,重点描述“用途”和“触发条件”。
5. return_direct (关键控制流)
- 类型:
bool - 定义: 是否将工具的输出直接返回给用户,而跳过 LLM 的最终总结。
- 默认值:
False - 行为对比:
False(默认): Tool 输出 -> LLM (根据 Tool 结果生成自然语言) -> 用户。True: Tool 输出 -> 用户 (LLM 流程强制中断)。
- 使用场景: 生成图片、生成文件下载链接、或者由于隐私原因不希望 LLM 再次复述数据的场景。
6. coroutine (异步专用)
-
类型:
Callable(通常是async def函数) -
定义:
func的异步版本。 -
工程细节:
-
如果 Agent 使用
ainvoke运行,LangChain 会优先检查是否有coroutine。如果有,则await coroutine(...);如果没有,则将func放入线程池。- 性能优化: 在高并发 IO 场景(如爬虫、数据库查询),务必提供此参数,以实现真正的非阻塞并发。
7. infer_schema (生成控制)
-
类型:
bool -
定义: 是否尝试从函数签名中推断 Schema。
-
默认值:
True -
工程细节: 如果你没有提供
args_schema,且infer_schema=False,且函数没有类型注解,初始化会报错。
8. parse_docstring (智能解析)
-
类型:
bool -
定义: 是否解析函数的 docstring (Google Style / NumPy Style) 来提取参数描述。
-
默认值:
False -
工程细节: 如果开启,它可以读取如下格式的文档并自动填入 Schema 的 description:
Python
def add(a: int, b: int): """ Args: a: The first number. b: The second number. """
9. **kwargs (元数据注入)
- 常见 Key:
metadata(dict) - 定义: 传递给工具实例的额外属性。
- 工程细节: 这里是注入
metadata={"owner": "payment"}的地方。这些数据不会传给 LLM,仅用于 LangChain 内部的回调(Callbacks)、日志(LangSmith)和路由控制。
三、总结:什么时候用什么参数?
| 参数 | 必填性 | 建议场景 |
|---|---|---|
| func | ✅ | 任何场景 |
| args_schema | 强烈建议 | 生产环境、复杂参数、需要精确 Prompt 描述时 |
| name | 建议 | 当函数名(如 f)无法准确表达含义时 |
| description | 建议 | 当 docstring 不够清晰,或需要针对 LLM 优化指令时 |
| return_direct | 按需 | 生成图片、报表导出、不想让 LLM 废话时 |
| coroutine | 高并发建议 | 数据库操作、API 请求、RAG 检索时 |
| metadata | 建议 | 需要做权限控制、计费统计、链路追踪时 |
四、 代码对照演示
以下代码展示了所有关键参数的实际应用位置:
import asyncio
from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool
# 1. 定义契约
class SearchInput(BaseModel):
query: str = Field(..., description="搜索关键词")
# 2. 定义同步逻辑
def sync_search(query: str) -> str:
return f"Sync result for {query}"
# 3. 定义异步逻辑
async def async_search(query: str) -> str:
await asyncio.sleep(0.1) # 模拟 IO
return f"Async result for {query}"
# 4. 全参数构建
search_tool = StructuredTool.from_function(
# --- 核心参数 ---
func=sync_search, # 同步入口
name="web_search", # 自定义工具名
description="搜索互联网实时信息", # Prompt 描述
args_schema=SearchInput, # 强校验契约
return_direct=False, # 结果回流给 LLM
# --- 异步参数 ---
coroutine=async_search, # 异步入口
# --- 辅助参数 ---
infer_schema=False, # 既然给了 args_schema,就关闭自动推断
# --- 元数据 (通过 kwargs 传入) ---
metadata={
"source": "google_api",
"cost": 0.01
}
)
五、 总结:什么时候用什么参数?
| 参数 | 必填性 | 建议 | 场景 |
|---|---|---|---|
| func | ✅ | 必须 | 任何场景。 |
| args_schema | ❌ | 强烈建议 | 生产环境、复杂参数、需要精确 Prompt 描述时。 |
| name | ❌ | 建议 | 当函数名(如 f)无法准确表达含义时。 |
| description | ❌ | 建议 | 当 docstring 不够清晰,或需要针对 LLM 优化指令时。 |
| return_direct | ❌ | 按需 | 生成图片、报表导出、不想让 LLM 废话时。 |
| coroutine | ❌ | 高并发建议 | 数据库操作、API 请求、RAG 检索。 |
| metadata | ❌ | 建议 | 需要做权限控制、计费统计、链路追踪时。 |
什么是“契约”?
契约 (Contract):指的就是代码中的 args_schema
- 它向 LLM 承诺:“只要你给我符合
args_schema格式的数据(两个 int),我就一定能运行” - 在 v1.0 中,LangChain 依赖 Pydantic 将这个 Python 类转换成 OpenAI 需要的 JSON Schema
解耦 (Decoupling):
- 代码中
add_impl是纯逻辑,AddArgs是纯定义。 - 场景:假设你需要换一个更复杂的加法实现(比如调用远程云函数),你只需要修改
func=new_impl,而args_schema=AddArgs完全不用动。Agent 的 Prompt 不会发生变化,模型的行为也保持一致
3、Runnable:
将 Runnable(即 LCEL 链) 直接转化为 Tool,是 LangChain 架构中最具威力的设计模式之一。这标志着 Agent 开发从“调用简单函数”进化到了“调用复杂业务流”的阶段。
这种模式在工程上被称为 “Fat Tool, Thin Agent”(厚工具,薄 Agent),即,将一个现有的、包含复杂逻辑的 纯 Python 函数 快速转化为一个可追踪的 Runnable,再转化为 Tool
方法一:RunnableLambda + StructuredTool.from_function(适合已有纯函数)
这是最灵活、最常用的方式,适用于你已经有一个复杂 Python 函数或业务 Pipeline
步骤:
- 先用 RunnableLambdaRunnableLambdaRunnableLambda 将函数包装成 RunnableRunnableRunnable 对象
- 再用
invoke方法将其绑定到StructuredTool中的func - 示例:
from langchain_core.runnables import RunnableLambda
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
from typing import Any
# 1. 定义工具的输入契约(非常重要!Agent 靠这个生成正确的参数)
class BusinessInput(BaseModel):
user_id: str = Field(..., description="用户的唯一标识ID")
query: str = Field(..., description="用户想要查询的具体业务内容")
# 2. 编写复杂的业务逻辑(可以包含数据库、API 调用、多个 LLM 调用等)
def run_full_pipeline(input_dict: dict) -> str:
user_id = input_dict["user_id"]
query = input_dict["query"]
# 这里可以放任意复杂的逻辑
# 例如:权限检查 → 调用内部微服务 → 缓存 → 格式化输出
result = f"【模拟结果】用户 {user_id} 查询 '{query}' 已完成,数据来自内部系统。"
return result
# 3. 把普通函数包装成 Runnable
pipeline_runnable = RunnableLambda(run_full_pipeline)
# 4. 关键步骤:用 StructuredTool.from_function 包装
# 注意这里传入的是 pipeline_runnable.invoke,而不是 run_full_pipeline 本身
business_tool = StructuredTool.from_function(
func=pipeline_runnable.invoke, # 必须是 invoke 方法
name="internal_business_query",
description="查询公司内部业务系统,必须提供用户ID和具体查询内容。只有在确认用户身份后才能调用。",
args_schema=BusinessInput, # 绑定 Pydantic 输入模型
return_direct=False # 是否直接把工具结果返回给用户(通常设 False)
)
# 测试工具是否能正常调用
print(business_tool.invoke({"user_id": "U12345", "query": "近30天订单统计"}))
四、工具形态(工业化视角)
工具可以是任何行为:
- 数学计算(Calculator)
- HTTP 调用(REST / RPC)
- 数据库查询(SQL / NoSQL)
- 向量数据库检索(RAG)
- 文件系统访问
- Shell 指令执行
- 图像生成与处理
- 内部服务封装(如 CRM、ERP、OMS)
工业化通常会按业务领域拆分成 N 套工具包,例如:
FinanceToolsWeatherToolsUserManagementToolsProductQueryTools
每个工具包都是“模型的能力模块”
五、工具的关键属性
为了在工业系统里稳定运行,一个工具需要具备:
-
明确的类型定义(强制)
-
模型不允许传错类型
-
清晰的 docstring(强制)
-
模型靠这个选择工具
-
幂等性(建议)
-
工具失败时可重试
-
可审计(建议)
-
每次工具调用记录日志
-
安全边界(强制)
-
敏感工具必须权限检查
-
可复用 & 可测试(自然具备)
-
因为工具就是 Python 函数
五、实战:构建两个工具:
- add():计算器
- weather():模拟天气查询
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
import random
# ============================
# 1. 工具定义(符合V1.0)
# ============================
@tool
def add(a: int, b: int) -> int:
"""执行两个整数的加法。"""
return a + b
@tool
def get_weather(city: str) -> dict:
"""查询城市天气(模拟版)。"""
return {
"city": city,
"temperature": random.randint(15, 33),
"condition": random.choice(["晴朗", "多云", "小雨"])
}
# ============================
# 2. 模型初始化(V1.0语法)
# ============================
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools([add, get_weather])
# ============================
# 3. 工具调用处理(优化版)
# ============================
def run_v1_compatible(query: str):
"""V1.0兼容的工具调用流程"""
print(f"用户查询: {query}")
# 第一轮:模型判断
messages = [HumanMessage(content=query)]
first_response = llm_with_tools.invoke(messages)
print(f"模型第一次回复: {first_response.content}")
if first_response.tool_calls:
tool_messages = []
for tool_call in first_response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
print(f"调用工具: {tool_name}, 参数: {tool_args}")
# 执行工具
if tool_name == "add":
result = add.invoke(tool_args)
elif tool_name == "get_weather":
result = get_weather.invoke(tool_args)
else:
result = f"未知工具: {tool_name}"
print(f"工具结果: {result}")
# 使用ToolMessage(V1.0推荐)
tool_messages.append(
ToolMessage(
content=str(result),
tool_call_id=tool_call["id"]
)
)
# 第二轮:基于工具结果生成最终回复
final_messages = messages + [first_response] + tool_messages
second_response = llm_with_tools.invoke(final_messages)
print(f"最终回答: {second_response.content}")
else:
print("无需工具调用")
# 测试
if __name__ == "__main__":
run_v1_compatible("北京的天气怎么样?")
run_v1_compatible("计算 27 + 15")
- .tool_calls:
- ToolMessage():
企业级工具设计指南(Best Practices)
在生产环境构建 Agent 时,工具设计需遵循以下原则:
1. 容错处理 (Error Handling)
模型调用工具时可能会传入错误参数。工具不应直接抛出异常导致程序崩溃,而应捕获异常并返回错误描述,引导模型自我修正。
Python
@tool
def safe_divide(a: float, b: float) -> str:
"""执行除法运算 a / b"""
try:
return str(a / b)
except ZeroDivisionError:
# 返回错误信息给模型,模型通常会尝试重新调用或请求用户澄清
return "Error: 除数不能为0,请检查参数。"
except Exception as e:
return f"Error: 计算发生未知错误 {e}"
2. 单一职责原则 (Single Responsibility)
- 错误设计:
manage_user(action, id, data)—— 试图用一个工具完成增删改查,导致参数 Schema 过于复杂,模型难以正确填充。 - 正确设计: 拆分为
create_user,delete_user,update_user_email。粒度越细,模型的意图识别与路由越准确。
3. 提示词增强 (Prompt Engineering via Docstring)
Docstring 即工具的 Prompt。若模型频繁调用失败或不愿调用,应优化 Docstring:
- 增加示例 (Few-shot):在 docstring 中注明
Example: invoke(city='Beijing')。 - 明确触发条件:使用“当用户询问…时,必须使用此工具”等强指令语言。
六、技术演进:从 LCEL 到 LangGraph
(拓展知识点)
上述代码展示了手动编写循环(Loop)的过程。在最新的 LangGraph 框架中,这种模式已被标准化:
- AgentNode: 负责推理与决策。
- ToolNode: 预构建组件,自动接收
AIMessage,并行执行所有工具调用,并自动返回ToolMessage。 - Conditional Edge: 自动判断流程流向(是继续调用工具,还是输出最终结果)。
这种架构极大地简化了多工具、多轮次复杂 Agent 的开发难度。
382

被折叠的 条评论
为什么被折叠?



