Function Calling —— 工具调用
引言:让LLM从“说话”到“做事”
在前两篇文章中,我们分别拆解了AI Agent的四大核心模块和三种主流范式。无论架构如何设计、范式如何选择,有一个组件始终处于Agent能力扩展的最前沿——工具调用(Tool Calling / Function Calling) 。
如果说LLM是Agent的“大脑”,那么工具调用就是让大脑指挥“四肢”的神经系统。没有工具调用,Agent只能是一个高级的对话机器人——它能说会道,却无法真正改变世界。有了工具调用,Agent可以查询数据库、发送邮件、调用API、执行代码——从“会说话”进化到“会做事” 。
2023年6月,OpenAI在GPT-4和GPT-3.5 Turbo中首次引入了函数调用(Function Calling)功能。此后,这一能力迅速成为所有主流LLM厂商的标配。然而,工具调用远非“把函数名和参数传给模型”那么简单。从JSON Schema的结构化约束,到并行调用的延迟优化,再到工具描述的文字艺术——每一个细节都深刻影响着Agent系统的可靠性、效率和成本。
本文将系统讲解工具调用的核心技术原理与工程实践,涵盖:结构化输出的JSON Schema约束机制、并行工具调用的延迟分析、OpenAI tools与tool_choice参数的设计哲学,以及最容易被低估却最关键的一环——工具描述撰写技巧。
一、结构化输出——JSON Schema的约束力量
1.1 从“写JSON”到“返回结构化调用”
在函数调用出现之前,让LLM输出结构化数据的唯一方式是提示工程——在prompt中要求模型“以JSON格式返回”。这种方式的问题显而易见:模型可能会忘记加引号、漏掉字段、输出格式不统一。本质上,这是把格式约束的责任完全推给了模型的“自觉”。
函数调用的革命性之处在于:它不再要求模型“生成一段JSON文本”,而是让模型直接返回一个结构化的函数调用对象。
你通过JSON Schema描述函数,模型会返回一个带有函数名和JSON参数的结构化调用。你的应用解析这个调用,执行对应的函数,然后(可选)将结果发回给模型生成最终回答。
1# 定义工具(使用JSON Schema描述参数)2tools = [{3 "type": "function",4 "function": {5 "name": "get_weather",6 "description": "获取指定位置的当前天气",7 "parameters": {8 "type": "object",9 "properties": {10 "location": {11 "type": "string",12 "description": "城市名称,如'北京'"13 },14 "unit": {15 "type": "string",16 "enum": ["celsius", "fahrenheit"],17 "description": "温度单位"18 }19 },20 "required": ["location"]21 }22 }23}]1.2 JSON Schema的数学本质
从数学角度看,JSON Schema是对模型输出空间的一个约束。在没有约束的情况下,模型的输出空间是整个token序列空间 V∗(其中 V 是词表)。JSON Schema将这一空间限制为一个结构化的子集:
Sschema={x∈V∗∣validate(x,schema)=true}
模型在生成时,需要在满足格式约束的前提下最大化条件概率:
x∗=argmaxx∈SschemaP(x∣prompt)
这就是为什么函数调用比纯提示工程更可靠——模型不是在“努力”输出JSON,而是在一个被约束的输出空间中进行概率采样。
1.3 Structured Outputs:从“有效JSON”到“匹配Schema”
需要特别注意的是,普通的JSON模式(JSON mode)只保证输出是有效的JSON,并不保证输出匹配你指定的具体Schema。
OpenAI在2024年推出了Structured Outputs(结构化输出)功能。当开启Structured Outputs时,模型为函数调用生成的参数保证匹配你提供的JSON Schema。
1# 开启strict模式,确保参数严格匹配Schema2{3 "parameters": {4 "type": "object",5 "properties": {6 "attendee": {"type": "string"},7 "date": {"type": "string"},8 "time": {"type": "string"}9 },10 "required": ["attendee", "date", "time"],11 "additionalProperties": false # 禁止额外字段12 },13 "strict": true # 开启Strict模式14}Structured Outputs的核心机制在数学上可以理解为:在解码过程中动态地屏蔽那些会导致输出不符合Schema的token。这相当于在每个解码步骤 t,将可行token集合从整个词表 V 缩小为:
Vt(valid)={v∈V∣prefixt−1⊕v 能扩展为某个符合Schema的完整输出}
然后模型只在 Vt(valid) 上进行概率采样:
xt∼P(⋅∣x<t,prompt),且 xt∈Vt(valid)
1.4 实践建议:用Pydantic做双重保障
即使使用了Structured Outputs,在生产环境中仍然建议使用验证库(如Pydantic)对参数进行二次校验。原因有二:
- 模型可能出错:虽然概率极低,但Structured Outputs并非100%可靠
- 防御性编程:即使模型输出符合Schema,参数值本身可能不合法(如日期格式错误)
1from pydantic import BaseModel, ValidationError2
3class WeatherParams(BaseModel):4 location: str5 unit: str # 可以在Pydantic中进一步校验6
7# 解析并验证模型返回的参数8try:9 params = WeatherParams(**json.loads(tool_call.function.arguments))10except ValidationError as e:11 # 处理参数验证失败12 pass二、并行工具调用——延迟的艺术与权衡
2.1 核心概念:一次请求,多个调用
并行工具调用(Parallel Tool Calling) 是OpenAI等厂商提供的一项关键能力,它允许模型在单次响应中返回多个工具调用。
这意味着什么?假设一个用户问:“苹果、谷歌、微软今天的股价分别是多少?”在没有并行调用的情况下,模型可能需要三次独立的API往返——每次调用一个股票查询工具,串行等待结果。而有了并行调用,模型可以一次性返回三个工具调用,你的应用可以并发执行它们,然后一次性将结果返回给模型。
2.2 延迟对比:数学分析
设单个工具调用的平均执行延迟为 L(包括网络往返、API处理等)。对于 n 个独立的工具调用:
串行执行的总延迟为:
Tserial=n⋅L
并行执行的总延迟为:
Tparallel=maxiLi+overhead
如果所有工具延迟相近(Li≈L),则:
Tparallel≈L+overhead
延迟降低的倍数为:
speedup=TparallelTserial≈n
实际数据更具说服力:
- 如果每个工具调用耗时500ms,5个串行调用需要2.5秒,而并行调用只需要约500ms
- 有实测数据显示,串行执行(4-12个工具调用)每轮需要12-24秒,而并行执行仅需1-2秒,延迟降低约4倍
- 行业基准测试表明,并行工具调用可将延迟降低60%到70%
2.3 并行调用的现实挑战
然而,并行工具调用并非银弹。实践中存在几个重要挑战:
模型行为不一致:不同模型对并行调用的支持程度差异显著。有开发者报告,GPT-4.1在无需额外提示的情况下,约80%的场景会自动进行并行工具调用;而GPT-5即便加了额外提示,在相同场景下只有约25%的概率会并行调用。
流式输出的延迟感知:当模型只选择一个工具时,工具调用事件几乎立即到达;但当模型选择多个工具时,会出现数秒的停顿,然后所有工具调用事件同时到达。这并非真正的“流式”,而是批处理式的爆发。
特定API的限制:在Responses API中,Web Search和File Search等内置工具可能会阻止自定义函数被并行调用。
2.4 何时启用并行调用
适合并行调用的场景:
- 工具调用之间相互独立,无数据依赖
- 需要从多个数据源并行获取信息
- 用户查询涉及多个独立实体(如多个公司的股价)
不适合并行调用的场景:
- 工具调用之间存在顺序依赖(B需要A的结果作为输入)
- 需要对工具调用结果进行严格排序的场景
在实践中,可以通过设置 parallel_tool_calls 参数来控制是否启用此功能。
1response = client.chat.completions.create(2 model="gpt-4",3 messages=messages,4 tools=tools,5 parallel_tool_calls=True # 启用并行调用6)三、tools与tool_choice——设计哲学与精细控制
3.1 从functions到tools:一次重要的API演进
在OpenAI的早期实现中,函数调用通过 functions 和 function_call 参数实现。这些参数如今已被弃用,所有新代码都应改用 tools 和 tool_choice 的新风格。
这一演进背后的设计哲学是什么?
tools 是一个更通用的抽象。它不仅支持自定义函数(function),还支持内置工具(如 file_search、code_interpreter、web_search 等)。将函数调用纳入 tools 的统一框架下,为未来的扩展预留了空间。
正如OpenAI官方文档所述,工具调用的完整流程包含五个高层步骤:
- 向模型发起请求,附带它可以调用的工具列表
- 模型返回一个工具调用
- 你的应用执行该工具
- 将执行结果返回给模型
- 模型生成最终回答
3.2 tool_choice的三种模式
tool_choice 参数是控制模型工具调用行为的核心开关。它提供了三种模式:
"auto"(默认) :模型自行判断是否调用工具、调用哪个工具。这是最灵活的模式,适用于通用对话场景。
"none" :强制模型不调用任何工具,即使工具定义可用,模型也仅生成纯文本回复。适用于知识问答、创意内容生成等场景。
"required" :强制模型必须调用至少一个工具。适用于那些确定需要工具参与的流程化任务。
此外,还可以指定特定的工具名称,强制模型调用某个具体的工具:
1tool_choice={2 "type": "function",3 "function": {"name": "get_weather"}4}3.3 设计哲学:三种模式的数学含义
从决策论的角度看,这三种模式对应着不同的决策策略:
"auto":模型在行动空间 A=Atext∪Atools 上自由选择,最大化 P(a∣context)"none":将行动空间限制为 A=Atext,即只允许文本回复"required":将行动空间限制为 A=Atools,且至少选择一个工具
"required" 模式的意义在于将“是否使用工具”这个决策从模型中剥离出来,交给开发者控制。这在某些场景下至关重要——例如,当一个工作流确定需要调用某个API时,你不希望模型“自作主张”地跳过它。
💡 实践建议:除非有特殊需求,否则保持
tool_choice: "auto"是最安全的选择。"required"适用于需要确保工具被调用的场景,"none"适用于需要纯文本回复的场景。
3.4 多工具的“发现-选择”机制
当向模型提供多个工具时,模型需要完成两个任务:发现(从工具列表中找到合适的工具)和选择(决定使用哪个工具)。这个过程的数学本质是:
tool∗=argmaxt∈TP(t∣context,descriptions(T))
其中 T 是所有可用工具的集合,descriptions 是每个工具的描述文本。
这意味着:工具选择的准确率直接取决于描述文本的质量。这引出了我们下一个——也是最容易被低估的——话题。
四、工具描述撰写——最被低估的工程能力
4.1 一个真实的数据点
先看一组数据:同样的GPT-4.1模型,同样的工具代码,模糊描述下工具调用准确率约60%,精心优化的描述下准确率超过90%。
这30个百分点的差距来自哪里?完全来自 description 字段的写法。
LLM选择工具时看不到你的函数实现——它只能看到你写的description。这段文字就是LLM理解工具的唯一窗口。
工具描述写得好不好,直接决定了Agent的“智商”。再强的模型,遇到模糊的工具描述也会选错工具。写得好,工具调用准确率能从60%提升到95%以上。
4.2 常见的反面案例
1# ❌ 反面案例:三个工具的描述几乎一样2tools = [3 {4 "name": "get_order",5 "description": "获取订单信息", # 太模糊了!6 "parameters": {"order_id": {"type": "string"}}7 },8 {9 "name": "search_orders",10 "description": "搜索订单", # 和上面有什么区别?11 "parameters": {"query": {"type": "string"}}12 },13 {14 "name": "get_user_orders",15 "description": "获取用户订单", # 和get_order有什么区别?16 "parameters": {"user_id": {"type": "string"}}17 }18]当用户说“帮我查一下我的订单”时,LLM完全不知道该选哪个工具。
4.3 好工具描述的六要素
根据多位工程师的实战经验,一份高质量的工具描述应包含以下要素:
① 一句话功能说明(最重要)
LLM扫描工具列表时,首先看到的就是第一句话。公式是:动词 + 操作对象 + 核心能力边界。
1# ❌ 太模糊2"description": "处理文本"3
4# ❌ 太技术化(与调用决策无关)5"description": "基于Transformer的NLP管道"6
7# ✅ 清晰明确8"description": "总结用户提供的长文本,输出3条核心观点"② 适用场景(何时用)
写清楚“什么情况下应该调用这个工具”。
1"description": """2获取当前股价和涨跌幅。3〖适用场景〗用户询问具体股票的实时价格,如'苹果现在多少钱'、'特斯拉今天涨了吗'4"""③ 不适用场景(何时不用)——最容易被忽略
明确告诉模型什么时候不应该调用这个工具,可以大幅减少误用。
1"description": """2通过精确的订单号查询单个订单的详细信息。3〖适用场景〗用户提供了具体的订单号(如'ORDER-20240315-001')4〖不适用场景〗用户只说'我的订单'但没有提供订单号;需要查询多个订单时5"""④ 参数格式和示例(不要让LLM猜)
每个参数都应该有清晰的格式说明和示例值。
1"order_id": {2 "type": "string",3 "description": "订单号,格式为'ORDER-YYYYMMDD-XXX',例如'ORDER-20240315-001'。如果用户没有提供订单号,不要猜测,应该向用户询问。",4 "pattern": "^ORDER-\\d{8}-\\d{3}$"5}⑤ 区分相似工具
当有多个相似工具时,必须明确区分它们的适用边界。
⑥ 为LLM写作,不是为人类写作
工具描述首先服务于模型的“选择判断”,不是服务于人类炫技。避免使用过于专业或晦涩的术语。
4.4 一个完整的正面案例
1# ✅ 正面案例:清晰、精确、有边界2tools = [3 {4 "name": "get_order_by_id",5 "description": """6通过精确的订单号查询单个订单的详细信息,包括订单状态、商品列表、支付信息和物流追踪号。7〖适用场景〗用户提供了具体的订单号(如'ORDER-20240315-001')8〖不适用场景〗用户只说'我的订单'但没有提供订单号;需要查询多个订单时9""",10 "parameters": {11 "type": "object",12 "properties": {13 "order_id": {14 "type": "string",15 "description": "订单号,格式为'ORDER-YYYYMMDD-XXX',例如'ORDER-20240315-001'。如果用户没有提供订单号,不要猜测,应该向用户询问。",16 "pattern": "^ORDER-\\d{8}-\\d{3}$"17 }18 },19 "required": ["order_id"],20 "additionalProperties": false21 },22 "strict": True23 },24 {25 "name": "get_user_all_orders",26 "description": """27获取指定用户的所有订单列表,按时间倒序排列,最多返回50条。28〖适用场景〗用户想查看自己的全部订单历史,或说'我的所有订单'/'最近的订单'但没有提供订单号29〖不适用场景〗用户已经提供了具体订单号;需要搜索特定条件的订单时30""",31 "parameters": {32 "type": "object",33 "properties": {34 "user_id": {35 "type": "string",36 "description": "用户ID,从当前会话的用户上下文中获取,不要向用户询问"37 },38 "limit": {39 "type": "integer",40 "description": "返回订单数量,默认10,最大50",41 "default": 10,42 "minimum": 1,43 "maximum": 5044 }45 },46 "required": ["user_id"],47 "additionalProperties": false48 },49 "strict": True50 }51]4.5 为什么描述质量如此重要——信息论视角
从信息论的角度看,工具描述的质量决定了LLM在工具选择时的信息熵。
设工具集合为 T,描述文本为 D。LLM选择工具 t 的决策基于:
P(t∣query,D)
好的描述 Dgood 使得后验概率高度集中——LLM能明确知道该选哪个工具:
H(T∣query,Dgood)≈0
差的描述 Dbad 使得后验概率分散——LLM在多个工具之间犹豫不决:
H(T∣query,Dbad)≫0
这就是为什么精心撰写的描述能将准确率从60%提升到90%以上。
4.6 工具数量的最优实践
关于工具数量,建议不超过20个。当工具数量较多时,可以考虑:
- 将相似意图合并为一个工具,用参数区分(如用一个
query_database工具,通过query_type参数区分不同查询) - 如果必须分开,确保每个工具的描述足够清晰地区分彼此
五、工具调用的未来——MCP与Responses API
5.1 MCP:工具调用的标准化协议
模型上下文协议(Model Context Protocol, MCP) 正在成为AI Agent工具调用的标准化方案。
MCP的核心价值在于:它标准化了工具发现、工具调用和结果返回的方式,使得Agent可以动态地发现和调用来自不同提供者的工具,而无需为每个工具编写定制的集成代码。
截至2025年7月,已有超过4000个MCP服务器、覆盖40多个类别被部署。MCP正在从“可选协议”变成“事实标准”。
5.2 Responses API:新一代Agentic API
OpenAI在2025年3月推出了Responses API,这是新一代的Agentic API。与传统的Chat Completions API相比,Responses API的核心理念是 “Agentic by default” ——它原生支持多工具调用、多轮对话和不同类型的数据处理。
Responses API的一个重要特性是服务端托管的工具(如Code Interpreter、Web Search、File Search等),这些工具在OpenAI的服务端执行,无需在每次调用时都通过你的后端进行往返。
总结:从“能调用”到“会调用”
工具调用表面上看是一个简单的API功能——定义几个函数,传给模型,执行返回的调用。但在工程实践中,从“能调用”到“会调用”之间,隔着一条巨大的鸿沟。
这条鸿沟由四个层次的能力跨越:
- 结构化约束(JSON Schema + Structured Outputs):确保模型输出的参数可靠、可解析
- 性能优化(并行调用):将多个独立工具调用的延迟从 n⋅L 降低到 max(Li)
- 行为控制(
tool_choice):精确控制模型“何时调用”、“调用什么” - 描述工程(高质量的工具描述):将工具调用准确率从60%提升到95%以上
这四个层次中,描述工程是最容易被低估、却投入产出比最高的一环。再强的模型、再完善的Schema,遇到模糊的工具描述也会选错工具。正如一位工程师所说:“LLM怎么用你的工具,80%取决于Schema写得好不好” 。
在AI Agent从“玩具”走向“工具”的今天,掌握工具调用的工程化实践,是每一个Agent开发者必须跨越的门槛。
Some information may be outdated