返回首页

Function Calling 入门:让大模型学会"用工具"

大模型自己不会查天气、不会算精确数学、动不了你的数据库 —— Function Calling 就是给它"开外挂"的标准协议。这篇讲清楚它是什么、为什么需要、一次完整的调用长什么样,以及它和 Agent / MCP 到底是什么关系。

29 约 10 分钟 · 12392 字 AI

上一篇 Transformer 解码策略 把 LLM 怎么"挑下一个词"讲完了 —— 模型的输出是一段段文字接龙。

但你打开 ChatGPT 问"北京今天天气怎么样",它能给你一个实时正确的答案;你让它"把这段会议纪要发给我邮箱",它真的能发出去。

模型只会接龙,为什么它能干这些超出"语言"范围的事?

答案就是 —— Function Calling(函数调用,也叫 Tool Use / Tool Calling)。

一句话定义

模型本身不会调函数,它只是在"该用工具的时候"按约定吐出一段结构化 JSON,描述自己想调哪个工具、用什么参数 —— 真正执行函数的是你写的代码,执行完再把结果喂回去让模型继续接龙。

读两遍,这一句话基本就把 function calling 的精髓讲完了。剩下都是细节。

为什么需要这玩意

LLM 自身有三个改不掉的短板:

短板 例子
知识有截止日期 问它"昨天 A 股收盘多少",它不知道
算数不靠谱 问它 983476 × 12947 等于多少,大概率算错
没法影响外部世界 让它发邮件、改数据库、下单 —— 它只会跟你说"好的我帮你下单了",其实啥也没干

业界目前两条主流的"打补丁"思路:

  • RAG(检索增强生成):在提问前先把相关文档查出来,塞进上下文 —— 解决"知识不够新 / 不够专"
  • Function Calling:让模型能主动调用你定义好的函数 —— 解决"读、写、动作"全套

RAG 只解决了"读"的问题,而且是你主动塞数据。Function Calling 是更通用的方案:模型自己决定什么时候要查、查什么、查哪里,以及查完之后是不是还要查别的。

一次完整的调用长什么样

这里是 function calling 最关键的部分,一定要看懂。

整个过程是一次或多次"模型 ↔ 你"的来回,不是模型自己跑出去调了个函数。

以"用户问北京天气"为例,完整流程是这样的:

┌─────────────┐                                ┌──────────┐
│   你的代码   │                                │   模型    │
└─────┬───────┘                                └────┬─────┘
      │                                              │
      │  1. messages=[用户:"北京天气"]               │
      │     tools=[get_weather, send_email, ...]    │
      ├────────────────────────────────────────────►│
      │                                              │
      │  2. 模型决定要调工具                          │
      │     finish_reason: "tool_calls"             │
      │     tool_calls: [{                          │
      │       name: "get_weather",                  │
      │       arguments: { city: "北京" }            │
      │     }]                                       │
      │◄────────────────────────────────────────────┤
      │                                              │
      │  3. 你的代码真的去调 get_weather("北京")     │
      │     拿到 "晴,28℃,东南风 3 级"                │
      │                                              │
      │  4. messages=[                              │
      │       用户:"北京天气",                      │
      │       助手:tool_calls,                      │
      │       工具结果:"晴,28℃,东南风 3 级"         │
      │     ]                                        │
      ├────────────────────────────────────────────►│
      │                                              │
      │  5. 模型基于真实数据组织自然语言              │
      │     "北京今天天气晴朗,气温 28 度..."         │
      │◄────────────────────────────────────────────┤
      │                                              │

注意几个关键点:

  1. 模型从头到尾没有"调用"任何函数,它只生成了一段 JSON
  2. 真正执行函数的是你的代码 —— 你拿到 tool_calls 之后,自己 switch (name) { case 'get_weather': ... }
  3. 工具结果要塞回 messages 里,模型才能"看见"
  4. 如果模型基于工具结果还想调另一个工具,可以再次返回 tool_calls,你再调、再塞回去 —— 这就是多轮工具调用 / Agent 的基础

工具长什么样:JSON Schema

要让模型知道"有哪些工具可用、参数怎么填",你得用 JSON Schema 把工具描述出来。

