第五章:工具系统与函数调用 —— 从定义到执行的完整链路

5.1 引言:为什么需要工具系统

大语言模型的核心能力是文本生成,但仅凭文本生成无法完成许多实际任务------查询数据库、调用 API、执行计算、操作文件系统。工具系统(Tool System)是连接 LLM "思考能力"与"行动能力"的桥梁。

LangChain 的工具系统建立在 Runnable 协议之上,提供了从简单函数到复杂工具链的完整抽象。本章将从源码层面解析:

  • 工具定义:四种创建工具的方式及其适用场景
  • Tool Calling 流程 :从 bind_tools()ToolMessage 的完整数据流
  • 输入多态处理:字符串、字典、ToolCall 三种输入格式的统一处理
  • 结构化输出with_structured_output() 与三种策略模式
  • 中间件增强:工具重试、调用限制等生产级能力

5.2 BaseTool:工具体系的根基

5.2.1 继承体系

所有 LangChain 工具的根类是 BaseTool,定义在 base.py

python 复制代码
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
    """Base class for all LangChain tools."""

这个类型签名揭示了两个关键设计决策:

  1. 继承 RunnableSerializable :工具是 Runnable,可以直接参与 LCEL 链式组合、使用 invoke()/ainvoke() 调用、享受回调和追踪能力
  2. 输入类型 str | dict | ToolCall:工具接受三种输入格式------简单字符串、结构化字典、标准化 ToolCall 对象

5.2.2 核心属性

BaseToolbase.py 定义了一组核心属性:

