LangChain设计与实现-第8章-工具系统

13 阅读15分钟

第8章 工具系统

本书章节导航


在 AI Agent 架构中,工具是模型与外部世界交互的桥梁。语言模型擅长理解和推理,但它无法直接访问数据库、调用 API 或执行计算 -- 这些能力需要通过工具来提供。LangChain 的工具系统将 Python 函数、Runnable 对象乃至第三方服务统一抽象为标准化的工具接口,使得 Agent 能够在推理过程中动态选择和调用这些工具。

本章将从 BaseTool 的核心抽象开始,逐层展开 ToolStructuredTool 的实现差异,深入剖析 @tool 装饰器的 schema 推导机制,探讨 InjectedToolArg 的运行时注入设计,以及工具系统与模型 function calling 之间的衔接方式。

:::tip 本章要点

  • 理解 BaseTool -> Tool / StructuredTool 的层级关系与设计差异
  • 掌握 @tool 装饰器的多种用法及其内部的 schema 推导流程
  • 理解 create_schema_from_function 如何从函数签名生成 Pydantic 模型
  • 了解 ToolException 的错误处理机制与 handle_tool_error 策略
  • 掌握 InjectedToolArg 的运行时参数注入设计
  • 理解 convert_runnable_to_tool 如何桥接 Runnable 与 Tool
  • 了解 render_text_description 系列函数的工具描述渲染机制 :::

8.1 工具的本质:从函数到 Runnable

在 LangChain 中,工具的本质是一个特殊的 Runnable -- 它接收字符串或字典输入,执行某种操作,返回结果。但与普通 Runnable 不同,工具携带了丰富的元信息:名称、描述、参数 schema,这些元信息用于告诉语言模型何时以及如何调用该工具。

工具系统的设计面临几个核心挑战:

  1. Schema 自动推导:开发者定义一个 Python 函数,系统需要自动提取其参数类型和描述,生成符合 OpenAI function calling 格式的 JSON Schema
  2. 执行安全性:工具执行可能失败,需要优雅地处理错误而不中断 Agent 循环
  3. 参数注入:某些参数(如回调管理器、运行时配置)不应暴露给模型,而是在执行时由系统注入
  4. 双模型支持:需要同时支持简单的单字符串输入工具和复杂的多参数结构化工具

8.2 BaseTool:工具的核心抽象

BaseTool 继承自 RunnableSerializable[str | dict | ToolCall, Any],这个类型签名清晰地表达了工具的输入输出契约:

# langchain_core/tools/base.py
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
    name: str
    description: str
    args_schema: ArgsSchema | None = None
    return_direct: bool = False
    handle_tool_error: bool | str | Callable[[ToolException], str] | None = False
    handle_validation_error: ...
    response_format: Literal["content", "content_and_artifact"] = "content"
    extras: dict[str, Any] | None = None
classDiagram
    class RunnableSerializable {
        +invoke(input, config) Any
        +ainvoke(input, config) Any
        +batch(inputs, config) list
        +stream(input, config) Iterator
    }

    class BaseTool {
        +name: str
        +description: str
        +args_schema: ArgsSchema
        +return_direct: bool
        +handle_tool_error: bool|str|Callable
        +response_format: str
        +extras: dict
        +args: dict
        +tool_call_schema: ArgsSchema
        +is_single_input: bool
        #_run(args, config, run_manager) Any*
        #_arun(args, config, run_manager) Any*
    }

    class Tool {
        +func: Callable
        +coroutine: Callable
        +from_function() Tool
    }

    class StructuredTool {
        +func: Callable
        +coroutine: Callable
        +args_schema: ArgsSchema [required]
        +from_function() StructuredTool
    }

    RunnableSerializable <|-- BaseTool
    BaseTool <|-- Tool
    BaseTool <|-- StructuredTool

8.2.1 核心属性解析

