‹ 博客列表

LangChain 1.2 入门(十七):Tools 是什么——让模型从「会说」到「会做」

工具是模型与外部世界交互的接口:直接调用与绑定模型两种方式,完整调用流程的四个步骤,以及 AIMessage / ToolMessage 在消息列表里的流转。

LangChain 1.2 入门(十七):Tools 是什么——让模型从「会说」到「会做」

前面几篇解决的都是输入侧的问题:模型怎么初始化、消息怎么组织、提示词怎么模板化。但不管输入多讲究,模型本身只会输出文本——它不知道今天的天气,算不准大数乘法,也读不到你数据库里的订单。Tools 补的就是这一环:让模型从「会说话」变成「会干活」。

17.1 只会生成文本,能力就到顶了

工具是赋予大语言模型「与外部世界交互能力」的关键组件。有了工具,智能体才能执行搜索、计算、数据库查询、发邮件、调用第三方 API 这类动作。一句话概括:没有工具,大模型只能认识世界;有了工具,它才开始改变世界。

课件里这张图把「工具」的典型形态列全了:搜索、计算器、数据库/文件读写、邮件与消息发送。

LangChain 框架工具的应用场景:搜索、计算器、数据库/文件、邮件沟通四类工具围绕智能体工作

这也是工具被视为智能体核心要素的原因。一个只能输出文本的 Agent,本质上是个更贵的问答机器人:它能告诉你「应该查一下天气」,但没法自己查。

17.2 工具:明确定义了输入输出的可调用函数

在 LangChain 里,工具(Tools)不是某种特殊对象,而是一个明确声明了输入和输出的可调用函数。正因如此,工具调用(Tool Calling)在别处也叫函数调用(Function Calling)——两个词说的是同一件事。

调用方式有两种,用途完全不同。

方式 1:直接调用,用于测试。工具本身就是可调用对象,用 .invoke() 传一个字典即可。

from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """
    获取指定城市的天气信息
    Args:
        city: 城市名称,如 "北京"、"上海"
    Returns:
        天气信息字符串
    """
    return city + "晴天,温度 15°C"

# 直接调用,入参是 dict,键名与参数名一致
result = get_weather.invoke({"city": "北京"})
print(result)

输出:

北京晴天,温度 15°C

方式 2:绑定到模型,开发中的主流做法。用 bind_tools() 把工具交给模型,由模型自己决定要不要调、调哪个、参数填什么。这种方式下工具是给 AI 用的,不是给你手动调用的。

后续所有例子都基于同一段模型初始化:

from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
import os

load_dotenv(override=True)
model = init_chat_model(
    model="gpt-5.4-mini",
    model_provider="openai",
    api_key=os.getenv("CLOSEAI_API_KEY"),
    base_url=os.getenv("CLOSEAI_BASE_URL"),
)

初始化细节见 init_chat_model 统一初始化。

17.3 完整流程:四步,缺一不可

先看最小可运行版本:把工具绑上去,问一个需要天气的问题,看看模型返回什么。

from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """获取指定城市的天气"""
    return "晴天,温度 15°C"

model_with_tools = model.bind_tools([get_weather])

response = model_with_tools.invoke("北京天气如何?")
# 换成 "2 + 3 = ?" 时,模型判断不需要工具
if response.tool_calls:
    print("AI 想调用工具:", response.tool_calls)
else:
    print("AI 直接回答:", response.content)

输出:

AI 想调用工具: [{'name': 'get_weather', 'args': {'city': '北京'},
  'id': 'call_fR3LE8Wjqh9lnDosQ61Y892E', 'type': 'tool_call'}]

注意此刻 response.content 是空的:模型只给了一个「调用请求」,没有执行任何函数。它把工具名、参数、调用 ID 一并发给你,接下来轮到你动手。所以真正能拿到最终答案的流程有四步:

步骤 谁做 做什么
1 绑定工具 开发者 model.bind_tools([...]),一次可绑多个
2 生成调用请求 模型 需要工具时返回带 tool_calls 的 AIMessage
3 执行工具 开发者 从 tool_calls 取名称和参数,真正调用函数
4 回传结果 开发者 + 模型 把结果包成 ToolMessage 塞回消息列表,再调一次模型生成最终回复
工具调用整体流程图:用户提问、AI 助手打包工具描述问模型、模型给出调用参数、执行工具、结果回传模型

关键在于第 3 步:工具始终是开发者的代码在执行,模型不执行任何东西。模型负责「决定调用什么」和「读懂返回结果」,中间的搬运工作由你负责。

