Function Calling 详解:让大模型调用你的函数

QuibblerAgentQuibblerAgent 2026-11-02 约 12 分钟 23 次阅读

问大模型“北京今天多少度”,它只能用训练数据里的旧知识猜,或者老实承认不知道。但如果告诉它“我这里有个查天气的接口”,它会说:“帮我调用 get_weather,城市参数填北京。”你的程序执行调用,把结果递回去,它再组织成一句自然的回答。Function Calling(函数调用)让模型从“只会聊天”进化为“会办事”——它是大模型连接外部世界的第一块基石,也是一切 Agent 系统的地基。本文按“工作原理 → 工具定义 → 调用循环 → 设计最佳实践 → 踩坑 → 总结”组织全文。

1、概述

Function Calling 于 2023 年 6 月由 OpenAI 首次在 API 中引入,随后成为全行业标准能力,各家模型 API 均已支持,官方指南见 OpenAI Function Calling 文档。它的本质是一套消息协议:应用把可用的函数清单告诉模型,模型根据用户意图决定“要不要调、调哪个、参数填什么”,但它自己不执行——执行永远发生在你的应用侧。

这个分工是理解 Function Calling 的关键:模型的角色是“决策者”,负责把自然语言翻译成结构化的调用意图;应用的角色是“执行者”,负责真正跑函数、拿结果。两者通过消息列表接力协作,模型永远摸不到你的服务器。

它解决的核心问题有三个:

  • 把自然语言转成程序可执行的结构化调用(JSON 参数,而非自然语言描述)
  • 让模型获取训练数据之外的实时信息(天气、股价、库存)
  • 让模型触发真实世界的动作(发邮件、写数据库、下单)

需要注意的是,Function Calling 有两个相邻概念容易混淆:狭义的 Function Calling 特指上述 API 机制;广义的 Tool Use 则覆盖工具定义、执行、错误处理、工具生态的完整工程话题。本文聚焦机制本身,工具选型与生态留待工具使用全景的话题展开。

2、工作原理

一次完整的 Function Calling 涉及两轮模型交互。以“北京今天多少度”为例,时序如下:

sequenceDiagram participant U as 用户 participant A as 应用 participant M as 模型 participant F as 天气接口 U->>A: 北京今天多少度? A->>M: 消息 + 工具清单(get_weather) M-->>A: 决策调用 get_weather(城市=北京) A->>F: 执行函数调用 F-->>A: 返回 22 度 晴 A->>M: 回传函数执行结果 M-->>A: "北京今天 22 度,晴天。" A-->>U: 转述回答

拆开看四个关键点。第一,应用在请求里携带工具清单——每个工具的名字、描述、参数 Schema,这是模型决策的唯一依据。第二,模型的响应里包含一个“工具调用意图”(tool_calls 字段),含函数名与 JSON 格式的参数,模型此时只是“说”要调,并没有调。第三,应用解析意图、真正执行函数,把结果以 tool 角色消息追加进对话。第四,模型拿到结果,生成面向用户的最终回答。

这四步构成一个控制循环:复杂任务中,模型可能连续发起多次调用(查完天气再查空气),甚至串行依赖(先查城市 ID 再查天气)。控制循环由应用持有,何时终止由模型不再发起调用或应用设置的上限决定。

3、工具定义详解

工具用 JSON Schema 描述,核心是三要素:名字、描述、参数。JSON Schema 是参数定义的标准语言,模型依赖它理解每个参数的类型与含义。

以天气查询为例,Python SDK 的定义方式如下:

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "查询指定城市当前天气,返回温度与天气现象",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名,如 北京、上海"
                }
            },
            "required": ["city"]
        }
    }
}]

解读:定义里最值钱的是 description——模型判断“该不该调这个工具、参数怎么填”几乎完全依赖描述文本。required 列出必填参数,可选参数要写清缺省行为。参数越少越好,能用枚举就用枚举,枚举能从根本上杜绝填错取值。

定义完工具,第一轮请求把工具清单与用户消息一并发出:

from openai import OpenAI

client = OpenAI()
messages = [{"role": "user", "content": "北京今天多少度?"}]
resp = client.chat.completions.create(
    model="gpt-4o", messages=messages, tools=tools)
msg = resp.choices[0].message

解读:如果模型认为无需调用工具,msg.tool_calls 为空,直接取 msg.content 即可;如果需要调用,msg.tool_calls 里就有函数名与参数——应用从这里进入执行阶段。

4、完整的调用循环

模型决定调用后,接力棒交给应用。下面的代码完成“解析意图 → 执行 → 回传 → 拿到最终回答”的后半程:

