# 17 - Tools 工具调用 --- **本章课程目标:** - 理解 **Tool(工具)**、**Tool Calling(工具调用)**、**Function Calling(函数调用)** 分别是什么,建立“**模型负责决策,程序负责执行**”这条最核心的分工认知。 - 掌握使用 **`@tool`** 装饰器定义 LangChain 工具,会配合 **Pydantic** 编写参数 schema,并能看懂 `name`、`description`、`args`、`tool_calls`、`ToolMessage` 这些关键对象。 - 跑通并理解本章全部案例:**基础加法工具、Pydantic 参数 schema、天气查询工具、天气助手完整链路**,为后续 [记忆与对话历史](16-记忆与对话历史(含Redis基础).md)、[Agent 智能体](21-Agent智能体.md)、[MCP 模型上下文协议](20-MCP模型上下文协议.md) 打基础。 **学习建议:** 学工具调用时先守住边界:模型负责判断要不要调用工具和填参数,程序负责真正执行工具并把结果交回去。读本章可以按 `@tool`、`bind_tools`、`tool_calls`、`ToolMessage`、业务闭环这条线走。Pydantic 不是装饰,它是在帮工具参数变成一份更可靠的契约。 **官方文档与资源**:详见 [工具导航与参考资料索引 - 工具调用、MCP与智能体](工具导航与参考资料索引.md#工具调用、MCP与智能体)。 --- ## 1、Tools 简介 ### 1.1 定义 **Tool(工具)**,是给大模型准备的一项**外部能力**。它本质上通常就是一个可调用函数,只不过我们把它包装成模型能理解的形式,让模型知道: - 这个工具叫什么 - 这个工具是干什么的 - 这个工具接收哪些参数 **Tool Calling(工具调用)** 或 **Function Calling(函数调用)**,说的是:**模型在回答过程中,不直接给最终自然语言,而是先输出“我想调用某个工具,并附带参数”这一结构化意图。** 然后要特别记住一条最重要的分工: - **模型负责决定**:要不要调用工具、调用哪个工具、传什么参数 - **程序负责执行**:真正去调用函数 / API / 数据库 / 业务服务,并把结果再送回模型 所以从工程角度看,Tool 不是“模型自己突然学会调用外部系统”,而是**我们把外部能力以受控方式开放给模型使用**。 **一句话定义:** Tool 是外部能力本身,Tool Calling 是模型发起使用这项能力的过程。 > **术语约定:** 为了减少后续章节的切换成本,本教程后文默认把这层机制统称为 **Tool Calling**;如果引用 OpenAI 或其他平台文档时出现 **Function Calling**,本质上仍然是同一层问题的不同叫法。 ### 1.2 Tools 的作用 如果没有工具,大模型虽然会“说”,但它能做的事情仍然非常有限。它擅长语言理解、信息组织、总结改写、解释说明,但它并不能天然替你完成真实世界里的查询和操作。 最典型的限制包括: - **不能稳定访问实时数据**,比如最新天气、实时股价、今天的订单状态 - **不能直接操作外部系统**,比如查数据库、发 HTTP 请求、读文件、调企业内部接口 - **不能保证计算与执行完全准确**,比如复杂计算、严格表单提交、支付下单、状态变更 这也是为什么在真实项目里,几乎所有“真正有业务价值”的 LLM 应用,最后都会走到 Tools: - 智能客服要查订单、查物流、查售后状态 - 企业助手要查知识库、查数据库、查工单系统 - 数据分析助手要跑 SQL、读报表、调统计接口 - 生活类助手要查天气、查地图、查航班、查日程 ![未开启联网或工具时,对话产品无法给出实时天气,只能提示用户开启能力(示意图)](images/17/17-1-2-1.jpeg) > **图意说明:** 界面中用户询问「今天北京天气」,在未启用「联网搜索」等外部能力时,模型无法给出实时数据,只能建议去气象网站或**开启联网搜索**。该图用于说明:**没有接入工具/插件时,模型再强也拿不到实时世界状态**,与后文通过 Tool 调用天气 API 形成对照。 入门阶段可先把握一个基本判断:**没有 Tool,大模型主要是在“说”;有了 Tool,它才开始能“做”。** ### 1.3 Tool、Tool Calling、Agent 三者关系 这三者要先分清,因为后面学 Agent 时很多人都会把它们混在一起。 | 概念 | 它是什么 | 核心职责 | | :---------------------------------- | :--------------------------------- | :----------------------------------------------- | | **Tool** | 一个被封装好的外部能力 | 负责“做事” | | **Tool Calling / Function Calling** | 模型输出结构化调用意图的机制 | 负责“发起调用请求” | | **Agent** | 会推理、会规划、会多步决策的智能体 | 负责“决定什么时候调用、调用几次、按什么顺序调用” | 可以把它们理解成这样: - **Tool** 像工具箱里的“螺丝刀、扳手、计算器、天气接口” - **Tool Calling** 像“我现在决定要用哪把工具,并说出参数” - **Agent** 像“有判断能力的工人或调度者”,它会自己决定先用哪个工具、后用哪个工具、要不要继续调用 所以本章的重点,是先把 **“工具本身”** 和 **“单轮/手动工具调用链路”** 学明白。到了 [第 21 章 Agent 智能体](21-Agent智能体.md),你再去理解“让模型自己循环地、多步地调用工具”,就会轻松很多。 ### 1.4 本章在项目中的位置 本章对应仓库中的 `案例与源码-2-LangChain框架/08-tools` 目录,整体学习路径非常清晰: | 文件 | 作用 | 建议学习顺序 | | :------------------------- | :------------------------------------ | :----------- | | `Tool_AddNumberTool.py` | 最基础的 `@tool` 用法 | 先看 | | `PydanticDemo.py` | 先单独理解 Pydantic 校验与转换 | 再看 | | `Tool_AddNumberToolPro.py` | 给工具加上 `args_schema` | 接着看 | | `QueryWeatherTool.py` | 把真实 API 封装成 Tool | 再往后看 | | `LLMQueryWeatherDemo.py` | 把“模型 + 工具 + 解析 + 输出”串成闭环 | 最后看 | 本章不是零散介绍几个 API,而是围绕一个问题展开: **我们怎样把一个普通 Python 函数,逐步变成“可被模型正确理解、可被程序安全执行、可真正服务业务场景”的工具能力。** --- ## 2、工具调用的工作方式 ### 2.1 核心主线 只要先抓住一条主线,本章后面几乎所有 API 都会变得很好理解: 1. 用户提出问题 2. 程序把“用户消息 + 工具定义”一起发给模型 3. 模型判断是否需要调用工具 4. 如果需要,模型返回 `tool_calls` 5. 程序根据 `tool_calls` 真正执行工具 6. 程序把工具结果再放回消息流 7. 模型基于工具结果生成最终自然语言回复 这就是工具调用的完整闭环。 ![工具调用泳道图:用户、程序、工具、大模型四栏协作(用户提问 → 程序带工具定义调模型 → 模型判断是否调用工具 → 程序执行工具 → 结果再交模型生成回复)](images/17/17-2-1-1.jpeg) > **小知识:泳道图 vs 普通流程图(面试题)** > > 上图为**泳道图**(Swimlane diagram),按角色/系统分栏表示「谁在什么阶段做什么」。 > > - **普通流程图**:只表示步骤的先后顺序和分支/判断,不区分「谁」执行哪一步;适合单角色、单系统内的流程(如算法步骤、单一业务线)。 > - **泳道图**(Swimlane diagram):按角色/部门/系统划分泳道,每个步骤落在对应责任方的泳道里,一眼看出「谁做啥」;适合多角色协作、跨部门/跨系统流程。 > - **何时用泳道图**:流程涉及多个责任主体(用户、模型、应用程序、第三方服务等)、需要明确责任边界与交接点时用泳道图。例如工具调用(用户 → 模型 → 你的代码 → 模型)、审批流、跨系统对接等。 如果你看过 [第 11 章](11-Model-I-O与模型接入.md) 和 [第 13 章](13-提示词与消息模板.md),这里会看到一条连续的消息主线: - 第 11 章讲的是模型返回 `AIMessage` - 第 13 章讲的是消息流里有 `ToolMessage` - 第 17 章就是把这两件事真正接起来 也就是说,**Tool Calling 不是脱离消息机制另起炉灶,它本质上仍然发生在消息流之中。** ![bind_tools 与 Agent 自动执行工具的边界:绑定工具只提供 schema,Agent 才负责循环执行工具并写回观察结果](images/17/17-2-1-2.svg) ### 2.2 模型到底看到了什么 你可能会问:模型为什么知道“该调用天气工具,而不是直接胡编一个天气答案”? 原因是:当你使用 `bind_tools(...)` 把工具绑定给模型时,请求里除了用户问题,还会带上**工具定义信息**。根据 LangChain 和 OpenAI 官方文档,这些信息至少包括: - **工具名称**:例如 `get_weather` - **工具描述**:也就是 docstring 或手动提供的 description - **参数 schema**:例如参数名、类型、字段说明 模型看到的并不是“一个黑盒函数”,而是“一个带名字、带用途说明、带参数规则的结构化能力描述”。这也是为什么工具描述写得好不好,会直接影响工具调用效果:**模型不是靠读你的函数体来理解工具,而主要是靠 `name`、`description`、`args_schema` 来推断“什么时候该用、应该怎么传参数”。** ### 2.3 程序主要做了什么 模型会输出工具调用意图,但**模型不会替你执行工具**。真正执行工具的一定是你的应用代码。 这一步通常包括: - 读取模型返回的 `tool_calls` - 找到对应工具 - 取出参数 - 执行 Python 函数、HTTP 请求、数据库查询或其他业务逻辑 - 把结果重新回填给模型 这一点在实际项目里特别重要,因为它意味着: - **权限控制在你手里** - **是否真的允许调用某个工具,在你手里** - **参数要不要做二次校验,在你手里** - **工具结果要不要脱敏、限流、重试,也在你手里** 所以工具调用从来不是“模型直接操作你的系统”,而是**模型提出请求,应用代码审核并执行。** ### 2.4 tool_calls、AIMessage、ToolMessage 三者关系 这三个对象是最容易混淆、但又最关键的一组概念。 | 对象 | 它出现在哪一轮 | 作用 | | :--------------------- | :------------------- | :----------------------------------- | | `AIMessage.tool_calls` | 模型决定要调用工具时 | 表示“模型想调用哪个工具、参数是什么” | | 工具执行结果 | 程序执行工具后 | 表示“真实运行后的结果” | | `ToolMessage` | 把工具结果送回模型时 | 表示“这是某个工具执行后的返回消息” | 先建立一个非常实用的理解: - **`AIMessage.tool_calls` 是模型的请求** - **`ToolMessage` 是程序的回执** 这和现实世界里“发起请求 → 执行操作 → 返回结果”很像。 如果一次回复里有多个工具调用,`ToolMessage` 还需要通过 `tool_call_id` 对应回前面的某一个 `tool_call`,这样模型和运行时才知道“这条工具结果是回答哪一次工具请求的”。 在 LangChain 官方更通用的讲法里,经常会直接展示: 1. 模型返回 `AIMessage(tool_calls=[...])` 2. 程序执行工具 3. 程序构造 `ToolMessage` 4. 再把这些消息一起送回模型 而本课程当前案例为了更适合初学者理解,也保留了一种更直观的教学路径:**通过[输出解析器](14-输出解析器.md)把工具参数取出来,然后手动执行工具,再把结果交给下一条 [LCEL](15-LCEL与链式调用.md) 链去整理自然语言。** 这两种写法不矛盾,只是教学层次不同:一个适合看清过程,一个更贴近通用消息流。 ### 2.5 什么时候不需要工具 不是所有问题都必须上 Tool。 如果用户只是问: - “什么是 LangChain?” - “请解释一下 Python 装饰器” - “帮我总结这段文本” 这类问题模型本身就可以回答,此时直接 `Prompt → Model → Parser` 往往已经足够。 通常在下面几类场景,Tool 的价值才会明显体现出来:**需要实时数据**;**需要访问外部系统**;**需要高确定性执行**;**需要把模型输出落到真实动作上**。 这一点也很符合真实项目经验:**Tool 不是为了“显得高级”而加,而是为了补足模型本身做不到或不可靠的那部分能力。** --- ## 3、自定义 Tool:从最简单的工具开始 ### 3.1 使用 @tool 装饰器 在 LangChain 里,最简单的工具定义方式就是使用 **`@tool`** 装饰器。 如果你第一次接触装饰器,不必把它理解得过于复杂。对本章而言,可先把握:**装饰器的作用,是在不改函数核心逻辑的前提下,给函数额外加上一层“框架可识别的能力”。** 放到这里,`@tool` 做的事情就是: - 把一个普通 Python 函数包装成 LangChain Tool - 让它具备 `name`、`description`、`args` 等元信息 - 让它能够被模型或 Agent 识别并调用 LangChain 官方文档也明确强调:**最简单的创建工具方式,就是使用 `@tool` 装饰器;默认情况下,函数 docstring 会成为工具描述。** ![LangChain 文档摘录:使用 @tool 装饰器创建工具,默认以函数 docstring 作为工具描述(Basic tool definition)](images/17/17-3-1-1.jpeg) 所以先记住一句最重要的话:**`@tool` 的意义,不是把函数“变复杂”,而是把函数“变成模型看得懂的工具”。** ### 3.2 基础案例:加法工具 从教学上说,先别急着上天气、数据库、搜索引擎。最适合入门的,反而是一个最简单的数学工具,因为它能把重点都暴露出来。 【案例源码】`案例与源码-2-LangChain框架/08-tools/Tool_AddNumberTool.py` [Tool_AddNumberTool.py](案例与源码-2-LangChain框架/08-tools/Tool_AddNumberTool.py ":include :type=code") 这个案例最值得看懂的,不是“加法”本身,而是这三件事: - 普通函数经过 `@tool` 后就成了 Tool - Tool 可以直接通过 `invoke(...)` 执行 - Tool 会自动暴露名称、描述、参数结构 也就是说,从这一步开始,你就已经把“Python 函数”变成了“模型可用能力”。 ### 3.3 Tool 常用属性 你在实际开发中,经常会看到下面几个属性: | 属性 | 作用 | 初学阶段怎么理解 | | :-------------- | :----------------------- | :----------------------------------------- | | `name` | 工具名 | 模型要调用谁,首先看这个名字 | | `description` | 工具说明 | 模型判断“什么时候该用这个工具”的最重要依据 | | `args` | 参数结构 | 模型知道“应该传什么参数、参数是什么类型” | | `return_direct` | 是否直接把结果返回给用户 | 主要在 Agent 场景更常见 | 入门阶段最值得优先关注的是: - **`description` 决定模型是否容易选中这个工具** - **`args` 决定模型是否容易传对参数** 所以从工程实践上说,定义 Tool 时最怕的不是“函数实现难”,而是:名字取得太随意,描述写得太模糊,参数定义不清楚。 ![@tool 与函数名、类型注解、docstring 在工具定义中的角色示意(装饰器注册、函数名为工具 ID、类型帮助生成参数、文档字符串为「使用说明」)](images/17/17-3-3-1.jpeg) ### 3.4 工具描述的作用 很多人定义工具时,只写一句非常短的 docstring,比如“查天气”或“获取信息”。这样虽然勉强能跑,但在真实项目里通常不够好。更好的工具描述应该尽量让模型看懂三件事: 1. **这个工具是干什么的** 2. **什么时候应该调用** 3. **关键参数应该怎么填** 例如同样是天气工具: - “查天气” - “查询指定城市的当前天气,参数 loc 传城市英文名,如 Beijing、Shanghai” 显然后者更容易让模型在正确场景下调用,也更容易传对参数。 这也是为什么在项目里定义 Tool 时,通常不建议把 docstring 写得过于省略。**Tool 描述不是给人随便看看,它本身就是模型决策的重要输入。** ### 3.5 真实项目里的工具层 在真实项目里,Tool 一般不会只做“玩具函数”,而往往是下面这些能力的轻量封装: - 调第三方 API - 调内部服务 - 查数据库 - 跑检索 - 执行某个稳定的业务动作 所以 Tool 层的工程价值非常高。它相当于把“模型”和“业务系统”之间,插入了一层可控的能力边界。 从架构上说,比较常见的组织方式是: - **Tool 层**:暴露给模型的工具定义 - **Service 层**:真正的业务实现或 API 调用封装 - **Model / Chain / Agent 层**:负责决定何时调用工具 这样做的好处是:**模型能力和业务实现解耦**,后期替换模型、替换 API、替换调用策略都会更容易。 --- ## 4、参数 schema:为什么要配合 Pydantic ### 4.1 为什么只写函数参数还不够 只靠 Python 函数签名,当然也能定义工具,但一旦进入真实项目,你很快就会遇到两个问题:参数类型和格式不够清晰;参数校验不够严格。 例如你只写: ```python def add_number(a: int, b: int) -> int: ... ``` 这当然可以告诉模型“有两个整数参数”,但很多时候还不够。你还会希望表达: - 这个参数具体是什么意思 - 有没有取值范围 - 是否允许为空 - 参数传错时怎样更清晰地报错 这就是 `args_schema` 和 Pydantic 出场的原因。 ### 4.2 Pydantic 定义 **Pydantic** 是 Python 里非常常用的数据校验库,本质是把“参数结构、参数类型、字段说明、校验规则”统一收敛到一个模型类里。 在本章语境下,Pydantic 最重要的价值有两个:**给程序看**:做运行时校验和转换;**给模型看**:把参数 schema 描述得更清楚。 因此它适合用在 Tool 参数定义里。入门阶段可先把握一点: **Pydantic = 类型声明 + 自动校验 + 更清晰的参数说明。** ### 4.3 入门案例 在把 Pydantic 放进 Tool 之前,先单独理解它本身会更容易。 【案例源码】`案例与源码-2-LangChain框架/08-tools/PydanticDemo.py` [PydanticDemo.py](案例与源码-2-LangChain框架/08-tools/PydanticDemo.py ":include :type=code") 这个案例最值得注意的是: - Pydantic 会在实例化时做校验 - 合法输入可以自动转换 - 非法输入会明确报错 - 严格类型(如 `StrictInt`)可以避免“模糊转换” 这套能力放到 Tool 参数上会很有用,因为工具调用最怕“参数看起来像对,其实不对”。 ### 4.4 加法工具的 Pydantic 版 理解了 Pydantic 之后,再看工具版就很顺了。 【案例源码】`案例与源码-2-LangChain框架/08-tools/Tool_AddNumberToolPro.py` [Tool_AddNumberToolPro.py](案例与源码-2-LangChain框架/08-tools/Tool_AddNumberToolPro.py ":include :type=code") 这个案例比基础版多出来的关键点,是: - 用 `BaseModel` 定义参数结构 - 用 `Field(description=...)` 给参数写说明 - 用 `@tool(args_schema=...)` 把参数模型绑定给工具 这样之后,模型看到的工具就不再只是“有两个整数参数”,而是会看到: - 参数名是什么 - 参数类型是什么 - 参数用途是什么 这会显著提升模型生成正确参数的概率。 ### 4.5 args_schema 的实践价值 对于玩具案例,直接写函数参数就够了;但只要进入真实项目,`args_schema` 往往是非常值得养成的习惯。 原因主要有三个: - **更稳定**:参数结构更清晰,模型不容易乱传 - **更安全**:运行时可以做更严格校验 - **更可维护**:工具定义本身就像一份小型接口文档 这和后端开发里写 DTO、写请求参数对象,其实是同一种工程思想。 你不是为了“显得规范”才写 schema,而是为了让: - 模型更容易调用对 - 代码更容易排错 - 团队更容易协作 所以这一点意味着:**Pydantic 让 Tool 从“能跑”走向“更像真正的接口定义”。** --- ## 5、天气助手实战:把 Tool 跑成业务闭环 ### 5.1 需求与准备 前面的加法工具是为了让你先看懂 Tool 的本质,但真正接近业务项目的,是天气助手这种“模型 + 工具 + 外部 API”的组合。 本节目标非常明确:**让模型不只是“知道天气工具存在”,而是能在用户提问时,真的调用天气 API,再把结果整理成自然语言回复。** 这个案例也非常贴近真实项目,因为现实里的 Tool 往往都不是本地纯函数,而更像这样: - 调第三方接口 - 接收 JSON 数据 - 做必要加工 - 回到消息流或链路中继续生成最终答案 在运行本节案例前,你需要准备: - **OpenWeather API Key**:在 [OpenWeather API keys 页面](https://home.openweathermap.org/api_keys) 免费申请,将密钥写入**项目根目录** `.env`(例如 `OPENWEATHER_API_KEY=你的密钥`),并保证运行脚本时能加载到该变量。天气 HTTP API 总览见 [OpenWeatherMap 文档](https://openweathermap.org/api)。 > **版本说明:** OpenWeather 的免费额度、可用产品、订阅档位和调用限制会随官方策略调整;本章只说明 API Key 获取和本地配置流程,具体额度与计费以当前 [OpenWeather Pricing](https://openweathermap.org/price) 页面和账号订阅页为准。 ![OpenWeather 用户后台「API keys」页:查看已有密钥状态,或通过 Create key 生成新密钥(教程中用于配置天气工具)](images/17/17-5-1-1.png) ### 5.2 定义天气查询工具 先看天气工具本身,它负责把“真实 API 能力”包装成模型可用 Tool。 【案例源码】`案例与源码-2-LangChain框架/08-tools/QueryWeatherTool.py` [QueryWeatherTool.py](案例与源码-2-LangChain框架/08-tools/QueryWeatherTool.py ":include :type=code") 这个案例很适合帮助你建立两个关键认知: 第一,**模型不会替你发 HTTP 请求**。真正的请求逻辑仍然是你写的 `httpx.get(...)`。 第二,**Tool 的意义不是替代业务实现,而是把业务实现包装成“模型可调用的接口”。** 所以从这一步开始,你已经在做一件很像真实项目开发的事: - 把业务能力封装成一个可复用函数 - 用 Tool 的方式暴露给模型 - 让模型只决定“何时调、怎么调” ### 5.3 模型绑定工具后,到底会发生什么 定义好 Tool 后,还需要把它交给模型,这通常通过 `bind_tools(...)` 完成。 LangChain 官方文档对这一点讲得很清楚:**只有先把工具绑定给模型,后续模型调用时才有机会返回 `tool_calls`。** 也就是说,`bind_tools([get_weather])` 的含义不是“现在就执行工具”,而是: **把 `get_weather` 这项能力声明给模型,告诉它:你之后如果判断有必要,可以调用这个工具。** ![Function calling 技术思路:普通对话(用户 ↔ 大模型)与「查询天气」场景下,大模型发出函数调用请求 → 外部函数 get_weather → 请求 OpenWeather API → 响应回到模型再生成用户可见回复](images/17/17-5-3-1.jpeg) 模型收到用户问题后,可能出现两种情况:**不需要工具**:直接返回自然语言答案;**需要工具**:返回 `tool_calls`。 一旦返回 `tool_calls`,就意味着模型在说: > 我建议调用这个工具,参数如下,请你们应用程序去真正执行。 ### 5.4 案例:天气助手完整链路 接下来就是本章最重要的业务闭环案例。 【案例源码】`案例与源码-2-LangChain框架/08-tools/LLMQueryWeatherDemo.py` [LLMQueryWeatherDemo.py](案例与源码-2-LangChain框架/08-tools/LLMQueryWeatherDemo.py ":include :type=code") 其中第 4 步「解析工具调用参数」依赖 `JsonOutputKeyToolsParser`,其角色是把模型输出中的工具调用片段解析成可执行的 Python 结构,详见 [第 14 章 输出解析器](14-输出解析器.md)。 这个案例的教学价值非常高,因为它把本章所有核心概念都串起来了: 1. **定义 Tool** 2. **把 Tool 绑定给模型** 3. **让模型返回工具调用意图** 4. **解析工具调用参数** 5. **真正执行工具** 6. **把工具结果再加工成面向用户的自然语言回复** 从链路角度看,它做了两件连续的事: - **前半段**:用户问题 → 模型判断 → 解析工具参数 → 调天气工具 → 得到天气 JSON - **后半段**:把天气 JSON 再交给模型 → 生成更自然的中文天气描述 这特别适合入门,因为它把“工具调用”和“最终回复生成”拆成了两个清晰阶段,而不是一下子混在一起。 > **注意**:`LLMQueryWeatherDemo.py` 中通过 `from QueryWeatherTool import get_weather` 引用同目录天气工具,运行前请确保已配置 `OPENWEATHER_API_KEY`,并尽量在项目根目录执行脚本,避免 `.env` 读取不到。 > **接口说明:** 课程案例为了降低入门门槛,保留了 `q=城市名` 的 Current Weather API 调用方式。生产项目如果需要更稳定的地理位置解析,建议先用 OpenWeather Geocoding API 将城市名、邮编或地址转换为经纬度,再用 `lat/lon` 调用天气接口。 ### 5.5 课程案例写法与官方主线的关系 这一点很重要,建议单独说明清楚。本课程当前天气案例里,使用了: - `bind_tools([get_weather])` - `JsonOutputKeyToolsParser` - 手动执行工具 - 再走一条输出链生成自然语言 而在 LangChain 官方主线和 OpenAI 官方 Function Calling 文档里,更常见的讲法通常是: 1. 模型返回 `AIMessage.tool_calls` 2. 程序执行工具 3. 把结果包装成 `ToolMessage` 4. 再把这组消息送回模型 两种写法的共同本质完全一样: - **模型先发起工具调用意图** - **程序执行工具** - **结果再回到模型上下文里** 课程案例之所以保留解析器写法,是因为它更直观:你能非常清楚地看到“模型产出参数 → 工具被调用 → 结果再被整理成人话”的每一个中间步骤。 所以建议你这样理解: - **课程案例写法**:更适合入门,看清链路 - **官方推荐写法**:更贴近通用消息流和 Agent 扩展 这不是谁替代谁,而是**同一条原理在不同教学层次上的两种展开方式。** --- ## 6、从课程案例走向真实项目 ### 6.1 Tool 在项目里通常怎么落位 如果你只写脚本,Tool 可以直接和业务逻辑写在一起;但如果是正式项目,更推荐把 Tool 看成“暴露给模型的接口层”。 一种比较常见、也比较适合团队协作的组织方式是: - tool.py / tools/:定义给模型看的工具入口 - service.py / services/:写真实业务逻辑 - client.py / adapters/:封装第三方 API 或内部接口访问 - prompt / chain / agent 层:决定什么时候调用工具 这样做的好处是,后面无论你替换模型、替换 API 平台、替换 Agent 框架,都不会把整个工具层揉成一团。 ### 6.2 设计 Tool 时,最值得重视的工程原则 对真实项目来说,Tool 定义得好不好,往往比“模型提示词写得漂不漂亮”更影响稳定性。 比较重要的原则包括: - **职责单一**:一个 Tool 最好只做一件清晰的事 - **输入明确**:参数含义、类型、是否必填要说清楚 - **输出稳定**:返回结构尽量稳定,不要时而返回字符串、时而返回复杂嵌套对象 - **异常可控**:报错要能被程序捕捉和处理,不要直接把底层异常糊给用户 - **幂等与副作用隔离**:能做查询就不要顺手做写入;涉及状态变更时,尽量拆成“先确认、再执行”的显式步骤 - **超时、重试与限流**:外部 API 类 Tool 要设置超时、重试上限和频控,避免 Agent 在异常场景下反复调用 - **权限受控**:涉及写操作、支付、删除、外呼等工具,需要额外设防 - **可观测**:最好能记录调用日志、参数、耗时、错误信息,便于排障 这几点其实和普通后端接口设计的原则高度一致。 也正因为如此,Tool 本质上并不神秘,它只是把“后端能力”换了一种适合 LLM 使用的暴露方式。 ### 6.3 本章与记忆、Agent、MCP 的关系 这一章在整个教程体系里,位置非常关键。 - 和 [第 16 章记忆与对话历史](16-记忆与对话历史(含Redis基础).md) 的关系:工具调用结果经常也要进入消息流,与对话历史一起参与后续推理 - 和 [第 21 章Agent智能体](21-Agent智能体.md) 的关系:Agent 会在 Tool 基础上进一步解决“什么时候调、调几次、按什么顺序调” - 和 [第 20 章MCP模型上下文协议](20-MCP模型上下文协议.md) 的关系:MCP 解决的是“如何用标准协议把外部能力开放给模型”,可以看作更通用、更标准化的工具接入方式 一句话总结它们的关系:**Tool 提供能力,Memory 提供上下文,Agent 负责决策,MCP 负责标准化接入。** 所以本章虽然只讲 Tool,但它其实是后面很多高级能力的前置地基。 --- **章节思考题:** 1. 一个函数是否应该暴露成 Tool,判断标准是什么? **参考思路:** 看它是否需要模型根据自然语言判断调用时机和参数。如果是固定内部流程,普通代码即可;如果需要模型按上下文选择并填参,才值得暴露成 Tool。 2. 工具描述写得差,会导致哪些真实问题? **参考思路:** 模型可能不用工具、错用工具、参数填错、在不该执行时执行。模型看不到你的函数内部,只能依赖名称、描述和参数 schema 判断能力边界。 3. “模型负责决策,程序负责执行”在安全上意味着什么? **参考思路:** 模型可以提出调用意图,但真正访问数据库、发消息、下单、删除数据必须由程序执行,并加权限、校验、确认、审计和失败处理。不能把副作用完全交给模型自由发挥。 4. 有副作用的工具和只读工具,在设计上应该有什么不同? **参考思路:** 只读工具重点是参数和结果质量;有副作用工具还要加二次确认、权限控制、幂等、回滚、日志和告警。风险越高,自动化程度越要谨慎。 **本章小结:** - **Tool 是什么**:Tool 是暴露给模型的外部能力,本质上通常是被包装过的函数或接口;Tool Calling / Function Calling 则是模型输出调用意图的机制。 - **基本分工**:模型负责“要不要调、调哪个、传什么参数”,程序负责“真正执行工具并回填结果”。 - **怎么定义 Tool**:最简单的方式是使用 `@tool` 装饰器。模型主要通过 `name`、`description`、`args_schema` 理解工具,所以工具名、工具说明、参数定义都非常关键。 - **为什么要配合 Pydantic**:Pydantic 能让参数定义更清晰、校验更稳定、错误更容易定位,也更利于模型生成正确参数,是 Tool 从“能跑”走向“工程化”的重要一步。 - **和官方主线的关系**:本课程为了教学直观,保留了 `bind_tools + parser + 手动执行工具` 的展开方式;LangChain / OpenAI 官方则更常从 `AIMessage.tool_calls → ToolMessage` 的消息流角度讲解。两者本质一致,只是教学视角不同。 **建议下一步:** 先把本章 5 个案例全部跑一遍,重点观察“工具描述、参数 schema、模型返回 tool_calls、程序执行工具”这四个环节;然后继续学习 [第 21 章 Agent 智能体](21-Agent智能体.md),你会更容易理解为什么 Agent 的核心不是“多了几个 API”,而是在 Tool 基础上增加了自主决策与多步编排能力。