大模型调用工具是单次推理,一次请求给一个结果,不会自己循环。要连续调用多个工具,循环必须自己写——这部分在(十九)的多工具案例里展开。

17.4 从 Message 流转看工具调用

把上面四步拆到消息层面,整个过程就是消息列表的追加。课件里这张流程图把八个动作画全了:初始化模型与工具、创建 HumanMessage、调用模型、生成带 tool_calls 的 AIMessage、挂载 AI 响应、生成 ToolMessage、追加、最终响应。

工具调用的消息流转流程图:HumanMessage、AIMessage(含 tool_calls)、ToolMessage 与最终响应

先看不使用 @tool 装饰的写法,此时工具只是一个普通函数:

from langchain.messages import HumanMessage, ToolMessage

def get_weather(city: str):
    """获取天气的工具"""
    return f"{city}天气晴朗"

model_with_tools = model.bind_tools([get_weather])
messages = [HumanMessage("今天北京天气如何")]

# 模型生成调用工具请求(AIMessage)
response = model_with_tools.invoke(messages)
messages.append(response)

# 开发者手动执行工具,并自己拼 ToolMessage
for tool_call in response.tool_calls:
    if tool_call["name"] == "get_weather":
        tool_response = ToolMessage(
            content=get_weather(**tool_call["args"]),
            tool_call_id=tool_call["id"],
            name=tool_call["name"],
        )
        messages.append(tool_response)

# 带着工具结果再问一次,模型才会给出自然语言回复
final_response = model_with_tools.invoke(messages)
print(final_response.content)

这段代码里最容易被忽略的是 tool_call_id:它必须与服务端返回的 tool_call["id"] 一致,模型靠这个 ID 把「我要的结果」和「你给的结果」对上。手拼出错,模型就会忽略这条 ToolMessage。

换成 @tool 修饰后,工具自己就能吃下整个 tool_call,直接吐一个 ToolMessage,不用再手工拼:

import os
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
from langchain_core.tools import tool
from langchain.messages import HumanMessage

load_dotenv(override=True)
model = init_chat_model(
    model="gpt-5.4-mini",
    model_provider="openai",
    api_key=os.getenv("CLOSEAI_API_KEY"),
    base_url=os.getenv("CLOSEAI_BASE_URL"),
)

@tool
def get_weather(city: str):
    """获取天气的工具"""
    return f"{city}天气晴朗~"

model_with_tools = model.bind_tools([get_weather])
messages = [HumanMessage("今天北京天气如何")]

response = model_with_tools.invoke(messages)
messages.append(response)

for tool_call in response.tool_calls:
    if tool_call["name"] == "get_weather":
        tool_response = get_weather.invoke(tool_call)   # 直接返回 ToolMessage
        print(type(tool_response))
        messages.append(tool_response)

for msg in messages:
    msg.pretty_print()

final_response = model_with_tools.invoke(messages)
print(final_response.content)

输出(截取关键部分):

<class 'langchain_core.messages.tool.ToolMessage'>
================================ Human Message ================================
今天北京天气如何
================================== Ai Message ==================================
好的,我来帮你查一下今天北京的天气情况。
Tool Calls:
  get_weather (call_00_f65kV4JKjBPK0HhURzO99449)
 Call ID: call_00_f65kV4JKjBPK0HhURzO99449
  Args:
    city: 北京
================================= Tool Message ==================================
Name: get_weather
北京天气晴朗~
今天北京天气 **晴朗** ☀,是个好天气,可以放心出门。

消息序列读起来就是一次完整的对话:人类提问 → AI 说要查天气并给出参数 → 工具返回结果 → AI 总结。调试时 pretty_print() 打一遍这个序列,比读任何日志都直观。

17.5 小结

这一篇只讲清楚了「工具是什么」和「调用怎么跑起来」:工具是声明了输入输出的可调用函数,调用方式分直接调用和绑定模型两种,完整流程是绑定、生成调用请求、执行、回传四步,模型永远只负责决策不负责执行。

但上面的工具描述都很随意——docstring 写了一行甚至没写。模型凭什么知道什么时候该调 get_weather?参数该填什么格式?下一篇讲工具的定义方式:docstring 的写法、@tool 的参数,以及用 Pydantic 和 JSON Schema 精确定义参数。

上一篇:提示词模板 ChatPromptTemplate 与高级用法;系列第一篇:为什么需要 LangChain。

本文根据公开课程课件整理,并结合 LangChain 官方文档核对与补充。

本文来自 https://aimock.cn/blog,站内重新编排。
内容仅供学习交流,转载请注明出处。