工具调用模块深度解析: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》