Function Calling(也叫 tool calling / 工具调用)在 2026 年已经从「演示技巧」变成 Agent 产品的默认协议:模型不再用散文假装调用接口,而是在约定 schema 下吐出 JSON,由你的代码去真正请求天气、查库、改文件或跑测试。真正差异不在模型谁更会聊天,而在schema 是否可校验、执行是否可审计、副作用是否可隔离。
为什么纯文本 prompt 不够(Why)
早期做法是让模型输出「请调用 get_weather(city=北京)」,再用正则去抠。这在 demo 里能过,上线后会遇到三类硬伤:
- 解析不稳定——模型加礼貌用语、换参数顺序、漏引号,下游 JSON.parse 直接炸。
- 无法约束类型——该传枚举却传自由文本;该必填却省略;并行调用时分不清哪次对应哪次。
- 副作用与对话混写——模型「声称已经删除了文件」,其实你从未执行;或反过来,你执行了但模型不知道结果。
Function Calling 把「决策」留给模型、「执行」留给运行时:模型只选择工具与参数,HTTP、数据库、shell 永远不在模型进程里发生。这和 CI「一 job 一 workspace」同一思想——执行边界先于聪明程度。工具一旦能改磁盘,就要配 Agent 文件系统沙箱;一旦要跨会话记住用户偏好,就要把状态放到 独立 Memory 层,而不是塞进下一次 tool arguments。
Function Calling 三层分类(What)
不要把「接了 OpenAI tools」当成架构。至少拆三层:
| 层级 | 职责 | 典型载体 | 谁拥有 |
|---|---|---|---|
| L1 Schema | 工具名、JSON Schema、描述、必填字段 | OpenAI tools[].function、Gemini functionDeclarations、Claude input_schema | 产品 / 平台团队 |
| L2 Runtime | 校验参数、注入密钥、超时、重试、审计 | 自研 Agent loop、LangGraph、Claude Code | 你的后端或本机 Agent |
| L3 Transport | 如何发现与连接工具进程 | 直连 HTTP、MCP stdio/SSE、内部 RPC | 基础设施 |
非对称结论:厂商 API 只标准化了 L1 的「JSON 信封」;L2 的权限与 L3 的进程隔离,三家都不替你做完。MCP 解决的是 L3 发现与会话,不是「自动安全」。
完整循环如下。注意箭头方向:模型从不持有你的 API Key,密钥只存在 Runtime。
┌──────────────┐ JSON tool request ┌──────────────────┐
│ LLM API │ ────────────────────────► │ Your runtime │
│ (OpenAI / │ name + arguments │ (Agent loop) │
│ Gemini / │ │ │
│ Claude) │ ◄──────────────────────── │ execute tool │
└──────────────┘ tool result JSON │ • HTTP APIs │
│ • DB / search │
│ • shell / MCP │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Policy + secrets │
│ allowlist, audit │
│ Cloud Mac / VPC │
└──────────────────┘核心对比:OpenAI vs Gemini vs Claude
| 工具 | 入口 | 执行能力 | 上下文 | 适合人群 |
|---|---|---|---|---|
| OpenAI tools | Chat Completions / Responses API | 由你执行;arguments 常为 JSON 字符串 | 并行 tool_calls[];可选 strict schema | 已有 OpenAI SDK、网关统一成 OpenAI 形态 |
| Gemini functionDeclarations | Gemini API / Vertex | 由你执行;可 toolConfig.mode 强制/禁止调用 | Schema 类型名常用大写 OBJECT/STRING | GCP 栈、需要 mode 开关的产品 |
| Claude tool_use | Messages API / Claude Code | 由你执行;input 已是对象而非字符串 | content 块:tool_use / tool_result | Anthropic 栈、IDE Agent、MCP 生态 |
| MCP 工具 | 客户端连 MCP Server | Server 进程内执行;模型仍只出 JSON | 工具列表运行时发现 | 本地/远程工具目录要热插拔 |
真正分水岭不是「谁支持 Function Calling」(2026 年三家都支持),而是参数是字符串还是对象、并行调用怎么对齐、以及失败结果如何写回。网关层(OpenRouter / 自建)通常把三家归一成内部 ToolCall{name, id, args},再分发到同一 L2 Runtime。
三家 JSON 怎么写(How)
下面用同一个 get_weather 工具对照。生产环境请把 description 写清边界(「只查公开气象,不查个人位置」),避免模型把不该暴露的字段塞进参数。
OpenAI:tools + tool_calls
请求侧把工具挂在 tools。模型若决定调用,助手消息带 tool_calls;arguments 是字符串化的 JSON,你必须先 parse 再校验。
{
"model": "gpt-4.1",
"messages": [{"role": "user", "content": "What's the weather in Beijing?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
}]
}模型返回形态(简化):
{
"role": "assistant",
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Beijing\",\"unit\":\"celsius\"}"
}
}]
}执行后,你再发一条 role: tool(或 Responses API 等价字段),tool_call_id 对齐,content 放结果字符串。然后模型用自然语言或再一次 tool_call 继续。
Gemini:functionDeclarations + toolConfig
Gemini 把声明放在 tools[].functionDeclarations。类型字段习惯用大写。用 toolConfig.functionCallingConfig.mode 可设 AUTO、ANY(必须调工具)或 NONE(禁止调工具)——这是产品开关,不是安全沙箱。
{
"contents": [{"role": "user", "parts": [{"text": "What's the weather in Beijing?"}]}],
"tools": [{
"functionDeclarations": [{
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "OBJECT",
"properties": {
"city": {"type": "STRING"},
"unit": {"type": "STRING", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}]
}],
"toolConfig": {"functionCallingConfig": {"mode": "AUTO"}}
}模型侧常见 functionCall part(name + args 对象)。你执行后回 functionResponse part。不要假设字段名与 OpenAI 完全一致,SDK 版本之间也有 Chat vs Gemini 原生差异。
Claude:tools + tool_use / tool_result
Claude Messages API 的工具用 input_schema(JSON Schema)。助手回复是 content 块数组:可能先有一段 text,再跟 tool_use。input 已经是对象,少一次 parse,但仍要做 schema 校验——模型仍可能漏字段。
{
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"tools": [{
"name": "get_weather",
"description": "Current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}],
"messages": [{"role": "user", "content": "What's the weather in Beijing?"}]
}{
"role": "assistant",
"content": [{
"type": "tool_use",
"id": "toolu_01XYZ",
"name": "get_weather",
"input": {"city": "Beijing", "unit": "celsius"}
}]
}把结果写回时,用 role: user 的 tool_result,tool_use_id 必须一致:
{
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": "toolu_01XYZ",
"content": "{\"temp_c\": 28, \"condition\": \"clear\"}"
}]
}Claude Code / Cursor 等 IDE Agent 在此之上又加了 MCP:模型看到的仍是「一组带 JSON Schema 的工具」,只是进程通过 MCP 会话热插拔。协议变了,循环没变。
场景怎么选(决策矩阵)
| 如果你是… | 就选 | 原因 |
|---|---|---|
| 已有 OpenAI 兼容网关 | 统一成 OpenAI tools 方言 | 生态最大;下游 adapter 一次写成 |
| 主栈在 GCP / Vertex | Gemini functionDeclarations + mode | 少一层翻译;mode 适合「这一轮必须调检索」 |
| 主力是 Claude Code / Anthropic | Claude tools + MCP | content 块与 IDE 工具目录一致 |
| 要同时接三家模型 | 内部规范化 ToolCall + 每家 adapter | 不要让业务代码写三套 if-else |
| 工具会改文件 / 跑命令 | 远程 Mac 工作区 + 路径白名单 | JSON 再漂亮也挡不住错误 rm |
| 只要只读 SaaS API | 无服务器函数 + 密钥托管 | 无需整机;仍要做参数 allowlist |
推荐组合(Stack)
组合 A — 个人最快(半天)
- 单模型:OpenAI 或 Claude,注册 1–3 个只读工具(搜索、天气、文档检索)。
- 参数校验:用 JSON Schema 库拒绝额外字段;密钥放环境变量,永不进 prompt。
- 日志:记录
tool, args_hash, latency, ok,不要把完整密钥或 PII 打进日志。
组合 B — 小团队生产(推荐)
- 网关归一:对内
ToolCall;对外 adapter 到 OpenAI / Gemini / Claude。 - L2 Policy:按工具分超时、并发、日限额;危险工具二次确认或仅在隔离节点运行。
- 执行面:只读 API 用函数计算;写仓库 / 跑测试放到 Cloud Mac 会话,对齐 远程 Mac 自动化 的销毁纪律。
- 记忆与工具参数分离:用户偏好走 Memory,不塞进每次
arguments。
组合 C — 企业合规
- 工具目录走审批;schema 变更打 PR。
- MCP / 自定义 server 跑在无出网或受控 VPC;审计 JSONL 出工作区。
- 禁止把
bash做成万能工具而不带路径策略。
常见误区
- 以为模型会替你鉴权——Key 在 Runtime;模型只看到 schema 与历史结果。
- 把 OpenAI 的字符串 arguments 直接当对象用——先 parse,失败当模型错误重试或降级。
- description 写成「万能助手」——描述越泛,模型越爱乱调工具;写清何时不该调用。
- 忽略并行 tool_calls——两个调用共享可变状态会竞态;只读可并行,写入要串行或加锁。
- 把失败当无结果——应回传结构化 error,让模型改参数或换工具,而不是沉默。
- MCP = 安全——MCP 是传输与发现;root 设成家目录照样裸奔。
落地步骤(7 步 Action Plan)
- 列出工具清单——名称、副作用级别(只读 / 写入 / 不可逆)、超时、数据驻留。
- 写 JSON Schema——必填、enum、禁止 additionalProperties;description 写边界。
- 实现最小 loop——一次用户消息 → 可能的 tool_calls → 执行 → 回填 → 最终回答;先单工具。
- 加校验与密钥隔离——parse 失败、schema 失败、未知工具名一律拒绝并审计。
- 接第二家模型 adapter——证明内部
ToolCall可映射 Claude content 块与 Gemini parts。 - 给写入类工具加沙箱——路径白名单或远程工作区;对照文件系统指南做 deny 测试。
- 观测 7 天——统计误调用率、平均 round-trip、并行冲突;把高频误调用改成更窄的 schema。
FAQ
Function Calling 和普通 JSON mode 有什么区别?
JSON mode / structured output 约束的是最终回复形状(例如抽出订单字段)。Function Calling 约束的是中间的工具请求,并且默认会进入多轮:执行后再生成。两者可组合,但解决的问题不同。
模型会不会直接访问我的数据库?
不会。除非你的 Runtime 把「任意 SQL」做成工具且未做权限。默认循环里模型只能提出符合 schema 的参数。
三家能不能共用一份 schema?
核心 JSON Schema 可以共用,再写成各家信封(OpenAI parameters、Gemini 大写 type、Claude input_schema)。枚举与必填要回归测试,因为各家对 strict 的实现不完全一样。
Claude Code 里的工具是 Function Calling 吗?
是同一类循环:模型产出结构化调用,宿主执行(含 MCP)。差别在工具目录由 IDE/MCP 动态提供,而不是你每次在 HTTP 请求里手写 tools 数组。
工具结果要不要进长期记忆?
原始 API 响通常太大且含瞬时数据。应摘要后写入 Memory,而不是把整段 tool_result 当永久状态。详见 帮助中心 与 Memory 选型文。
总结
2026 年谈 Function Calling,请把它当成带 schema 的 RPC:OpenAI、Gemini、Claude 只是三种 JSON 方言,共同语法是「模型提议、运行时执行、结果回填」。选型看你的模型网关与工具副作用;安全看 L2 校验与执行面隔离,而不是看哪家的营销名。
上线前自问:如果模型此刻发出 write_file,实际落盘的是哪台机器、哪个目录? 若答不上来,先别接写入类工具。只读 API 可以今天就接;改代码、跑测试,请放到可销毁的 Cloud Mac 节点,并先看清 套餐定价。