import json

def get_weather(city: str) -> str:
    # 真实项目中这里调用天气服务
    return json.dumps({"city": city, "temp": 22, "condition": "晴"},
                      ensure_ascii=False)

if msg.tool_calls:
    messages.append(msg)  # 先把模型的调用意图入列,保持上下文完整
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(**args)  # 真正执行的是你的函数
        messages.append({
            "role": "tool",           # tool 角色消息:回传执行结果
            "tool_call_id": call.id,
            "content": result,
        })
    final = client.chat.completions.create(
        model="gpt-4o", messages=messages, tools=tools)
    print(final.choices[0].message.content)

解读:两个容易忽略的细节。其一,messages.append(msg) 不能省——模型的调用意图本身就是对话历史的一部分,丢了它,下一轮请求中的 tool 消息就成了无源之水,API 会直接报错。其二,tool 消息靠 tool_call_id 与调用意图配对,一次发起多个调用时全靠 ID 对号入座。

生产级的循环还要包一层“是否继续”:执行完一轮后检查新的响应是否又发起了调用,是则继续执行,直至模型不再调用或到达预设的轮数上限。这个上限必须有,否则模型陷入循环调用时,账单会替你做出反应。

5、设计工具的最佳实践

工具设计质量直接决定模型的调用准确率。同样的接口,描述写得好与坏,模型的调用成功率可以差出数倍——Anthropic 在工程实践中也把工具定义列为 Agent 系统回报率最高的投入点,详见 Building Effective Agents。核心原则如下:

原则 说明
名实相符 函数名即语义,动词开头,如 send_email
描述说清用途 写明何时该调、何时不该调
参数最少化 必填参数控制在一到三个
枚举约束取值 固定选项用 enum,别让模型自由发挥
结果为模型服务 返回精炼文本,错误要带可操作提示

其中“结果为模型服务”最常被忽视:工具返回值不是给人看的,是给模型看的。数据库查回来的原始行集,不如先在应用侧加工成“共 3 条,平均 22 度”这样的摘要;报错时返回“城市名不正确,支持的列表:北京、上海”远好于直接抛一个 500 状态码。

工具描述本质上是在给模型写 API 文档——你的文档糊弄模型,模型的参数就糊弄你。

6、踩坑与注意事项

第一,幻觉调用。模型有时会编造不存在的函数名或参数,尤其在工具清单很长、描述含混时。对策:应用侧始终校验函数名与参数合法性,Schema 里能用枚举的绝不放开。

第二,参数幻觉。日期、ID 这类模型猜不准的字段,它倾向于编一个看起来合理的值。对策:让工具返回“可选项列表”分两步调用(先 list_orders 再 get_order),而不是指望模型一次填对订单号。

第三,并行调用失控。模型可能一次发起十几个独立调用,如果你的后端扛不住,要么在请求参数里禁用并行调用,要么在应用侧做限流与去重。

第四,安全隐患。Function Calling 是把模型输出直接对接到真实动作的通道——邮件、支付、删库都可能是工具。必须坚持最小权限:工具只暴露必要能力,敏感操作加确认环节,description 里明确写入“该操作不可撤销”之类的警示,让模型的谨慎程度与操作风险对齐。

第五,消息协议的厂商差异。各家的字段名、消息角色、流式事件格式不完全一致,跨模型迁移时接口层要做适配,不要把某一家 SDK 的消息结构直接写死在业务代码里。

7、总结

回顾全文:Function Calling 是一套“模型决策、应用执行”的消息协议,模型负责把自然语言翻译成结构化调用意图,执行权始终在应用手里;完整链路是“工具清单 → 调用意图 → 本地执行 → 结果回传 → 最终回答”的两轮交互;设计上,函数名、描述与参数 Schema 就是写给模型的 API 文档,枚举约束与精炼返回值是准确率的保障;安全上,最小权限与敏感操作确认是不可省略的闸门。

这是理解一切智能体系统的第一步——Agent 的“手”就是工具调用,“脑”就是控制循环。对于刚上手的开发者,建议从一个只读工具(如查询类接口)开始跑通两轮交互,再逐步加动作类工具,并始终把“校验、限流、确认”三件事做在应用侧,而不是指望模型自觉。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
Polars 详解:新一代数据框引擎
精选Code

Polars 详解:新一代数据框引擎

过去两年,数据处理圈流行一句话:"用 pandas 写,换 Polars 跑,快了 10 倍"。Polars 用 Rust 重写了数据框引擎的底层,多线程、向量化、惰性求值一样不缺,官方演示过在笔记本上流式处理约 250…

44