第8章 工具系统
本书章节导航
- 前言
- 第1章 为什么需要理解 LangChain
- 第2章 架构总览
- 第3章 Runnable 与 LCEL 表达式语言
- 第4章 消息系统与多模态
- 第5章 语言模型抽象层
- 第6章 提示词模板引擎
- 第7章 输出解析与结构化输出
- 第8章 工具系统 (当前)
- 第9章 文档加载与文本分割
- 第10章 向量存储与检索器
- 第11章 Chain 组合模式
- 第12章 回调与可观测性
- 第13章 记忆与会话管理
- 第14章 Agent 架构与执行循环
- 第15章 工具调用与 Agent 模式
- 第16章 序列化与配置系统
- 第17章 Partner 集成架构
- 第18章 设计模式与架构决策
在 AI Agent 架构中,工具是模型与外部世界交互的桥梁。语言模型擅长理解和推理,但它无法直接访问数据库、调用 API 或执行计算 -- 这些能力需要通过工具来提供。LangChain 的工具系统将 Python 函数、Runnable 对象乃至第三方服务统一抽象为标准化的工具接口,使得 Agent 能够在推理过程中动态选择和调用这些工具。
本章将从 BaseTool 的核心抽象开始,逐层展开 Tool、StructuredTool 的实现差异,深入剖析 @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,这些元信息用于告诉语言模型何时以及如何调用该工具。
工具系统的设计面临几个核心挑战:
- Schema 自动推导:开发者定义一个 Python 函数,系统需要自动提取其参数类型和描述,生成符合 OpenAI function calling 格式的 JSON Schema
- 执行安全性:工具执行可能失败,需要优雅地处理错误而不中断 Agent 循环
- 参数注入:某些参数(如回调管理器、运行时配置)不应暴露给模型,而是在执行时由系统注入
- 双模型支持:需要同时支持简单的单字符串输入工具和复杂的多参数结构化工具
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 核心属性解析
name 和 description:这两个属性不仅仅是标识符 -- 它们会被发送给语言模型,模型根据这些信息决定何时调用哪个工具。因此,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_control、defer_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
StructuredTool 的 from_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)
这段代码展现了一种优雅的"按需注入"策略:
- 只有当函数签名中包含
callbacks参数时,才注入回调子管理器 - 只有当函数签名中包含
RunnableConfig类型的参数时,才注入配置 - 注入的参数对模型不可见(被
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.
"""
...
解析后,query 和 max_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 定义了两种注入机制:
- 元数据注入(
InjectedToolArg):通过Annotated类型标记 - 直接注入(
_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,
)
设计要点:
- 自动探测输入类型:通过检查 Runnable 的
input_schema来决定创建Tool还是StructuredTool - kwargs 转发:wrapper 函数将关键字参数打包为字典传给 Runnable,因为 Runnable 的
invoke接受单一输入 - 回调传播: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.signature 和 create_model 来构建 schema,而不是依赖已被标记为废弃的 validate_arguments。
response_format 的双模式设计
"content_and_artifact" 格式的引入解决了一个实际问题:模型只需要看到工具输出的简洁摘要,但应用程序可能需要完整的结构化结果。这种分离使得 Agent 的对话上下文保持精简,同时不丢失完整数据。
extras 字段的开放性
extras 字段的设计体现了 LangChain 对供应商生态的务实态度。不同的模型供应商需要传递不同的工具元信息(如 Anthropic 的缓存控制),与其为每个供应商添加专门字段,不如提供一个通用的扩展点。这遵循了"开放-封闭"原则 -- 对扩展开放,对修改封闭。
8.12 小结
LangChain 的工具系统构建了一套从函数签名到 JSON Schema、从 Agent 调用到安全执行的完整链路。BaseTool 作为 Runnable 的特化,天然融入 LCEL 生态;Tool 和 StructuredTool 分别服务于简单和复杂的参数场景;@tool 装饰器通过 schema 自动推导和 docstring 解析,将创建工具的开发体验简化到了极致。
InjectedToolArg 的设计优雅地解决了"哪些参数给模型看、哪些参数由系统注入"的问题。create_schema_from_function 虽然实现上依赖了 Pydantic 的内部机制,但确实做到了从任意 Python 函数自动生成合规的 JSON Schema。render_text_description 系列函数和 create_retriever_tool 则将工具系统与 Prompt 工程和 RAG 流程紧密衔接。
工具系统的成熟度直接决定了 Agent 的能力边界 -- 一个好的工具抽象应该让开发者专注于业务逻辑,而将 schema 生成、错误处理、参数注入等基础设施工作交给框架。LangChain 的工具系统在这方面做出了值得学习的努力。