namedescription:这两个属性不仅仅是标识符 -- 它们会被发送给语言模型,模型根据这些信息决定何时调用哪个工具。因此,description 的质量直接影响 Agent 的工具选择准确性。

args_schema:参数模式的类型是 ArgsSchema = TypeBaseModel | dict[str, Any],支持 Pydantic 模型和原始 JSON Schema 两种形式。这种灵活性允许开发者选择类型安全的 Pydantic 方式,也允许动态构建 schema。

response_format:控制工具输出如何被转换为 ToolMessage。当设为 "content_and_artifact" 时,工具返回值被期望是一个二元组 (content, artifact),其中 content 是发给模型的文本摘要,artifact 是完整的结构化数据(如文档列表)。

extras:这是为模型供应商的特定功能预留的扩展字段。例如,Anthropic 的 cache_controldefer_loading 等功能可以通过此字段传递。

8.2.2 tool_call_schema 与参数过滤

BaseTool 区分了两种 schema:args_schema 是完整的参数 schema,而 tool_call_schema 是去除了注入参数后的 schema -- 后者才是发送给语言模型的版本。

@property
def tool_call_schema(self) -> ArgsSchema:
    """Get the schema for tool calls, excluding injected arguments."""
    if isinstance(self.args_schema, dict):
        if self.description:
            return {**self.args_schema, "description": self.description}
        return self.args_schema
    ...

这个属性确保了标记为 InjectedToolArg 的参数不会出现在发送给模型的 schema 中,同时将工具描述注入到 schema 的 description 字段。

8.2.3 错误处理策略

handle_tool_error 提供了三种错误处理模式:

handle_tool_error: bool | str | Callable[[ToolException], str] | None = False
模式行为
False(默认)异常直接上抛,中断 Agent
True将异常消息作为工具输出返回给 Agent
str使用指定的错误消息字符串
Callable调用自定义函数处理异常,返回错误消息

这种分级设计使得开发者可以根据场景灵活选择:对于关键工具,可能希望异常直接中断;对于非关键工具,可以让 Agent 看到错误信息后尝试其他策略。

8.3 Tool vs StructuredTool:两种工具范式

LangChain 提供了两个具体的 BaseTool 子类,代表了两种不同的工具范式。

8.3.1 Tool -- 单输入工具

Tool 类是为简单的单参数工具设计的。它在底层强制要求所有参数合并为单一输入:

# langchain_core/tools/simple.py
class Tool(BaseTool):
    func: Callable[..., str] | None
    coroutine: Callable[..., Awaitable[str]] | None = None

    def _to_args_and_kwargs(self, tool_input, tool_call_id):
        args, kwargs = super()._to_args_and_kwargs(tool_input, tool_call_id)
        all_args = list(args) + list(kwargs.values())
        if len(all_args) != 1:
            msg = f"Too many arguments to single-input tool {self.name}."
            raise ToolException(msg)
        return tuple(all_args), {}

当参数超过一个时,Tool 会抛出 ToolException,提示开发者应该使用 StructuredTool

8.3.2 StructuredTool -- 多参数工具

StructuredTool 是功能更强大的工具类,支持任意数量的命名参数。它要求必须提供 args_schema

# langchain_core/tools/structured.py
class StructuredTool(BaseTool):
    args_schema: ArgsSchema = Field(..., description="The tool schema.")
    func: Callable[..., Any] | None = None
    coroutine: Callable[..., Awaitable[Any]] | None = None

StructuredToolfrom_function 类方法是创建工具的主要入口:

@classmethod
def from_function(
    cls,
    func=None, coroutine=None,
    name=None, description=None,
    args_schema=None, infer_schema=True,
    response_format="content",
    parse_docstring=False,
    error_on_invalid_docstring=False,
    **kwargs,
) -> StructuredTool:
    source_function = func or coroutine
    name = name or source_function.__name__

    if args_schema is None and infer_schema:
        args_schema = create_schema_from_function(name, source_function, ...)

    if description is None and not parse_docstring:
        description_ = source_function.__doc__ or None
    ...

