{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 10 工具调用\n", "\n", "## 学习目标\n", "1. 理解工具调用(Tool Calling)的基本机制和流程\n", "2. 掌握手动解析模型返回的工具调用请求\n", "3. 学会使用 `bind_tools` 让模型选择工具\n", "4. 掌握 `create_tool_calling_agent` 和 `AgentExecutor` 构建智能体\n", "5. 能够处理工具调用中的常见错误" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. 工具调用的基本流程\n", "\n", "工具调用不是由模型直接执行代码,而是模型「决定」要调用哪个工具、传什么参数,然后由程序代为执行。\n", "\n", "完整流程如下:\n", "\n", "```\n", "用户提问 -> 模型分析 -> 输出工具调用请求 -> 程序执行工具 -> 结果返回模型 -> 模型生成最终回答\n", "```\n", "\n", "模型输出的是结构化的调用请求,包含:\n", "- 工具名称(name)\n", "- 工具参数(arguments)\n", "\n", "程序根据请求执行对应函数,再把结果返回给模型。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. 准备工具集\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 multiply(a: float, b: float) -> float:\n", " \"\"\"计算两个数的乘积。\"\"\"\n", " return a * b\n", "\n", "@tool\n", "def query_weather(city: str) -> str:\n", " \"\"\"\n", " 查询指定城市的天气。\n", " 参数:\n", " city: 城市名称,如北京、上海\n", " \"\"\"\n", " weather_db = {'北京': '晴 25°C', '上海': '多云 28°C', '广州': '小雨 30°C'}\n", " return weather_db.get(city, f'未找到 {city} 的天气信息')\n", "\n", "tools = [add, multiply, query_weather]\n", "\n", "for t in tools:\n", " print(f'工具:{t.name},描述:{t.description}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. 手动执行工具调用\n", "\n", "在使用智能体之前,先手动走一遍工具调用流程,理解底层机制。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from langchain_openai import ChatOpenAI\n", "from langchain_core.messages import HumanMessage\n", "from dotenv import load_dotenv\n", "\n", "load_dotenv()\n", "\n", "\n", "llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n", "\n", "# 把工具绑定到模型上\n", "llm_with_tools = llm.bind_tools(tools)\n", "\n", "# 用户提问\n", "question = '北京和上海的气温相差多少度?'\n", "messages = [HumanMessage(content=question)]\n", "\n", "# 模型输出,可能包含工具调用请求\n", "response = llm_with_tools.invoke(messages)\n", "\n", "print('模型回复类型:', type(response))\n", "print('工具调用请求:', response.tool_calls)\n", "print('文本内容:', response.content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 代码解释\n", "\n", "- `llm.bind_tools(tools)`:把工具列表绑定到模型上\n", "- 模型看到问题后,如果觉得需要工具,会在 `response.tool_calls` 中返回调用请求\n", "- 每个 `tool_call` 包含 `name`(工具名)和 `args`(参数)\n", "- 如果不需要工具,`response.content` 中直接包含文本回答" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. 手动解析并执行工具调用\n", "\n", "模型只负责「决定」调用什么工具,真正的执行需要程序完成。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from langchain_openai import ChatOpenAI\n", "from langchain_core.messages import HumanMessage, ToolMessage\n", "\n", "llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n", "llm_with_tools = llm.bind_tools(tools)\n", "\n", "# 使用一个只需一次工具调用的简单问题\n", "question = '北京今天的天气怎么样?'\n", "messages = [HumanMessage(content=question)]\n", "\n", "# 第一步:获取模型的工具调用请求\n", "response = llm_with_tools.invoke(messages)\n", "messages.append(response)\n", "\n", "print('第一次模型输出:')\n", "print('tool_calls:', response.tool_calls)\n", "print('content:', response.content)\n", "\n", "# 第二步:执行工具调用\n", "for tool_call in response.tool_calls:\n", " tool_name = tool_call['name']\n", " tool_args = tool_call['args']\n", " \n", " # 找到对应的工具并执行\n", " selected_tool = {t.name: t for t in tools}[tool_name]\n", " tool_result = selected_tool.invoke(tool_args)\n", " \n", " # 把工具执行结果加入对话历史\n", " messages.append(ToolMessage(content=str(tool_result), tool_call_id=tool_call['id']))\n", " \n", " print(f'\\n执行工具:{tool_name},参数:{tool_args},结果:{tool_result}')\n", "\n", "# 第三步:把工具结果返回模型,生成最终回答\n", "final_response = llm_with_tools.invoke(messages)\n", "print('\\n最终回答:', final_response.content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 代码解释\n", "\n", "- `ToolMessage`:专门用于承载工具执行结果的消息类型\n", "- `tool_call_id`:必须和模型请求的 `id` 对应,这是 LangChain 的要求\n", "- 工具执行后,需要把结果追加到 `messages` 中,再次调用模型\n", "- 这种手动流程帮助我们理解智能体的底层工作原理" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. 使用 create_agent 构建智能体\n", "\n", "LangChain 1.x 推荐使用 `create_agent` 构建工具调用智能体。它会自动处理「模型决定调用工具 -> 执行工具 -> 返回结果 -> 再次调用模型」的循环,并返回一个可执行的状态图。\n", "\n", "`create_agent` 的核心参数:\n", "\n", "| 参数 | 说明 |\n", "| --- | --- |\n", "| `model` | 语言模型实例 |\n", "| `tools` | 工具列表 |\n", "| `system_prompt` | 系统提示词 |\n", "| `checkpointer` | 可选,用于保存对话历史 |" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from langchain_openai import ChatOpenAI\n", "from langchain.agents import create_agent\n", "\n", "llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n", "\n", "# 创建工具调用智能体\n", "# create_agent 是 LangChain 1.x 推荐的智能体构建方式,返回一个可执行的状态图\n", "agent = create_agent(\n", " model=llm,\n", " tools=tools,\n", " system_prompt='你是一个 helpful 的数学和天气助手,可以使用工具帮助用户。'\n", ")\n", "\n", "# 运行智能体\n", "result = agent.invoke({'messages': [('user', '3 加 5 乘以 2 等于多少?')]})\n", "print('\\n最终输出:', result['messages'][-1].content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 代码解释\n", "\n", "- `create_agent(model=llm, tools=tools, system_prompt='...')`:创建工具调用智能体\n", "- `create_agent` 会自动处理工具调用循环:模型选择工具 -> 执行工具 -> 返回结果 -> 再次调用模型\n", "- 返回的 `agent` 是一个 LangGraph 编译后的状态图(CompiledStateGraph),可以直接调用 `invoke`\n", "- 调用时传入 `{'messages': [('user', '...')]}`,表示用户消息\n", "- `result['messages'][-1].content`:获取最后一条消息,即智能体的最终回答" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. 观察智能体的执行过程\n", "\n", "`create_agent` 返回的是一个 LangGraph 状态图。运行后,`result['messages']` 会包含完整的对话历史,包括模型的思考过程、工具调用请求、工具执行结果和最终回答。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 同样的智能体,换个问题运行\n", "result = agent.invoke({'messages': [('user', '北京今天天气怎么样?广州呢?')]})\n", "for i in result['messages']:\n", " print(i)\n", "print('\\n最终输出:', result['messages'][-1].content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. 多轮对话:带记忆的智能体\n", "\n", "如果希望智能体能记住上下文,可以在 `create_agent` 中传入 `checkpointer`。这里使用 `MemorySaver` 作为内存型的 checkpointer。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from langgraph.checkpoint.memory import MemorySaver\n", "\n", "# 创建带记忆的智能体\n", "# checkpointer 会自动保存和加载对话历史\n", "memory = MemorySaver()\n", "agent_with_history = create_agent(\n", " model=llm,\n", " tools=tools,\n", " system_prompt='你是一个 helpful 的数学和天气助手,可以使用工具帮助用户。',\n", " checkpointer=memory\n", ")\n", "\n", "# 第一轮对话\n", "config = {'configurable': {'thread_id': 'user_001'}}\n", "result1 = agent_with_history.invoke(\n", " {'messages': [('user', '我叫张三')]},\n", " config=config\n", ")\n", "print('第一轮:', result1['messages'][-1].content)\n", "\n", "# 第二轮对话,测试是否记得名字\n", "result2 = agent_with_history.invoke(\n", " {'messages': [('user', '我叫什么名字?')]},\n", " config=config\n", ")\n", "print('第二轮:', result2['messages'][-1].content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 代码解释\n", "\n", "- `MemorySaver()`:内存型 checkpointer,用于保存对话历史\n", "- `checkpointer=memory`:把 checkpointer 传给 `create_agent`,让智能体具备记忆能力\n", "- `config={'configurable': {'thread_id': 'user_001'}}`:通过 thread_id 区分不同会话\n", "- 同一个 `thread_id` 下的历史消息会被保留并传给模型" ] }, { "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 safe_divide(a: float, b: float) -> str:\n", " \"\"\"安全除法,除数为0时返回友好提示。\"\"\"\n", " try:\n", " if b == 0:\n", " return '错误:除数不能为0'\n", " return str(a / b)\n", " except Exception as e:\n", " return f'计算出错:{str(e)}'\n", "\n", "safe_tools = [safe_divide, add, multiply]\n", "\n", "safe_agent = create_agent(\n", " model=llm,\n", " tools=safe_tools,\n", " system_prompt='你是一个 helpful 的数学助手,可以使用工具帮助用户。'\n", ")\n", "\n", "result = safe_agent.invoke({'messages': [('user', '10 除以 0 等于多少?')]})\n", "print('\\n最终输出:', result['messages'][-1].content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 代码解释\n", "\n", "- 工具内部用 try-except 捕获异常,返回错误信息而不是抛出异常\n", "- 这样模型能看到错误原因,并据此调整或告知用户\n", "- 不要把异常直接抛给 AgentExecutor,否则会导致智能体运行中断" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 9. 完整示例:智能客服助手\n", "\n", "下面把工具调用应用到智能客服场景:根据用户问题查询订单、库存或计算退款金额。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from langchain.tools import tool\n", "from langchain_openai import ChatOpenAI\n", "from langchain.agents import create_agent\n", "\n", "@tool\n", "def query_order(order_id: str) -> str:\n", " \"\"\"根据订单号查询订单信息。\"\"\"\n", " orders = {\n", " '1001': '已发货,预计明天送达',\n", " '1002': '已签收',\n", " '1003': '处理中'\n", " }\n", " return orders.get(order_id, '未找到该订单')\n", "\n", "@tool\n", "def query_stock(product_name: str) -> str:\n", " \"\"\"查询商品库存。\"\"\"\n", " stock = {'手机': 100, '耳机': 50, '充电器': 200}\n", " return f'{product_name} 库存:{stock.get(product_name, 0)} 件'\n", "\n", "@tool\n", "def calculate_refund(price: float, days: int) -> str:\n", " \"\"\"计算退款金额,每天折旧 1%。\"\"\"\n", " refund = price * max(0, 1 - days * 0.01)\n", " return f'预计退款金额:{refund:.2f} 元'\n", "\n", "cs_tools = [query_order, query_stock, calculate_refund]\n", "\n", "cs_agent = create_agent(\n", " model=llm,\n", " tools=cs_tools,\n", " system_prompt='你是电商客服助手,可以通过工具查询订单、库存和计算退款。'\n", ")\n", "\n", "cs_result = cs_agent.invoke({\n", " 'messages': [('user', '我的订单 1001 现在状态怎样?手机还有库存吗?如果手机 3000 元买了 10 天,退款多少?')]\n", "})\n", "print('\\n最终输出:', cs_result['messages'][-1].content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 10. 本节课练习\n", "\n", "1. 手动执行一次工具调用:让模型选择 `add` 或 `multiply`,然后自己解析 `tool_calls` 并执行\n", "2. 使用 `create_tool_calling_agent` 和 `AgentExecutor` 构建一个能计算 BMI 的工具智能体\n", "3. 给智能体添加 `verbose=True`,观察它执行工具调用的完整过程\n", "4. 修改一个工具,让它在参数错误时返回友好提示,而不是抛出异常\n", "5. 使用 `RunnableWithMessageHistory` 让智能体记住用户的连续提问" ] } ], "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.11.3" } }, "nbformat": 4, "nbformat_minor": 4 }