返回博客列表
技术文章LLMAgentTool CallFunction CallAI

function call 与 tool call,差的不只是一个名字

2026年05月🇨🇳 中文

这是 AI 开发入门系列 的第二篇,讲 function call 与 tool call 的本质差异。建议先读第一篇 四种主流的 Agent 架构 了解架构全景,再回来看这一篇理解工具调用的设计演进。第三篇 用 Python 从零写一个 LLM Agent 是 ReAct 实战。

如果你刚接触 LLM 的"让模型调工具"这个能力,很容易被两个词搞混:function call 和 tool call。有的文章写 function calling,有的写 tool calling,有的两个换着用。到底是不是同一个东西?

简单说:function calling 是 OpenAI 在 2023 年推出的原始命名,tool calling 是 2024 年改名后的版本。改名背后不是 marketing,而是一个工程语义的修正。 理解这个修正,对写 Agent 很重要。

function calling:一个容易让人误解的名字

2023 年 6 月,OpenAI 在 GPT-4 和 GPT-3.5 上推出了一个新能力:你可以给模型传入一组"函数"的 JSON Schema 定义(函数名、描述、参数类型),模型在回复时可以选择"调用"其中某一个函数,返回一个结构化的 JSON,包含函数名和参数值。

OpenAI 把整个机制叫 function calling。

这个名字在当时的语境下是合理的——你给模型定义了函数,它帮你决定什么时候该调哪个函数,帮你填参数。API 的设计也确实像一个"函数调用":你传 functions 参数,模型返回 function_call 字段。

import openai

# 老版 function calling:用 functions 参数描述工具
response = openai.ChatCompletion.create(
    model="gpt-4-0613",
    messages=[{"role": "user", "content": "北京明天天气怎么样"}],
    functions=[
        {
            "name": "get_weather",
            "description": "查询指定城市的天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名"}
                },
                "required": ["city"]
            }
        }
    ]
)

# 模型返回的 JSON 里是 function_call,不是真正的执行结果
fc = response["choices"][0]["message"]["function_call"]
# fc = {"name": "get_weather", "arguments": '{"city": "北京"}'}

但问题很快就来了。大量开发者误以为模型真的在执行函数——"我定义了一个 send_email,模型调了它,邮件发出去了?"事实是模型从来没有执行过任何函数。它只是输出了一段 JSON,告诉你"我想让你调 send_email,参数是这些"。真正执行 send_email 的是你的代码。

这个误解带来的后果不止概念混淆。有开发者直接把模型输出当作"已执行",跳过了自己代码里的校验和错误处理;也有开发者在 function calling 的 JSON Schema 里塞了执行逻辑的描述,期望模型能理解函数内部做了什么——而模型根本看不到函数的实现。

tool calling:名字修正背后的设计变化

2023 年底(11 月),OpenAI 在发布 gpt-3.5-turbo-1106 / gpt-4-1106-preview 时,把 functions 参数改名为 toolsfunction_call 改名为 tool_calls。这名字一换,很多误解自然消失——模型不是在"调用函数",而是在"选择工具"。

但改名不只是措辞调整。真正重要的变化是并行工具调用的出现。

老版 function calling 的 API 里,模型一次只能返回一个 function_call。如果你需要同时查北京和上海的天气,你的代码必须这样做:

第 1 轮: LLM 返回 function_call(get_weather, {"city": "北京"})
  → 你执行,拿到结果,喂回 messages
第 2 轮: LLM 返回 function_call(get_weather, {"city": "上海"})
  → 你执行,拿到结果,喂回 messages
第 3 轮: LLM 拿到两个结果 → 回答

新版 tool calling 里,模型可以在同一轮返回多个 tool_calls,彼此之间没有数据依赖:

第 1 轮: LLM 返回 [
  tool_call(get_weather, {"city": "北京"}),
  tool_call(get_weather, {"city": "上海"})
]
  → 你并行执行两个工具,结果一起喂回 messages
第 2 轮: LLM 拿到两个结果 → 回答

3 轮变 2 轮,而且两次工具调用是并行的。这是 tool calling 比 function calling 真正的能力跃迁,不只是改了个名字。

把这两者放在一起看,从软件开发的角度,差异主要体现在这几个维度:

对比维度function callingtool calling
调用模式串行:一次只能请求一个工具,有依赖的任务必须多轮对话支持并行:同一轮可请求多个工具,无依赖的调用可同时发出
代码复杂度简单:解析单个 function_call,一次处理一个结果稍高:需要遍历 tool_calls 数组,处理并行结果的聚合和部分失败
适用场景单步操作——查天气、发邮件、调用一个 API多步协作——同时拉多个数据源、批量文件操作、跨工具对比
扩展性增加新工具只需加一个 schema 定义,但工具多了之后 LLM 容易选错(没有并行策略减轻选择压力)同样容易扩展,且并行能力让多工具组合的效率更高。但工具数量太大时(50+),仍需要配合 Skill 分层或 RAG 检索来缩小选择范围
错误处理单一故障点:一个工具调用失败,本轮结果直接喂回 LLM 让它自己决定怎么处理部分失败可隔离:两个并行调用中一个成功一个失败,可以把部分结果 + 错误信息一起喂回,LLM 能基于已有信息继续推进
延迟影响多工具串行时延迟累加(3 个工具 = 3 轮 LLM 调用 + 3 次工具执行)无依赖的工具可并行执行(3 个独立工具 = 1 轮 LLM 调用 + 并行执行),延迟接近单个最慢工具的时间

Anthropic 的 tool use:同一个概念,不同的实现哲学