这里的 infer_schema=True 默认行为是工具系统便利性的关键 -- 大多数情况下,开发者只需要定义一个带类型注解的函数,schema 就能自动生成。

flowchart TD
    A[StructuredTool.from_function] --> B{args_schema 提供了?}
    B -->|是| E[直接使用]
    B -->|否| C{infer_schema?}
    C -->|是| D[create_schema_from_function]
    C -->|否| F[不推导 schema]
    D --> G[检查函数签名]
    G --> H[处理 Pydantic v1/v2 注解]
    H --> I[validate_arguments 创建模型]
    I --> J[过滤 run_manager/callbacks 等参数]
    J --> K[提取 docstring 描述]
    K --> L[创建子集 Pydantic 模型]
    L --> E
    E --> M[构建 StructuredTool 实例]

8.3.3 运行时参数注入:callbacks 和 config

两种工具在执行时都会自动注入回调管理器和运行时配置:

# Tool._run 和 StructuredTool._run 中的相同逻辑
def _run(self, *args, config, run_manager=None, **kwargs):
    if self.func:
        if run_manager and signature(self.func).parameters.get("callbacks"):
            kwargs["callbacks"] = run_manager.get_child()
        if config_param := _get_runnable_config_param(self.func):
            kwargs[config_param] = config
        return self.func(*args, **kwargs)

这段代码展现了一种优雅的"按需注入"策略:

  1. 只有当函数签名中包含 callbacks 参数时,才注入回调子管理器
  2. 只有当函数签名中包含 RunnableConfig 类型的参数时,才注入配置
  3. 注入的参数对模型不可见(被 FILTERED_ARGS 过滤)

8.4 @tool 装饰器:最佳开发体验

@tool 装饰器是创建工具的推荐方式。它支持多种使用模式,通过函数重载(@overload)提供了清晰的类型提示。

8.4.1 四种使用模式

# 模式 1:无参装饰器
@tool
def search(query: str) -> str:
    """Search the web for a query."""
    return "results..."

# 模式 2:自定义名称
@tool("web_search")
def search(query: str) -> str:
    """Search the web for a query."""
    return "results..."

# 模式 3:带参数的装饰器
@tool(parse_docstring=True, response_format="content_and_artifact")
def search(query: str) -> tuple[str, dict]:
    """Search the web.

    Args:
        query: The search query string.
    """
    return "summary", {"full": "results"}

# 模式 4:包装 Runnable
from langchain_core.runnables import RunnableLambda
runnable = RunnableLambda(lambda x: x["query"].upper())
search_tool = tool("uppercase", runnable)

8.4.2 内部分发逻辑

@tool 的实现通过分析参数类型来决定行为路径:

# langchain_core/tools/convert.py
def tool(name_or_callable=None, runnable=None, *, description=None, ...):
    if runnable is not None:
        # tool("name", runnable) -- 包装 Runnable
        return _create_tool_factory(name_or_callable)(runnable)
    if name_or_callable is not None:
        if callable(name_or_callable) and hasattr(name_or_callable, "__name__"):
            # @tool 无参装饰
            return _create_tool_factory(name_or_callable.__name__)(name_or_callable)
        if isinstance(name_or_callable, str):
            # @tool("name") 或 @tool("name", parse_docstring=True)
            return _create_tool_factory(name_or_callable)
    # @tool(parse_docstring=True) 带参数装饰器
    return _partial
flowchart TD
    A["@tool(...)"] --> B{runnable 参数?}
    B -->|是| C["tool(name, runnable)"]
    C --> D[包装 Runnable 为 Tool]
    B -->|否| E{name_or_callable?}
    E -->|None| F["@tool(kwargs...) 带参装饰器"]
    F --> G[返回 _partial 闭包]
    E -->|Callable| H["@tool 无参装饰器"]
    H --> I[提取 __name__ 作为工具名]
    I --> J[_create_tool_factory]
    E -->|str| K["@tool('name') 命名装饰器"]
    K --> L[返回 _tool_factory 闭包]

    J --> M{infer_schema?}
    L --> M
    M -->|是| N[StructuredTool.from_function]
    M -->|否| O[Tool 简单工具]

