前面创建天气 Agent 时,模型能够判断什么时候查询天气,并把城市名称交给 get_weather()。这个过程看起来像是模型执行了 Python 函数,实际并不是这样。
大模型只负责理解问题、选择工具并生成参数。真正查找工具、调用 Python 函数、处理返回值的仍然是应用程序。LangChain 的 Tool 负责在两者之间建立一份清晰的接口约定。
本篇不使用 Agent 自动循环,而是从普通 Python 函数开始,逐步学习 Tool 的定义方式,最后使用本地 Qwen3 手动完成一次完整的工具调用。
LangChain 可以从三类对象创建本地 Tool:Python 函数或协程、LangChain Runnable、继承 BaseTool 的自定义类。后面出现的 Pydantic、Annotated 和 Docstring 负责描述参数,StructuredTool.from_function() 负责包装函数,它们都不是额外的第四类来源。
1. 为什么模型需要工具
大模型擅长理解和生成文本,但只依靠模型自身,仍然无法稳定完成下面这些工作:
- 查询实时天气、库存或订单状态。
- 执行精确计算。
- 读取数据库或本地文件。
- 调用企业内部接口。
- 对外部系统执行写入操作。
Tool 可以把这些能力包装成模型能够理解的结构。以计算器为例,应用需要告诉模型:
工具名称:calculator
用途:计算两个数字的乘积
参数:a、b、operation
模型看到的不是 Python 函数源码,而是由名称、描述和参数组成的 JSON Schema。模型可以据此生成工具调用请求,应用程序再执行对应函数。
2. Tool 和普通函数有什么区别
普通 Python 函数已经可以执行计算:
def plain_add(a: int, b: int) -> int:
return a + b
但是模型并不知道这个函数叫什么、有什么用途、需要哪些参数。LangChain Tool 在函数外面增加了模型调用所需的元数据,并提供统一的 invoke()、ainvoke() 接口。
一个 Tool 最重要的四个属性如下:
| 属性 | 作用 |
|---|---|
| name | 工具的唯一名称,模型生成工具调用时会使用它 |
| description | 说明工具能做什么,帮助模型判断是否应该调用 |
| args_schema | 定义参数名称、类型、必填项、说明和校验规则 |
| return_direct | 在支持该语义的 Agent 执行循环中,工具执行后是否直接结束循环并返回结果 |
return_direct 不会改变普通 tool.invoke() 的返回方式。它主要供 Agent 执行器判断工具运行后是否还要继续调用模型。
下图从左到右分成三层:原始能力、转换方式和统一的 Tool 对象。无论从哪一类对象创建,最终都需要提供名称、用途、参数 Schema 和可执行实现。

