外观
04. Tool 与工具调用
学习目标
完成本章后,你应该能够:
- 说明 LangChain 中的 Tool 包含哪些信息。
- 理解“模型请求工具”和“程序执行工具”的职责边界。
- 用
@tool把普通 Python 函数暴露给聊天模型。 - 识别本地 Ollama 模型在工具调用上的常见限制。
- 按合理顺序调试工具调用失败。
Tool 是什么
Tool 是 Agent 可以使用的外部能力。它通常由四部分组成:
- name:工具名称,例如
add或multiply。模型会用这个名称表达“我要调用哪个工具”。 - description:工具描述,通常来自函数 docstring。它告诉模型什么时候应该用这个工具。
- schema:参数结构,例如
a: int, b: int。模型需要按这个结构生成参数。 - return:工具执行后的返回值。程序会把结果交回模型,或者直接展示给用户。
关键点是:模型本身不会真的执行 Python 函数。模型只会生成一个工具调用请求;你的程序读取请求、找到对应工具、执行函数,再处理结果。
最小示例
运行本章示例:
bash
uv run python examples/03_tools_cli.py示例中有两个工具:
python
@tool
def add(a: int, b: int) -> int:
"""Add two integers and return the sum."""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers and return the product."""
return a * b程序把工具绑定到模型:
python
model = ChatOllama(..., temperature=0).bind_tools([add, multiply])之后调用模型时,模型可能返回 response.tool_calls。如果它请求 add 或 multiply,程序会根据工具名查找本地函数并执行,再打印结果。
好工具描述的标准
好的工具描述应该清楚、具体、可执行:
- 名称短而稳定,能准确表达能力。
- docstring 说明工具做什么,而不是泛泛地写“处理输入”。
- 参数类型尽量简单,字段名能表达含义。
- 返回值容易被模型继续理解,例如数字、短文本或结构清晰的对象。
- 一个工具只做一类事,不把多个互不相关的能力塞进同一个函数。
如果模型经常选错工具,先检查工具名称、docstring 和参数 schema,再检查 prompt。
本地模型限制
并不是所有 Ollama 本地模型都稳定支持 tool calling。有些模型能正常聊天,但不会生成 tool_calls;有些模型会生成格式不完整的参数;还有些模型会把工具调用写进普通文本,而不是结构化字段。
调试时建议按这个顺序检查:
- Ollama 是否运行,模型是否已经
ollama pull。 - 当前模型是否明确支持工具调用。
bind_tools(...)是否传入了正确工具列表。- 工具 docstring 和参数类型是否足够清楚。
- 打印
response.content和response.tool_calls,确认模型到底返回了什么。
如果示例提示没有收到工具调用请求,不一定是代码错误,也可能是当前本地模型的工具调用能力不可靠。
工具设计原则
一个好工具应该“小而确定”。它应该完成一件清楚的事情,而不是把多个行为混在一起。
推荐:
search_knowledge_base(query: str) -> stradd(a: int, b: int) -> intget_weather(city: str) -> str
不推荐:
handle_user_request(text: str) -> strprocess_data(data: str) -> strdo_everything(task: str) -> str
模型选择工具时主要依赖工具名称、描述和参数 schema。如果工具太抽象,模型很难判断什么时候该用它,也很难给出正确参数。
代码阅读重点
阅读 examples/03_tools_cli.py 时,重点看三件事:
@tool如何把 Python 函数变成 LangChain Tool。ChatOllama(...).bind_tools(TOOLS)如何把工具 schema 暴露给模型。- 程序如何读取
response.tool_calls并真正执行对应工具。
注意:这个示例不是完整 Agent。它只演示“模型是否会请求工具调用”。完整的工具循环在下一章由 create_agent 管理。
实验练习
- 把
add的 docstring 改得含糊一些,观察模型是否还会稳定调用它。 - 新增一个
subtract(a: int, b: int)工具,并更新TOOLS。 - 输入一个不需要数学工具的问题,观察模型是否仍然请求工具。
自测问题
- 模型会直接执行 Python 函数吗?
- 工具返回值应该长还是短?
- 为什么工具参数类型越简单越适合初学阶段?
深入理解:Tool Calling 的真实边界
Tool calling 不是模型真的调用了函数。模型只是生成一个结构化意图,通常包含:
- 工具名。
- 参数 JSON。
- 可能还有调用 id。
权限与副作用
真正执行函数的是 LangChain 或你的应用代码。这个边界非常重要,因为它决定了安全模型:模型永远不应该直接拥有数据库、文件系统、网络请求或支付权限。它只能请求,程序负责验证、执行和记录。
在本地模型场景里,tool calling 的不稳定通常来自三个地方:
- 模型没有学好函数调用格式。
- 工具 schema 对模型来说太复杂。
- 工具说明不足以让模型判断何时使用。
因此,提升工具调用稳定性不只是换模型,也包括减少工具数量、拆分工具职责、简化参数结构、缩短返回值。
工具 schema 设计细节
参数字段应该尽量贴近用户语言。例如:
city比location_input_value更好。query适合搜索类工具。a、b适合教学中的数学工具,但业务工具里应使用更有语义的字段。
参数类型应该先从简单类型开始:
- 优先:
str、int、float、bool。 - 谨慎:嵌套对象、数组、联合类型。
- 避免:让模型传入大段自由格式 JSON,再由工具猜测含义。
工具返回值也要面向模型设计。返回太长会污染上下文,返回太短又缺少事实依据。RAG 工具通常应该返回“来源 + 摘要或片段”,而不是整篇文档。
反模式
- 万能工具:一个
execute_task(task: str)处理所有事情,模型无法判断边界。 - 高风险工具无保护:删除文件、发邮件、扣款等操作没有人工确认。
- 工具描述写给人看,不写给模型看:描述里没有说明什么时候用、参数是什么、返回什么。
- 工具返回内部异常栈:模型读不懂,也可能泄漏敏感信息。
- 工具数量过多:十几个相似工具会让模型选择困难。
真实项目中,工具设计应该像 API 设计一样严肃。一个坏工具会把模型推向错误决策,一个清晰工具能显著降低 prompt 压力。
完整示例
python
from __future__ import annotations
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).resolve().parents[1]))
from langchain_core.tools import tool
from langchain_ollama import ChatOllama
from examples.common.config import load_config, ollama_client_kwargs
from examples.common.ollama_check import assert_model_available, assert_ollama_running
@tool
def add(a: int, b: int) -> int:
"""Add two integers and return the sum."""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers and return the product."""
return a * b
TOOLS = [add, multiply]
def main() -> None:
config = load_config()
try:
assert_ollama_running(config.ollama_base_url)
assert_model_available(config.ollama_base_url, config.ollama_model)
except RuntimeError as exc:
print(f"错误:{exc}")
return
model = ChatOllama(
model=config.ollama_model,
base_url=config.ollama_base_url,
client_kwargs=ollama_client_kwargs(),
temperature=0,
).bind_tools(TOOLS)
default_question = "What is (12 + 8) * 3?"
question = input(f"数学问题(默认:{default_question}):").strip() or default_question
response = model.invoke(question)
print("\n模型原始内容:")
print(response.content or "(空)")
tool_calls = response.tool_calls
print("\n模型请求的工具调用:")
if not tool_calls:
print(
"没有收到工具调用请求。当前选择的本地模型可能不能稳定支持 tool calling,"
"可以尝试更换支持工具调用的 Ollama 模型,或先检查工具描述和模型输出。"
)
return
tool_by_name = {tool_item.name: tool_item for tool_item in TOOLS}
for tool_call in tool_calls:
tool_name = tool_call.get("name")
tool_args = tool_call.get("args")
if not isinstance(tool_name, str) or not isinstance(tool_args, dict):
print(f"- 无法执行不完整的工具调用:{tool_call}")
print(" 建议:检查模型是否稳定支持 tool calling,并简化工具参数。")
continue
print(f"- {tool_name}({tool_args})")
selected_tool = tool_by_name.get(tool_name)
if selected_tool is None:
print(f" 结果:未找到名为 `{tool_name}` 的工具。")
continue
try:
result = selected_tool.invoke(tool_args)
except Exception as exc: # Tool args can be malformed when local models call tools.
print(f" 工具执行失败:{exc}")
print(" 建议:检查工具参数类型,或换用更稳定支持工具调用的模型。")
continue
print(f" 结果:{result}")
if __name__ == "__main__":
main()