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."""
这个类型签名揭示了两个关键设计决策:
- 继承
RunnableSerializable:工具是 Runnable,可以直接参与 LCEL 链式组合、使用invoke()/ainvoke()调用、享受回调和追踪能力 - 输入类型
str | dict | ToolCall:工具接受三种输入格式------简单字符串、结构化字典、标准化 ToolCall 对象
5.2.2 核心属性
BaseTool 在 base.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_manager和config,子类不需要的参数不会被强制传入 - 异常分层处理 :
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 中定义)的工作流程:
- 通过
inspect.signature()获取函数签名 - 通过
get_type_hints()获取类型注解 - 过滤掉
run_manager、callbacks等内部参数 - 过滤掉被
InjectedToolArg标记的注入参数 - 用 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_wrapper 和 ainvoke_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 为例,实现流程是:
- 遍历
tools列表 - 对每个工具调用
convert_to_openai_tool()转换为 OpenAI 格式 - 调用
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]:
转换逻辑的分支判断:
- 已知 OpenAI 工具类型 (
function、file_search、computer、web_search等)→ 直接返回 - 自定义工具 (
metadata.type == "custom_tool")→ 构造{"type": "custom", "name": ..., "description": ...} - 其他类型 (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] # 供应商特定元数据
同时还有 InvalidToolCall(content.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 ------ 工具结果载体
ToolMessage(tool.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):
- 字符串 :简单场景,直接作为位置参数传递给
_run() - 字典 :结构化参数,经过
args_schema验证后作为关键字参数 - ToolCall 对象 :模型输出的标准化格式,自动提取
args和tool_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 的实际应用
InjectedToolCallId(base.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 级别的结构化输出控制:
ToolStrategy (structured_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 SchemaoneOf------通过_iter_variants()展开为多个独立 schema handle_errors提供灵活的错误处理策略(布尔、字符串模板、异常类型、自定义函数)tool_message_content允许自定义在工具调用成功时返回给模型的确认消息
ProviderStrategy (structured_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。
AutoStrategy (structured_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 描述
_SchemaSpec(structured_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
ToolRetryMiddleware(tool_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
ToolCallLimitMiddleware(tool_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 的工具系统是一个精心设计的多层架构:
- 定义层 提供了从简单到复杂的四种创建方式,底层统一收敛到
StructuredTool - 调用层 通过
bind_tools()→convert_to_openai_tool()→AIMessage.tool_calls→ToolMessage的标准化流程,将模型意图转化为实际行动 - 注入层 通过
InjectedToolArg体系实现"对模型透明、对开发者便利"的参数注入 - 输出层通过三种结构化输出策略,让模型直接产生类型安全的结构化数据
- 增强层通过中间件系统提供重试、限流、审批等生产级能力
下一章我们将进入 LangGraph 编排引擎,看工具系统如何在状态图中被编排和调度,构建完整的 Agent 工作流。