{ "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 }