LangChain V1.0 30日学习计划 --- Day 5:工具体系

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 工具调用的稳定性,本质上取决于:

  1. 输入参数的结构化(schema)
  2. 类型注解(type hints)
  3. 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)
  1. 准备(Binding / 宣告):

    • 在 agent 启动之前,平台把可用工具的 name / description / input_schema / metadata 注入到 LLM 的系统 prompt(或模型的工具注册接口)
    • 这一步确保模型“知道”工具是什么、何时该用、怎么传参
      • description 用作触发条件
      • schema 决定参数格式
      • metadata(如 sensitivityowner)不暴露给 LLM
  2. 思考(Reasoning/ Plan)

    • 模型接收到 HumanMessage(用户请求)后,基于上下文 + prompt + 工具描述决定是否需要调用工具以及选择哪个工具与参数
    • 产物:一个或多个 AIMessage(可能包含 tool_calls 字段)
    • 注意:
      • 如果没有工具调用,直接走常规回复分支(降低延迟)
      • 模型可能生成 多步计划(先校验、后查询、再格式化),这时候可能会生成多个 tool_call
  3. 生成指令(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
            }
          ]
        }
      
  4. 执行工具(Execution)

    • 执行层(Tool Executor / AgentExecutor / LangGraph 的 ToolNode)收到 tool_call,执行以下流程:
      1. 参数校验(基于 args_schema/Pydantic)
      2. 权限检查(metadata -> 鉴权策略)
      3. 幂等检测(若为副作用操作,需要 idempotency key)
      4. 选择执行模式:
        1. 同步执行:直接调用 func(**args)
        2. 异步执行:await coroutine(**args)
        3. 并行/批量执行(如果 AIMessage 中有多个 tool_calls 且可并行)。
        4. 流式执行:如果工具支持 streaming(如大文件生成),执行层需要把流片段回填给模型(见步骤 4 流式说明)。
      5. 捕获返回或异常,生成 ToolMessage
  5. 观察回填(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 附近才能生成稳定回答(取决于实现)。
  6. 最终响应(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
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) 中运行此函数,以防止阻塞异步事件循环。
  • 注意: 函数必须有类型注解 (Type Hints),否则 infer_schema=True 时会报错。
2. args_schema (核心/强烈推荐)
  • 类型: Type[BaseModel] (Pydantic v2 BaseModel)
  • 定义: 输入参数的严格定义(契约)。
  • 默认值: 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 套工具包,例如:

  • FinanceTools
  • WeatherTools
  • UserManagementTools
  • ProductQueryTools

每个工具包都是“模型的能力模块”

五、工具的关键属性

为了在工业系统里稳定运行,一个工具需要具备:

  • 明确的类型定义(强制)

  • 模型不允许传错类型

  • 清晰的 docstring(强制)

  • 模型靠这个选择工具

  • 幂等性(建议)

  • 工具失败时可重试

  • 可审计(建议)

  • 每次工具调用记录日志

  • 安全边界(强制)

  • 敏感工具必须权限检查

  • 可复用 & 可测试(自然具备)

  • 因为工具就是 Python 函数

五、实战:构建两个工具:

  1. add():计算器
  2. 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 框架中,这种模式已被标准化:

  1. AgentNode: 负责推理与决策。
  2. ToolNode: 预构建组件,自动接收 AIMessage,并行执行所有工具调用,并自动返回 ToolMessage
  3. Conditional Edge: 自动判断流程流向(是继续调用工具,还是输出最终结果)。

这种架构极大地简化了多工具、多轮次复杂 Agent 的开发难度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值