8.4.3 _create_tool_factory 的内部工厂

def _create_tool_factory(tool_name: str):
    def _tool_factory(dec_func: Callable | Runnable) -> BaseTool:
        if isinstance(dec_func, Runnable):
            # Runnable 包装逻辑
            schema = runnable.input_schema
            async def ainvoke_wrapper(callbacks=None, **kwargs):
                return await runnable.ainvoke(kwargs, {"callbacks": callbacks})
            def invoke_wrapper(callbacks=None, **kwargs):
                return runnable.invoke(kwargs, {"callbacks": callbacks})
            ...
        elif inspect.iscoroutinefunction(dec_func):
            coroutine = dec_func; func = None
        else:
            coroutine = None; func = dec_func

        if infer_schema or args_schema is not None:
            return StructuredTool.from_function(func, coroutine, name=tool_name, ...)
        else:
            return Tool(name=tool_name, func=func, description=f"{tool_name} tool", ...)
    return _tool_factory

注意 Runnable 包装的巧妙之处:它创建了两个 wrapper 函数(同步和异步),将 kwargs 转发给 Runnable 的 invoke/ainvoke,同时将 callbacks 传入配置。这使得 Runnable 在被包装为工具后,仍然能够正确传播回调。

8.5 create_schema_from_function:从函数签名到 Pydantic 模型

这是工具系统中最复杂的单个函数,它负责将任意 Python 函数的签名转换为一个 Pydantic 模型:

# langchain_core/tools/base.py
def create_schema_from_function(
    model_name: str,
    func: Callable,
    *,
    filter_args: Sequence[str] | None = None,
    parse_docstring: bool = False,
    error_on_invalid_docstring: bool = False,
    include_injected: bool = True,
) -> type[BaseModel]:
    sig = inspect.signature(func)

    # 检测 Pydantic v1/v2 注解
    if _function_annotations_are_pydantic_v1(sig, func):
        validated = validate_arguments_v1(func, config=_SchemaConfig)
    else:
        validated = validate_arguments(func, config=_SchemaConfig)

    inferred_model = validated.model
    ...

整个过程分为几个阶段:

flowchart TD
    A[函数签名 inspect.signature] --> B{Pydantic v1 注解?}
    B -->|是| C[validate_arguments_v1]
    B -->|否| D[validate_arguments v2]
    C --> E[获取 inferred_model]
    D --> E
    E --> F[确定 filter_args]
    F --> G[过滤 self/cls/run_manager/callbacks]
    G --> H{parse_docstring?}
    H -->|是| I[解析 Google Style docstring]
    H -->|否| J[提取 Annotated 描述]
    I --> K[获取参数描述]
    J --> K
    K --> L[构建 valid_properties 列表]
    L --> M[_create_subset_model]
    M --> N[返回 Pydantic 模型]

8.5.1 参数过滤机制

默认的 FILTERED_ARGS 包含 ("run_manager", "callbacks"),这些是 LangChain 内部使用的参数。此外,还会过滤掉:

  • 类方法的 self/cls 参数
  • 标记为 InjectedToolArg 的参数(当 include_injected=False 时)
  • 类型为 RunnableConfig 的参数

8.5.2 docstring 解析

parse_docstring=True 时,函数会解析 Google Style docstring 来提取参数描述:

@tool(parse_docstring=True)
def search(query: str, max_results: int = 10) -> str:
    """Search the web for information.

    Args:
        query: The search query string.
        max_results: Maximum number of results to return.
    """
    ...

解析后,querymax_results 的描述会被注入到生成的 Pydantic 模型的字段描述中,最终出现在发送给模型的 JSON Schema 里。

