工具调用模块深度解析:Function Calling 背后的工程设计
🧠 知识地图位置:第3层 → 工具调用模块 | 📊 难度:进阶
本文是「Agent技术笔记」关于 Agent 工具调用模块的系统性解析。如果说决策机制决定了 Agent "怎么想",记忆模块决定了 Agent "记住什么",那工具调用模块就决定了 Agent "能做什么"。没有工具的 Agent,思考再缜密也只是纸上谈兵——这篇就来拆解"工具端"背后的工程设计。
先说结论
工具调用模块是 Agent 系统的 "手脚",但它的设计难度被严重低估了。
很多人觉得工具调用就是「定义几个函数 + 写几段 JSON Schema」,但实际上,工具端的设计质量直接决定了整个 Agent 系统的上限——决策再强、记忆再好,如果工具返回的结果 LLM 读不懂、参数填不对、失败了没有有效信号,整个系统就会在工具这个环节卡死。
本文从以下五个维度展开:
| # | 维度 | 核心问题 |
|---|---|---|
| 1 | 为什么需要工具 | 没有工具的 Agent 到底缺了什么? |
| 2 | Agent 工具的特殊要求 | 为什么传统的工具设计方式在 Agent 时代行不通? |
| 3 | 工具的设计形式 | 函数式、API、代码解释器、插件——分别适用什么场景? |
| 4 | 工具调用的设计范式 | Function Calling、MCP 等范式的底层逻辑与选型考量 |
| 5 | 工程实践与常见陷阱 | 真实落地中踩过的坑与对应的解法 |
一、为什么需要工具:Agent 的"手和脚"
1.1 一个最直观的类比
Agent 如果没有工具,就好比一个人只有大脑没有手脚。
这个类比虽然简单,但非常精确。我们设想这样一个场景:
你让一个没有工具的 Agent 帮你"查一下北京今天天气,如果空气质量好就帮我订一张明天去北京的机票"。
它的推理过程可能是这样的:
Thought: 我需要先查北京天气,但我没有天气查询工具。
Thought: 我需要订机票,但我没有航班搜索工具。
Thought: 我无法完成这个任务。
Final Answer: 抱歉,我无法查询天气或预订机票。
它"想"得到该做什么,但做不了。而有工具的 Agent,思考完就能立刻行动:
Thought: 先查北京今天天气
Action: get_weather("北京", "today")
Observation: {天气: "晴", AQI: 45, 温度: "22°C"}
Thought: AQI 45,空气质量良好,应该订票
Action: search_flights("上海", "北京", "2026-06-11")
Observation: {航班列表: [...]}
Thought: 选择最优航班
Final Answer: 已为您预订...
1.2 工具的本质:赋予 Agent "向外探索"的能力
从信息论的角度看,LLM 的知识是闭包的——它只能基于训练数据做推理,无法获取实时信息,也无法对外部世界产生实际影响。工具打破了这层闭包:
┌─────────────────────────────────────────────────────┐
│ LLM 的知识边界 │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 训练数据 │ │ 推理能力 │ │
│ │ (截止到某天) │ │ (基于已知) │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ✕ 无法获取实时信息 │
│ ✕ 无法执行外部操作 │
│ ✕ 无法感知环境变化 │
│ │
├─────────────────────────────────────────────────────┤
│ │
│ 工具 = 打破闭包,连接外部世界 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 实时数据 │ │ 业务系统 │ │ 物理世界 │ │
│ │ 天气/航班 │ │ CRM/ERP │ │ IoT/设备 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────┘
工具赋予 Agent 的三类核心能力:
| 能力 | 说明 | 典型工具 |
|---|---|---|
| 感知外部 | 获取实时数据、查询外部系统 | 天气查询、数据库查询、网页搜索 |
| 操作外部 | 在外部系统中执行写操作 | 发送邮件、创建工单、调用 API |
| 计算增强 | 弥补 LLM 的计算弱项 | 代码执行、数学计算、数据处理 |
1.3 工具在 Agent 循环中的位置
回顾 Agent 的决策循环,工具是「行动(Action)」和「观察(Observation)」两个环节的载体:
感知 → 推理 → [工具调用: 行动 → 观察] → 再推理 → ...
↑
工具模块在这里
💡 关键认知:工具不只是"被调用"的那一下。它包括了工具的定义、参数的填充、结果的解析、错误的处理——这是一个完整的交互闭环,任何一个环节出问题,都会让整个 Agent 循环断裂。
二、Agent 工具的特殊要求:不是"能跑就行"
这是本篇文章我最想表述的点:Agent 时代的工具设计,和传统软件工程里的工具设计有本质区别。
传统工具调用的标准是:能跑通就行。参数对了,返回结果了,状态码 200,就算成功。
Agent 工具的标准是:LLM 能高效、准确地和你交互。这完全不是一个维度。
| 维度 | 传统工具设计 | Agent 工具设计 |
|---|---|---|
| 调用方 | 人类开发者(读文档、IDE 提示) | LLM(读文本描述、填参数) |
| 容错性 | 调用方自己处理异常 | LLM 需要从返回值中自己判断成败 |
| 返回格式 | 状态码 + 数据(开发者自己解析) | 自然语言友好的结构化结果 |
| 交互模式 | 单次调用 | 多轮调用,结果影响后续决策 |
下面逐一展开 Agent 工具的三项核心要求。
2.1 工具描述与定义:边界清晰,匹配执行策略
核心问题:LLM 怎么知道什么时候该用哪个工具?
答案只有一个:靠工具描述。工具描述是 LLM 选择工具的唯一依据,如果描述写不好,LLM 就不知道该用谁。
① 不同工具之间要有易区分的边界
假设你的 Agent 有三个工具:
❌ 错误示范(边界模糊):
- tool_a: "查询信息"
- tool_b: "搜索内容"
- tool_c: "获取数据"
LLM:这三个好像都能干同一件事?随便选一个吧...
✅ 正确示范(边界清晰):
- get_weather: "查询指定城市和日期的天气信息,返回温度、天气状况、AQI"
- search_flights: "搜索指定日期和航线的航班,返回航班号、价格、时间"
- check_hotel: "查询指定城市和日期的酒店,返回酒店名、价格、评分"
② 工具的功能点要和任务中的执行策略相匹配
工具的粒度要合适。太粗(一个工具干太多事),LLM 不知道怎么用;太细(拆得太碎),调用次数暴增,延迟变大。
以物流场景为例:
❌ 太粗:
- "处理货运需求" → LLM 永远不知道这个工具具体能干啥
✅ 合适粒度:
- search_cargo(起点, 终点, 货物类型) → 搜索匹配货源
- calc_price(路线, 货物重量) → 计算预估运价
- check_route(起点, 终点) → 路线可行性检查
💡 实践经验:工具粒度设计的黄金法则是——"一个工具只做一件事,且这件事可以用一句话清晰描述"。当你发现自己需要用"和/或"来描述一个工具的功能时,就该拆分了。
2.2 入参设计:越少越好,越清晰越好
核心问题:和工具交互的本质上是 LLM,而 LLM 擅推理、弱填充。
这是一个工程上的重要发现:如果让 LLM 去填充大量字段,会出现较大的延时和不稳定性。
原因很简单:
-
LLM 的强项是推理和决策(这个场景该不该用这个工具?参数大概是什么?)
-
LLM 的弱项是大量结构化字段的精确填充(尤其是字段多、类型复杂、需要精确格式的情况)
① 参数数量:能少则少
❌ 参数过多(8 个必填字段):
{
"name": "create_shipment_order",
"parameters": {
"origin_province": "出发省",
"origin_city": "出发市",
"origin_district": "出发区",
"dest_province": "目的省",
"dest_city": "目的市",
"dest_district": "目的区",
"cargo_type": "货物类型",
"cargo_weight": "货物重量",
"loading_time": "装货时间",
"contact_phone": "联系电话",
"special_requirements": "特殊要求",
"insurance_type": "保险类型"
}
}
LLM 填充这种 12 字段的工具,常见问题:
-
填到一半逻辑断了,只填了 8 个字段
-
字段格式不统一(有的用全称有的用缩写)
-
latency 从 2s 飙升到 8s+
✅ 精简后(4 个必填 + 工具内部补全):
{
"name": "create_shipment_order",
"parameters": {
"origin": "出发地(城市名)",
"destination": "目的地(城市名)",
"cargo_desc": "货物描述(类型+重量,如'钢材 20吨')",
"loading_date": "装货日期(YYYY-MM-DD)"
}
}
省掉的字段去哪了?在工具内部通过业务逻辑自动补全——省市区从城市名查字典、联系电话从用户档案读取、保险默认给基础套餐。LLM 只填最关键的几个字段,其他由工程代码兜底。
② 参数描述:类型明确 + 示例驱动
每个参数除了类型,一定要写清楚:
-
取值范围(枚举值有哪些?数字的合理范围?)
-
格式要求(日期是 YYYY-MM-DD 还是 MM/DD?)
-
至少一个示例(示例比任何文字描述都管用)
✅ 参数描述最佳实践:
"origin": {
"type": "string",
"description": "出发城市。必须是中国地级市全称,如'北京'、'上海'、'广州'。
不接受简称或省份名。示例:'杭州'(正确),'浙'(错误)",
}
💡 一个反直觉的经验:很多时候工具调用不准确,不是模型的问题,是参数描述写得太抽象。你把参数描述改得更具体、加上示例,命中率可能从 70% 提升到 95%,比你微调模型还管用。
2.3 出参的"Agentic 化":让 LLM 能读懂的结果
这是 Agent 工具设计和传统工具设计最大的分水岭。
传统工具返回的结果是给人看的(或者给下一段代码解析的)。Agent 工具的返回结果是给LLM读的,它需要从这个结果里提取信息、判断成败、决定下一步。
如果出参不"Agentic 化",你会发现一个诡异的现象:明明所有环节都做得不错,Agent 的整体效果就是上不去——瓶颈就在工具返回的结果上。
① 出参必须具有 LLM 可读性
❌ 传统返回(LLM 难以理解):
{
"code": 200,
"data": {
"items": [
{"id": 8832, "n": "京A·京B", "pr": 4500, "st": 1}
],
"pg": {"cur": 0, "sz": 10, "ttl": 47}
}
}
LLM:code=200 是成功了吗?st=1 是什么意思?pg 是啥?
✅ Agentic 返回(LLM 可直接理解):
{
"success": true,
"summary": "找到 47 条货源,当前返回前 10 条",
"cargo_list": [
{
"id": 8832,
"route": "北京 → 上海",
"cargo_type": "建材",
"price": "¥4,500",
"status": "待接单",
"publish_time": "2026-06-10 10:30"
}
]
}
核心差异:
| 维度 | 传统出参 | Agentic 出参 |
|---|---|---|
| 成功标识 | code: 200 (LLM 不一定理解) |
success: true (LLM 直接理解) |
| 字段命名 | 缩写( st , pr , n ) |
全称( status , price , route ) |
| 业务含义 | 数字编码( st: 1 ) |
可读字符串( status: "待接单" ) |
| 摘要信息 | 无 | summary 字段总结关键信息 |
② 好的工具要有清晰的指导性信号
这是"Agentic 化"最容易被忽视却最重要的部分。工具返回的结果不只是数据,还应该包含指导 Agent 下一步行动的信号。
✅ 带指导信号的出参:
{
"success": true,
"summary": "找到 47 条货源,当前返回前 10 条",
"cargo_list": [...],
// 👇 指导性信号
"suggestion": {
"action": "当前结果较多,建议让用户筛选条件(如货物类型、价格区间)",
"has_more": true,
"next_page_token": "eyJwYWdlIjoyfQ=="
}
}
✅ 失败时的指导信号:
{
"success": false,
"error": "未找到匹配货源",
"error_detail": "出发地'太阳系火星基地'不是有效城市名",
// 👇 关键:告诉 LLM 错在哪、怎么改
"correction_hint": {
"invalid_field": "origin",
"reason": "出发地不是有效的中国城市名",
"suggestion": "请使用城市全称,如'北京'、'上海'、'广州'",
"valid_examples": ["北京", "上海", "广州", "深圳", "成都"]
}
}
💡 这是我个人实践中感受最深的一点:当工具失败时,如果你只返回
{"success": false, "error": "参数错误"},LLM 大概率会再试一次同样的错误参数,或者直接放弃。但如果你返回了correction_hint,告诉它"哪个字段不对、预期范围是什么",LLM 会立刻修正重试——成功率可以从 30% 跳到 85%+。
③ 出参内容不是越多越好,需要高度凝练
一个容易犯的错误是:把数据库查出来的所有字段一股脑返回。LLM 的 context window 是有限的,冗余信息会分散注意力。只返回 LLM 决策需要的字段,其他的通过分页/详情接口按需获取。
2.4 小结:Agent 工具设计的核心原则
┌─────────────────────────────────────────────────┐
│ Agent 工具设计三原则 │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ 原则 1:描述即契约 │ │
│ │ 工具名称 + 描述 + 参数 = LLM 的判断依据 │ │
│ │ 边界模糊 = 选择随机 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ 原则 2:入参最小化 │ │
│ │ 减少 LLM 的填充负担 │ │
│ │ 能工程补全的字段,不让 LLM 填 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ 原则 3:出参 Agentic 化 │ │
│ │ 成功的信号 + 失败的原因 + 修正的建议 │ │
│ │ LLM 能读懂 > 人能读懂(格式完整度) │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
三、工具的设计形式
前面讨论了 Agent 工具"应该满足什么要求"。接下来看工具在工程上有哪些设计形式——不同的形式决定了工具的接入方式、运行环境和安全性。
3.1 函数式工具(Function-based Tools)
最主流的形式,也是 Function Calling 的原生形态。
将功能封装成函数,通过 JSON Schema 定义函数名、功能描述、参数及类型,LLM 生成参数来调用。
"""
函数式工具定义示例
"""
from typing import Dict, Any
# 工具函数
def get_weather(city: str, date: str = "today") -> Dict[str, Any]:
"""查询指定城市和日期的天气信息"""
# 实际调用天气 API...
return {"weather": "晴", "temp": "22°C", "aqi": 45}
# JSON Schema 定义(供 LLM 理解)
WEATHER_TOOL_SCHEMA = {
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市和日期的天气信息,返回天气状况、温度和空气质量指数",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,必须是中文全称。示例:'北京'、'上海'"
},
"date": {
"type": "string",
"description": "查询日期,格式 YYYY-MM-DD。默认 today 表示今天。示例:'2026-06-11'"
}
},
"required": ["city"]
}
}
}
核心特点:
| 维度 | 说明 |
|---|---|
| 集成方式 | 在 Agent 进程内直接调用 |
| 延迟 | ⭐⭐⭐⭐⭐ 最低(本地函数调用) |
| 安全性 | ⭐⭐⭐ 中(需自行控制权限) |
| 适用场景 | 内部业务逻辑、简单数据查询、不需要跨进程通信的场景 |
| 代表实现 | OpenAI Function Calling、LangChain Tool、LangGraph ToolNode |
优点:延迟最低、开发最快、调试方便。 缺点:安全性需要自己管控(函数在 Agent 进程内执行),不适合需要完全隔离的场景。
3.2 RESTful API / Webhook 工具
通过 HTTP 请求与外部服务交互。 Agent 构造请求头、参数、请求体,以 API 调用的方式使用工具。
Agent 进程 外部服务
┌──────────┐ HTTP Request ┌──────────┐
│ │ ──────────────────────→ │ │
│ Agent │ │ 天气API │
│ │ ←────────────────────── │ │
└──────────┘ HTTP Response └──────────┘
核心特点:
| 维度 | 说明 |
|---|---|
| 集成方式 | HTTP 请求(RESTful 或 Webhook) |
| 延迟 | ⭐⭐⭐ 中(网络开销) |
| 安全性 | ⭐⭐⭐⭐ 较高(进程隔离) |
| 适用场景 | 第三方服务集成、微服务架构、跨系统调用 |
| 代表规范 | OpenAPI / Swagger(可直接转为工具描述) |
OpenAPI 协议与工具定义的自动转换:
OpenAPI / Swagger 规范的一大优势是可以自动转换为 Function Calling 的 JSON Schema。如果你的后端已经用 OpenAPI 定义了接口,几乎可以零成本生成工具描述:
# OpenAPI 定义
/api/weather:
get:
summary: 查询天气信息
parameters:
- name: city
in: query
required: true
schema:
type: string
- name: date
in: query
schema:
type: string
↓ 自动转换 ↓
{
"name": "get_api_weather",
"description": "查询天气信息",
"parameters": {
"city": { "type": "string", "description": "..." },
"date": { "type": "string", "description": "..." }
}
}
💡 工程建议:如果你有大量已有 API 需要暴露给 Agent,OpenAPI → Function Schema 的自动转换是最省力的方案。但要注意,自动生成的描述通常比较"干",建议人工补充示例和字段说明——这就是前面说的"Agentic 化"。
3.3 代码解释器(Code Interpreter)
一种特殊而强大的工具形式。 Agent 在一个安全的沙箱环境中,直接编写并运行代码来完成复杂计算、数据处理或生成图表。
Agent 推理 沙箱环境
┌──────────┐ 代码 ┌──────────────┐
│ "需要计算 │ ────────→ │ Python 运行时 │
│ 这组数据 │ │ (沙箱隔离) │
│ 的标准差"│ ←─────── │ │
└──────────┘ 结果 └──────────────┘
核心特点:
| 维度 | 说明 |
|---|---|
| 集成方式 | 代码字符串 → 沙箱执行 → 结果回传 |
| 灵活性 | ⭐⭐⭐⭐⭐ 最高(图灵完备) |
| 安全性 | 依赖沙箱隔离 |
| 适用场景 | 复杂计算、数据可视化、文件处理、需要逻辑组合的开放式任务 |
| 代表实现 | OpenAI Code Interpreter、E2B、自建 Docker 沙箱 |
为什么代码解释器是一种"工具"?
它本质上是把"写代码并执行"这个能力封装成了一个工具。LLM 不需要提前知道所有可能的计算逻辑,只需要知道"我有一台计算机可以用"就行了——这极大地扩展了 Agent 的能力边界。
适用场景示例:
"""
Agent 使用 Code Interpreter 的典型场景
"""
# 场景 1:复杂计算
# LLM 不需要内置"标准差"的计算工具,直接写代码算
code = """
import statistics
data = [23, 45, 67, 34, 56, 78, 89]
result = statistics.stdev(data)
print(f"标准差: {result:.2f}")
"""
# 场景 2:数据可视化
code = """
import matplotlib.pyplot as plt
plt.plot(data)
plt.savefig('chart.png')
"""
# 场景 3:文件处理
code = """
import csv
with open('sales.csv') as f:
reader = csv.DictReader(f)
total = sum(float(row['amount']) for row in reader)
print(f"总销售额: ¥{total:,.2f}")
"""
💡 Code Interpreter 的独特价值:其他工具形式都是"提前定义好的能力",Code Interpreter 是"给 Agent 一个通用计算引擎"。它特别适合那些你无法预先穷举所有可能的计算逻辑的场景。
3.4 插件式工具(Plugin-based Tools)
封装好的独立模块,Agent 像使用插件一样加载和调用。 它有明确的接口,可能在独立进程或容器中运行,安全性更高。
┌─────────────────────────────────────────────┐
│ Agent 运行时 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 天气插件 │ │ 搜索插件 │ │ 计算插件 │ │
│ │(独立进程)│ │(独立进程)│ │(独立容器)│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ┌────┴────────────┴────────────┴────┐ │
│ │ 插件管理 / 协议层 │ │
│ │ (加载、卸载、权限、版本管理) │ │
│ └──────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
核心特点:
| 维度 | 说明 |
|---|---|
| 集成方式 | 独立进程/容器,通过协议通信 |
| 延迟 | ⭐⭐ 较高(进程间通信开销) |
| 安全性 | ⭐⭐⭐⭐⭐ 最高(完全隔离) |
| 适用场景 | 需要严格权限控制、第三方工具集成、构建 Agent 工具生态 |
| 代表实现 | ChatGPT Plugins、MCP Server、LangChain Plugin |
插件 vs 函数式,怎么选?
| 维度 | 函数式工具 | 插件式工具 |
|---|---|---|
| 开发速度 | 快 | 较慢(需要定义接口和部署) |
| 进程隔离 | 无(共享进程) | 有(独立进程) |
| 权限管控 | 需自建 | 天然隔离 |
| 热更新 | 需重启 Agent | 可热插拔 |
| 适用方 | 内部定制 | 开放生态、第三方集成 |
💡 趋势判断:插件式工具是构建 Agent 生态的基础。当你的 Agent 系统需要集成越来越丰富的外部能力时,函数式工具的"全写在一个进程里"就会变成瓶颈。MCP(Model Context Protocol)正是为了解决这个标准化问题而生的——下面会详细讲。
3.5 四种形式的横向对比
Code Interpreter ● (最高灵活性)
│
│
Plugin ● (独立模块,生态基础)
│
RESTful API ● (微服务集成)
│
Function ● (最快、最直接)
| 形式 | 灵活度 | 安全性 | 延迟 | 开发成本 | 最适合 |
|---|---|---|---|---|---|
| 函数式 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 内部业务逻辑 |
| RESTful API | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | 第三方集成 |
| Code Interpreter | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ | 开放式计算 |
| 插件式 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ | 生态构建 |
四、工具调用的设计范式
有了工具的设计形式,接下来就是 "Agent 怎么和工具交互" 的问题——也就是工具调用的设计范式。
4.1 Function Calling:原生工具调用的标准范式
Function Calling 是目前最主流的工具调用范式。它的核心思想是:把工具的定义直接注入到 LLM 的上下文中,让 LLM 自主决定何时调用、如何调用。
工作流程:
┌─────────┐ ① User Message + Tools Schema ┌─────────┐
│ │ ──────────────────────────────────→ │ │
│ Agent │ │ LLM │
│ 框架 │ ←────────────────────────────────── │ │
│ │ ② Function Call Request └─────────┘
│ │ {name: "get_weather",
│ │ arguments: {city: "北京"}}
│ │
│ │ ③ 执行工具,获取结果
│ │
│ │ ④ Function Result + 下一轮推理 ┌─────────┐
│ │ ──────────────────────────────────→ │ LLM │
│ │ ←────────────────────────────────── │ │
└─────────┘ ⑤ 最终回复 └─────────┘
代码示例(以 OpenAI 格式为例):
"""
Function Calling 完整调用流程
"""
import json
# ===== Step 1: 定义工具 Schema =====
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市和日期的天气。返回天气状况、温度、AQI。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名,中文全称。示例:'北京'"
}
},
"required": ["city"]
}
}
}]
# ===== Step 2: 第一次调用 LLM,LLM 返回 function_call =====
messages = [{"role": "user", "content": "北京今天天气怎么样?"}]
# response = llm.chat(messages, tools=tools)
# → response.choices[0].message.tool_calls:
# [{"function": {"name": "get_weather", "arguments": '{"city": "北京"}'}}]
# ===== Step 3: 执行工具 =====
def execute_tool(tool_call):
name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"])
if name == "get_weather":
result = {"weather": "晴", "temp": "22°C", "aqi": 45}
return json.dumps(result, ensure_ascii=False)
# tool_result = execute_tool(response.choices[0].message.tool_calls[0])
# ===== Step 4: 二轮调用 LLM,携带工具结果 =====
# messages.append(response.choices[0].message) # assistant message with tool_calls
# messages.append({
# "role": "tool",
# "tool_call_id": "...",
# "content": tool_result
# })
# final_response = llm.chat(messages)
Function Calling 的核心设计要点:
| 要点 | 说明 | 实践建议 |
|---|---|---|
| Schema 精度 | LLM 完全依赖 Schema 理解工具 | 每个字段的描述要包含类型、范围、示例 |
| 并行调用 | 多个独立工具可并行调用,降低延迟 | 利用 LLM 的 parallel tool calls 能力 |
| 强制调用 | 可设置 tool_choice: "required" 强制 LLM 调用工具 |
在规划型 Agent 中很有用 |
| 结果回注 | 工具结果以 role: "tool" 消息形式注入对话 |
保持消息格式一致性 |
4.2 MCP(Model Context Protocol):面向工具生态的标准化协议
MCP 是 Anthropic 提出的模型与外部工具/数据源之间的开放协议,目标是做"工具调用的 USB-C 接口"——统一标准,即插即用。
MCP 的核心架构:
┌───────────────────────────────────────────────────┐
│ MCP 架构 │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ MCP Host │ │ MCP Host │ │
│ │ (Claude) │ │ (自研Agent) │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ │ MCP Protocol │ │
│ │ (JSON-RPC 2.0) │ │
│ │ │ │
│ ┌──────┴────────────────────────┴──────┐ │
│ │ MCP Client │ │
│ │ (协议层:连接管理、工具注册) │ │
│ └──────┬──────────┬──────────┬────────┘ │
│ │ │ │ │
│ ┌────┴────┐┌────┴────┐┌────┴────┐ │
│ │ 天气 ││ 文件系统 ││ 数据库 │ │
│ │ Server ││ Server ││ Server │ │
│ └─────────┘└─────────┘└─────────┘ │
│ │
└───────────────────────────────────────────────────┘
MCP 的三个核心概念:
| 概念 | 说明 | 类比 |
|---|---|---|
| Resources | 暴露数据(文件内容、数据库记录等) | 读操作 |
| Tools | 暴露可执行操作(API 调用、计算等) | 写/执行操作 |
| Prompts | 预定义的 Prompt 模板 | 快捷指令 |
MCP vs Function Calling 的定位差异:
| 维度 | Function Calling | MCP |
|---|---|---|
| 定位 | LLM 级别的调用能力 | 系统级别的工具协议 |
| 标准化程度 | 各厂商有差异 | 统一协议(JSON-RPC 2.0) |
| 工具复用性 | 每个 Agent 需单独集成 | 一次开发,所有 MCP Host 可用 |
| 生态属性 | 封闭(工具随 Agent) | 开放(工具独立于 Agent) |
| 适合 | 内部工具、快速开发 | 开放生态、跨平台工具共享 |
💡 我的判断:Function Calling 和 MCP 不是互相替代的关系,而是分层关系。Function Calling 解决的是"单次调用怎么传参数"的问题,MCP 解决的是"工具怎么标准化地接入 Agent"的问题。实际工程中,你可以用 MCP 管理工具,用 Function Calling 执行调用——两层结合。
4.3 其他范式简述
① Tool-use via Prompt Engineering(Prompt 内嵌工具调用)
在没有原生 Function Calling 支持的模型上,通过 Prompt 工程手动实现工具调用:
System Prompt:
你可以使用以下工具:
1. get_weather(city) - 查询天气
2. search_web(query) - 搜索网页
当需要使用工具时,按以下格式输出:
<tool>get_weather("北京")</tool>
当不需要工具时,直接回复。
这是最原始也最通用的方式,适用于任何模型。缺点是解析不稳定——LLM 可能输出格式错误、忘记闭合标签等。
② Tool-flow / Graph-based Tool Orchestration
在 LangGraph 等图结构框架中,工具不是"被 LLM 选中后调用",而是作为图的固定节点,由代码逻辑控制调用时机:
# LangGraph 中的工具编排
graph = StateGraph(AgentState)
# 工具是固定节点
graph.add_node("weather_check", WeatherTool())
graph.add_node("flight_search", FlightTool())
graph.add_node("hotel_booking", HotelTool())
# 代码控制调用顺序(而非 LLM 决定)
graph.add_edge("weather_check", "flight_search")
graph.add_conditional_edges("flight_search", decide_next, {
"success": "hotel_booking",
"retry": "flight_search"
})
五、工程实践中的常见陷阱与解法
这部分来自实际工程项目中的经验教训。
5.1 陷阱一:工具太多,LLM 选不对
症状:Agent 有 30+ 个工具,LLM 频繁选错工具或"忘记"有某个工具可用。
根因:工具 Schema 全部注入 System Prompt 后,context window 被大量占用,LLM 的注意力被稀释。且工具越多,边界模糊的概率越大。
解法:
-
分层工具注册:按任务阶段动态注册工具(任务开始时只需 3-5 个工具可见)
-
工具分组:将工具按领域分组,先让 LLM 选"领域",再在领域内选具体工具
-
工具描述去重:定期审查工具描述,确保边界清晰无交叉
# 分层工具注册示例
def get_tools_for_phase(phase: str) -> list:
"""根据任务阶段,只暴露当前阶段需要的工具"""
phase_tools = {
"understanding": [user_profile_tool, history_tool],
"searching": [cargo_search_tool, route_check_tool],
"negotiating": [price_calc_tool, offer_tool],
"confirming": [order_create_tool, payment_tool],
}
return phase_tools.get(phase, [])
5.2 陷阱二:工具返回太大,撑爆 Context Window
症状:查询返回 1000 条数据,作为 tool result 注入后,后续轮次的 LLM 调用直接超 context 限制。
根因:工具设计时没有考虑返回数据量控制。
解法:
-
强制分页:所有查询类工具必须有 page_size 和 page_token 参数
-
返回摘要:先返回
summary+total_count,LLM 需要时再请求详情 -
结果截断:工具层自动截断超长返回(如在 2000 token 处截断并追加"...(结果已截断,共 XXX 条)")
5.3 陷阱三:工具调用失败,LLM 进入死循环
症状:工具返回错误后,LLM 反复用同样的参数重试,进入 10+ 轮的死循环。
根因:工具的错误信息不够"Agentic"——只返回了 error: "参数错误" 但没有说明具体哪里错、怎么改。
解法(前面 2.3 节的延伸):
❌ 死循环型错误返回:
{"success": false, "error": "Invalid parameter"}
✅ 引导型错误返回:
{
"success": false,
"error": "出发地参数无效",
"correction": {
"invalid_field": "origin",
"received_value": "京",
"expected_format": "城市全称",
"valid_examples": ["北京", "上海", "广州"]
}
}
5.4 陷阱四:工具延迟不可控,影响用户体验
症状:某个工具调用耗时 15 秒,整个 Agent 对话卡住无响应。
根因:Agent 的默认实现是同步等待工具返回,没有超时和降级机制。
解法:
-
超时中断:所有工具设置超时时间(建议 5-8 秒),超时后返回降级结果
-
流式反馈:工具执行过程中,通过 streaming 告知用户"正在查询 XXX..."
-
异步化:独立工具并行调用(如同时查天气和航班),减少总延迟
"""
工具超时与降级示例
"""
import asyncio
async def call_tool_with_timeout(tool_func, args, timeout=8):
try:
result = await asyncio.wait_for(
tool_func(**args),
timeout=timeout
)
return result
except asyncio.TimeoutError:
# 超时降级:返回缓存结果或告知用户
return {
"success": False,
"error": "工具调用超时",
"suggestion": "请稍后重试,或尝试缩小查询范围"
}
六、总结
这篇文章从"工具为什么重要"出发,系统梳理了 Agent 工具调用模块的核心设计逻辑:
| 维度 | 核心观点 |
|---|---|
| 为什么需要 | 工具是 Agent 的"手脚",打破 LLM 的知识闭包,赋予感知外部、操作外部、计算增强三类能力 |
| 特殊要求 | Agent 工具 ≠ 传统工具。描述要边界清晰、入参要最小化、出参要 Agentic 化(可读 + 带指导信号) |
| 设计形式 | 四种形式各有适用场景:函数式(快)、RESTful API(集成)、Code Interpreter(灵活)、插件式(安全) |
| 设计范式 | Function Calling ;MCP 面向工具生态标准化;两者分层结合是最优解 |
| 工程陷阱 | 工具太多→分层注册;返回太大→强制分页;死循环→引导型错误返回;延迟高→超时降级 |
最后说一下自己实践后的心得:
工具调用模块经常成为 Agent 系统的"隐形天花板"。 决策机制可以打磨到极致,记忆系统可以做到完美,但如果工具端返回的结果 LLM 读不懂、失败了没有修正线索、参数填来填去都不对——整个系统就会被"卡"在工具这个环节。把工具的"Agentic 化"做好,往往是花最少力气、收获最大提升的优化方向。
📖 下一步阅读建议
这篇文章是全景图中第 3 层(核心能力) → 工具调用模块的核心内容。至此,第 3 层的三大核心能力模块(决策推理、记忆管理、工具调用)已全部覆盖,后面还会基于这些模块,向内进行扩展,向外进行延伸。
-
同层回顾:《Agent 决策机制深度解析》——决策决定"怎么想"(第 03 篇)
-
同层回顾:《Memory 模块设计实录》——记忆决定"记什么"(第 04 篇)
-
向上延伸:《LangGraph 实战》——如何用图结构框架优雅地编排工具调用流程
-
向下夯实:《LLM 微调实战 SFT + GRPO》——如果 Function Calling 效果始终不理想,微调是终极解法
-
工具生态:MCP 实战——如何用 MCP 构建可复用的工具生态
💡 和前三篇文章的关系:第 02 篇建立了全景地图,第 03 篇讲决策机制(怎么想),第 04 篇讲记忆管理(记什么),本篇讲工具调用(怎么做)——四篇合在一起,覆盖了 Agent 技术栈中最核心的三大能力模块。后面我还会向上延伸到框架层和业务层,看如何把这些模块组装成真正可用的 Agent 系统,同时也会对这些模块做更深入的扩展。
如果这篇文章对你有帮助,欢迎转发给正在研究 Agent 技术的朋友。有任何问题或想深入探讨的技术点,随时后台留言 💬
下一篇预告:《LangGraph 实战:用图结构框架搭建生产级 Agent》
