LangChain 1.3 完全教程:从入门到精通-Part 11: Tools(工具系统)

文章目录

Part 11: Tools(工具系统)

11.1 工具概述

工具(Tool)是 LangChain 中让 LLM 与外部世界交互的桥梁。通过工具,LLM 可以执行搜索、计算、文件操作、API 调用等实际操作。
#mermaid-svg-rZAEB5DNo3G4pVw7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rZAEB5DNo3G4pVw7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rZAEB5DNo3G4pVw7 .error-icon{fill:#552222;}#mermaid-svg-rZAEB5DNo3G4pVw7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rZAEB5DNo3G4pVw7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rZAEB5DNo3G4pVw7 .marker.cross{stroke:#333333;}#mermaid-svg-rZAEB5DNo3G4pVw7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rZAEB5DNo3G4pVw7 p{margin:0;}#mermaid-svg-rZAEB5DNo3G4pVw7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rZAEB5DNo3G4pVw7 .cluster-label text{fill:#333;}#mermaid-svg-rZAEB5DNo3G4pVw7 .cluster-label span{color:#333;}#mermaid-svg-rZAEB5DNo3G4pVw7 .cluster-label span p{background-color:transparent;}#mermaid-svg-rZAEB5DNo3G4pVw7 .label text,#mermaid-svg-rZAEB5DNo3G4pVw7 span{fill:#333;color:#333;}#mermaid-svg-rZAEB5DNo3G4pVw7 .node rect,#mermaid-svg-rZAEB5DNo3G4pVw7 .node circle,#mermaid-svg-rZAEB5DNo3G4pVw7 .node ellipse,#mermaid-svg-rZAEB5DNo3G4pVw7 .node polygon,#mermaid-svg-rZAEB5DNo3G4pVw7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rZAEB5DNo3G4pVw7 .rough-node .label text,#mermaid-svg-rZAEB5DNo3G4pVw7 .node .label text,#mermaid-svg-rZAEB5DNo3G4pVw7 .image-shape .label,#mermaid-svg-rZAEB5DNo3G4pVw7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-rZAEB5DNo3G4pVw7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rZAEB5DNo3G4pVw7 .rough-node .label,#mermaid-svg-rZAEB5DNo3G4pVw7 .node .label,#mermaid-svg-rZAEB5DNo3G4pVw7 .image-shape .label,#mermaid-svg-rZAEB5DNo3G4pVw7 .icon-shape .label{text-align:center;}#mermaid-svg-rZAEB5DNo3G4pVw7 .node.clickable{cursor:pointer;}#mermaid-svg-rZAEB5DNo3G4pVw7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rZAEB5DNo3G4pVw7 .arrowheadPath{fill:#333333;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rZAEB5DNo3G4pVw7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rZAEB5DNo3G4pVw7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rZAEB5DNo3G4pVw7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rZAEB5DNo3G4pVw7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rZAEB5DNo3G4pVw7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rZAEB5DNo3G4pVw7 .cluster text{fill:#333;}#mermaid-svg-rZAEB5DNo3G4pVw7 .cluster span{color:#333;}#mermaid-svg-rZAEB5DNo3G4pVw7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rZAEB5DNo3G4pVw7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rZAEB5DNo3G4pVw7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-rZAEB5DNo3G4pVw7 .icon-shape,#mermaid-svg-rZAEB5DNo3G4pVw7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rZAEB5DNo3G4pVw7 .icon-shape p,#mermaid-svg-rZAEB5DNo3G4pVw7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rZAEB5DNo3G4pVw7 .icon-shape .label rect,#mermaid-svg-rZAEB5DNo3G4pVw7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rZAEB5DNo3G4pVw7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rZAEB5DNo3G4pVw7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rZAEB5DNo3G4pVw7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 选择工具
选择工具
选择工具
返回结果
返回结果
返回结果
用户问题
大语言模型
搜索工具
计算器工具
代码执行工具
最终回答

11.2 @tool 装饰器

