工具

之前的章节中,我们构建的智能体曾多次使用过“工具(Tool)”,例如第一章Demo里的get_weather函数。工具(Tool)是智能体与外部世界交互的核心机制,智能体需要通过工具调用完成搜索、查询数据库、执行代码、调用API等各种操作。这篇笔记我们将系统介绍如何定义工具、如何在普通ChatModel和智能体中使用工具,以及如何通过MCP协议接入海量的第三方工具。

工具调用的本质

LLM为什么能调用工具?实际上没什么神奇的,调用工具对于LLM来说仍是输入一段文本,推理后得到一些响应文本,只不过在工具调用时,我们在提示词里描述了都有哪些可用工具、参数类型、返回值类型、用途等,LLM会按照规则输出想要调用的函数和参数值,我们解析出这些信息后,真正去反射调用工具函数的仍是代码。

在LangChain中,工具(Tool)本质上就是一个带有元数据的Python函数。LangChain将这个函数包装成一个BaseTool对象,并附带名称(name)、描述(description)和参数Schema(args_schema)信息,这些元数据最终会发送给LLM,LLM根据这些信息判断何时、以何种参数调用该工具。

定义工具

使用@tool装饰器

最简单也是最推荐的工具定义方式是使用@tool装饰器,下面是一个例子。

from langchain_core.tools import tool


@tool
def get_weather(city: str) -> str:
    """Get current weather for a given city.

    Args:
        city: City name, for example "Los Angeles" or "Beijing".

    Returns:
        A weather summary string, for example "Sunny 14°C".
    """
    # 实际项目中这里应调用真实的天气API
    return "晴 14℃"

工具函数一定要有参数类型约束和docstring注释,编写高质量的docstring非常重要,尤其是多工具或工具的用途比较复杂,仅靠函数名难以判断的时候,这些信息对LLM选择工具和正确调用工具非常有帮助。装饰器@tool会自动从函数名和docstring中提取工具的名称和描述,并根据类型注解生成参数Schema。定义完成后,我们可以查看工具的元信息。

print(get_weather.name)        # get_weather
print(get_weather.description) # Get current weather for a given city. ...
print(get_weather.args)        # {'city': {'title': 'City', 'type': 'string', ...}}

多参数工具

工具可以定义多个参数,LangChain会自动根据类型注解生成对应的参数Schema。

from langchain_core.tools import tool


@tool
def search_product(product_name: str, category: str = "all", max_results: int = 10) -> list[dict]:
    """Search for products in the e-commerce database.

    Args:
        product_name: The name or keywords to search for.
        category: Product category filter, e.g. "electronics", "clothing". Defaults to "all".
        max_results: Maximum number of results to return. Defaults to 10.

    Returns:
        A list of product dicts with name, price, and category fields.
    """
    # 模拟返回结果
    return [{"name": product_name, "price": 99.9, "category": category}]

使用Pydantic定义参数Schema

对于复杂的参数结构,可以使用Pydantic模型显式定义参数Schema,让参数约束更加精确清晰。

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


class SendEmailInput(BaseModel):
    to: str = Field(description="Recipient email address")
    subject: str = Field(description="Email subject line, keep it concise")
    body: str = Field(description="Email body content in plain text or markdown")
    cc: list[str] = Field(default=[], description="CC email addresses, optional")


@tool(args_schema=SendEmailInput)
def send_email(to: str, subject: str, body: str, cc: list) -> str:
    """Email the specified recipient.

    Returns:
        Confirmation message indicating success or failure.
    """
    if cc is None:
        cc = []
    recipients = [to] + cc
    return f"Email sent to {', '.join(recipients)} with subject '{subject}'"

继承BaseTool实现工具类

除了使用@tool装饰器,LangChain中定义工具还有一种更灵活的方式是继承BaseTool实现工具类,它适合需要维护内部状态或依赖注入的场景。

from langchain_core.tools import BaseTool
from pydantic import Field


class DatabaseQueryTool(BaseTool):
    name: str = "database_query"
    description: str = "Query the database with SQL and return results as a string."

    # 可以在工具类中定义依赖项
    connection_string: str = Field(default="sqlite:///app.db")

    def _run(self, query: str) -> str:
        """Execute a SQL query and return the results."""
        # 实际项目中这里连接数据库并执行查询
        return f"Query '{query}' executed successfully. 3 rows returned."

    async def _arun(self, query: str) -> str:
        """Async version of _run."""
        return self._run(query)

在普通ChatModel中使用工具

绑定工具

