Deep Read

LangChain - Tools

大模型本身只会生成文本——它不能查数据库、不能上网搜索、更不能打开你电脑上的计算器。Tool(工具)就是挂给模型的"可调用函数清单":模型负责决定"调哪个、传什么参数",真正执行的永远是你的代码。

不带工具时:

你:  现在几点了?
AI:  抱歉,我无法获取实时时间。     ← 它真的做不到

给模型挂上一个 get_current_time 工具后:

你:  现在几点了?
AI:  (不直接回答,而是返回一个请求)
     → 我想调用 get_current_time,参数 {}
[你的代码执行函数,把结果 "14:32" 送回去]
AI:  现在是下午 2 点 32 分。

一个容易误解的点:模型从头到尾没有执行任何东西,它只是输出一段结构化文本:"我想调 X,参数是 Y"。解析请求、运行函数、把结果送回去的,都是 LangChain 和你的代码。

模型负责决策,代码负责执行,消息列表负责在两者之间传递。

也正因为决策权在模型,工具的名称和描述写得好不好,直接决定模型能不能在对的时机选对工具——它们是提示词的一部分,不是注释。

工具的定义要素

从上面的流程能反推出,一个完整的工具要有五个要素:

要素作用给谁看
名称 (name)工具的唯一标识,模型在 tool_calls 里用它指定调用目标模型
描述 (description)告诉模型这个工具能做什么、什么时候该用模型
参数 Schema (args_schema)输入参数的 JSON 模式:有哪些参数、什么类型、什么含义模型
执行函数 (func)实际执行的 Python 函数机器
返回结果函数的输出,会送回模型作为下一步推理的依据模型

注意"给谁看"这一列:前三个要素是给模型看的,它们会被序列化成 JSON Schema 拼进请求里,属于提示词的一部分;只有函数体是给机器执行的。所以工具的 docstring 和参数命名要像写文档一样认真:写得含糊,模型就会选错工具、传错参数。

另外,面向同一个目标的一组工具可以打包成 Toolkit(工具包),比如 SQLDatabaseToolkit 就打包了"列表名、查 Schema、执行 SQL"等一整套数据库工具。

工具调用的完整流程

diagram.png

使用工具分三步:定义 → 组装 → 接入 agent,需要写的代码只有这些:

1. 定义工具       @tool 把函数变成工具(名称/描述/参数 Schema 自动生成)
2. 组装成工具集    把要用的工具放进一个列表 tools = [...]
3. 接入 agent     create_agent(model, tools=tools),之后 invoke 即可
# 1. 定义工具
@tool
def get_current_time() -> str:
    """获取当前时间"""
    from datetime import datetime
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# 2. 组装成工具集
tools = [get_current_time]

# 3. 接入 agent
agent = create_agent(model=model, tools=tools)

result = agent.invoke({"messages": [{"role": "user", "content": "现在几点了?"}]})
print(result["messages"][-1].content)   # "现在是 14 点 32 分。"

invoke 内部会自动跑一个"决策 → 执行 → 回传 → 回答"的运行时循环:

模型返回 tool_calls(调谁、传什么)→ agent 执行对应函数 → 结果包成 ToolMessage 送回模型 → 模型给出答案,或继续要求调工具。

循环的每一步都记录在 result["messages"] 里,打印出来可以逐条对照:

HumanMessage("现在几点了?")                                ← 你的输入
AIMessage(tool_calls=[{'name': 'get_current_time',
                       'args': {}, 'id': 'call_xxx'}])    ← 决策:调谁、传什么
ToolMessage(content="2026-07-02 14:32:00",
            tool_call_id="call_xxx")                      ← 执行 + 回传
AIMessage("现在是 14 点 32 分。")                           ← 最终回答

几个关键细节:

  • tool_calls 是一个列表——模型一轮可能同时要求调多个工具;
  • ToolMessage 带着对应的 tool_call_id,模型靠它把"请求"和"结果"对上号;
  • 拿到工具结果后模型也可能再次返回 tool_calls(比如先查了表名、接着要查 Schema),agent 会一直循环,直到模型不再要求调工具才停。

定义工具的几种方式

方式一:Tool 类直接实例化(旧式,简单直接)

最原始的方式:先写普通函数,再用 Tool 构造函数手动包装,名称和描述都显式传入:

from datetime import datetime
from langchain_core.tools import Tool

def get_current_time(input: str = "") -> str:
    return f"当前时间{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"

def recom_drink(input: str = "") -> str:
    return "距离您500米内有:蜜雪冰城、茶颜悦色"

tools = [
    Tool(name="get_current_time", func=get_current_time,
         description="当你想知道现在的时间时调用"),
    Tool(name="recom_drink", func=recom_drink,
         description="用户口渴,推荐附近的饮料店"),
]

两个特点:

  • 三要素全手动:模型看到的名称、描述以构造参数为准,函数名和 docstring 不参与;
  • 只支持单字符串输入Tool 是旧式工具,不解析类型注解,参数 Schema 固定是"一个字符串"——所以即使函数不需要参数,也得留一个 input: str = "" 的占位参数;多参数、带类型的工具它表达不了。

适合无参或单字符串参数的简单工具;参数复杂时用下面的方式。

方式二:@tool 装饰器(最常用)

一行装饰器把普通 Python 函数变成工具,函数名→名称,docstring→描述,类型注解→参数 Schema,三个"给模型看的要素"全部自动生成:

from langchain.tools import tool

@tool
def multiply(a: int, b: int) -> int:
    """计算两个整数的乘积"""
    return a * b