@tool 是定义工具最简洁的方式,推荐在大多数场景下使用。

基本用法 Demo
python 复制代码
"""
@tool 装饰器基本用法 Demo
"""
from langchain_core.tools import tool


# ==========================================
# 1. 最简单的工具定义
# ==========================================

@tool
def get_word_length(word: str) -> int:
    """计算单词或文本的字符长度。

    Args:
        word: 要计算长度的文本

    Returns:
        文本的字符数
    """
    return len(word)


# 使用工具
result = get_word_length.invoke("Hello World")
print(f"长度: {result}")  # 输出: 11

# 查看工具信息
print(f"工具名称: {get_word_length.name}")
print(f"工具描述: {get_word_length.description}")
print(f"工具参数: {get_word_length.args}")
参数类型提示 Demo
python 复制代码
"""
@tool 参数类型提示 Demo
"""
from langchain_core.tools import tool
from typing import Optional, List


@tool
def search_documents(
    query: str,
    top_k: int = 5,
    category: Optional[str] = None,
    tags: Optional[List[str]] = None,
) -> str:
    """在文档库中搜索相关文档。

    Args:
        query: 搜索关键词
        top_k: 返回结果数量,默认为 5
        category: 文档类别过滤(可选)
        tags: 标签过滤列表(可选)

    Returns:
        搜索结果的 JSON 字符串
    """
    # 模拟搜索逻辑
    results = [
        f"结果{i+1}: 关于 '{query}' 的文档内容..."
        for i in range(top_k)
    ]
    return "\n".join(results)


# 查看工具的参数 schema(会自动从类型提示生成)
import json
print("工具参数 Schema:")
print(json.dumps(search_documents.args_schema.model_json_schema(), indent=2, ensure_ascii=False))

# 调用工具
result = search_documents.invoke({
    "query": "机器学习",
    "top_k": 3,
    "category": "AI",
})
print(f"\n搜索结果:\n{result}")
默认参数 Demo
python 复制代码
"""
@tool 默认参数 Demo
"""
from langchain_core.tools import tool


@tool
def calculate_bmi(
    weight: float,
    height: float,
    unit: str = "metric",
) -> str:
    """计算身体质量指数(BMI)。

    Args:
        weight: 体重(公斤或磅)
        height: 身高(米或英寸)
        unit: 单位系统,"metric"(公制,默认)或 "imperial"(英制)

    Returns:
        BMI 值和健康评估
    """
    if unit == "metric":
        bmi = weight / (height ** 2)
    else:
        bmi = (weight * 703) / (height ** 2)

    if bmi < 18.5:
        category = "偏瘦"
    elif bmi < 24:
        category = "正常"
    elif bmi < 28:
        category = "偏胖"
    else:
        category = "肥胖"

    return f"BMI: {bmi:.1f}, 分类: {category}"


# 调用(使用默认 unit)
result = calculate_bmi.invoke({"weight": 70, "height": 1.75})
print(result)

# 调用(指定 unit)
result = calculate_bmi.invoke({"weight": 154, "height": 69, "unit": "imperial"})
print(result)
复杂参数(Pydantic 模型)Demo
python 复制代码
"""
@tool 复杂参数 Demo(使用 Pydantic 模型)
"""
from langchain_core.tools import tool
from pydantic import BaseModel, Field


# ==========================================
# 定义输入模型
# ==========================================

class WeatherQuery(BaseModel):
    """天气查询的输入参数"""
    city: str = Field(..., description="城市名称,如:北京、上海")
    unit: str = Field(
        default="celsius",
        description="温度单位: celsius(摄氏度)或 fahrenheit(华氏度)"
    )
    days: int = Field(
        default=1,
        ge=1,
        le=7,
        description="预报天数,1-7 天"
    )