三类来源的使用重点不同:
- Python 函数或协程:最常用。可以直接交给 Agent,也可以使用 @tool 或 StructuredTool.from_function() 明确生成 Tool。
- Runnable:已有 Runnable 调用链时,可以使用 as_tool() 复用现有逻辑。
- BaseTool 子类:适合封装客户端、连接、配置或复杂生命周期。
Pydantic、Annotated 和 Docstring 只是在补充 args_schema,不是新的工具来源。发送给模型的 JSON Schema 也只是 Tool 的可见说明;真正可执行的函数或协程仍然保留在应用程序中。
MCP 是后续独立的工具接入方式,不属于本篇讨论的本地 Tool 创建方式。
3. 第一类:使用 @tool 包装 Python 函数
@tool 是最简单、最常用的定义方式。它会读取函数名、类型注解和说明,创建一个 LangChain Tool。
langgraph/p05_local_tools/01_tool_basics.py 的完整代码如下:
"""对比普通 Python 函数与 LangChain Tool,并查看工具的核心属性。"""
import json
from langchain_core.tools import tool
from langchain_core.utils.function_calling import convert_to_openai_tool
def plain_add(a: int, b: int) -> int:
"""计算两个整数之和。"""
return a + b
@tool("add_numbers", description="计算两个整数之和。")
def add_numbers(a: int, b: int) -> int:
"""计算两个整数之和。"""
return a + b
print("普通函数类型:", type(plain_add).__name__)
print("普通函数具有 invoke:", hasattr(plain_add, "invoke"))
print("工具类型:", type(add_numbers).__name__)
print("工具名称:", add_numbers.name)
print("工具描述:", add_numbers.description)
print("return_direct:", add_numbers.return_direct)
# args_schema 是由函数参数类型自动生成的 Pydantic 模型。
print("参数 Schema:")
print(json.dumps(add_numbers.args_schema.model_json_schema(), ensure_ascii=False, indent=2))
# convert_to_openai_tool() 可以查看发送给 OpenAI 兼容模型的工具结构。
print("OpenAI 工具 Schema:")
print(json.dumps(convert_to_openai_tool(add_numbers), ensure_ascii=False, indent=2))
# 传入普通参数字典时,invoke() 返回函数本身的执行结果。
result = add_numbers.invoke({"a": 3, "b": 5})
print("字典调用返回类型:", type(result).__name__)
print("字典调用结果:", result)
# 传入完整 tool_call 时,invoke() 会返回包含调用 ID 的 ToolMessage。
tool_call = {
"name": "add_numbers",
"args": {"a": 3, "b": 5},
"id": "call_demo",
"type": "tool_call",
}
tool_message = add_numbers.invoke(tool_call)
print("tool_call 调用返回类型:", type(tool_message).__name__)
print("ToolMessage 内容:", tool_message.content)
print("ToolMessage 名称:", tool_message.name)
print("ToolMessage 调用 ID:", tool_message.tool_call_id)
运行:
cd source/_posts/llm_learning
source .venv_langgraph/bin/activate
python langgraph/p05_local_tools/01_tool_basics.py
实际输出中的关键内容如下:
普通函数类型: function
普通函数具有 invoke: False
工具类型: StructuredTool
工具名称: add_numbers
工具描述: 计算两个整数之和。
return_direct: False
字典调用返回类型: int
字典调用结果: 8
tool_call 调用返回类型: ToolMessage
ToolMessage 内容: 8
ToolMessage 名称: add_numbers
ToolMessage 调用 ID: call_demo
普通函数没有 invoke()。经过 @tool 包装后,得到的是 StructuredTool 对象,并且自动拥有参数 Schema。
3.1 模型实际看到的 JSON Schema
convert_to_openai_tool() 可以把 Tool 转换成 OpenAI 兼容模型能够接收的格式。上面的代码实际生成:
{
"type": "function",
"function": {
"name": "add_numbers",
"description": "计算两个整数之和。",
"parameters": {
"properties": {
"a": {
"type": "integer"
},
"b": {
"type": "integer"
}
},
"required": [
"a",
"b"
],
"type": "object"
}
}
}
这份结构告诉模型:工具名是 add_numbers,需要两个整数参数,并且两个参数都是必填项。它没有把函数源码或执行权限交给模型。
3.2 两种 invoke() 输入的区别
传入普通参数字典时:
add_numbers.invoke({"a": 3, "b": 5})
返回函数的原始结果 8。
传入完整的 tool_call 时:
add_numbers.invoke(
{
"name": "add_numbers",
"args": {"a": 3, "b": 5},
"id": "call_demo",
"type": "tool_call",
}
)
返回 ToolMessage。它除了保存结果,还保留 tool_call_id,使模型能够知道这个结果对应哪一次工具请求。后面的 Qwen3 示例会使用这种调用方式。
4. 为函数工具使用 Pydantic 参数 Schema
简单函数可以依靠类型注解自动生成 Schema。当参数较多、需要枚举值或希望提供更清楚的字段说明时,可以显式定义 Pydantic 模型。
langgraph/p05_local_tools/02_pydantic_args_schema.py:
"""使用 Pydantic 描述工具参数,并验证必填字段和枚举值。"""
import json
from typing import Literal
from langchain_core.tools import tool
from pydantic import BaseModel, Field, ValidationError
class CalculatorInput(BaseModel):
"""计算器工具的输入参数。"""
a: float = Field(description="第一个数字")
b: float = Field(description="第二个数字")
operation: Literal["add", "subtract", "multiply", "divide"] = Field(
description="运算类型:加、减、乘、除"
)
@tool(args_schema=CalculatorInput)
def calculator(a: float, b: float, operation: str) -> str:
"""根据指定运算类型计算两个数字。"""
if operation == "add":
result = a + b
elif operation == "subtract":
result = a - b
elif operation == "multiply":
result = a * b
else:
if b == 0:
return "除数不能为 0"
result = a / b
return str(result)
print("参数 Schema:")
print(json.dumps(calculator.args_schema.model_json_schema(), ensure_ascii=False, indent=2))
print("正确参数结果:", calculator.invoke({"a": 12, "b": 4, "operation": "divide"}))
try:
calculator.invoke({"a": 12, "b": 4, "operation": "power"})
except ValidationError as error:
first_error = error.errors()[0]
print("枚举校验错误位置:", first_error["loc"])
print("枚举校验错误类型:", first_error["type"])
try:
calculator.invoke({"a": 12, "operation": "add"})
except ValidationError as error:
first_error = error.errors()[0]
print("缺少参数错误位置:", first_error["loc"])
print("缺少参数错误类型:", first_error["type"])
实际输出:
正确参数结果: 3.0
枚举校验错误位置: ('operation',)
枚举校验错误类型: literal_error
缺少参数错误位置: ('b',)
缺少参数错误类型: missing
Literal 把 operation 限制为四个固定值。模型能够在 Schema 中看到这些候选值,应用执行工具前也会进行真实校验。Schema 不能保证模型永远生成正确参数,但可以阻止错误参数直接进入函数。
5. 使用 Annotated 和 Docstring 补充函数参数说明
除了 Pydantic,还可以使用 Annotated 或 Google 风格 Docstring 为参数增加说明。
langgraph/p05_local_tools/03_annotated_and_docstring.py:
"""演示 Annotated 参数说明和 Google 风格 Docstring 解析。"""
import json
from typing import Annotated
from langchain_core.tools import tool
@tool
def add_integers(
a: Annotated[int, "第一个整数"],
b: Annotated[int, "第二个整数"],
) -> str:
"""计算两个整数之和。"""
return str(a + b)
@tool(parse_docstring=True)
def greet(name: str, title: str = "朋友") -> str:
"""生成一条简单问候语。
Args:
name: 被问候人的姓名。
title: 对被问候人的称呼。
"""
return f"{title}{name},你好!"
print("Annotated 参数 Schema:")
print(json.dumps(add_integers.args_schema.model_json_schema(), ensure_ascii=False, indent=2))
print("Annotated 工具结果:", add_integers.invoke({"a": 4, "b": 6}))
print("Docstring 参数 Schema:")
print(json.dumps(greet.args_schema.model_json_schema(), ensure_ascii=False, indent=2))
print("Docstring 工具结果:", greet.invoke({"name": "小明"}))
try:
@tool(parse_docstring=True)
def invalid_greet(name: str) -> str:
"""生成问候语。
Args:
username: 被问候人的姓名。
"""
return f"你好,{name}"
except ValueError as error:
print("无效 Docstring 错误类型:", type(error).__name__)
print("无效 Docstring 错误:", str(error))
实际输出:
Annotated 工具结果: 10
Docstring 工具结果: 朋友小明,你好!
无效 Docstring 错误类型: ValueError
无效 Docstring 错误: Arg username in docstring not found in function signature.
启用 parse_docstring=True 后,Args: 中的参数名称必须和函数签名一致。示例故意把 name 写成 username,工具在创建阶段就抛出了 ValueError,而不是等到模型调用时才失败。
这些写法都属于第一类工具来源,只是参数说明方式不同,可以按复杂程度选择:
- 参数很少时,使用类型注解和 Annotated。
- 已经有规范 Google 风格 Docstring 时,使用 parse_docstring=True。
- 需要枚举、默认值、字段约束和独立复用时,使用 Pydantic。
6. 使用 StructuredTool 包装函数和异步协程
@tool 适合直接装饰函数。如果需要显式指定同步函数、异步协程、名称和描述,可以使用 StructuredTool.from_function()。它仍然属于“从 Python 函数或协程创建工具”,不是新的来源类别。
langgraph/p05_local_tools/04_structured_tool.py:
"""使用 StructuredTool 为同一个工具配置同步函数和异步协程。"""
import asyncio
from langchain_core.tools import StructuredTool
def add_sync(a: float, b: float) -> str:
"""同步计算两个数字之和。"""
return f"同步结果:{a + b}"
async def add_async(a: float, b: float) -> str:
"""异步计算两个数字之和。"""
# sleep(0) 主动让出一次执行权,用于演示真实协程。
await asyncio.sleep(0)
return f"异步结果:{a + b}"
calculator_tool = StructuredTool.from_function(
func=add_sync,
coroutine=add_async,
name="structured_calculator",
description="计算两个数字之和。",
)
print("工具类型:", type(calculator_tool).__name__)
print("同步调用:", calculator_tool.invoke({"a": 2, "b": 3}))
print("异步调用:", asyncio.run(calculator_tool.ainvoke({"a": 2, "b": 3})))
实际输出:
工具类型: StructuredTool
同步调用: 同步结果:5.0
异步调用: 异步结果:5.0
invoke() 调用同步函数,ainvoke() 调用异步协程。对于网络请求、数据库访问等 I/O 操作,可以提供真正的异步实现,避免在异步程序中阻塞事件循环。
7. 第二类:把 Runnable 转换成 Tool
如果已有一段 Runnable 逻辑,不必再复制成新函数,可以使用 as_tool() 转换。
langgraph/p05_local_tools/05_runnable_as_tool.py:
"""把一个简单 Runnable 转换为 LangChain Tool。"""
import json
from langchain_core.runnables import RunnableLambda
from pydantic import BaseModel, Field
class GreetingInput(BaseModel):
"""问候语 Runnable 的输入参数。"""
name: str = Field(description="需要问候的姓名")
# RunnableLambda 是最简单的 Runnable,输入和输出逻辑保持直观。
greeting_runnable = RunnableLambda(
lambda values: f"{values['name']},欢迎学习 LangChain!"
)
# langchain-core==1.2.22 会提示 as_tool() 仍是 Beta API,文章保留这条真实警告。
greeting_tool = greeting_runnable.as_tool(
args_schema=GreetingInput,
name="generate_greeting",
description="为指定姓名生成一条欢迎语。",
)
print("工具类型:", type(greeting_tool).__name__)
print("工具名称:", greeting_tool.name)
print("参数 Schema:")
print(json.dumps(greeting_tool.args_schema.model_json_schema(), ensure_ascii=False, indent=2))
print("调用结果:", greeting_tool.invoke({"name": "小明"}))
实际运行时先出现 Beta 警告,然后正常得到结果:
LangChainBetaWarning: This API is in beta and may change in the future.
工具类型: StructuredTool
工具名称: generate_greeting
调用结果: 小明,欢迎学习 LangChain!
这是 langchain-core==1.2.22 的真实行为。Beta 表示 API 仍处于测试状态,不表示不能使用。用于长期维护的项目时,应锁定依赖版本并在升级后重新测试。
8. 第三类:继承 BaseTool 创建复杂工具
BaseTool 是工具的基础抽象。继承它需要自己声明名称、描述、参数模型,并实现 _run()。
langgraph/p05_local_tools/06_base_tool.py:
"""继承 BaseTool,创建一个只查询本地固定书单的工具。"""
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
BOOKS = [
"Python 编程入门",
"杭州旅行指南",
"中国古诗词精选",
]
class BookSearchInput(BaseModel):
"""本地书籍搜索工具的输入参数。"""
keyword: str = Field(description="书名中需要包含的关键词")
class LocalBookSearchTool(BaseTool):
"""在内存中的固定书单里搜索书名。"""
name: str = "search_local_books"
description: str = "根据关键词在本地固定书单中查找书名。"
args_schema: type[BaseModel] = BookSearchInput
def _run(self, keyword: str) -> str:
"""执行同步书名搜索。"""
matches = [book for book in BOOKS if keyword in book]
return "、".join(matches) if matches else "没有找到匹配的书籍"
book_search = LocalBookSearchTool()
print("工具类型:", type(book_search).__name__)
print("工具名称:", book_search.name)
print("搜索‘杭州’:", book_search.invoke({"keyword": "杭州"}))
print("搜索‘音乐’:", book_search.invoke({"keyword": "音乐"}))
实际输出:
工具类型: LocalBookSearchTool
工具名称: search_local_books
搜索‘杭州’: 杭州旅行指南
搜索‘音乐’: 没有找到匹配的书籍
这个示例只查询内存中的固定书单,不访问网络。对于简单、无状态的函数,继承 BaseTool 会显得过重;当工具需要持有客户端、连接池、配置或复杂生命周期时,这种方式更容易组织代码。
9. 使用本地 Qwen3 完成工具调用
前面的示例都是应用直接执行 Tool。接下来把计算器 Schema 绑定到本地 Qwen3,让模型自己决定工具名称和参数。
9.1 启动本地模型服务
在 llm_learning 根目录使用 LangGraph 系列 3 创建的模型服务环境启动 MLX-LM:
cd source/_posts/llm_learning
source .venv_tool_server/bin/activate
"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
--model model \
--host 127.0.0.1 \
--port 18080 \
--chat-template-args '{"enable_thinking": false}'
另开终端执行健康检查:
curl --noproxy '*' --fail --silent \
http://127.0.0.1:18080/v1/models
确认返回模型列表后再运行代码。否则连接失败、502 或模型加载错误容易被误判成 Tool 的问题。
9.2 完整代码
langgraph/p05_local_tools/07_bind_tools_with_qwen3.py:
"""把本地计算器绑定到 Qwen3,并手动完成一次工具调用循环。"""
from typing import Literal
import httpx
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class CalculatorInput(BaseModel):
"""计算器工具的输入参数。"""
a: float = Field(description="第一个数字")
b: float = Field(description="第二个数字")
operation: Literal["multiply"] = Field(description="运算类型,只能是 multiply")
@tool(args_schema=CalculatorInput)
def calculator(a: float, b: float, operation: str) -> str:
"""计算两个数字的乘积。"""
return str(a * b)
# trust_env=False 避免系统代理或 VPN 转发本机请求。
with httpx.Client(trust_env=False) as http_client:
model = ChatOpenAI(
model="default_model",
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
temperature=0,
max_tokens=128,
http_client=http_client,
)
model_with_tools = model.bind_tools([calculator])
tools_by_name = {calculator.name: calculator}
messages = [HumanMessage(content="请使用计算器工具计算 18 乘以 7。")]
# 第一次调用只让模型生成工具调用请求。
ai_message = model_with_tools.invoke(messages)
messages.append(ai_message)
if not ai_message.tool_calls:
raise RuntimeError("Qwen3 没有生成工具调用")
for tool_call in ai_message.tool_calls:
print("模型选择的工具:", tool_call["name"])
print("模型生成的参数:", tool_call["args"])
selected_tool = tools_by_name[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
print("工具消息类型:", type(tool_message).__name__)
print("工具执行结果:", tool_message.content)
messages.append(tool_message)
# 第二次调用把 ToolMessage 交给模型,由模型组织最终回答。
final_message = model_with_tools.invoke(messages)
print("Qwen3 最终回答:", final_message.content)
运行:
source .venv_langgraph/bin/activate
python langgraph/p05_local_tools/07_bind_tools_with_qwen3.py
一次真实运行结果:
模型选择的工具: calculator
模型生成的参数: {'a': 18, 'b': 7, 'operation': 'multiply'}
工具消息类型: ToolMessage
工具执行结果: 126.0
Qwen3 最终回答: 18 乘以 7 的结果是 126。
模型回答可能有轻微措辞变化,但工具名称、参数结构和计算结果应保持正确。
下图按照真实消息顺序展开这次调用。第一次模型请求只得到 tool_calls,中间由应用执行计算器;第二次模型请求必须同时包含用户消息、原始 AIMessage 和匹配调用 ID 的 ToolMessage。

10. 手动工具调用循环是怎样工作的
这段代码包含两个模型调用,中间夹着一次本地函数调用。
10.1 bind_tools() 只绑定工具说明
model_with_tools = model.bind_tools([calculator])
bind_tools() 把计算器的 JSON Schema 附加到模型请求中。它不会提前执行工具,也不会给模型 Python 运行权限。
10.2 第一次模型调用生成 tool_calls
ai_message = model_with_tools.invoke(messages)
Qwen3 返回 AIMessage,其中的 tool_calls 包含工具名称、参数和调用 ID。此时还没有计算出结果。
10.3 应用程序执行本地工具
selected_tool = tools_by_name[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
应用根据工具名找到 calculator,执行 Python 函数,并得到 ToolMessage。这一步才真正计算了 18 × 7。
实际项目还应检查模型返回的工具名是否在允许列表中,并对可能产生副作用的工具增加权限、超时和人工确认。
10.4 第二次模型调用组织最终回答
final_message = model_with_tools.invoke(messages)
消息列表中已经包含:
- 用户问题。
- 模型生成的工具调用请求。
- 应用返回的 ToolMessage。
Qwen3 根据工具结果生成用户能够直接阅读的最终回答。这就是一次最小但完整的工具调用循环。Agent 会自动完成类似循环,而本篇手动展开是为了看清每一步的职责。
11. Qwen3 工具调用的基础检查
工具定义正确,并不代表任意模型服务都能正确返回 tool_calls。排查时应按下面的顺序进行。
11.1 先检查模型服务
curl --noproxy '*' --fail --silent \
http://127.0.0.1:18080/v1/models
模型服务没有启动时,先解决端口、模型路径和虚拟环境问题,不要先修改工具代码。
11.2 检查模型是否真的返回 tool_calls
print(ai_message.tool_calls)
如果列表为空,常见原因包括:
- 模型或服务端不支持 OpenAI 兼容的工具调用格式。
- 工具描述不清楚,模型认为不需要调用工具。
- 参数 Schema 过于复杂或字段说明不足。
- 提示词没有明确要求使用工具。
本篇使用的本地 Qwen3 和 MLX-LM 服务已经真实返回标准 tool_calls,不需要切换到其他推理框架。
11.3 先使用非流式调用排查
工具调用可能被分散到多个流式消息块中。初次调试时使用普通 invoke() 更容易观察完整的 AIMessage.tool_calls。确认非流式调用正确后,再处理 AIMessageChunk、消息块合并和推理服务解析器;这些内容放在下一篇完整展开。
11.4 本机请求避免经过代理
httpx.Client(trust_env=False)
这可以避免 VPN 或系统代理把 127.0.0.1 请求转发出去。它只影响 HTTP 客户端,不影响 Tool 的定义和执行。
12. 常见错误
12.1 工具名称含糊或重复
工具名应简短、稳定并能区分用途。多个工具使用相同名称时,应用无法可靠分发调用。
12.2 description 只重复工具名
描述应说明工具适合解决什么问题。模型主要依靠名称、描述和参数 Schema 选择工具,模糊描述会直接影响选择结果。
12.3 参数缺少类型和说明
没有类型约束时,模型更容易传入错误格式。复杂参数优先使用 Pydantic,枚举值使用 Literal。
12.4 直接信任模型生成的工具名和参数
模型输出必须视为外部输入。应用需要检查工具白名单,并让 Pydantic 完成参数校验。写文件、发消息、删除数据等工具还需要权限和人工确认。
12.5 忘记把 AIMessage 和 ToolMessage 放回消息列表
第二次调用前必须保留模型原始工具请求,并追加与调用 ID 对应的 ToolMessage。只把字符串结果发送给模型,会破坏标准工具调用上下文。
12.6 误以为 bind_tools() 会自动执行工具
bind_tools() 只向模型提供工具 Schema。自动循环需要 Agent 或自己编写分发、执行和回传逻辑。
13. 小结
Tool 是模型和应用能力之间的结构化接口。模型依据 Schema 选择工具和生成参数,应用程序负责校验、执行和回传结果。
LangChain 创建本地 Tool 的来源可以归纳为三类:Python 函数或协程、Runnable、BaseTool 子类。对于大多数简单函数,直接交给 Agent 或使用 @tool 已经足够;需要明确同步和异步实现时使用 StructuredTool.from_function();已有 Runnable 可以通过 as_tool() 转换;需要封装状态或复杂生命周期时再继承 BaseTool。
本篇最后的 Qwen3 示例没有依赖 Agent,手动展示了 AIMessage.tool_calls -> Tool 执行 -> ToolMessage -> 最终 AIMessage 的完整过程。理解这个循环后,再学习 Agent 自动执行工具时,每一层的职责会更清楚。
| Tool 来源 | 具体写法 | 参数 Schema | 适用场景 | 本篇实测结果 |
|---|---|---|---|---|
| Python 函数或协程 | 直接交给 Agent、@tool、StructuredTool.from_function() | 类型注解、Pydantic、Annotated 或 Docstring | 最常用,适合无状态或逻辑清晰的函数 | 同步和异步函数均执行成功 |
| Runnable | Runnable.as_tool() | 建议显式提供 Pydantic Schema | 复用已有 Runnable 调用链 | 执行成功,langchain-core==1.2.22 给出 Beta 警告 |
| BaseTool 子类 | 继承 BaseTool,实现 _run() / _arun() | 显式定义 | 封装状态、客户端或复杂生命周期 | 本地书单查询成功 |
ChatOpenAI.bind_tools() 不负责创建新的 Tool 来源,它把已经创建好的 Tool Schema 交给模型。本地 Qwen3 实测能够生成正确参数,并完成 18 × 7 = 126 的手动调用循环。
参考文档:LangChain Tools、Models and tool calling、langchain-core tools API。
下一篇将继续处理流式工具调用:观察 AIMessageChunk 和 ToolCallChunk,说明推理服务工具解析器的作用,并使用 BaseTool 调用真实互联网接口。