文章目录
-
- [Part 11: Tools(工具系统)](#Part 11: Tools(工具系统))
-
- [11.1 工具概述](#11.1 工具概述)
- [11.2 @tool 装饰器](#11.2 @tool 装饰器)
-
- [基本用法 Demo](#基本用法 Demo)
- [参数类型提示 Demo](#参数类型提示 Demo)
- [默认参数 Demo](#默认参数 Demo)
- [复杂参数(Pydantic 模型)Demo](#复杂参数(Pydantic 模型)Demo)
- 工具描述的重要性
- [11.3 StructuredTool](#11.3 StructuredTool)
- [11.4 Tool 类](#11.4 Tool 类)
- [11.5 DynamicTool](#11.5 DynamicTool)
- [11.6 内置工具](#11.6 内置工具)
-
- [搜索工具 Demo](#搜索工具 Demo)
- [Python REPL Demo](#Python REPL Demo)
- [文件工具 Demo](#文件工具 Demo)
- [HTTP 请求工具 Demo](#HTTP 请求工具 Demo)
- [Wikipedia 工具 Demo](#Wikipedia 工具 Demo)
- [11.7 ToolKit](#11.7 ToolKit)
- [11.8 工具调用](#11.8 工具调用)
-
- [bind_tools 方法 Demo](#bind_tools 方法 Demo)
- [手动执行工具调用 Demo](#手动执行工具调用 Demo)
- 工具调用错误处理
- [11.9 工具最佳实践](#11.9 工具最佳实践)
- ==========================================
- [1. 工具设计原则](#1. 工具设计原则)
- =========================================="""
- ==========================================
- [2. 工具描述编写技巧](#2. 工具描述编写技巧)
- =========================================="""
- ==========================================
- [3. 错误处理策略](#3. 错误处理策略)
- =========================================="""
- ==========================================
- [4. 安全考虑](#4. 安全考虑)
- =========================================="""
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. 工具设计原则
=========================================="""
- 单一职责:每个工具只做一件事
- 清晰命名:工具名称应该简洁且具有描述性
- 详细描述:docstring 是 LLM 选择工具的依据,务必详细
- 参数明确:使用类型提示和 Field 描述每个参数
- 返回简洁:工具返回的结果应该简洁明了
- 错误友好:工具出错时返回有意义的错误信息
"""
==========================================
2. 工具描述编写技巧
=========================================="""
好的描述:
"在公司的员工数据库中查找员工信息。当用户询问员工联系方式、
部门归属或职位信息时使用此工具。输入为员工姓名。"
差的描述:
"查找员工。"
技巧:
- 说明工具的功能
- 说明适用场景(何时使用)
- 说明不适用场景(何时不使用)
- 提供输入示例
"""
==========================================
3. 错误处理策略
=========================================="""
- 使用 ToolException 抛出工具错误(而非普通异常)
- 错误信息应该清晰、可操作
- 对输入参数进行验证
- 设置超时时间,防止工具挂起
- 记录工具调用日志,便于调试
"""
==========================================
4. 安全考虑
=========================================="""
- 限制 Python REPL 的权限(使用沙箱)
- 对文件操作进行路径验证(防止路径遍历)
- 对 HTTP 请求进行 URL 白名单限制
- 对数据库查询进行参数化(防止 SQL 注入)
- 限制工具的执行时间和资源消耗
- 敏感操作需要二次确认
"""