@tool(args_schema=WeatherQuery)
def get_weather(query: WeatherQuery) -> str:
    """获取指定城市的天气预报。

    这个工具可以查询中国主要城市的天气预报信息。
    """
    city = query.city
    unit = query.unit
    days = query.days

    # 模拟天气数据
    temp = 25 if unit == "celsius" else 77
    result = f"{city}未来{days}天天气预报:\n"
    for i in range(days):
        result += f"  第{i+1}天: 晴,{temp + i}°{'C' if unit == 'celsius' else 'F'}\n"

    return result.strip()


# 调用
result = get_weather.invoke({
    "city": "北京",
    "unit": "celsius",
    "days": 3,
})
print(result)

# 查看 args_schema
print(f"\n参数 Schema: {get_weather.args_schema.model_json_schema()}")
工具描述的重要性
python 复制代码
"""
工具描述的重要性

工具的 docstring 不仅是给人看的文档,更是 LLM 决定是否使用该工具的依据。
好的描述应该:
1. 清晰说明工具的功能
2. 说明适用场景
3. 提供使用示例
"""
from langchain_core.tools import tool


# 好的描述
@tool
def good_search_tool(query: str) -> str:
    """在内部知识库中搜索技术文档。

    当用户询问关于公司产品、技术架构、API 使用方法等问题时使用此工具。
    不要用于搜索一般性知识或实时信息。

    Args:
        query: 搜索关键词,应该简洁明确

    Returns:
        相关文档的摘要
    """
    return f"搜索 '{query}' 的结果..."


# 差的描述(LLM 难以判断何时使用)
@tool
def bad_search_tool(query: str) -> str:
    """搜索东西。

    Args:
        query: 查询
    """
    return f"搜索 '{query}' 的结果..."

11.3 StructuredTool

python 复制代码
"""
StructuredTool Demo
使用 Pydantic 模型定义输入
"""
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field


# ==========================================
# 定义输入 Schema
# ==========================================

class EmailInput(BaseModel):
    """发送邮件的输入参数"""
    to: str = Field(..., description="收件人邮箱地址")
    subject: str = Field(..., description="邮件主题")
    body: str = Field(..., description="邮件正文")
    cc: list[str] = Field(default_factory=list, description="抄送列表")
    is_html: bool = Field(default=False, description="是否为 HTML 格式")


# ==========================================
# 定义工具函数
# ==========================================

def send_email_func(to: str, subject: str, body: str, cc: list = None, is_html: bool = False) -> str:
    """发送邮件"""
    cc_str = f", 抄送: {', '.join(cc)}" if cc else ""
    return f"邮件已发送至 {to}{cc_str}\n主题: {subject}\n正文: {body[:50]}..."


# ==========================================
# 创建 StructuredTool
# ==========================================

email_tool = StructuredTool.from_function(
    func=send_email_func,
    name="send_email",               # 工具名称
    description="向指定收件人发送邮件。当用户要求发送邮件时使用此工具。",  # 工具描述
    args_schema=EmailInput,          # 参数 Schema
)

# 使用
result = email_tool.invoke({
    "to": "alice@example.com",
    "subject": "项目进度更新",
    "body": "本周项目进展顺利,已完成核心功能开发。",
    "cc": ["bob@example.com"],
})
print(result)

# 查看 Schema
print(f"\n工具名称: {email_tool.name}")
print(f"参数: {email_tool.args_schema.model_json_schema()}")

11.4 Tool 类

python 复制代码
"""
Tool 类 Demo
直接使用 Tool 类创建工具
"""
from langchain_core.tools import Tool


# ==========================================
# 方式一:使用 Tool 类
# ==========================================

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


multiply_tool = Tool(
    name="multiply",                    # 工具名称
    func=multiply,                      # 工具函数
    description="计算两个整数的乘积。当用户需要进行乘法运算时使用。",  # 描述
    # args_schema=None,                # 可选:自定义参数 Schema
    # return_direct=False,             # 可选:是否直接返回结果(不经过 LLM)
)

result = multiply_tool.invoke({"a": 6, "b": 7})
print(f"6 x 7 = {result}")

# ==========================================
# 方式二:从函数快速创建
# ==========================================

