langchain学习笔记(二):工具的调用

10 阅读10分钟

Tool Calling

定义简单tool

创建工具最简单的方法是使用 @tool 装饰器。

@tool(description="Returns the current time in yyyy-MM-dd HH:mm:ss format.")
def get_current_time(*args, **kwargs) -> str:
    """
    获取当前系统时间。
    格式为:yyyy-MM-dd HH:mm:ss
    """
    now = datetime.datetime.now()
    return now.strftime("%Y-%m-%d %H:%M:%S")

如果description不写,则默认注释里的就是描述信息,否则则使用description,描述信息需要易于大模型的理解。

"""
获取当前系统时间。
格式为:yyyy-MM-dd HH:mm:ss
"""

llm绑定tool

借助 bind_tools,实现llm与工具的绑定,在底层,这些都会被转换为 OpenAI 工具模式,其格式如下:

{
    "name": "...",
    "description": "...",
    "parameters": {...}  # JSONSchema
}

代码示例:

# ========== 方式1:自动工具调用(推荐) ==========
def demo_simple_tool_calling():
    """
    方式1:使用 bind_tools + 手动执行
    特点:需要手动处理工具调用和结果
    """
    print("=" * 50)
    print("🛠️ 方式1:bind_tools + 手动执行工具")
    print("=" * 50)

    llm_tools = llm.bind_tools(tools=[get_current_time])
    messages = [
        {"role": "user", "content": "请获取当前时间"}
    ]

    # 第一步:LLM 返回工具调用请求
    result = llm_tools.invoke(messages)
    print(f"\n📤 LLM 响应类型: {type(result)}")
    print(f"📤 LLM 内容: {result.content}")
    print(f"📤 工具调用请求: {result.tool_calls}")

    # 第二步:手动执行工具,判断大模型的reponse是不是tool_call,如果是tool_call类型,则真正执行工具的调用
    if result.tool_calls:
        for tool_call in result.tool_calls:
            print(f"\n🔧 执行工具: {tool_call['name']}")
            print(f"🔧 工具参数: {tool_call['args']}")

            if tool_call['name'] == 'get_current_time':
                # 手动调用工具
                tool_result = get_current_time.invoke(tool_call['args'])
                print(f"✅ 工具执行结果: {tool_result}")

                # 第三步:将工具结果返回给 LLM(可选)
                messages.append(result)  # 添加 AI 的工具调用消息
                messages.append(ToolMessage(
                    content=tool_result,
                    tool_call_id=tool_call['id']
                ))

                # 让 LLM 根据工具结果生成最终回答
                final_response = llm_tools.invoke(messages)
                print(f"\n💬 最终回答: {final_response.content}")

模拟工具调用流程,通过ToolMessage

def demo_simple_tool_calling_2():
    """
    方式2:手动构造 AIMessage 和 ToolMessage
    特点:完全手动控制消息流,适合测试和调试
    """
    print("\n" + "=" * 50)
    print("🛠️ 方式2:手动构造消息流")
    print("=" * 50)

    # 手动构造 AI 的工具调用消息
    ai_message = AIMessage(
        content="",  # 通常为空,因为 AI 选择调用工具而不是直接回答
        tool_calls=[{
            "name": "get_current_time",
            "args": {},
            "id": "call_123"
        }]
    )
    print(f"\n📤 模拟 AI 工具调用: {ai_message.tool_calls}")

    # 执行工具并创建结果消息
    tool_result = get_current_time.invoke({})
    print(f"✅ 工具执行结果: {tool_result}")

    tool_message = ToolMessage(
        content=tool_result,
        tool_call_id="call_123"
    )

    # 构造完整的消息历史
    messages = [
        HumanMessage("请获取当前时间"),
        ai_message,  # AI 的工具调用
        tool_message,  # 工具执行结果
    ]

    # LLM 根据工具结果生成最终回答
    response = llm.invoke(messages)
    print(f"\n💬 最终回答: {response.content}")

多工具调用

# ========== 额外示例:多个工具 ==========
@tool
def calculate(expression: str) -> str:
    """计算数学表达式。"""
    try:
        result = eval(expression)
        return f"计算结果: {result}"
    except Exception as e:
        return f"计算错误: {str(e)}"


