469 lines
16 KiB
Plaintext
469 lines
16 KiB
Plaintext
{
|
||
"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
|
||
}
|