一个典型的工具定义:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "获取指定城市当前的天气情况。仅支持中国大陆城市,不支持港澳台和海外。",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "城市的中文名,例如 '北京' / '上海' / '深圳'"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "温度单位,默认摄氏度",
          "default": "celsius"
        }
      },
      "required": ["city"]
    }
  }
}

模型选不选这个工具、参数填得对不对,90% 取决于 description 写得好不好。所以工具描述这件事千万不能糊弄,后面会专门讲。

完整代码示例:OpenAI Python SDK

为了把抽象的流程落到代码,这里用 OpenAI 的 Python SDK 演示一次"问天气"的完整 round-trip。

from openai import OpenAI
import json

client = OpenAI(api_key="sk-...")

def get_weather(city: str, unit: str = "celsius") -> dict:
    return {
        "city": city,
        "temperature": 28 if unit == "celsius" else 82,
        "condition": "晴",
        "wind": "东南风 3 级",
    }

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "获取指定城市当前的天气。仅支持中国大陆城市。",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "城市的中文名"},
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "default": "celsius",
                },
            },
            "required": ["city"],
        },
    },
}]

messages = [
    {"role": "user", "content": "北京今天天气怎么样?"}
]

resp = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
)

msg = resp.choices[0].message
messages.append(msg)

if msg.tool_calls:
    for call in msg.tool_calls:
        name = call.function.name
        args = json.loads(call.function.arguments)
        print(f"模型想调用 {name}({args})")

        if name == "get_weather":
            result = get_weather(**args)
        else:
            result = {"error": f"unknown tool: {name}"}

        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result, ensure_ascii=False),
        })

    final = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)
else:
    print(msg.content)

跑一遍的实际输出大概是这样:

模型想调用 get_weather({'city': '北京'})
北京今天天气晴朗,气温约 28℃,东南风 3 级,适合外出。

整个过程有两次 API 调用:

  1. 第一次:把工具列表给模型,模型决定要调 get_weather
  2. 第二次:把工具执行结果塞进 messages,模型组织自然语言回复

第二次调用时还是要带上 tools 参数,因为模型可能基于第一个工具的返回值,决定再调另一个工具(比如根据天气决定要不要调 send_notification)。

多工具 / 并行调用

现实场景里,工具往往不止一个。比如同时给模型 get_weather / search_web / send_email / query_database 等十几个工具,让它自己选。

主流模型现在都支持并行工具调用(parallel tool calling) —— 一次响应里同时返回多个 tool_calls,你并发地执行所有工具,再把所有结果一次塞回去。

例如用户问"北京和上海今天哪里更热",模型可能一次返回:

{
  "tool_calls": [
    { "id": "call_1", "name": "get_weather", "arguments": "{\"city\":\"北京\"}" },
    { "id": "call_2", "name": "get_weather", "arguments": "{\"city\":\"上海\"}" }
  ]
}

你的代码用 asyncio.gather / Promise.all 并发跑两次 get_weather,然后把两条 tool 消息都塞回 messages,再调一次模型 —— 一回合解决,不需要往返两次

并行调用的支持情况(写这篇时):

模型 并行支持
GPT-4o / GPT-4.1 / GPT-5 系列 ✅ 默认开启
Claude 3.5 / 3.7 / 4 系列 ✅ 默认开启,可手动关
Gemini 2.0+
DeepSeek-V3 / V3.1
千问 Qwen2.5+

工具调用 vs 普通对话:模型怎么"决定"调不调

很多人有个误解:"是不是我提供了工具,模型就一定会调?"

不是。 模型每一步都在做两个判断:

  1. 当前用户的需求,能不能只靠模型自己回答?(比如"1 + 1 等于几"显然不需要查工具)
  2. 如果需要外部信息 / 动作,哪个工具最匹配?

这两个判断完全靠你写的 description:

  • 工具的 description —— 模型从一堆工具里挑哪个
  • 参数的 description —— 模型怎么从用户的话里抽出参数值

举个反例:

{
  "name": "search",
  "description": "搜索"
}

这种工具描述,模型完全不知道:能搜什么?搜哪里?返回什么?于是要么不调,要么乱调。

正确的写法应该是:

{
  "name": "search_internal_docs",
  "description": "在公司内部技术文档库里检索。适用场景:用户问 '我们公司用 xxx 框架的最佳实践'、'部署流程文档在哪'。不适用:通用编程问题、外部新闻、实时数据 —— 这些场景应该让我直接回答或者使用 web_search 工具。"
}

