Files
ai-agent-dev/09_工具定义.ipynb
2026-07-08 10:09:42 +08:00

471 lines
15 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 09 工具定义\n",
"\n",
"## 学习目标\n",
"1. 理解工具Tools在智能体开发中的核心作用\n",
"2. 掌握使用 `@tool` 装饰器定义自定义工具\n",
"3. 学会为工具编写清晰的参数说明和文档字符串\n",
"4. 理解工具描述如何影响模型选择工具的能力\n",
"5. 能够定义计算类、查询类和文件处理类工具"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 为什么需要工具\n",
"\n",
"大语言模型本身只能处理文本,它的知识截止于训练数据,也无法直接访问外部世界。\n",
"\n",
"**工具Tools** 让模型能够:\n",
"\n",
"- 执行数学计算(模型不擅长精确计算)\n",
"- 查询数据库、API、搜索引擎\n",
"- 读写文件、操作数据库\n",
"- 获取实时信息(天气、股价、新闻)\n",
"\n",
"智能体Agent的核心工作就是**根据用户问题选择合适的工具,调用工具,再根据结果回答问题**。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. 使用 @tool 装饰器定义第一个工具\n",
"\n",
"LangChain 提供了 `@tool` 装饰器,可以把一个普通 Python 函数变成智能体可调用的工具。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.tools import tool\n",
"\n",
"# 使用 @tool 装饰器定义一个加法工具\n",
"@tool\n",
"def add(a: int, b: int) -> int:\n",
" \"\"\"计算两个整数的和。\"\"\"\n",
" return a + b\n",
"\n",
"# 查看工具信息\n",
"print('工具名称:', add.name)\n",
"print('工具描述:', add.description)\n",
"print('参数Schema', add.args)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- `@tool`:把 `add` 函数注册为 LangChain 工具\n",
"- `a: int, b: int`:参数类型会被自动解析为工具的输入参数格式\n",
"- 函数文档字符串 `\"\"\"...\"\"\"`会被自动提取为工具描述description\n",
"- `add.name`:工具名,默认使用函数名\n",
"- `add.description`:工具描述,来自函数的 docstring\n",
"- `add.args`:参数的 JSON Schema模型根据它决定如何传参"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 手动调用工具\n",
"\n",
"工具定义好后,可以通过 `invoke` 方法或 `run` 方法调用。\n",
"\n",
"注意:`@tool` 装饰后的对象是一个 `StructuredTool` 对象,**不能直接像普通函数那样加括号调用**(如 `multiply(3, 4)`),需要传入字典参数。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.tools import tool\n",
"\n",
"@tool\n",
"def multiply(a: int, b: int) -> int:\n",
" \"\"\"计算两个整数的乘积。\"\"\"\n",
" return a * b\n",
"\n",
"# 通过 invoke 调用,传入字典参数\n",
"print('invoke 调用:', multiply.invoke({'a': 3, 'b': 4}))\n",
"\n",
"# 通过 run 调用LangChain 工具的传统调用方式)\n",
"print('run 调用:', multiply.run({'a': 5, 'b': 6}))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- `@tool` 装饰后的对象是一个 `StructuredTool`,不是普通函数\n",
"- `multiply.invoke({'a': 3, 'b': 4})`LangChain 风格调用,智能体实际使用这种方式\n",
"- `multiply.run({'a': 5, 'b': 6})`:传统工具调用方式,与 `invoke` 效果相同\n",
"- 注意:不能写成 `multiply(3, 4)`,否则会报 `StructuredTool object is not callable`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 自定义工具名称和描述\n",
"\n",
"默认情况下,工具名就是函数名,描述来自 docstring。你也可以通过 `@tool` 参数自定义。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.tools import tool\n",
"\n",
"@tool('weather-query', description='查询指定城市的当前天气情况')\n",
"def get_weather(city: str) -> str:\n",
" \"\"\"\n",
" 查询天气。\n",
" 参数:\n",
" city: 城市名称,例如 \"北京\"、\"上海\"\n",
" \"\"\"\n",
" # 这里用模拟数据,实际可接入天气 API\n",
" weather_data = {\n",
" '北京': '晴25°C',\n",
" '上海': '多云28°C',\n",
" '广州': '小雨30°C'\n",
" }\n",
" return weather_data.get(city, f'未找到 {city} 的天气信息')\n",
"\n",
"print('工具名称:', get_weather.name)\n",
"print('工具描述:', get_weather.description)\n",
"print('调用结果:', get_weather.invoke({'city': '北京'}))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- `@tool('weather-query', description='...')`:自定义工具名和描述\n",
"- 工具描述非常重要,模型主要依赖它判断什么时候使用该工具\n",
"- 描述要清晰、具体,包含「工具能做什么」和「参数含义」\n",
"- 函数 docstring 可以继续补充参数细节"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. 定义带多个参数的工具\n",
"\n",
"工具可以有多个参数LangChain 会自动根据类型注解生成参数 Schema。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.tools import tool\n",
"\n",
"@tool\n",
"def calculate_bmi(height: float, weight: float) -> str:\n",
" \"\"\"\n",
" 根据身高和体重计算 BMI 指数,并返回健康建议。\n",
" 参数:\n",
" height: 身高,单位米\n",
" weight: 体重,单位千克\n",
" \"\"\"\n",
" bmi = weight / (height ** 2)\n",
" if bmi < 18.5:\n",
" suggestion = '体重过轻,建议加强营养'\n",
" elif bmi < 24:\n",
" suggestion = '体重正常,请保持'\n",
" elif bmi < 28:\n",
" suggestion = '超重,建议适当运动'\n",
" else:\n",
" suggestion = '肥胖,建议咨询医生'\n",
" return f'BMI: {bmi:.2f}{suggestion}'\n",
"\n",
"print(calculate_bmi.invoke({'height': 1.75, 'weight': 70}))\n",
"print('\\n参数 Schema')\n",
"print(calculate_bmi.args)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- `height: float, weight: float`:类型注解会被 LangChain 自动识别\n",
"- 生成的 `args` 中会包含参数名、类型、是否必填等信息\n",
"- 模型会根据这个 Schema 构造正确的 JSON 参数调用工具"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. 定义模拟 API 查询工具\n",
"\n",
"实际应用中,工具通常会调用外部 API。下面我们用一个模拟的「产品库存查询」工具来演示。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.tools import tool\n",
"\n",
"@tool\n",
"def query_product_stock(product_name: str) -> str:\n",
" \"\"\"\n",
" 查询指定商品的库存数量。\n",
" 参数:\n",
" product_name: 商品名称\n",
" \"\"\"\n",
" # 模拟数据库查询\n",
" stock_db = {\n",
" '手机': 150,\n",
" '笔记本电脑': 45,\n",
" '耳机': 200,\n",
" '充电宝': 80\n",
" }\n",
" stock = stock_db.get(product_name)\n",
" if stock is None:\n",
" return f'未找到商品 \"{product_name}\" 的库存信息'\n",
" return f'商品 \"{product_name}\" 当前库存:{stock} 件'\n",
"\n",
"# 测试工具\n",
"print(query_product_stock.invoke({'product_name': '手机'}))\n",
"print(query_product_stock.invoke({'product_name': '相机'}))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- 这个工具模拟了查询数据库或 API 的场景\n",
"- 返回的字符串会作为观察结果Observation交给模型继续处理\n",
"- 工具内部可以替换为真实的数据库查询、HTTP 请求等"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. 定义文件处理工具\n",
"\n",
"工具也可以读写文件。下面是读取文本文件和写入文本文件的工具示例。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.tools import tool\n",
"\n",
"@tool\n",
"def read_text_file(file_path: str) -> str:\n",
" \"\"\"\n",
" 读取指定文本文件的内容。\n",
" 参数:\n",
" file_path: 文件路径\n",
" \"\"\"\n",
" try:\n",
" with open(file_path, 'r', encoding='utf-8') as f:\n",
" return f.read()\n",
" except FileNotFoundError:\n",
" return f'文件不存在:{file_path}'\n",
" except Exception as e:\n",
" return f'读取失败:{str(e)}'\n",
"\n",
"@tool\n",
"def write_text_file(file_path: str, content: str) -> str:\n",
" \"\"\"\n",
" 将内容写入指定文本文件。\n",
" 参数:\n",
" file_path: 文件路径\n",
" content: 要写入的内容\n",
" \"\"\"\n",
" try:\n",
" with open(file_path, 'w', encoding='utf-8') as f:\n",
" f.write(content)\n",
" return f'已成功写入文件:{file_path}'\n",
" except Exception as e:\n",
" return f'写入失败:{str(e)}'\n",
"\n",
"# 测试写入和读取\n",
"write_text_file.invoke({'file_path': 'test_note.txt', 'content': '这是测试内容'})\n",
"print(read_text_file.invoke({'file_path': 'test_note.txt'}))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- `read_text_file`:读取指定文件内容\n",
"- `write_text_file`:把字符串写入指定文件\n",
"- 文件路径使用相对路径时,会以当前工作目录为基准\n",
"- 工具内部要做好异常处理,避免程序崩溃"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 8. 工具列表与工具选择\n",
"\n",
"在构建智能体时,通常会把多个工具放在一个列表里,交给模型选择使用。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.tools import tool\n",
"\n",
"@tool\n",
"def add(a: int, b: int) -> int:\n",
" \"\"\"计算两个整数的和。\"\"\"\n",
" return a + b\n",
"\n",
"@tool\n",
"def multiply(a: int, b: int) -> int:\n",
" \"\"\"计算两个整数的乘积。\"\"\"\n",
" return a * b\n",
"\n",
"@tool\n",
"def power(base: int, exponent: int) -> int:\n",
" \"\"\"计算 base 的 exponent 次方。\"\"\"\n",
" return base ** exponent\n",
"\n",
"# 把工具放入列表\n",
"tools = [add, multiply, power]\n",
"\n",
"# 查看工具列表信息\n",
"for t in tools:\n",
" print(f'名称:{t.name},描述:{t.description}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"- 智能体通过工具列表了解有哪些工具可用\n",
"- 模型会根据用户问题和工具描述,决定调用哪个工具、传什么参数\n",
"- 工具名称和描述的清晰度直接影响模型选择工具的准确率"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 9. 完整示例:多功能计算器工具集\n",
"\n",
"下面把多个数学工具组合成一个工具集,模拟智能体可使用的工具环境。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain.tools import tool\n",
"\n",
"@tool\n",
"def add(a: float, b: float) -> float:\n",
" \"\"\"计算两个数的和。\"\"\"\n",
" return a + b\n",
"\n",
"@tool\n",
"def subtract(a: float, b: float) -> float:\n",
" \"\"\"计算两个数的差。\"\"\"\n",
" return a - b\n",
"\n",
"@tool\n",
"def multiply(a: float, b: float) -> float:\n",
" \"\"\"计算两个数的乘积。\"\"\"\n",
" return a * b\n",
"\n",
"@tool\n",
"def divide(a: float, b: float) -> float:\n",
" \"\"\"计算两个数的商如果除数为0则返回错误信息。\"\"\"\n",
" if b == 0:\n",
" return '错误除数不能为0'\n",
" return a / b\n",
"\n",
"math_tools = [add, subtract, multiply, divide]\n",
"\n",
"# 测试每个工具\n",
"for t in math_tools:\n",
" print(f'{t.name}: {t.description}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 10. 本节课练习\n",
"\n",
"1. 使用 `@tool` 定义一个摄氏度转华氏度的工具,参数和返回值都使用 float 类型\n",
"2. 定义一个「查询课程信息」工具,用字典模拟数据库,根据课程名返回上课时间\n",
"3. 定义一个「写入学习笔记」工具,接收文件名和内容,把内容写入指定文件\n",
"4. 把上面三个工具放入一个列表,打印每个工具的名称、描述和参数 Schema\n",
"5. 尝试修改一个工具的描述,观察参数 Schema 是否会变化"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.4"
}
},
"nbformat": 4,
"nbformat_minor": 4
}