8.5.3 Pydantic v1/v2 兼容性

create_schema_from_function 内部有一个关键的版本检测步骤:

def _function_annotations_are_pydantic_v1(signature, func):
    any_v1_annotations = any(
        _is_pydantic_annotation(param.annotation, pydantic_version="v1")
        for param in signature.parameters.values()
    )
    any_v2_annotations = any(
        _is_pydantic_annotation(param.annotation, pydantic_version="v2")
        for param in signature.parameters.values()
    )
    if any_v1_annotations and any_v2_annotations:
        raise NotImplementedError("Mixed v1 and v2 annotations not supported")
    return any_v1_annotations and not any_v2_annotations

如果函数参数中包含 Pydantic v1 的模型类型,则使用 v1 的 validate_arguments;如果包含 v2 类型,则使用 v2 版本。混合使用两个版本的注解会直接报错。这种检测确保了在 Pydantic 版本迁移期间的平稳过渡。

8.6 InjectedToolArg:运行时参数注入

InjectedToolArg 是 LangChain 工具系统中一个精巧的设计。它允许某些参数在模型看来"不存在",但在工具实际执行时被自动注入。

# langchain_core/tools/base.py
class InjectedToolArg:
    """Annotation for tool arguments that are injected at runtime."""

class _DirectlyInjectedToolArg:
    """Annotation for args injected via direct type annotation."""

class InjectedToolCallId(InjectedToolArg):
    """Annotation for injecting the tool call ID."""