基础ChatModel本身不具备工具调用能力,但我们可以通过bind_tools()方法将工具列表绑定到模型上,此后模型在输出时可能会包含工具调用请求(而非直接输出文本)。

from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from datetime import datetime
import pytz


@tool
def get_weather(city: str) -> str:
    """Get current weather for a given city."""
    return "晴 14℃"


@tool
def get_time(timezone: str) -> str:
    """Get the current time in a given timezone, e.g. 'Asia/Shanghai'."""
    tz = pytz.timezone(timezone)
    return datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")


model = ChatOpenAI(
    model="qwen3:30b-a3b",
    base_url="http://localhost:11434/v1/",
    api_key="dummy",
    temperature=1,
    top_p=1,
    max_tokens=16384,
    timeout=120,
    max_retries=6
)

# 绑定工具列表到模型
model_with_tools = model.bind_tools([get_weather, get_time])

# 调用绑定了工具的模型
resp = model_with_tools.invoke("洛杉矶现在的天气怎么样?")
print(resp.content)  # 如果LLM决定调用工具,这里可能为空
print(resp.tool_calls)  # 工具调用请求列表

执行上面代码后,一般来说,模型会输出工具调用请求。

[{'name': 'get_weather', 'args': {'city': 'Los Angeles'}, 'id': 'call_72099e1c761e45c984acea', 'type': 'tool_call'}]

手动处理工具调用

使用bind_tools()后,模型返回了工具调用请求,我们需要手动处理这些请求、执行工具、并将结果反馈给模型。

from langchain_core.messages import HumanMessage, ToolMessage

# 第一轮:用户提问,模型决定调用工具
messages = [HumanMessage("洛杉矶现在天气如何?")]
resp = model_with_tools.invoke(messages)
messages.append(resp)  # 将AI的工具调用请求加入消息历史

# 执行工具调用
for tool_call in resp.tool_calls:
    tool_name = tool_call["name"]
    tool_args = tool_call["args"]

    # 根据工具名称找到对应工具并执行
    tools_map = {"get_weather": get_weather, "get_time": get_time}
    tool_result = tools_map[tool_name].invoke(tool_args)

    # 将工具结果以 ToolMessage 的形式加入消息历史
    messages.append(ToolMessage(
        content=str(tool_result),
        tool_call_id=tool_call["id"]
    ))

# 第二轮:将工具结果发回给模型,得到最终回答
final_resp = model_with_tools.invoke(messages)
print(final_resp.content)

上面的手动处理流程比较繁琐,实际开发中我们基本不会这样写,但这也正是智能体(Agent)存在的意义,智能体封装了这个循环,让我们不必手动处理工具调用的循环逻辑。

在智能体中使用工具

智能体会自动处理工具调用的完整循环,我们只需将工具列表传给create_agent()即可。这部分内容在"构建智能体"章节中已有详细介绍,这里简要回顾。

from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI


@tool
def get_weather(city: str) -> str:
    """Get current weather for a given city."""
    return "晴 14℃"


@tool
def get_population(city: str) -> str:
    """Get the current population of a given city."""
    return "约400万人"


model = ChatOpenAI(
    model="qwen3:30b-a3b",
    base_url="http://localhost:11434/v1/",
    api_key="dummy",
    temperature=1,
    top_p=1,
    max_tokens=16384,
    timeout=120,
    max_retries=6
)

agent = create_agent(
    model=model,
    tools=[get_weather, get_population],
    system_prompt="You are a helpful AI assistant that can answer questions about cities.",
)

resp = agent.invoke({
    "messages": [{"role": "user", "content": "洛杉矶的天气和人口分别是多少?"}]
})
print(resp["messages"][-1].content)

智能体会自动决定调用哪些工具、以什么顺序调用、调用多少次,最终综合所有工具的结果生成回答。

接入MCP工具

什么是MCP

MCP(Model Context Protocol,模型上下文协议)是由Anthropic提出的一种开放标准,它试图统一AI应用与外部工具、数据源之间的连接方式,这有点类似于AI领域的“USB”接口标准,相比于前面介绍的内置工具,MCP更像是连接到我们应用中的“外设”。2025年12月,MCP被捐赠给Linux基金会旗下的Agentic AI Foundation,获得了包括OpenAI、Google、Microsoft、AWS等主流AI公司的支持,成为工具集成的事实标准。

MCP的生态系统目前已经相当丰富,社区已经发布了数千个MCP服务器,涵盖GitHub、Google Drive、Slack、数据库、Stripe等各类常用服务,我们可以直接复用许多现成的服务器,而不必为每个工具单独编写集成代码,这也是MCP的意义所在。