def demo_multiple_tools():
    """演示多个工具的使用"""
    print("\n" + "=" * 50)
    print("🛠️ 多工具示例")
    print("=" * 50)

    llm_tools = llm.bind_tools(tools=[get_current_time, calculate])
    tool_map = {
        "get_current_time": get_current_time,
        "calculate": calculate
    }

    messages = [HumanMessage(content="现在几点?然后帮我计算 15 * 8")]

    max_iterations = 10
    iteration = 0

    while iteration < max_iterations:
        iteration += 1
        response = llm_tools.invoke(messages)
        messages.append(response)

        if not response.tool_calls:
            print(f"\n✅ 最终回答: {response.content}")
            break

        for tool_call in response.tool_calls:
            tool_name = tool_call['name']
            print(f"\n🔧 调用工具: {tool_name}")

            tool_result = tool_map[tool_name].invoke(tool_call['args'])
            print(f"✅ 结果: {tool_result}")

            messages.append(ToolMessage(
                content=tool_result,
                tool_call_id=tool_call['id']
            ))

结构化入参

tool 装饰器里,有个 args_schema 参数,可以配置结构化的类

# ========== 1. 基础结构化入参 ==========
class SearchInput(BaseModel):
    """搜索工具的输入参数"""
    query: str = Field(description="搜索关键词")
    max_results: int = Field(default=10, description="最大返回结果数", ge=1, le=100)
    language: str = Field(default="zh", description="语言代码,如 zh, en")


@tool(args_schema=SearchInput)
def search_web(query: str, max_results: int = 10, language: str = "zh") -> str:
    """在网络上搜索信息。"""
    return f"搜索 '{query}',返回 {max_results}{language} 结果"


def demo_basic_structured_input():
    """演示基础结构化入参"""
    print("=" * 60)
    print("📝 示例1:基础结构化入参")
    print("=" * 60)

    llm_tools = llm.bind_tools([search_web])
    messages = [HumanMessage("搜索 LangChain 相关信息,返回5条结果")]

    response = llm_tools.invoke(messages)
    print(f"\n🤖 LLM 工具调用:")
    for tool_call in response.tool_calls:
        print(f"  工具: {tool_call['name']}")
        print(f"  参数: {tool_call['args']}")

        # 执行工具
        result = search_web.invoke(tool_call['args'])
        print(f"  结果: {result}")

嵌套复杂的结构化入参

# ========== 2. 复杂结构化入参(嵌套对象) ==========
class Address(BaseModel):
    """地址信息"""
    street: str = Field(description="街道地址")
    city: str = Field(description="城市")
    country: str = Field(default="中国", description="国家")
    postal_code: Optional[str] = Field(None, description="邮政编码")


class UserInfo(BaseModel):
    """用户信息"""
    name: str = Field(description="用户姓名")
    age: int = Field(description="年龄", ge=0, le=150)
    email: str = Field(description="邮箱地址")
    address: Address = Field(description="地址信息")
    tags: List[str] = Field(default=[], description="用户标签")

    @field_validator('email')
    def validate_email(cls, v):
        if '@' not in v:
            raise ValueError('邮箱格式不正确')
        return v


@tool(args_schema=UserInfo)
def create_user(name: str, age: int, email: str, address: Address, tags: List[str] = None) -> str:
    """创建新用户。"""
    tags = tags or []
    return f"创建用户: {name}, {age}岁, {email}, 地址: {address.city}, 标签: {tags}"


def demo_nested_structured_input():
    """演示嵌套结构化入参"""
    print("\n" + "=" * 60)
    print("📝 示例2:嵌套结构化入参")
    print("=" * 60)

    # 手动测试
    user_data = {
        "name": "张三",
        "age": 25,
        "email": "zhangsan@example.com",
        "address": {
            "street": "中关村大街1号",
            "city": "北京",
            "country": "中国",
            "postal_code": "100000"
        },
        "tags": ["VIP", "技术"]
    }

    tool_call = {
        "name": "create_user",
        "args": user_data,
        "id": "call_123",  # 必须提供 tool_call_id
        "type": "tool_call"
    }

    result = create_user.invoke(tool_call)
    print(f"\n✅ 执行结果: {result}")

结构化输出

response_format@tool 装饰器的一个重要参数,用于控制工具返回值的格式和处理方式。

描述返回类型
"content"默认值,工具返回值直接作为内容执行返回一个str
"content_and_artifact"返回内容和原始数据(artifact)工具必须返回一个元组 (content, artifact):- content: 给 LLM 看的简洁文本描述- artifact: 完整的原始数据(可以是任意类型)

代码示例

# ========== 4. 结构化出参 (content_and_artifact) ==========
class ProductInfo(BaseModel):
    """产品信息"""
    id: int
    name: str
    price: float
    stock: int
    category: str


class SearchResult(BaseModel):
    """搜索结果"""
    total: int
    products: List[ProductInfo]
    page: int
    page_size: int