"什么时候用 + 什么时候不用",这两条信息比一堆参数说明还重要。

"Functions" 还是 "Tools"?

这个命名上的混乱坑了不少新人,简单说一下。

  • OpenAI 2023 年 6 月首次推出这个能力,叫 functions + function_call
  • 2023 年 11 月改名为 tools + tool_choice(functions 字段保留但标记 deprecated)
  • Anthropic 一开始就叫 tools + tool_use
  • Google Gemini 也叫 tools

所以现在写新代码,统一用 tools 就对了。除非你维护的是老代码,看到 functions / function_call 字段不要奇怪 —— 等同语义,只是历史包袱。

由于命名混乱,业界对这个能力本身的称呼也有两套:

  • Function Calling(函数调用) —— OpenAI / 国内中文圈用得多
  • Tool Use(工具使用) —— Anthropic / 学术圈用得多

说的是同一件事

工程实践:踩坑要点

写过几个真实项目的工具集成之后,总结下值得注意的地方。

1. 工具描述要"行为驱动"

错的写法(只描述"是什么"):

"description": "查询订单表"

对的写法(描述"什么时候用 / 输入输出 / 限制"):

"description": "根据订单号查询订单的当前状态、下单时间、收货地址。只支持最近 90 天的订单;查询历史订单请用 query_archived_order。"

模型挑工具的逻辑非常像"读招标书"—— 你描述得清楚,它就挑得准。

2. 参数加约束,别让模型自由发挥

枚举(enum)、范围(minimum/maximum)、必填(required)、默认值(default)、格式(format: "date"format: "email")能加都加。

模型不是 100% 守规矩,但加约束能显著降低参数填错的概率

3. 工具内部错误要"说人话"返回

工具执行出错时,不要 raise / throw 让请求挂掉,而是返回一段模型能读懂的错误信息:

def get_weather(city: str):
    if city not in SUPPORTED_CITIES:
        return {
            "error": "city_not_supported",
            "message": f"暂不支持 {city} 的天气查询,目前只支持中国大陆地级市及以上",
            "hint": "如果用户问的是港澳台或海外,请明确告知用户暂不支持",
        }

模型看到这段会自己组织一句"抱歉,暂不支持 xxx 的天气查询..."。如果你直接 throw,整个请求就挂了,体验很差。

4. 危险操作要二次确认

涉及"动外部世界"的工具(发邮件、删数据、下单、扣款),强烈建议:

  • 工具内部加 dry-run 模式,只校验不执行
  • 真正执行前要人工 confirm(返回一个 confirm_token,前端弹窗,用户点确认后再调一个 execute_action 工具)
  • 日志全量记录,出问题能追溯

模型再聪明也会幻觉 —— 你能想象它把"取消订阅"调成"取消订单"的场景吗?这事真的发生过。

5. 别让模型自由生成 SQL

很多人第一反应是给模型一个 run_sql(query: str) 工具。不推荐

原因:

  • 模型生 SQL 容易写出全表 scan / 死循环 / 修改数据的语句
  • 注入攻击面巨大
  • 一旦表结构变了,模型生成的 SQL 全废

推荐做法:暴露"业务层"高级函数,例如 query_orders_by_date(start, end) / get_user_by_email(email),把 SQL 封在工具实现里。

如果一定要 SQL 灵活查询,至少:

  • 强制 SELECT 开头(禁止 INSERT/UPDATE/DELETE/DROP)
  • 强制带 LIMIT
  • 走只读账号
  • 加超时

6. 控制 Token 消耗

工具定义本身是要算 token 的。一个 20 个工具的项目,光工具描述就能占 2-3k token。每次请求都要带上,长对话里成本不低。

应对策略:

  • 工具描述精炼,删掉冗余
  • 工具按需注入 —— 比如检测到用户问编程问题再注入代码相关的工具,问天气再注入天气工具(用"工具路由"思路)
  • 多轮对话的历史里,老的 tool_calls + tool 消息可以适当截断或概要

7. 多轮 Agent 一定要有 max_iter

模型可以基于一次工具结果再调下一个工具,这种"自主循环"就叫 Agent

但模型偶尔会陷入死循环,比如:

