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

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 工作流。

相关推荐
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【14】ReactAgent 工具执行异常处理
java·人工智能·spring
fzxwl2 小时前
集成MidScene的AI测试管理平台
人工智能
涵星同学2 小时前
从深度学习到大模型的跃迁:Transformer的核心突破
人工智能·深度学习·transformer
Magic-Yuan2 小时前
如何提高AI落地的成功率 - 成功率函数
大数据·人工智能
Zldaisy3d2 小时前
数字孪生与AI的共生将如何影响职业发展和企业竞争力
人工智能
ShiMetaPi2 小时前
NeurIPS 2024 | 丝滑视觉新极限:EPA 框架利用事件相机突破插帧伪影瓶颈
人工智能·嵌入式硬件·计算机视觉·自动驾驶·事件相机·evs
丶党玲儿2 小时前
AI-agent工程化(开源git分享)
人工智能·git·开源
code_li2 小时前
淘宝动效全链路解决方案:一次制作多端复用
网络·人工智能·电商·淘宝技术
Yao.Li2 小时前
PVN3D Full ONNX 导出与自定义算子说明
人工智能·3d·具身智能