def add_numbers(a: float, b: float) -> float:
    """计算两个数的和"""
    return a + b


add_tool = Tool.from_function(
    func=add_numbers,
    name="add",
    description="计算两个数的和。",
)

result = add_tool.invoke({"a": 3.14, "b": 2.86})
print(f"3.14 + 2.86 = {result}")

11.5 DynamicTool

python 复制代码
"""
DynamicTool Demo
动态创建工具(运行时生成)
"""
from langchain_core.tools import Tool


def create_lookup_tool(table_name: str, data: dict) -> Tool:
    """动态创建查找工具"""
    def lookup(key: str) -> str:
        """在表中查找数据"""
        return str(data.get(key, f"未找到 '{key}'"))

    return Tool(
        name=f"lookup_{table_name}",
        func=lookup,
        description=f"在 {table_name} 表中查找数据。可用的键: {', '.join(data.keys())}",
    )


# 动态创建多个查找工具
employee_tool = create_lookup_tool("员工", {
    "张三": "工程师, 工程部",
    "李四": "设计师, 设计部",
    "王五": "产品经理, 产品部",
})

department_tool = create_lookup_tool("部门", {
    "工程部": "50人, 负责产品研发",
    "设计部": "20人, 负责UI/UX设计",
    "产品部": "15人, 负责产品规划",
})

# 使用
print(employee_tool.invoke("张三"))
print(department_tool.invoke("工程部"))

11.6 内置工具

搜索工具 Demo
python 复制代码
"""
搜索工具 Demo
安装: pip install duckduckgo-search langchain-community
"""
# from langchain_community.tools import DuckDuckGoSearchRun
# from langchain_community.tools import DuckDuckGoSearchResults

# # 基本搜索
# search = DuckDuckGoSearchRun()
# result = search.invoke("LangChain 最新版本")
# print(f"搜索结果: {result}")

# # 结构化搜索结果
# search_results = DuckDuckGoSearchResults()
# results = search_results.invoke("Python 3.12 新特性")
# print(f"结构化结果: {results}")
Python REPL Demo
python 复制代码
"""
Python REPL 工具 Demo
安装: pip install langchain-community
"""
# from langchain_community.tools import PythonREPLTool

# python_tool = PythonREPLTool()

# # 执行 Python 代码
# result = python_tool.invoke("print([x**2 for x in range(10)])")
# print(result)

# # 执行数学计算
# result = python_tool.invoke("import math\nprint(math.sqrt(144))")
# print(result)

# 注意: Python REPL 工具有安全风险,生产环境慎用!
文件工具 Demo
python 复制代码
"""
文件工具 Demo
"""
from langchain_core.tools import tool
import os


@tool
def read_file(file_path: str) -> str:
    """读取指定路径的文本文件内容。

    Args:
        file_path: 文件的绝对路径

    Returns:
        文件的文本内容
    """
    if not os.path.exists(file_path):
        return f"错误: 文件 '{file_path}' 不存在"
    with open(file_path, "r", encoding="utf-8") as f:
        return f.read()


@tool
def write_file(file_path: str, content: str) -> str:
    """将内容写入指定路径的文本文件。

    Args:
        file_path: 文件的绝对路径
        content: 要写入的内容

    Returns:
        操作结果
    """
    os.makedirs(os.path.dirname(file_path), exist_ok=True)
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(content)
    return f"成功写入 {len(content)} 个字符到 {file_path}"


@tool
def list_files(directory: str) -> str:
    """列出指定目录下的文件和子目录。

    Args:
        directory: 目录路径

    Returns:
        文件和目录列表
    """
    if not os.path.exists(directory):
        return f"错误: 目录 '{directory}' 不存在"
    items = os.listdir(directory)
    return "\n".join(items)


# 使用
result = write_file.invoke({
    "file_path": "/data/user/work/test_tool.txt",
    "content": "这是通过工具写入的测试文件。",
})
print(result)