调 search("ABC") → 没结果
调 search("ABC ") → 没结果(就多了个空格)
调 search(" ABC") → 没结果
...

务必加 max_iterations 上限(一般 10-30 次),超了就强制返回当前结果 / 报错。

Function Calling、Agent、MCP 是什么关系

这三个词在 LLM 圈经常一起出现,新人很容易蒙。一张表理清楚:

概念 是什么 在哪一层
Function Calling / Tool Use 模型"喊"工具调用的协议(JSON Schema + tool_calls) 最底层
Tool 一个具体的可调用单元(get_weather / send_email ...) 在 Function Calling 之上
Agent 拿到工具结果后能继续思考、再调工具、自主循环直到完成任务的系统 在 Function Calling 之上的应用模式
MCP(Model Context Protocol) Anthropic 提出的"工具提供方"标准化协议,让工具能像浏览器插件一样被任何 AI 客户端复用 工具的分发标准,在 Function Calling 上一层

关系大致是这样:

       ┌──────────────────────────┐
       │   Agent / AI 应用层       │  ← Cursor / Claude Desktop / 你的产品
       └────────────┬─────────────┘

       ┌────────────▼─────────────┐
       │   MCP(工具发现 / 分发)   │  ← 可选,用于把工具做成"插件"
       └────────────┬─────────────┘

       ┌────────────▼─────────────┐
       │   Function Calling 协议   │  ← 模型层 API 的能力
       └────────────┬─────────────┘

       ┌────────────▼─────────────┐
       │       基础模型             │  ← GPT / Claude / Gemini / ...
       └──────────────────────────┘
  • Function Calling 是模型自带的能力,没有它后面都谈不上
  • Tool 是 Function Calling 的具体实例
  • Agent 是"工具调用 + 自主循环"的工程模式 —— 没新东西,就是循环加上记忆 / 规划
  • MCP 解决的是"工具复用"问题 —— 不至于每个 AI 客户端都重新实现一遍 GitHub / Slack / Postgres 的对接代码

简单类比:

  • Function Calling ≈ HTTP 协议(底层规范)
  • Tool ≈ 一个 HTTP API(具体功能)
  • Agent ≈ 一个能自己浏览网页的浏览器(组合调用)
  • MCP ≈ Chrome 插件标准(工具的可移植化)

局限性

Function Calling 不是银弹,主要的"翻车点":

翻车点 表现 缓解
工具选错 描述不清,模型用了不该用的工具 优化 description,加"不适用场景"
参数填错 日期格式 / 嵌套对象 / 边界值搞错 加 schema 约束 + 在工具内部二次校验
幻觉调用 假装调了工具其实没调(老模型多见) 用支持 tool_calls 的新模型,严格判 finish_reason
死循环 反复调同一工具 / 自己跟自己聊 max_iterations 上限
上下文爆炸 多轮工具调用历史太长,超 context window 历史摘要 / 工具调用结果裁剪
延迟叠加 每轮工具调用 = 一次模型往返,5 轮就 30 秒+ 并行调用、流式输出
成本叠加 工具定义 + 历史调用 = 大量 token 工具按需注入、对话压缩

上手建议

如果你想自己玩一下 function calling,推荐这条学习路径:

  1. 先跑通"问天气"这种 hello-world 例子(就是上面的代码),把 round-trip 流程吃透
  2. 再加第二个工具,看模型怎么挑工具
  3. 再加并行调用,看模型怎么一次调多个
  4. 再做个"自主循环"Agent,加个 max_iter,跑个"查天气 → 发邮件提醒"的多步任务
  5. 再去看 MCP,体会"工具可移植化"的价值

不建议一上来就上 LangChain / LlamaIndex 这种重框架 —— 它们把 function calling 包了好几层抽象,反而看不清底下的协议长什么样。先把原生 SDK 跑通,你就会发现 90% 的"Agent 框架"都是在原生 function calling 上加点循环和 prompt 模板而已。

一句话总结

Function Calling 是 LLM 的"USB 接口"—— 模型自己不长手,但你可以给它接上任何工具,让它学会用。

会用 Function Calling 之后,你就不再是"问 LLM 一个问题让它回答",而是**"派 LLM 去完成一个任务,中间它需要什么资源自己找"**。这是从"聊天机器人"到"Agent"的本质跨越。


下次见。

-EOF-