属性 类型 说明
name str 工具唯一名称,模型用此识别工具
description str 工具描述,告诉模型何时/如何使用
args_schema `ArgsSchema None`
return_direct bool True 时跳过后续 Agent 循环,直接返回结果
response_format Literal["content", "content_and_artifact"] 返回格式,支持内容+附件分离
handle_tool_error `bool str
extras `dict[str, Any] None`

其中 ArgsSchema 的类型定义(base.py)体现了灵活性:

python 复制代码
ArgsSchema = TypeBaseModel | dict[str, Any]

既接受 Pydantic BaseModel 子类(强类型验证),也接受 JSON Schema 字典(动态 schema)。

5.2.3 工具调用 schema 与注入参数过滤

BaseTool 通过 tool_call_schema 属性(base.py)为模型生成调用 schema。这个属性的关键职责是过滤掉注入参数

python 复制代码
@property
def tool_call_schema(self) -> ArgsSchema:
    full_schema = self.get_input_schema()
    fields = []
    for name, type_ in get_all_basemodel_annotations(full_schema).items():
        if not _is_injected_arg_type(type_):
            fields.append(name)
    return _create_subset_model(
        self.name, full_schema, fields, fn_description=self.description
    )

InjectedToolArg 注解标记的参数不会出现在发送给模型的 schema 中------模型不需要知道这些参数的存在,它们在运行时由框架自动注入。

5.2.4 invoke 到 run 的调用链

当工具被调用时,数据流经过以下路径(base.py):

复制代码
invoke(input, config)
  → _prep_run_args(input, config)   # 预处理:ToolCall → (tool_input, kwargs)
    → run(tool_input, **kwargs)       # 核心执行方法
      → _to_args_and_kwargs()         # 转换为位置/关键字参数
        → _parse_input()              # 参数验证(通过 args_schema)
          → _run(*args, **kwargs)     # 子类实现的实际逻辑

_prep_run_args 是入口的关键适配层------如果输入是 ToolCall 对象,它会提取 args 字典和 tool_call_id,统一转换为 run() 方法需要的格式。

5.2.5 run() 方法的完整执行管道

run() 方法(base.py)是工具执行的核心,包含完整的生命周期管理:

python 复制代码
def run(self, tool_input, ..., tool_call_id=None, **kwargs):
    # 1. 配置回调管理器
    callback_manager = CallbackManager.configure(callbacks, self.callbacks, ...)

    # 2. 过滤注入参数(不暴露给回调)
    filtered_tool_input = self._filter_injected_args(tool_input)

    # 3. 触发 on_tool_start 回调
    run_manager = callback_manager.on_tool_start(...)

    # 4. 执行核心逻辑
    try:
        child_config = patch_config(config, callbacks=run_manager.get_child())
        with set_config_context(child_config) as context:
            tool_args, tool_kwargs = self._to_args_and_kwargs(tool_input, tool_call_id)
            # 注入 run_manager 和 config(如果子类 _run 接受这些参数)
            if signature(self._run).parameters.get("run_manager"):
                tool_kwargs |= {"run_manager": run_manager}
            if config_param := _get_runnable_config_param(self._run):
                tool_kwargs |= {config_param: config}
            response = context.run(self._run, *tool_args, **tool_kwargs)

        # 5. 处理 content_and_artifact 格式
        if self.response_format == "content_and_artifact":
            content, artifact = response  # 期望二元组

    # 6. 异常处理层
    except (ValidationError, ValidationErrorV1) as e:
        content = _handle_validation_error(e, flag=self.handle_validation_error)
    except ToolException as e:
        content = _handle_tool_error(e, flag=self.handle_tool_error)

    # 7. 格式化输出并触发 on_tool_end
    output = _format_output(content, artifact, tool_call_id, self.name, status)
    run_manager.on_tool_end(output, ...)
    return output

几个设计亮点:

  • 回调注入透明化 :通过检查 _run 签名来决定是否注入 run_managerconfig,子类不需要的参数不会被强制传入
  • 异常分层处理ValidationError(参数验证失败)和 ToolException(工具业务异常)分别处理,前者可自动纠正,后者可降级为观察结果
  • _format_output :将内容统一格式化为 ToolMessage 或字符串,确保输出一致性

5.3 四种工具定义方式

5.3.1 方式一:@tool 装饰器(推荐)

@tool 装饰器定义在 convert.py,是最简洁的工具创建方式:

python 复制代码
def tool(
    name_or_callable: str | Callable | None = None,
    runnable: Runnable | None = None,
    *args: Any,
    description: str | None = None,
    return_direct: bool = False,
    args_schema: ArgsSchema | None = None,
    infer_schema: bool = True,
    response_format: Literal["content", "content_and_artifact"] = "content",
    parse_docstring: bool = False,
    error_on_invalid_docstring: bool = True,
    extras: dict[str, Any] | None = None,
) -> BaseTool | Callable[[Callable | Runnable], BaseTool]:

通过 4 个 @overload 签名(convert.py),@tool 支持四种使用模式:

模式 1:裸装饰器

python 复制代码
@tool
def search(query: str) -> str:
    """搜索互联网获取信息。"""
    return f"搜索结果: {query}"

此时 name_or_callable 接收到函数本身,走 convert.py 的分支:

python 复制代码
if callable(name_or_callable) and hasattr(name_or_callable, "__name__"):
    return _create_tool_factory(name_or_callable.__name__)(name_or_callable)

模式 2:自定义名称

python 复制代码
@tool("web_search")
def search(query: str) -> str:
    """搜索互联网。"""
    return f"搜索结果: {query}"

模式 3:带参数装饰器

python 复制代码
@tool(parse_docstring=True, response_format="content_and_artifact")
def search(query: str) -> tuple[str, dict]:
    """搜索互联网获取信息。

    Args:
        query: 搜索关键词。
    """
    return "摘要结果", {"full_results": [...]}

模式 4:包装 Runnable

python 复制代码
from langchain_core.runnables import RunnableLambda

runnable = RunnableLambda(lambda x: f"处理: {x['input']}")
search_tool = tool("process", runnable, description="处理输入数据")

核心转换逻辑在 _create_tool_factory 内部(convert.py)------当 infer_schema=True(默认)时,统一委托给 StructuredTool.from_function()

python 复制代码
if infer_schema or args_schema is not None:
    return StructuredTool.from_function(
        func, coroutine,
        name=tool_name, description=tool_description,
        return_direct=return_direct, args_schema=schema,
        infer_schema=infer_schema, response_format=response_format,
        parse_docstring=parse_docstring, extras=extras,
    )

只有当 infer_schema=False 且未提供 args_schema 时,才会退化为简单的 Tool(单字符串输入)。

5.3.2 方式二:StructuredTool.from_function()

StructuredTool 定义在 structured.py,是多参数工具的标准实现:

python 复制代码
class StructuredTool(BaseTool):
    """Tool that can operate on any number of inputs."""

    func: Callable[..., Any] | None = None
    coroutine: Callable[..., Awaitable[Any]] | None = None

from_function() 类方法(structured.py)是其核心工厂方法。当未提供 args_schema 时,它通过 create_schema_from_function() 自动推断:

python 复制代码
if args_schema is None and infer_schema:
    args_schema = create_schema_from_function(
        name, source_function,
        parse_docstring=parse_docstring,
        error_on_invalid_docstring=error_on_invalid_docstring,
        filter_args=_filter_schema_args(source_function),
    )

create_schema_from_function()base.py 中定义)的工作流程:

  1. 通过 inspect.signature() 获取函数签名
  2. 通过 get_type_hints() 获取类型注解
  3. 过滤掉 run_managercallbacks 等内部参数
  4. 过滤掉被 InjectedToolArg 标记的注入参数
  5. 用 Pydantic create_model() 动态创建 schema 类

显式 schema 的用法:

python 复制代码
from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool

class SearchInput(BaseModel):
    query: str = Field(description="搜索关键词")
    max_results: int = Field(default=5, description="最大结果数")

def search(query: str, max_results: int = 5) -> str:
    """搜索互联网获取最新信息。"""
    return f"搜索 '{query}' 的前 {max_results} 条结果"

search_tool = StructuredTool.from_function(
    func=search,
    name="web_search",
    args_schema=SearchInput,
)

5.3.3 方式三:子类化 BaseTool

适用于需要复杂初始化、状态管理或自定义验证的场景:

python 复制代码
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field

class DatabaseQueryInput(BaseModel):
    sql: str = Field(description="SQL 查询语句")
    database: str = Field(default="main", description="数据库名称")

class DatabaseQueryTool(BaseTool):
    name: str = "database_query"
    description: str = "执行 SQL 查询并返回结果"
    args_schema: type[BaseModel] = DatabaseQueryInput

    connection_string: str  # 自定义属性

    def _run(self, sql: str, database: str = "main",
             config: RunnableConfig = None,
             run_manager: CallbackManagerForToolRun | None = None) -> str:
        """执行查询。"""
        # 使用 self.connection_string 连接数据库
        return f"在 {database} 上执行: {sql}"

    async def _arun(self, sql: str, database: str = "main",
                    config: RunnableConfig = None,
                    run_manager: AsyncCallbackManagerForToolRun | None = None) -> str:
        """异步执行查询。"""
        # 异步实现
        return f"异步在 {database} 上执行: {sql}"

子类化时需要注意:_run() 是抽象方法必须实现(base.py),而 _arun() 有默认实现------如果未覆写,会自动在线程池中运行 _run()base.py)。

5.3.4 方式四:convert_runnable_to_tool()

将已有的 Runnable 转换为工具(convert.py):

python 复制代码
def convert_runnable_to_tool(
    runnable: Runnable,
    args_schema: type[BaseModel] | None = None,
    *,
    name: str | None = None,
    description: str | None = None,
    arg_types: dict[str, type] | None = None,
) -> BaseTool:

内部逻辑根据 Runnable 的输入 schema 类型做分支:

  • 如果输入 schema 是 string 类型 → 创建 Tool(简单字符串工具)
  • 如果输入 schema 是 object 类型且有 properties → 直接使用 runnable.input_schema
  • 否则 → 通过 _get_schema_from_runnable_and_arg_types() 推断 schema

最终统一通过 StructuredTool.from_function() 创建工具,内部包装了 invoke_wrapperainvoke_wrapper 来桥接 Runnable 与工具接口。

5.3.5 四种方式对比

方式 适用场景 复杂度 Schema 推断
@tool 装饰器 日常使用,函数式工具 自动
StructuredTool.from_function() 需要显式 schema 或双入口(sync+async) 手动或自动
子类化 BaseTool 有状态工具、复杂初始化 手动
convert_runnable_to_tool() 复用已有 Runnable 自动

5.4 Tool Calling 完整流程

5.4.1 流程总览

Tool Calling 是 LLM 与外部世界交互的标准化协议。完整流程如下:

复制代码
用户消息
  ↓
BaseChatModel.bind_tools(tools)   # 步骤1:绑定工具定义
  ↓
model.invoke(messages)            # 步骤2:模型推理
  ↓
AIMessage(tool_calls=[...])       # 步骤3:模型返回工具调用请求
  ↓
ToolNode / 手动执行工具           # 步骤4:执行工具
  ↓
ToolMessage(content=result)       # 步骤5:工具返回结果
  ↓
messages.append(ToolMessage)      # 步骤6:追加到消息列表
  ↓
model.invoke(messages)            # 步骤7:模型再次推理
  ↓
循环直到 AIMessage 无 tool_calls  # 步骤8:最终响应

5.4.2 步骤1:bind_tools() ------ 工具绑定

bind_tools() 定义在 BaseChatModel 上(chat_models.py):

python 复制代码
def bind_tools(
    self,
    tools: Sequence[dict[str, Any] | type | Callable | BaseTool],
    *,
    tool_choice: str | None = None,
    **kwargs: Any,
) -> Runnable[LanguageModelInput, AIMessage]:
    raise NotImplementedError

这是一个抽象接口------BaseChatModel 定义签名,各 Partner 包提供实现。以 OpenAI 为例,实现流程是:

  1. 遍历 tools 列表
  2. 对每个工具调用 convert_to_openai_tool() 转换为 OpenAI 格式
  3. 调用 self.bind(tools=openai_tools, tool_choice=tool_choice) 绑定参数

5.4.3 convert_to_openai_tool():统一 schema 转换

convert_to_openai_tool()function_calling.py)是工具 schema 转换的枢纽,支持 8 种输入格式:

python 复制代码
def convert_to_openai_tool(
    tool: Mapping[str, Any] | type[BaseModel] | Callable | BaseTool,
    *,
    strict: bool | None = None,
) -> dict[str, Any]:

转换逻辑的分支判断:

  1. 已知 OpenAI 工具类型functionfile_searchcomputerweb_search 等)→ 直接返回
  2. 自定义工具metadata.type == "custom_tool")→ 构造 {"type": "custom", "name": ..., "description": ...}
  3. 其他类型 (Pydantic、Callable、BaseTool、dict)→ 先通过 convert_to_openai_function() 转为函数 schema,再包装为 {"type": "function", "function": {...}}

5.4.4 步骤3:AIMessage 与 ToolCall

模型返回的消息中,工具调用请求存储在 AIMessage.tool_calls 字段(ai.py):

python 复制代码
class AIMessage(BaseMessage):
    tool_calls: list[ToolCall] = Field(default_factory=list)
    invalid_tool_calls: list[InvalidToolCall] = Field(default_factory=list)
    usage_metadata: UsageMetadata | None = None

ToolCall 是一个 TypedDict(content.py):

python 复制代码
class ToolCall(TypedDict):
    type: Literal["tool_call"]    # 类型鉴别器
    id: str | None                # 唯一标识符,关联请求与响应
    name: str                     # 工具名称
    args: dict[str, Any]          # 工具参数
    index: NotRequired[int | str] # 流式传输中的位置索引
    extras: NotRequired[dict]     # 供应商特定元数据

同时还有 InvalidToolCallcontent.py)用于处理模型生成错误(如无效 JSON):

python 复制代码
class InvalidToolCall(TypedDict):
    type: Literal["invalid_tool_call"]
    id: str | None
    name: str | None
    args: str | None     # 注意:是原始字符串,不是 dict
    error: str | None    # 错误描述

5.4.5 步骤5:ToolMessage ------ 工具结果载体

ToolMessagetool.py)是工具执行结果的标准化表示:

python 复制代码
class ToolMessage(BaseMessage, ToolOutputMixin):
    tool_call_id: str                              # 关联 ToolCall.id
    type: Literal["tool"] = "tool"
    artifact: Any = None                           # 不发送给模型的完整输出
    status: Literal["success", "error"] = "success"

关键设计:

  • tool_call_id :关联机制的核心------模型可以并行发起多个工具调用,每个 ToolMessage 通过 tool_call_id 与对应的 ToolCall 配对
  • artifact :分离关注点------content 是发送给模型的摘要文本,artifact 保存完整数据供程序其他部分使用
  • status:显式标记成功/失败,让模型知道工具执行是否成功
  • ToolOutputMixin :混入类标记,工具的 _run() 方法可以直接返回 ToolMessage 实例(而非字符串),框架不会再次包装

5.4.6 完整流程示例

python 复制代码
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

@tool
def get_weather(city: str) -> str:
    """获取指定城市的天气信息。"""
    return f"{city}:晴,25°C"

# 1. 绑定工具
model = ChatOpenAI(model="gpt-4o")
model_with_tools = model.bind_tools([get_weather])

# 2. 首次调用 → 模型请求工具
messages = [HumanMessage(content="北京今天天气怎么样?")]
response = model_with_tools.invoke(messages)
# response.tool_calls = [ToolCall(name="get_weather", args={"city": "北京"}, id="call_xxx")]

# 3. 执行工具
messages.append(response)
for tool_call in response.tool_calls:
    result = get_weather.invoke(tool_call)  # 传入 ToolCall 对象
    messages.append(result)  # result 是 ToolMessage

# 4. 再次调用 → 模型生成最终回复
final = model_with_tools.invoke(messages)
# final.content = "北京今天天气晴朗,温度25°C,适合外出。"

5.5 工具输入多态处理

5.5.1 三种输入格式

BaseTool.invoke() 接受三种输入格式(类型签名 str | dict | ToolCall):

  1. 字符串 :简单场景,直接作为位置参数传递给 _run()
  2. 字典 :结构化参数,经过 args_schema 验证后作为关键字参数
  3. ToolCall 对象 :模型输出的标准化格式,自动提取 argstool_call_id

5.5.2 _parse_input():输入验证核心

_parse_input() 方法(base.py)处理验证和注入参数的填充:

python 复制代码
def _parse_input(self, tool_input: str | dict, tool_call_id: str | None) -> str | dict:
    input_args = self.args_schema

    # 字符串输入:简单验证
    if isinstance(tool_input, str):
        if input_args is not None:
            key_ = next(iter(get_fields(input_args).keys()))
            input_args.model_validate({key_: tool_input})
        return tool_input

    # 字典输入:完整验证 + InjectedToolCallId 注入
    if input_args is not None:
        for k, v in get_all_basemodel_annotations(input_args).items():
            if _is_injected_arg_type(v, injected_type=InjectedToolCallId):
                tool_input[k] = tool_call_id  # 自动注入 tool_call_id
        result = input_args.model_validate(tool_input)
        # ... 构建 validated_input(含默认值和注入参数)

这里体现了 LangChain 工具系统的核心设计理念------对模型透明,对开发者便利 :模型看到的 schema 不包含注入参数,但开发者在 _run() 中可以直接使用这些参数。

5.5.3 _to_args_and_kwargs():参数转换

经过验证后,_to_args_and_kwargs()base.py)将输入转换为函数调用格式:

python 复制代码
def _to_args_and_kwargs(self, tool_input, tool_call_id):
    # 无参工具
    if not get_fields(self.args_schema):
        return (), {}
    tool_input = self._parse_input(tool_input, tool_call_id)
    # 字符串 → 位置参数
    if isinstance(tool_input, str):
        return (tool_input,), {}
    # 字典 → 关键字参数(浅拷贝以防修改)
    if isinstance(tool_input, dict):
        return (), tool_input.copy()

5.5.4 _filter_injected_args():回调保护

_filter_injected_args()base.py)在工具输入传递给回调系统时,过滤掉敏感的注入参数:

python 复制代码
def _filter_injected_args(self, tool_input: dict) -> dict:
    filtered_keys = set(FILTERED_ARGS)          # "run_manager", "callbacks"
    filtered_keys.update(self._injected_args_keys)  # 函数签名中的注入参数
    # 从 args_schema 注解中识别注入参数
    if self.args_schema is not None:
        annotations = get_all_basemodel_annotations(self.args_schema)
        for field_name, field_type in annotations.items():
            if _is_injected_arg_type(field_type):
                filtered_keys.add(field_name)
    return {k: v for k, v in tool_input.items() if k not in filtered_keys}

5.6 InjectedToolArg 注入体系

5.6.1 注入参数的设计动机

在 Agent 场景中,工具常常需要一些"运行时上下文"------当前的 tool_call_id、Agent 的状态存储、会话上下文等。这些信息不应由模型提供(模型也不知道这些),而应由框架在执行时自动注入。

5.6.2 注入类型层次

base.py 定义了注入类型的层次结构:

复制代码
InjectedToolArg                  # 基类:通过 Annotated 元数据注入
├── InjectedToolCallId           # 注入当前 tool_call_id
└── _DirectlyInjectedToolArg     # 基类:通过直接类型注解注入
    └── ToolRuntime              # 注入完整运行时上下文(state、store、context)

两种注入风格的对比:

Annotated 元数据风格InjectedToolArg):

python 复制代码
from typing import Annotated
from langchain_core.tools import tool, InjectedToolArg

@tool
def search(query: str, api_key: Annotated[str, InjectedToolArg]) -> str:
    """搜索工具,api_key 由框架注入,不暴露给模型。"""
    return f"使用 {api_key} 搜索 {query}"

直接类型注解风格_DirectlyInjectedToolArg):

python 复制代码
from langchain_core.tools import tool, ToolRuntime

@tool
def stateful_search(query: str, runtime: ToolRuntime) -> str:
    """有状态搜索,可访问 runtime.state、runtime.store 等。"""
    history = runtime.state.get("search_history", [])
    return f"搜索 {query},历史记录: {len(history)} 条"

5.6.3 检测机制

_is_injected_arg_type() 函数(base.py)负责判断参数是否是注入类型:

python 复制代码
def _is_injected_arg_type(type_, injected_type=None) -> bool:
    if injected_type is None:
        # 先检查直接注入类型(如 ToolRuntime)
        if _is_directly_injected_arg_type(type_):
            return True
        injected_type = InjectedToolArg
    # 检查 Annotated 元数据中是否包含 InjectedToolArg 实例/子类
    return any(
        isinstance(arg, injected_type)
        or (isinstance(arg, type) and issubclass(arg, injected_type))
        for arg in get_args(type_)[1:]  # 跳过第一个参数(实际类型)
    )

5.6.4 InjectedToolCallId 的实际应用

InjectedToolCallIdbase.py)是最常用的注入类型,让工具可以直接构造 ToolMessage

python 复制代码
from typing import Annotated
from langchain_core.messages import ToolMessage
from langchain_core.tools import tool, InjectedToolCallId

@tool(response_format="content_and_artifact")
def search_with_artifact(
    query: str,
    tool_call_id: Annotated[str, InjectedToolCallId],
) -> tuple[str, dict]:
    """搜索并返回完整结果。"""
    full_results = {"items": [...], "total": 100}
    summary = f"找到 100 条关于 '{query}' 的结果"
    return summary, full_results

5.7 结构化输出

5.7.1 with_structured_output()

with_structured_output()chat_models.py)让模型直接返回结构化数据:

python 复制代码
def with_structured_output(
    self,
    schema: dict[str, Any] | type,
    *,
    include_raw: bool = False,
    **kwargs: Any,
) -> Runnable[LanguageModelInput, dict[str, Any] | BaseModel]:

schema 参数支持多种格式:

  • Pydantic BaseModel → 模型输出经过 Pydantic 验证,返回实例
  • TypedDict → 返回 dict
  • JSON Schema dict → 返回 dict(无验证)
  • OpenAI function schema → 直接传递

使用示例:

python 复制代码
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI

class WeatherInfo(BaseModel):
    """天气信息。"""
    city: str = Field(description="城市名称")
    temperature: float = Field(description="温度(摄氏度)")
    condition: str = Field(description="天气状况")

model = ChatOpenAI(model="gpt-4o")
structured_model = model.with_structured_output(WeatherInfo)
result = structured_model.invoke("北京今天天气怎么样?")
# result 是 WeatherInfo 实例

5.7.2 三种结构化输出策略

LangChain 在 structured_output.py 中定义了三种策略,用于 Agent 级别的结构化输出控制:

ToolStrategystructured_output.py

通过工具调用实现结构化输出------将 schema 转换为一个"虚拟工具",让模型通过 tool call 返回结构化数据:

python 复制代码
@dataclass(init=False)
class ToolStrategy(Generic[SchemaT]):
    schema: type[SchemaT] | UnionType | dict[str, Any]
    schema_specs: list[_SchemaSpec[Any]]
    tool_message_content: str | None
    handle_errors: bool | str | type[Exception] | tuple[type[Exception], ...] | Callable

特色功能:

  • 支持 Union 类型和 JSON Schema oneOf------通过 _iter_variants() 展开为多个独立 schema
  • handle_errors 提供灵活的错误处理策略(布尔、字符串模板、异常类型、自定义函数)
  • tool_message_content 允许自定义在工具调用成功时返回给模型的确认消息

ProviderStrategystructured_output.py

使用模型供应商的原生结构化输出能力(如 OpenAI 的 response_format):

python 复制代码
@dataclass(init=False)
class ProviderStrategy(Generic[SchemaT]):
    schema: type[SchemaT] | dict[str, Any]
    schema_spec: _SchemaSpec[SchemaT]

    def to_model_kwargs(self) -> dict[str, Any]:
        json_schema = {
            "name": self.schema_spec.name,
            "schema": self.schema_spec.json_schema,
        }
        if self.schema_spec.strict:
            json_schema["strict"] = True
        return {"response_format": {"type": "json_schema", "json_schema": json_schema}}

to_model_kwargs() 生成的参数直接传递给模型 API,由供应商侧保证输出符合 schema。

AutoStrategystructured_output.py

自动选择最佳策略------框架根据模型能力(通过 Model Profile)决定使用 Tool 策略还是 Provider 策略:

python 复制代码
class AutoStrategy(Generic[SchemaT]):
    schema: type[SchemaT] | dict[str, Any]

    def __init__(self, schema: type[SchemaT] | dict[str, Any]) -> None:
        self.schema = schema

5.7.3 _SchemaSpec:统一 schema 描述

_SchemaSpecstructured_output.py)是三种策略的底层支撑,负责将各种 schema 格式统一为标准化描述:

python 复制代码
@dataclass(init=False)
class _SchemaSpec(Generic[SchemaT]):
    schema: type[SchemaT] | dict[str, Any]
    name: str
    description: str
    schema_kind: SchemaKind  # "pydantic" | "dataclass" | "typeddict" | "json_schema"
    json_schema: dict[str, Any]
    strict: bool | None = None

__init__ 中自动检测 schema 类型:

python 复制代码
if isinstance(schema, dict):
    self.schema_kind = "json_schema"
elif isinstance(schema, type) and issubclass(schema, BaseModel):
    self.schema_kind = "pydantic"
    self.json_schema = schema.model_json_schema()
elif is_dataclass(schema):
    self.schema_kind = "dataclass"
    self.json_schema = TypeAdapter(schema).json_schema()
elif is_typeddict(schema):
    self.schema_kind = "typeddict"
    self.json_schema = TypeAdapter(schema).json_schema()

解析时通过 _parse_with_schema() 还原为原始类型:

python 复制代码
def _parse_with_schema(schema, schema_kind, data):
    if schema_kind == "json_schema":
        return data  # dict 原样返回
    adapter = TypeAdapter(schema)
    return adapter.validate_python(data)  # Pydantic/dataclass/TypedDict 验证

5.7.4 策略对比

策略 实现方式 优势 限制
ToolStrategy 虚拟工具调用 通用性强,支持 Union/oneOf 占用一次工具调用
ProviderStrategy 原生 response_format 精确度高,供应商级保证 依赖供应商支持
AutoStrategy 自动选择 开发者无需关心底层 可能不是最优选择

类型联合定义:

python 复制代码
ResponseFormat = ToolStrategy[SchemaT] | ProviderStrategy[SchemaT] | AutoStrategy[SchemaT]

5.8 中间件系统:工具增强

LangChain 的 Agent 中间件系统(middleware/init.py)提供了多种工具增强能力。本节聚焦与工具直接相关的中间件。

5.8.1 ToolRetryMiddleware

ToolRetryMiddlewaretool_retry.py)为工具调用提供自动重试和退避策略:

python 复制代码
class ToolRetryMiddleware(AgentMiddleware):
    def __init__(
        self,
        *,
        max_retries: int = 2,
        tools: list[BaseTool | str] | None = None,  # 指定重试哪些工具
        retry_on: RetryOn = (Exception,),            # 重试的异常类型
        on_failure: OnFailure = "continue",           # 重试耗尽后的行为
        backoff_factor: float = 2.0,                  # 指数退避因子
        initial_delay: float = 1.0,                   # 初始延迟
        max_delay: float = 60.0,                      # 最大延迟
        jitter: bool = True,                          # 随机抖动(防止惊群效应)
    ):

核心执行逻辑通过 wrap_tool_call() 钩子实现:

python 复制代码
def wrap_tool_call(self, request, handler):
    tool_name = request.tool.name
    if not self._should_retry_tool(tool_name):
        return handler(request)

    for attempt in range(self.max_retries + 1):
        try:
            return handler(request)
        except Exception as exc:
            if not should_retry_exception(exc, self.retry_on):
                return self._handle_failure(tool_name, tool_call_id, exc, attempts_made)
            if attempt < self.max_retries:
                delay = calculate_delay(attempt, ...)
                time.sleep(delay)
            else:
                return self._handle_failure(tool_name, tool_call_id, exc, attempts_made)

on_failure 参数控制重试耗尽后的行为:

  • "continue":返回带错误信息的 ToolMessage,让模型决定如何处理
  • "error":重新抛出异常,终止 Agent 执行
  • 自定义 Callable:自定义错误消息格式

5.8.2 ToolCallLimitMiddleware

ToolCallLimitMiddlewaretool_call_limit.py)限制工具调用次数,防止 Agent 陷入无限循环:

python 复制代码
class ToolCallLimitMiddleware(AgentMiddleware):
    def __init__(
        self,
        *,
        tool_name: str | None = None,      # 限制特定工具,None 表示所有
        thread_limit: int | None = None,    # 线程级限制(跨轮次持久化)
        run_limit: int | None = None,       # 运行级限制(单次调用)
        exit_behavior: ExitBehavior = "continue",
    ):

该中间件通过 after_model 钩子在模型返回后检查工具调用数量。三种退出行为:

exit_behavior 行为
"continue" 为超限的工具调用注入错误 ToolMessage,其他工具继续执行
"error" 抛出 ToolCallLimitExceededError 异常
"end" 注入 ToolMessage + AIMessage 后跳转到 END 节点终止

使用示例:

python 复制代码
from langchain.agents import create_agent
from langchain.agents.middleware import ToolRetryMiddleware, ToolCallLimitMiddleware

agent = create_agent(
    "openai:gpt-4o",
    tools=[search_tool, calculator_tool],
    middleware=[
        ToolRetryMiddleware(max_retries=2, retry_on=(TimeoutError,)),
        ToolCallLimitMiddleware(run_limit=10, exit_behavior="continue"),
    ],
)

5.8.3 其他工具相关中间件

中间件系统还提供了更多与工具协作的能力:

中间件 说明
LLMToolSelectorMiddleware 基于 LLM 的智能工具过滤,在工具过多时动态选择相关工具
LLMToolEmulator 工具不可用时的 LLM 模拟降级
HumanInTheLoopMiddleware 工具执行前请求人工审批
ShellToolMiddleware Shell 命令执行的安全沙箱策略

5.9 关键数据类型速查

复制代码
工具定义层
├── BaseTool (RunnableSerializable[str | dict | ToolCall, Any])
│   ├── StructuredTool          # 多参数工具
│   │   └── from_function()     # 工厂方法
│   └── Tool                    # 单字符串输入工具(向后兼容)
├── @tool 装饰器                # 便捷创建入口
└── convert_runnable_to_tool()  # Runnable → Tool 适配器

消息层
├── ToolCall (TypedDict)        # 模型的工具调用请求
│   ├── name, args, id
│   └── type: "tool_call"
├── InvalidToolCall (TypedDict) # 解析失败的工具调用
│   └── error: str | None
├── AIMessage                   # 携带 tool_calls 列表
└── ToolMessage                 # 工具执行结果
    ├── tool_call_id            # 关联 ToolCall.id
    ├── artifact                # 不发送给模型的完整数据
    └── status                  # "success" | "error"

注入参数层
├── InjectedToolArg             # Annotated 元数据注入基类
│   └── InjectedToolCallId      # 注入 tool_call_id
└── _DirectlyInjectedToolArg    # 直接类型注解注入基类
    └── ToolRuntime             # 运行时上下文(state/store/context)

结构化输出层
├── _SchemaSpec                 # 统一 schema 描述
├── ToolStrategy                # 通过工具调用实现
├── ProviderStrategy            # 通过供应商原生能力
├── AutoStrategy                # 自动选择策略
└── ResponseFormat              # 三者的 Union 类型

5.10 总结

LangChain 的工具系统是一个精心设计的多层架构:

  1. 定义层 提供了从简单到复杂的四种创建方式,底层统一收敛到 StructuredTool
  2. 调用层 通过 bind_tools()convert_to_openai_tool()AIMessage.tool_callsToolMessage 的标准化流程,将模型意图转化为实际行动
  3. 注入层 通过 InjectedToolArg 体系实现"对模型透明、对开发者便利"的参数注入
  4. 输出层通过三种结构化输出策略,让模型直接产生类型安全的结构化数据
  5. 增强层通过中间件系统提供重试、限流、审批等生产级能力

下一章我们将进入 LangGraph 编排引擎,看工具系统如何在状态图中被编排和调度,构建完整的 Agent 工作流。

相关推荐
eastyuxiao1 小时前
思维导图拆解项目范围 3 个真实落地案例
大数据·运维·人工智能·流程图
风落无尘1 小时前
《智能重生:从垃圾堆到AI工程师》——第五章 代码与灵魂
服务器·网络·人工智能
冬奇Lab2 小时前
RAG 系列(八):RAG 评估体系——用数据说话
人工智能·llm
landyjzlai2 小时前
蓝迪哥玩转Ai(8)---端侧AI:RK3588 端侧大语言模型(LLM)开发实战指南
人工智能·python
ZhengEnCi4 小时前
05-自注意力机制详解 🧠
人工智能·pytorch·深度学习
前端程序媛-Tian5 小时前
前端 AI 提效实战:从 0 到 1 打造团队专属 AI 代码评审工具
前端·人工智能·ai
weixin_417197055 小时前
DeepSeek V4绑定华为:一场飞行中换引擎的国产算力革命
人工智能·华为
Irissgwe5 小时前
LangChain之核心组件(输出解析器)
ai·langchain·llm·ai编程·输出解析器