result = read_file.invoke({"file_path": "/data/user/work/test_tool.txt"})
print(result)
HTTP 请求工具 Demo
python 复制代码
"""
HTTP 请求工具 Demo
"""
from langchain_core.tools import tool
import json


@tool
def http_get(url: str, params: dict = None) -> str:
    """发送 HTTP GET 请求。

    Args:
        url: 请求的 URL
        params: 查询参数(可选)

    Returns:
        响应的 JSON 内容
    """
    try:
        import requests
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        return response.text[:2000]  # 限制返回长度
    except Exception as e:
        return f"请求失败: {e}"


@tool
def http_post(url: str, data: dict = None, json_data: dict = None) -> str:
    """发送 HTTP POST 请求。

    Args:
        url: 请求的 URL
        data: 表单数据(可选)
        json_data: JSON 数据(可选)

    Returns:
        响应内容
    """
    try:
        import requests
        response = requests.post(url, data=data, json=json_data, timeout=10)
        response.raise_for_status()
        return response.text[:2000]
    except Exception as e:
        return f"请求失败: {e}"


# 使用
result = http_get.invoke({"url": "https://httpbin.org/get"})
print(f"GET 响应: {result[:200]}...")
Wikipedia 工具 Demo
python 复制代码
"""
Wikipedia 工具 Demo
安装: pip install wikipedia langchain-community
"""
# from langchain_community.tools import WikipediaQueryRun
# from langchain_community.utilities import WikipediaAPIWrapper

# # 创建 Wikipedia 工具
# wikipedia = WikipediaQueryRun(
#     api_wrapper=WikipediaAPIWrapper(
#         top_k_results=3,           # 返回结果数量
#         doc_content_chars_max=500, # 每个结果的最大字符数
#         lang="zh",                 # 语言设置
#     )
# )

# # 搜索
# result = wikipedia.invoke("人工智能")
# print(result)

11.7 ToolKit

python 复制代码
"""
ToolKit Demo
将多个工具组合在一起
"""
from langchain_core.tools import tool


# 定义多个工具
@tool
def calculator(expression: str) -> str:
    """计算数学表达式。支持加减乘除、幂运算等。

    Args:
        expression: 数学表达式,如 "2 + 3 * 4"

    Returns:
        计算结果
    """
    try:
        # 安全地计算数学表达式
        allowed = set("0123456789+-*/.() ")
        if not all(c in allowed for c in expression):
            return "错误: 表达式包含不允许的字符"
        result = eval(expression)
        return f"{expression} = {result}"
    except Exception as e:
        return f"计算错误: {e}"


@tool
def unit_converter(value: float, from_unit: str, to_unit: str) -> str:
    """单位转换工具。支持长度、重量、温度等单位。

    Args:
        value: 要转换的数值
        from_unit: 原始单位
        to_unit: 目标单位

    Returns:
        转换结果
    """
    conversions = {
        ("km", "m"): lambda x: x * 1000,
        ("m", "km"): lambda x: x / 1000,
        ("kg", "g"): lambda x: x * 1000,
        ("g", "kg"): lambda x: x / 1000,
        ("celsius", "fahrenheit"): lambda x: x * 9/5 + 32,
        ("fahrenheit", "celsius"): lambda x: (x - 32) * 5/9,
    }

    key = (from_unit.lower(), to_unit.lower())
    if key in conversions:
        result = conversions[key](value)
        return f"{value} {from_unit} = {result:.2f} {to_unit}"
    return f"不支持从 {from_unit} 到 {to_unit} 的转换"


@tool
def string_operations(text: str, operation: str) -> str:
    """字符串操作工具。

    Args:
        text: 输入文本
        operation: 操作类型 - "upper"(大写)、"lower"(小写)、
                   "reverse"(反转)、"length"(长度)、"word_count"(词数)

    Returns:
        操作结果
    """
    ops = {
        "upper": lambda t: t.upper(),
        "lower": lambda t: t.lower(),
        "reverse": lambda t: t[::-1],
        "length": lambda t: str(len(t)),
        "word_count": lambda t: str(len(t.split())),
    }
    if operation in ops:
        return ops[operation](text)
    return f"不支持的操作: {operation}。可用操作: {', '.join(ops.keys())}"