class ProductSearchInput(BaseModel):
    """产品搜索输入"""
    keyword: str = Field(description="搜索关键词")
    category: Optional[str] = Field(None, description="产品分类")
    min_price: Optional[float] = Field(None, description="最低价格")
    max_price: Optional[float] = Field(None, description="最高价格")
    page: int = Field(default=1, description="页码", ge=1)
    page_size: int = Field(default=10, description="每页数量", ge=1, le=100)


@tool(
    args_schema=ProductSearchInput,
    response_format="content_and_artifact"
)
def search_products(
        keyword: str,
        category: Optional[str] = None,
        min_price: Optional[float] = None,
        max_price: Optional[float] = None,
        page: int = 1,
        page_size: int = 10
) -> Tuple[str, SearchResult]:
    """搜索产品。"""
    # 模拟数据库查询
    mock_products = [
        ProductInfo(id=1, name="iPhone 15", price=6999, stock=50, category="手机"),
        ProductInfo(id=2, name="MacBook Pro", price=12999, stock=30, category="电脑"),
        ProductInfo(id=3, name="AirPods Pro", price=1999, stock=100, category="耳机"),
    ]

    # 过滤产品
    filtered = mock_products
    if category:
        filtered = [p for p in filtered if p.category == category]
    if min_price:
        filtered = [p for p in filtered if p.price >= min_price]
    if max_price:
        filtered = [p for p in filtered if p.price <= max_price]

    # 构造结果
    result = SearchResult(
        total=len(filtered),
        products=filtered,
        page=page,
        page_size=page_size
    )

    # content: 给 LLM 的简洁描述
    content = f"找到 {result.total} 个产品"
    if category:
        content += f",分类: {category}"

    # artifact: 完整的结构化数据
    return content, result


def demo_structured_output():
    """演示结构化出参"""
    print("\n" + "=" * 60)
    print("📝 示例4:结构化出参 (content_and_artifact)")
    print("=" * 60)

    llm_with_tools = llm.bind_tools([search_products])

    messages = [HumanMessage(content="搜索价格在5000以上的手机")]

    # LLM 决策
    response = llm_with_tools.invoke(messages)

    if response.tool_calls:
        tool_call = response.tool_calls[0]
        print(f"\n🤖 LLM 决定调用工具:")
        print(f"   工具: {tool_call['name']}")
        print(f"   参数: {tool_call['args']}")

        # 执行工具
        ## 这里返回的是ToolMessage
        result = search_products.invoke(tool_call)

        print(f"\n✅ 工具执行结果:")
        print(f"   Content: {result.content}")
        print(f"   Artifact 类型: {result.artifact}")
        print(f"   产品数量: {result.artifact.total}")

structure_tool

StructuredTool 是 LangChain 里的一个类,专门用于处理多参数输入的工具。 与基础的 Tool 类(通常只接受一个字符串输入)不同,StructuredTool 利用 Pydantic 来定义和校验复杂的输入结构(Schema)。

相比 @tool 装饰器提供了更多的灵活性和控制能力。

参数类型说明
funcCallable同步执行函数
coroutineCallable异步执行函数
namestr工具名称(LLM 看到的名称)
descriptionstr工具描述(LLM 用来决定是否调用)
args_schemaType[BaseModel]Pydantic 模型定义输入参数
return_directboolTrue时直接返回给用户,不经过 LLM
handle_tool_errorbool/str/Callable错误处理方式
response_formatstr"content""content_and_artifact"
# ========== 5. 使用 StructuredTool 创建工具 ==========
class CalculatorInput(BaseModel):
    """计算器输入"""
    expression: str = Field(description="数学表达式,如 '2 + 3 * 4'")
    precision: int = Field(default=2, description="小数精度", ge=0, le=10)


class CalculatorOutput(BaseModel):
    """计算器输出"""
    expression: str
    result: float
    formatted_result: str


def calculate_func(expression: str, precision: int = 2) -> Dict[str, Any]:
    """计算数学表达式"""
    try:
        result = eval(expression)
        formatted = f"{result:.{precision}f}"
        return {
            "expression": expression,
            "result": result,
            "formatted_result": formatted
        }
    except Exception as e:
        return {
            "expression": expression,
            "result": 0,
            "formatted_result": f"错误: {str(e)}"
        }


calculator_tool = StructuredTool.from_function(
    func=calculate_func,
    name="calculator",
    description="计算数学表达式",
    args_schema=CalculatorInput,
    return_direct=False
)


def demo_structured_tool():
    """演示 StructuredTool"""
    print("\n" + "=" * 60)
    print("📝 示例5:StructuredTool 创建工具")
    print("=" * 60)

    test_cases = [
        {"expression": "15 + 25 * 3", "precision": 2},
        {"expression": "100 / 3", "precision": 4},
        {"expression": "2 ** 10", "precision": 0}
    ]

    for params in test_cases:
        result = calculator_tool.invoke(params)
        print(f"\n表达式: {params['expression']}")
        print(f"结果: {result}")

