# 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 基础上增加了自主决策与多步编排能力。