# ==========================================
# 组合为工具集
# ==========================================

toolkit = [calculator, unit_converter, string_operations]

print("工具集包含以下工具:")
for t in toolkit:
    print(f"  - {t.name}: {t.description[:50]}...")

# 使用
print(f"\n计算: {calculator.invoke('2 ** 10')}")
print(f"转换: {unit_converter.invoke({'value': 100, 'from_unit': 'km', 'to_unit': 'm'})}")
print(f"操作: {string_operations.invoke({'text': 'Hello World', 'operation': 'upper'})}")

11.8 工具调用

bind_tools 方法 Demo
python 复制代码
"""
bind_tools 方法 Demo
将工具绑定到 LLM,让 LLM 能够自动选择和调用工具
"""
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool


# 定义工具
@tool
def get_stock_price(symbol: str) -> str:
    """获取股票的当前价格。

    Args:
        symbol: 股票代码,如 AAPL、GOOGL、TSLA

    Returns:
        股票当前价格信息
    """
    prices = {"AAPL": 178.50, "GOOGL": 141.20, "TSLA": 245.80}
    price = prices.get(symbol.upper(), "未知")
    return f"{symbol} 当前价格: ${price}"


@tool
def get_stock_info(symbol: str) -> str:
    """获取股票的基本信息。

    Args:
        symbol: 股票代码

    Returns:
        股票基本信息
    """
    info = {
        "AAPL": "Apple Inc. - 科技公司,市值最大",
        "GOOGL": "Alphabet Inc. - Google 母公司",
        "TSLA": "Tesla Inc. - 电动汽车制造商",
    }
    return info.get(symbol.upper(), "未找到该股票信息")


# ==========================================
# 使用 bind_tools 绑定工具
# ==========================================

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 将工具列表绑定到 LLM
llm_with_tools = llm.bind_tools([get_stock_price, get_stock_info])

# LLM 会决定是否需要调用工具
response = llm_with_tools.invoke("苹果公司的股票现在多少钱?")

# 检查是否有工具调用
if response.tool_calls:
    print("LLM 决定调用工具:")
    for tool_call in response.tool_calls:
        print(f"  工具: {tool_call['name']}")
        print(f"  参数: {tool_call['args']}")

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

        if tool_name == "get_stock_price":
            result = get_stock_price.invoke(tool_args)
        elif tool_name == "get_stock_info":
            result = get_stock_info.invoke(tool_args)
        print(f"  结果: {result}")
else:
    print(f"LLM 直接回答: {response.content}")
手动执行工具调用 Demo
python 复制代码
"""
手动执行工具调用 Demo
"""
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage


@tool
def add(a: int, b: int) -> int:
    """计算两个整数的和"""
    return a + b


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


tools = [add, multiply]
tools_by_name = {t.name: t for t in tools}

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)

# 第一轮:用户提问
messages = [HumanMessage(content="计算 (3 + 5) * 2 等于多少?")]
response = llm_with_tools.invoke(messages)
messages.append(response)

# 工具调用循环
while response.tool_calls:
    for tool_call in response.tool_calls:
        tool_name = tool_call["name"]
        tool_args = tool_call["args"]
        tool_result = tools_by_name[tool_name].invoke(tool_args)

        # 将工具结果添加到消息列表
        messages.append(
            ToolMessage(
                content=str(tool_result),
                tool_call_id=tool_call["id"],
            )
        )

    # 让 LLM 继续处理
    response = llm_with_tools.invoke(messages)
    messages.append(response)

print(f"最终回答: {response.content}")
工具调用错误处理
python 复制代码
"""
工具调用错误处理 Demo
"""
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool, ToolException
from langchain_core.messages import HumanMessage, ToolMessage


