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

469 lines
16 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": [
"# 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
}