print(multiply.name)         # multiply
print(multiply.description)  # 计算两个整数的乘积
print(multiply.args)         # {'a': {...'type': 'integer'}, 'b': {...'type': 'integer'}}

参数含义比较复杂、想给模型更多提示时,用 Pydantic 模型精细描述每个参数:

from pydantic import BaseModel, Field
from langchain.tools import tool

class SearchInput(BaseModel):
    query: str = Field(description="搜索关键词,应提取用户问题中的核心实体")
    limit: int = Field(default=5, description="返回结果条数,1~20")

@tool(args_schema=SearchInput)
def search_docs(query: str, limit: int = 5) -> str:
    """在内部知识库中搜索文档,当用户询问公司制度、产品细节时使用"""
    ...

方式三:StructuredTool.from_function

不方便加装饰器(比如函数来自第三方库、或要动态批量注册)时,用类方法包装,还能同时挂同步/异步两个实现:

from langchain_core.tools import StructuredTool

def query_order(order_id: str) -> str:
    return f"订单 {order_id}:已发货"

async def aquery_order(order_id: str) -> str:
    ...

order_tool = StructuredTool.from_function(
    func=query_order,
    coroutine=aquery_order,          # 异步版本,agent 异步执行时自动选用
    name="query_order",
    description="根据订单号查询订单状态",
)

方式四:继承 BaseTool(完全控制)

需要在工具里维护状态、自定义错误处理等复杂场景时,直接继承基类实现 _run。日常用得少,了解即可:

from langchain_core.tools import BaseTool

class WeatherTool(BaseTool):
    name: str = "get_weather"
    description: str = "查询指定城市的实时天气"

    def _run(self, city: str) -> str:
        return f"{city}:晴,28℃"

方式五:预定义工具 / Toolkit(拿来即用)

社区包里有大量现成工具,实例化后直接使用:

# 内置搜索工具:Tavily
from langchain_community.tools import TavilySearchResults

search = TavilySearchResults(max_results=2)
client_with_tools = client.bind_tools([search])
# Toolkit:一次拿到一组数据库工具
from langchain_community.agent_toolkits import SQLDatabaseToolkit

toolkit = SQLDatabaseToolkit(db=db, llm=client)
tools = toolkit.get_tools()   # 列表名/查Schema/执行SQL 等一整套

五种方式对比:

方式适用场景定义成本
Tool 类实例化无参/单字符串参数的简单工具(旧式)
@tool 装饰器自己写函数,绝大多数场景最低
StructuredTool.from_function包装已有函数、需要同步+异步双实现
继承 BaseTool工具内部要维护状态、深度定制
预定义工具 / Toolkit搜索、数据库等通用能力零(开箱即用)

工具调用的几种方式

定义好的工具,有三种调用方式,自动化程度从低到高。

方式一:直接 invoke(测试工具本身)

工具本身就是可执行对象,可以不经过模型直接调用。开发时用来单测工具逻辑:

print(get_current_time.invoke({}))            # "2026-07-02 14:32:00"
print(multiply.invoke({"a": 3, "b": 4}))      # 12

这一步没有模型参与,纯粹验证函数本身能不能跑对。

方式二:bind_tools 手动循环

把工具绑到模型上,模型只负责返回 tool_calls,解析、执行、回传都自己写:

from langchain.messages import HumanMessage, ToolMessage

# 绑定:把工具的说明书挂到模型上
model_with_tools = model.bind_tools([get_current_time])

# 决策:模型不直接回答,而是返回 tool_calls
messages = [HumanMessage(content="现在几点了?")]
ai_msg = model_with_tools.invoke(messages)

# 执行 + 回传:跑函数,结果包成 ToolMessage 追加进消息列表
messages.append(ai_msg)
for tc in ai_msg.tool_calls:
    result = get_current_time.invoke(tc["args"])
    messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))

# 最终回答:模型看到工具结果,给出自然语言答案
final = model_with_tools.invoke(messages)
print(final.content)

模型可能连续多轮要求调工具,实际要写成 while 循环。这种方式能精确控制每一步,但代码繁琐,主要价值是理解原理。

方式三:create_agent 自动循环(推荐)

就是前面"使用工具三步"的写法:工具接入 agent,"决策 → 执行 → 回传 → 回答"的循环由 invoke 内部完成:

agent = create_agent(model=model, tools=[get_current_time])
result = agent.invoke({"messages": [{"role": "user", "content": "现在几点了?"}]})

还可以配 checkpointer(多轮记忆)、system_promptmiddleware 等能力,实际项目用这种。

三种方式对比:

方式模型参与循环谁来写用途
直接 invoke无循环单测工具逻辑
bind_tools决策自己写理解原理、精确控制
create_agent决策框架写实际项目

小结

  • Tool 的本质:给模型一份函数说明书,模型输出"调谁、传什么"的决策,执行在你的代码里;
  • 五要素里名称、描述、参数 Schema 是给模型看的提示词,写工具先把这三样当文档写好;
  • 使用工具三步:定义工具 → 组装工具集 → 接入 agent;"决策 → 执行 → 回传 → 回答"的运行时循环由 invoke 内部自动完成,tool_call_id 负责把请求和结果配对;
  • 定义方式首选 @tool,参数复杂配 Pydantic args_schemaTool 类实例化只适合单字符串输入的简单工具;通用能力优先找现成的预定义工具和 Toolkit;
  • 调用方式:直接 invoke 单测工具、bind_tools 手动循环理解原理、create_agent 自动循环用于实际项目。