@tool
def safe_divide(a: float, b: float) -> float:
    """安全地计算两个数的除法。

    Args:
        a: 被除数
        b: 除数

    Returns:
        商

    Raises:
        ToolException: 当除数为零时
    """
    if b == 0:
        raise ToolException("除数不能为零!请提供一个非零的除数。")
    return a / b


llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools([safe_divide])

# 测试错误处理
messages = [HumanMessage(content="计算 10 除以 0")]
response = llm_with_tools.invoke(messages)
messages.append(response)

if response.tool_calls:
    tool_call = response.tool_calls[0]
    try:
        result = safe_divide.invoke(tool_call["args"])
        messages.append(ToolMessage(content=str(result), tool_call_id=tool_call["id"]))
    except ToolException as e:
        # 将错误信息返回给 LLM
        messages.append(
            ToolMessage(
                content=f"工具执行错误: {e}",
                tool_call_id=tool_call["id"],
            )
        )

    # LLM 根据错误信息重新回答
    final_response = llm_with_tools.invoke(messages)
    print(f"最终回答: {final_response.content}")

11.9 工具最佳实践

工具最佳实践总结

==========================================

1. 工具设计原则

=========================================="""

  1. 单一职责:每个工具只做一件事
  2. 清晰命名:工具名称应该简洁且具有描述性
  3. 详细描述:docstring 是 LLM 选择工具的依据,务必详细
  4. 参数明确:使用类型提示和 Field 描述每个参数
  5. 返回简洁:工具返回的结果应该简洁明了
  6. 错误友好:工具出错时返回有意义的错误信息
    """

==========================================

2. 工具描述编写技巧

=========================================="""

好的描述:

"在公司的员工数据库中查找员工信息。当用户询问员工联系方式、

部门归属或职位信息时使用此工具。输入为员工姓名。"

差的描述:

"查找员工。"

技巧:

  • 说明工具的功能
  • 说明适用场景(何时使用)
  • 说明不适用场景(何时不使用)
  • 提供输入示例
    """

==========================================

3. 错误处理策略

=========================================="""

  1. 使用 ToolException 抛出工具错误(而非普通异常)
  2. 错误信息应该清晰、可操作
  3. 对输入参数进行验证
  4. 设置超时时间,防止工具挂起
  5. 记录工具调用日志,便于调试
    """

==========================================

4. 安全考虑

=========================================="""

  1. 限制 Python REPL 的权限(使用沙箱)
  2. 对文件操作进行路径验证(防止路径遍历)
  3. 对 HTTP 请求进行 URL 白名单限制
  4. 对数据库查询进行参数化(防止 SQL 注入)
  5. 限制工具的执行时间和资源消耗
  6. 敏感操作需要二次确认
    """

相关推荐
夜勤月1 小时前
AQS 与 ThreadPoolExecutor 深度拆解:JDK 高并发底层设计精髓
android·java·开发语言
luj_17681 小时前
R语言生态优势与学习曲线分析
c语言·开发语言·网络·经验分享·算法
程序大视界1 小时前
【C++ 从基础到项目实战】C++(二):数组、字符串与结构体——组织数据的容器
开发语言·c++·cpp
叶子野格1 小时前
《C语言学习:文件操作》16
c语言·开发语言·c++·学习·visual studio
AI科技星1 小时前
万有引力G与真空介电常数ε0全维度完整关系式汇编(基于v=c螺旋时空理论)
c语言·开发语言·前端·javascript·网络·汇编·electron
如竟没有火炬2 小时前
寻找峰值——二分
java·开发语言·数据结构·python·算法·散列表
JAVA社区2 小时前
Java高级全套教程(十三)—— 分布式锁超详细实战详解(原理+三种方案企业级落地)
java·开发语言·分布式·spring cloud·面试·java-zookeeper
超梦dasgg2 小时前
Java 生产环境 Maven 实战指南
java·开发语言·maven
叶子野格2 小时前
《C语言学习:位运算》17
c语言·开发语言·c++·学习·visual studio