Claude Code、Codex 这类 AI Agent 能读文件、跑终端命令、改整个项目的代码,而大模型本身只是按上下文预测下一个 Token。中间的桥梁就是 Function Calling(函数调用):一种让模型选择工具、生成结构化调用请求、再由外部程序执行的机制。
从一个天气查询说起
问一个没有工具能力的大模型「北京现在的天气怎么样」,它只能基于训练知识解释气候特点,拿不到此刻的气温——模型内部知识不等于实时外部信息。要回答实时问题,就得借助天气 API。
传统程序调 API 不难,难在用户说的是自然语言:「北京现在多少度」「我出门要不要带伞」,表达千变万化,程序需要确定的函数名和参数。早期方案靠 Prompt 约定输出格式,比如让模型输出 ACTION: get_weather("Beijing"),再由代码解析。能用,但有三个硬伤:格式不可靠,模型可能输出错误的工具名或参数;解析成本高,容错逻辑全得自己写;行为不稳定,换个模型或 Prompt 表现就漂移。
2023 年 6 月 OpenAI 在 API 里推出原生 Function Calling,工具调用从「Prompt 里的文本约定」变成模型 API 的原生能力,Anthropic、Google 随后跟进。
一次调用拆成两半
定义上就一个核心:模型负责决策(要不要调、调哪个、参数是什么),外部程序负责执行(跑真正的函数、把结果递回去)。
模型生成的是这样的请求:
{
"name": "get_weather",
"arguments": {
"city": "Beijing"
}
}
一个必须刻在脑子里的区别:模型生成工具调用请求,不等于工具已经被执行。真正发 HTTP 请求查天气的始终是你的程序。
完整流程四步。第一步定义工具,告诉模型有什么可用:
{
"name": "get_weather",
"description": "查询指定城市的实时天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如 Beijing"
}
},
"required": ["city"]
}
}
parameters 用 JSON Schema 描述结构:type 管类型、properties 管字段、required 管必填、enum 管候选值。description 不是摆设——模型靠它判断什么场景该用这个工具,写得含糊工具就会被误选或漏选。
第二步,模型结合用户输入和工具定义,生成结构化调用请求;第三步,程序解析参数、校验权限、执行函数;第四步,把执行结果作为工具响应回传,模型生成自然语言回答。结果不够时模型可以继续发起调用,多轮交互是所有 Agent 系统的地基。
三家格式对照
各家思路一致,数据结构不同:
| 对比维度 | OpenAI | Anthropic | Gemini |
|---|---|---|---|
| 工具定义 | tools |
tools |
functionDeclarations |
| 参数 Schema | parameters |
input_schema |
parameters |
| 工具调用 | function_call |
tool_use |
functionCall |
| 调用参数 | arguments |
input |
args |
| 参数表示 | JSON 字符串 | 对象 | 对象 |
| 执行结果 | function_call_output |
tool_result |
functionResponse |
OpenAI 的 arguments 是 JSON 字符串,Anthropic 的 input 直接是对象——这个差异是真实的高频坑:拿到 OpenAI 风格的 arguments 直接当字典取键会报 TypeError,得先 json.loads 反序列化。同一家厂商的不同 API 之间格式也可能不一致,OpenAI 的 Chat Completions 用 tool_calls,Responses API 用 function_call,接新接口前先看文档别凭肌肉记忆。
大量第三方服务兼容 OpenAI 格式,但「兼容」不等于高级特性全对齐,strict mode、并行调用这些能力逐个验证。
Schema 不是护身符
参数格式正确不代表调用决策正确。{"city": "Beijing"} 完全符合 Schema,但用户想查的可能是上海。JSON Schema 约束的是参数结构,不保证模型选对工具、理解意图、给出语义正确的参数。部分 API 提供严格模式(受约束解码),支持情况因接口而异。生产环境里参数校验、错误处理、权限控制一样不能少——模型说要用什么参数是一回事,程序放不放行是另一回事。
和 MCP、Harness 的分工
Function Calling 回答「模型如何表达工具调用」;MCP(Model Context Protocol)回答「AI 应用如何标准化连接外部工具」——它不只管工具,还定义了 Resources、Prompts 等能力,是应用与外部服务之间的开放协议。两者不是替代关系,一个典型编程 Agent 里它们各占一层:
用户:帮我读取 main.py
|
Agent
|
模型 Function Calling
|
read_file("main.py")
|
Agent Harness
|
MCP Client
|
文件系统 MCP Server
|
读取 main.py
Function Calling 表达调用意图,MCP 标准化与外部服务的交互,Agent Harness 把这些组件连起来、管理完整执行流程。也不是所有工具都必须走 MCP,直接调本地函数或 HTTP API 完全合法。
真正的 Agent 还需要 Harness 承担循环管理:Model Adapter 适配各家 API、Tool Registry 注册工具、Tool Executor 执行调用、Agent Loop 组织多轮推理、Context Management 维护上下文、Error Handling 处理超时重试。概念性骨架长这样:
while True:
response = call_model(messages, tools)
if not response.has_tool_calls:
break
for tool_call in response.tool_calls:
result = execute_tool(tool_call)
messages.append(result)
真实系统还要保存工具调用消息、按 call_id 关联请求与结果、限制循环次数、处理并行调用。特别提醒并行调用这一处:模型一次吐出多个 tool_call 时,结果必须按各自的 call_id 对号入座回传,塞错了轻则答非所问,重则把 A 工具的结果喂给 B 工具引发连锁错误。
落地建议
接多家模型时,在执行层之前加一层 Provider Adapter,把三家格式转成统一的内部调用结构,模型层随便换,工具执行层不动。测试策略上用 mock executor 做单元测试:固定模型返回的 tool_call 样本,断言解析后的参数结构、必填校验和权限拦截行为——把「模型说对」和「程序做对」分开验证,出问题时才能定位到是哪一层的锅。最后记住那条铁律:执行侧永远要有校验和权限,模型的输出只是建议,不是指令。

评论0