Anthropic 从第一天就用 tool use 这个词,没走过 function calling 的弯路。它的 API 设计也是"模型选择工具、输出结构化参数、开发者执行、结果喂回"这个模式。

但在具体实现上,OpenAI 和 Anthropic 有一个关键差异:对并行工具调用的处理方式不同。

OpenAI 的 tool calling(新)是模型自己决定要不要并行——它看到两个独立的工具调用没有数据依赖,就会在一次回复里返回两个 tool_calls

Anthropic 的 tool use 则默认一次返回一个 tool_use。如果你想让 Claude 同时处理多个独立的工具调用,需要在 prompt 里明确指示"如果可以使用多个工具且彼此之间不需要等待结果,请一并列出"。它的默认行为更保守——宁可多一轮交互,也不要错误并行。

这个差异没有谁对谁错。OpenAI 的默认行为更快,但如果模型误判了依赖关系(以为两个调用独立,实际有依赖),就会产生错误。Anthropic 的默认行为更慢但更安全,代价是你要多写 prompt 来获得并行能力。

在你写 Agent 时,这一层差异影响什么

回到第三篇手写的 ReAct Agent。那个 30 行循环里,每轮 LLM 只返回一个动作——要么 tool_call,要么 answer。这实际上对应的是老版 function calling 的行为模式:一次一个工具,串行执行。

如果你把这个循环直接接到新版 tool calling 的模型上,你需要额外处理一种情况:LLM 可能在一次回复里返回多个 tool_calls。如果你的循环只解析第一个,会丢失后面的工具调用;如果你依次执行但忘了并行,会浪费能力。

一个更稳的做法是,在循环里把两个模式都兼容:

def run_agent(user_query, llm_call, tools):
    messages = [{"role": "user", "content": user_query}]
    max_turns = 10

    for turn in range(max_turns):
        response = llm_call(messages)
        actions = parse_actions(response)  # 返回列表,支持多个 tool_call

        if len(actions) == 0:
            return response  # 模型直接回答

        results = []
        for action in actions:
            if action["type"] == "tool_call":
                tool = tools.get(action["tool_name"])
                if tool:
                    results.append(tool(action["tool_args"]))

        # 把所有工具结果一次性喂回
        messages.append({"role": "user", "content": f"Tool results: {results}"})

    return "Agent stopped: max turns reached."

和原来的区别只有两处:parse_actions 返回列表而不是单个对象,所有工具结果一次性喂回而不是一个一个喂。这个改动很小,但它让你的 ReAct 循环自然地兼容了 tool calling 的并行能力。

理解 tool calling 和 function calling 的区别,不只是在做"术语对齐"。它让你知道:当 LLM 说"我要用这个工具"的时候,它不是在执行,是在请求。真正执行的是你的代码。工具的职责是"被调",LLM 的职责是"决定该调哪个",你的代码的职责是"安全地执行并把结果交给 LLM 继续思考"。

这三层分工,是 Agent 能稳定工作的基础。

Agent 里的 tools 和 Skill 里的 tools:初学者最容易混的两个概念

如果你刚开始接触 AI 开发,很可能同时听到"在 Agent 里定义 tools"和"在 Skill 里定义 tools"这两种说法。它们名字一样,但不在同一个维度上。

Agent 里的 tools—— 是 Agent 运行时给 LLM 用的。你在 Agent 循环里定义一组工具描述(如 get_weathersearch_webcalculator),LLM 在每一轮循环中自己判断该调哪一个。这些工具的职责是"被 LLM 选中并执行"。

打个比方:Agent 就像一个厨师,tools 是他面前的锅、刀、砧板。厨师在做饭过程中随时拿起来用,用完放回去。

Skill 里的 tools—— 是一组相关工具的封装包。一个 Skill 不只是装了几个工具,它还包含了这些工具的使用方式、默认的 prompt 模板、以及工具之间的协作逻辑。Agent 不会在每一轮循环里都去选 Skill——Skill 是对话开始时根据任务按需加载的

延续厨师比喻:Skill 不是单件工具,而是一套"专长模块"。比如"烘焙模块"(装好了打蛋器、烤箱手套、量杯)和"日料模块"(装好了刺身刀、寿司帘、喷枪)。你不需要在厨师面前摊开 100 件工具让他自己挑——你根据客人点了什么菜,先把对应的模块搬出来。

把这个关系落到代码层面,大概是这样的层级:

Skill(技能层)
  ├── 封装了一组专属工具
  │   ├── parse_pdf(解析 PDF)
  │   ├── extract_table(提取表格)
  │   └── search_document(搜索文档)
  ├── 包含这些工具的实现函数
  └── 包含 Skill 级别的 prompt 模板

Agent(运行时层)
  ├── 根据用户任务判断"需要加载哪些 Skill"
  ├── 加载 Skill,将其中的工具注册到 LLM 可选工具列表
  └── ReAct 循环中 LLM 在工具列表里自由选择

怎么理解这个分层的好处?假设你的 Agent 有 100 个工具。如果全部摊给 LLM,每轮循环的 system prompt 会膨胀到几万个 token——不仅贵,而且 LLM 可能在 100 个工具里选错。有了 Skill 这层封装,Agent 可以先把问题缩小到"这个任务需要哪几个 Skill",再在 Skill 内部的工具池里做精确选择。这和第四篇 RAG讲的"工具知识检索"是同一个思路——用检索缩小选择范围,而不是把所有东西都塞进 prompt。

所以简单总结:Agent tools = 给 LLM 在循环中选用的可调用能力。Skill = 一组相关 Agent tools 的封装包,按需加载。Skill 包含 tools,但 tools 不一定是 Skill——就像厨师刀可以在"烘焙模块"里,也可以直接放在厨师面前。