LangChain 定义了两种注入机制:

  1. 元数据注入InjectedToolArg):通过 Annotated 类型标记
  2. 直接注入_DirectlyInjectedToolArg):通过直接使用特定类型(如 ToolRuntime

使用方式:

from typing import Annotated
from langchain_core.tools import tool, InjectedToolCallId
from langchain_core.messages import ToolMessage

@tool
def create_report(
    topic: str,
    tool_call_id: Annotated[str, InjectedToolCallId]
) -> ToolMessage:
    """Generate a report on the given topic."""
    content = f"Report on {topic}"
    return ToolMessage(content, name="create_report", tool_call_id=tool_call_id)

在这个例子中,模型只会看到 topic 参数的 schema。tool_call_id 参数会在工具被 Agent 调用时自动注入当前 tool_call 的 ID。

检测逻辑:

def _is_injected_arg_type(type_, injected_type=None):
    if _is_directly_injected_arg_type(type_):
        return True
    if injected_type is None:
        injected_type = InjectedToolArg
    return any(
        isinstance(arg, injected_type)
        or (isinstance(arg, type) and issubclass(arg, injected_type))
        for arg in get_args(type_)[1:]
    )

这个函数检查类型注解的 Annotated 元数据中是否包含 InjectedToolArg 实例或子类。StructuredTool 缓存了被注入参数的键名集合,在参数分发时将这些键排除在 schema 之外。

flowchart LR
    subgraph 模型视角
        S1["{ topic: str }"]
    end

    subgraph 实际签名
        S2["topic: str, tool_call_id: Annotated[str, InjectedToolCallId]"]
    end

    subgraph 运行时
        A[Agent 调用] --> B[tool_call_id 从 ToolCall.id 注入]
        B --> C["create_report(topic='AI', tool_call_id='call_123')"]
    end

    S2 -->|过滤 InjectedToolArg| S1
    S1 -->|发送给模型| A

8.7 convert_runnable_to_tool:桥接 Runnable 与 Tool

convert_runnable_to_tool 函数将任意 Runnable 转换为 BaseTool,这是工具系统的一个重要扩展点。

# langchain_core/tools/convert.py
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:
    description = description or _get_description_from_runnable(runnable)
    name = name or runnable.get_name()

    schema = runnable.input_schema.model_json_schema()
    if schema.get("type") == "string":
        # 单字符串输入 -> Tool
        return Tool(name=name, func=runnable.invoke,
                    coroutine=runnable.ainvoke, description=description)

    # 多参数输入 -> StructuredTool
    async def ainvoke_wrapper(callbacks=None, **kwargs):
        return await runnable.ainvoke(kwargs, config={"callbacks": callbacks})

    def invoke_wrapper(callbacks=None, **kwargs):
        return runnable.invoke(kwargs, config={"callbacks": callbacks})

    return StructuredTool.from_function(
        name=name, func=invoke_wrapper, coroutine=ainvoke_wrapper,
        description=description, args_schema=args_schema,
    )

设计要点:

  1. 自动探测输入类型:通过检查 Runnable 的 input_schema 来决定创建 Tool 还是 StructuredTool
  2. kwargs 转发:wrapper 函数将关键字参数打包为字典传给 Runnable,因为 Runnable 的 invoke 接受单一输入
  3. 回调传播:wrapper 从参数中提取 callbacks 并注入到配置中

当 Runnable 的输入类型无法自动推断时,可以通过 arg_types 参数手动指定:

def _get_schema_from_runnable_and_arg_types(runnable, name, arg_types=None):
    if arg_types is None:
        try:
            arg_types = get_type_hints(runnable.InputType)
        except TypeError as e:
            msg = "Tool input must be str or dict. If dict, dict arguments must be typed."
            raise TypeError(msg) from e
    fields = {key: (key_type, Field(...)) for key, key_type in arg_types.items()}
    return create_model(name, **fields)

8.8 render_text_description:工具描述渲染

在某些 Agent 实现(特别是基于 Prompt 的 ReAct Agent)中,需要将工具列表渲染为纯文本描述嵌入到 Prompt 中。LangChain 提供了两个渲染函数:

# langchain_core/tools/render.py
def render_text_description(tools: list[BaseTool]) -> str:
    descriptions = []
    for tool in tools:
        if hasattr(tool, "func") and tool.func:
            sig = signature(tool.func)
            description = f"{tool.name}{sig} - {tool.description}"
        else:
            description = f"{tool.name} - {tool.description}"
        descriptions.append(description)
    return "\n".join(descriptions)

def render_text_description_and_args(tools: list[BaseTool]) -> str:
    tool_strings = []
    for tool in tools:
        args_schema = str(tool.args)
        description = f"{tool.name} - {tool.description}"
        tool_strings.append(f"{description}, args: {args_schema}")
    return "\n".join(tool_strings)

渲染结果示例:

# render_text_description
search(query: str, max_results: int = 10) - Search the web for information
calculator(expression: str) - Evaluate a mathematical expression

# render_text_description_and_args
search - Search the web, args: {"query": {"type": "string"}, "max_results": {"type": "integer"}}
calculator - Evaluate math, args: {"expression": {"type": "string"}}

第一种格式包含了完整的函数签名,对于模型理解参数类型很有帮助;第二种格式包含了 JSON Schema 形式的参数定义,更加精确。ToolsRenderer 类型别名 Callable[[list[BaseTool]], str] 则允许开发者自定义渲染函数。

8.9 create_retriever_tool:检索器工具化

LangChain 提供了一个专门的函数将检索器包装为工具,这是 RAG Agent 的核心组件:

# langchain_core/tools/retriever.py
class RetrieverInput(BaseModel):
    query: str = Field(description="query to look up in retriever")

def create_retriever_tool(
    retriever: BaseRetriever,
    name: str,
    description: str,
    *,
    document_prompt: BasePromptTemplate | None = None,
    document_separator: str = "\n\n",
    response_format: Literal["content", "content_and_artifact"] = "content",
) -> StructuredTool:
    document_prompt_ = document_prompt or PromptTemplate.from_template("{page_content}")

    def func(query: str, callbacks=None):
        docs = retriever.invoke(query, config={"callbacks": callbacks})
        content = document_separator.join(
            format_document(doc, document_prompt_) for doc in docs
        )
        if response_format == "content_and_artifact":
            return (content, docs)
        return content
    ...

设计亮点:

  • 固定 schema:使用 RetrieverInput(只有 query 字段),保持工具接口的简洁性
  • 文档格式化:通过 document_prompt 控制检索到的文档如何转换为文本
  • artifact 模式"content_and_artifact" 格式允许将原始 Document 对象作为 artifact 保留,便于后续处理

8.10 工具执行流程全景

从模型产生 tool_call 到工具执行完成返回 ToolMessage,完整的执行流程如下:

sequenceDiagram
    participant M as Language Model
    participant A as Agent
    participant T as BaseTool
    participant F as User Function

    M->>A: AIMessage with tool_calls
    A->>T: invoke(tool_call)
    T->>T: _to_args_and_kwargs(tool_input)
    T->>T: 验证 args_schema
    T->>T: 注入 InjectedToolArg
    T->>T: 创建 CallbackManager
    T->>F: _run(*args, config, run_manager, **kwargs)
    alt 执行成功
        F-->>T: result
        T->>T: 构造 ToolMessage(content=result)
    else ToolException
        T->>T: handle_tool_error 处理
        T->>T: 构造 ToolMessage(content=error_msg)
    else ValidationError
        T->>T: handle_validation_error 处理
    end
    T-->>A: ToolMessage
    A->>M: 将 ToolMessage 加入对话

8.11 设计决策分析

Tool vs StructuredTool 的历史演变

最初,LangChain 只有 Tool,它接受单个字符串输入。随着 Agent 架构的演进和 function calling 的出现,需要支持多参数工具,StructuredTool 应运而生。保留 Tool 是为了向后兼容,同时它在某些简单场景下确实更加方便。

为什么使用 validate_arguments 推导 schema?

create_schema_from_function 使用了 Pydantic 的 validate_arguments 装饰器来推导函数签名。这是一个巧妙但有些 hacky 的做法 -- 它利用了 validate_arguments 会自动从函数签名生成 Pydantic 模型这一特性。源码中的注释坦率地承认了这一点:

# This code should be re-written to simply construct a Pydantic model
# using inspect.signature and create_model.

这个注释暗示了未来可能的重构方向:直接使用 inspect.signaturecreate_model 来构建 schema,而不是依赖已被标记为废弃的 validate_arguments

response_format 的双模式设计

"content_and_artifact" 格式的引入解决了一个实际问题:模型只需要看到工具输出的简洁摘要,但应用程序可能需要完整的结构化结果。这种分离使得 Agent 的对话上下文保持精简,同时不丢失完整数据。

extras 字段的开放性

extras 字段的设计体现了 LangChain 对供应商生态的务实态度。不同的模型供应商需要传递不同的工具元信息(如 Anthropic 的缓存控制),与其为每个供应商添加专门字段,不如提供一个通用的扩展点。这遵循了"开放-封闭"原则 -- 对扩展开放,对修改封闭。

8.12 小结

LangChain 的工具系统构建了一套从函数签名到 JSON Schema、从 Agent 调用到安全执行的完整链路。BaseTool 作为 Runnable 的特化,天然融入 LCEL 生态;ToolStructuredTool 分别服务于简单和复杂的参数场景;@tool 装饰器通过 schema 自动推导和 docstring 解析,将创建工具的开发体验简化到了极致。

InjectedToolArg 的设计优雅地解决了"哪些参数给模型看、哪些参数由系统注入"的问题。create_schema_from_function 虽然实现上依赖了 Pydantic 的内部机制,但确实做到了从任意 Python 函数自动生成合规的 JSON Schema。render_text_description 系列函数和 create_retriever_tool 则将工具系统与 Prompt 工程和 RAG 流程紧密衔接。

工具系统的成熟度直接决定了 Agent 的能力边界 -- 一个好的工具抽象应该让开发者专注于业务逻辑,而将 schema 生成、错误处理、参数注入等基础设施工作交给框架。LangChain 的工具系统在这方面做出了值得学习的努力。