安装依赖

MCP支持不在LangChain核心包中,而是需要通过官方维护的langchain-mcp-adapters包来接入MCP工具。

uv add langchain-mcp-adapters

MCP 传输协议

MCP目前支持两种传输协议。

stdio(标准输入输出):客户端以子进程方式启动MCP服务端程序,通过标准输入输出进行通信,适合本地工具服务器或命令行工具的集成场景。

HTTP(Streamable HTTP):客户端通过HTTP请求与远程MCP服务端通信,适合Web服务、远程API等场景。注意,旧版本中使用的SSE(Server-Sent Events)作为独立传输协议已在MCP协议版本2025-03-26中弃用,现已被Streamable HTTP取代。

对于MCP使用的传输协议,官方最新版文档明确只列出这两种,但实际上社区早期可能还广泛使用了一些过时或第三方扩展的协议,例如SSE、WebSocket等,langchain-mcp-adapters实际上支持stdio、Streamable HTTP、SSE、WebSocket四种传输协议,不过未来情况可能发生变化,如果用到这些扩展,最好参考LangChain官方文档确认现在是否仍在支持。

创建本地MCP服务器

在演示接入MCP之前,我们得先有一个MCP服务端,这里我们先用fastmcp创建一个简单的本地MCP服务器作为示例。fastmcp是一套基于Python的MCP Server开发框架,有关它的使用不是学习LangChain的重点,我们这里不多介绍,仅简单演示使用。

首先安装fastmcp相关依赖。

uv add fastmcp

然后创建weather_server.py编写以下代码。注意编写完成后,我们不必手动启动它,stdio模式的MCP Server一般都由客户端启动并连接标准输入输出进行通信。

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Math")


@mcp.tool()
def get_weather(city: str) -> str:
    """Get current weather for a given city.

    Args:
        city: City name, for example "Los Angeles" or "Beijing".

    Returns:
        A weather summary string, for example "Sunny 14°C".
    """
    # 实际项目中这里应调用真实的天气API
    return "晴 14℃"


if __name__ == "__main__":
    mcp.run(transport="stdio")

通过stdio接入MCP工具

LangChain中,通过stdio接入MCP工具时,我们要添加MCP Server的启动参数,LangChain框架会帮我们启动它。连接MCP Server后,我们可以使用load_mcp_tools()函数从MCP服务器加载工具,并将其自动转换为LangChain兼容的工具对象,此时就可以接入智能体并调用了,下面是例子代码。

import asyncio

from langchain_openai import ChatOpenAI
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain.agents import create_agent

server_params = StdioServerParameters(
    command="C:\\Users\\HUAWEI\\workspace\\workspace-me\\demo-langchain\\.venv\\Scripts\\python.exe",
    args=["-u", "C:\\Users\\HUAWEI\\workspace\\workspace-me\\demo-langchain\\weather_server.py"],
)


async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 初始化连接
            await session.initialize()

            # 加载MCP工具,自动转换为LangChain工具
            tools = await load_mcp_tools(session)
            print(f"已加载 {len(tools)} 个工具:{[t.name for t in tools]}")

            # 将MCP工具绑定到智能体
            model = ChatOpenAI(
                model="qwen3:30b-a3b",
                base_url="http://localhost:11434/v1/",
                api_key="dummy",
                temperature=1,
                top_p=1,
                max_tokens=16384,
                timeout=120,
                max_retries=6
            )
            agent = create_agent(model, tools)
            resp = await agent.ainvoke({"messages": "洛杉矶现在的天气怎么样?"})
            print(resp["messages"][-1].content)


asyncio.run(main())

注:Python的-u参数用于禁用标准输入输出的缓冲区,以保证流式输出的实时性。

使用MultiServerMCPClient连接多个服务器

实际应用中,如果我们需要同时连接多个MCP服务器,MultiServerMCPClient提供了统一的多服务器管理能力,能让我们同时连接stdio和HTTP类型的服务器,并将所有工具合并为一个列表。

client = MultiServerMCPClient(
    {
        "math": {
            "transport": "stdio",
            "command": "C:\\Users\\HUAWEI\\workspace\\workspace-me\\demo-langchain\\.venv\\Scripts\\python.exe",
            "args": ["-u", "C:\\Users\\HUAWEI\\workspace\\workspace-me\\demo-langchain\\weather_server.py"],
        },
        "weather": {
            "transport": "http",
            "url": "http://localhost:8000/mcp",
            "headers": {
                "Authorization": "Bearer <YOUR_TOKEN>",
            },
        },
    }
)
tools = await client.get_tools()
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。