base_tool

BaseTool 是 LangChain 中所有工具的基类。通过继承它可以创建完全自定义的工具,拥有最大的灵活性和控制能力。

方式灵活性复杂度适用场景
@tool简单快速定义简单工具
StructuredTool中等动态创建、自定义配置
BaseTool复杂完全自定义、复杂逻辑

核心参数与方法:

from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Type, Any, Optional

class MyToolInput(BaseModel):
    """工具输入参数 Schema"""
    param1: str = Field(description="参数1描述")
    param2: int = Field(default=10, description="参数2描述")

class MyTool(BaseTool):
    """自定义工具"""
    
    # ========== 必须定义的属性 ==========
    name: str = "my_tool"                    # 工具名称
    description: str = "这是一个自定义工具"    # 工具描述
    args_schema: Type[BaseModel] = MyToolInput  # 输入参数 Schema
    
    # ========== 可选属性 ==========
    return_direct: bool = False              # 是否直接返回结果
    response_format: str = "content"         # 响应格式
    
    # ========== 自定义属性 ==========
    custom_config: str = "default"           # 可以添加任意自定义属性
    
    # ========== 必须实现的方法 ==========
    def _run(self, param1: str, param2: int = 10) -> str:
        """同步执行方法(必须实现)"""
        return f"执行结果: {param1}, {param2}"
    
    # ========== 可选实现的方法 ==========
    async def _arun(self, param1: str, param2: int = 10) -> str:
        """异步执行方法(可选)"""
        return f"异步执行结果: {param1}, {param2}"

代码示例

# ========== 6. 继承 BaseTool 实现完全自定义 ==========
class WeatherInput(BaseModel):
    """天气查询输入"""
    city: str = Field(description="城市名称")
    date: Optional[str] = Field(None, description="日期 (YYYY-MM-DD),默认今天")
    unit: str = Field(default="celsius", description="温度单位: celsius 或 fahrenheit")


class WeatherOutput(BaseModel):
    """天气查询输出"""
    city: str
    date: str
    temperature: float
    condition: str
    humidity: int
    wind_speed: float


class WeatherTool(BaseTool):
    name: str = "get_weather"
    description: str = "获取指定城市的天气信息"
    args_schema: type[BaseModel] = WeatherInput
    response_format: str = "content_and_artifact"

    # 自定义属性
    api_key: str = ""

    def _run(
            self,
            city: str,
            date: Optional[str] = None,
            unit: str = "celsius"
    ) -> Tuple[str, WeatherOutput]:
        """同步执行"""
        # 模拟 API 调用
        date = date or datetime.datetime.now().strftime("%Y-%m-%d")

        weather_data = {
            "北京": {"temp": 25, "condition": "晴天", "humidity": 45, "wind": 3.5},
            "上海": {"temp": 28, "condition": "多云", "humidity": 60, "wind": 4.2},
            "广州": {"temp": 32, "condition": "小雨", "humidity": 75, "wind": 2.8},
        }

        data = weather_data.get(city, {"temp": 20, "condition": "未知", "humidity": 50, "wind": 3.0})
        temp = data["temp"]

        if unit == "fahrenheit":
            temp = temp * 9 / 5 + 32

        output = WeatherOutput(
            city=city,
            date=date,
            temperature=temp,
            condition=data["condition"],
            humidity=data["humidity"],
            wind_speed=data["wind"]
        )

        content = f"{city} {date}: {output.condition}, {temp}°{'F' if unit == 'fahrenheit' else 'C'}"

        return content, output

    async def _arun(self, *args, **kwargs):
        """异步执行"""
        return self._run(*args, **kwargs)


def demo_base_tool():
    """演示继承 BaseTool"""
    print("\n" + "=" * 60)
    print("📝 示例6:继承 BaseTool 实现自定义工具")
    print("=" * 60)

    weather_tool = WeatherTool(api_key="your-api-key")

    test_cases = [
        {"city": "北京"},
        {"city": "上海", "unit": "fahrenheit"},
        {"city": "广州", "date": "2024-01-15"}
    ]

    for params in test_cases:
        tool_call = {
            "name": weather_tool.name,
            "args": params,
            "id": "call_123" + params["city"],
            "type": "tool_call"
        }
        result = weather_tool.invoke(tool_call)
        print(f"\n查询: {params}")
        print(f"Content: {result.content}")
        print(f"Artifact: {result.artifact.model_dump()}")