欢迎光临
我们一直在努力

Function Calling 和 Tool Calling 到底有什么区别?一文讲透边界、循环与踩坑【2026最新】

在开发大模型 Agent、RAG 增强应用的时候,大家经常会听到两个词:Function Calling 和 Tool Calling。很多文章直接把它们当成同一个东西,代码里也混着叫。

但严格来说二者不是等价概念:Function Calling 是大模型原生能力,Tool Calling 是上层应用范式。

一句话总览:

Function Calling:大模型输出结构化参数,用来调用工具的能力(模型侧能力)。

Tool Calling:一套完整的应用流程,利用 Function Calling 去调用外部工具(应用侧框架)。

用户、LLM、应用、工具四个角色

1.Function Calling:模型只负责「决定调什么」

1.1 定义:模型的原生能力

Function Calling 是大模型本身具备的能力。

开发者提前给模型描述好一个工具的名字、用途描述、参数结构(JSON Schema)。当模型判断需要使用这个函数获取信息时,它不会直接生成自然语言回答,而是输出一个固定格式的结构化 JSON,包含工具名和参数。

需要说明的是,这里的「原生」是相对于「用提示词诱导」而言的。

在 2023 年 6 月,OpenAI 正式推出 function calling 之前,让模型调用工具的主流做法是在 prompt 里写清工具清单和输出格式,再要求模型输出 JSON,最后由开发者解析。模型经常不守规矩:多写解释、格式跑偏、参数编造。

OpenAI 通过定向指令微调(SFT),让模型学会这套输出格式,再通过 API 的 tools 参数暴露给开发者。此后各家模型陆续跟进,字段名略有差异,但「传入工具描述 → 模型返回结构化调用意图」成了事实标准。

所以「原生」的意思是:这个能力由模型厂商提供,而不是应用开发者用提示词临时拼出来的。

但要注意它的边界:模型的产出物只有一段结构化 JSON。工具怎么执行、结果怎么用,模型一概不管。

1.2 完整示例:从工具描述到返回 tool_calls

我们用一个查询天气的例子,完整走一遍模型侧的过程。

① 工具描述(JSON Schema)

开发者需要告诉模型三件事:工具叫什么、用来做什么、需要什么参数。

要素内容
名称 get_weather
用途 查询城市天气
参数 city,字符串,必填

把这些信息写成 JSON Schema,就是下面请求体中 tools 字段的样子。

② 第一轮请求

{
"model": "gpt-4o",
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询城市天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}
],
"messages": [
{"role": "system", "content": "你是天气助手"},
{"role": "user", "content": "武汉今天天气怎么样?"}
]
}

③ 模型返回 tool_calls

模型判断需要调用工具,于是并不输出自然语言,而是返回一条携带 tool_calls 的 assistant 消息:

{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\\"city\\":\\"武汉\\"}"}
}
]
}

这里有两个细节值得注意:

  • content 是 null:模型没有说「人话」,只给出了调用请求;
  • arguments 是一个 JSON 字符串,不是对象,使用前需要解析。

1.3 划一条线:到这里,Function Calling 就结束了

模型返回 tool_calls 这一刻,Function Calling 的任务已经全部完成。

回头看,模型做的事情只有四件:

  • 读懂我们给的工具描述;
  • 判断这个请求要不要调用工具;
  • 选定调用哪一个工具;
  • 生成符合参数要求的 JSON。
  • 它不能执行函数,不知道执行结果,也不会自己发起第二次请求。

    接下来的执行、回填、再请求——没有一件是模型做的。

    还需要补充一点:Function Calling 并没有让工具调用变得「可靠」。

    它解决的是格式问题:模型输出的是结构化调用请求,程序能稳定接住。但请求内容是不是对的——工具选得对不对、参数填得对不对——仍然是概率问题,FC 不负责。

    正因为如此,模型返回之后,应用侧必须做校验、执行和出错处理。这也正是下一节 Tool Calling 存在的理由。

    image-20261020134032187

    2.Tool Calling:应用负责「把决定变成结果」

    Function Calling 负责「决定调什么」,Tool Calling 负责「把决定变成结果」。

    模型返回 tool_calls 之后,应用侧还要接着把事情做完,整条链路才算闭环。

    2.1 模型返回之后,应用做了什么

    接着上面的例子。模型给出了调用请求,接下来轮到应用侧:

    应用侧操作(重点)

  • 把模型返回的 assistant 消息追加进 messages(保留原有的 system、user 消息不改动);
  • 执行工具 API,拿到结果:{"city":"武汉","temp":24,"weather":"多云"};
  • 新建一条 role=tool 的消息,追加到 messages 数组尾部。模型返回的 id,对应这条消息里的 tool_call_id,用于让模型匹配「这个结果属于哪一次工具调用」。
  • 追加之后,完整的 messages 变成这样:

    "messages": [
    {"role": "system", "content": "你是天气助手"},
    {"role": "user", "content": "武汉今天天气怎么样?"},
    // 追加:模型上一轮返回的 assistant 消息(携带 tool_calls)
    {"role": "assistant", "content": null,
    "tool_calls": [{"id": "call_123", "type": "function",
    "function": {"name": "get_weather", "arguments": "{\\"city\\":\\"武汉\\"}"}}]},
    // 追加:全新的 tool 结果消息,不是修改旧消息!
    {"role": "tool", "tool_call_id": "call_123",
    "content": "{\\"city\\":\\"武汉\\",\\"temp\\":24,\\"weather\\":\\"多云\\"}"}
    ]

    然后,应用把这份 messages 连同 tools 定义,再次发送给 LLM:

    {
    "model": "gpt-4o",
    "tools": [
    {
    "type": "function",
    "function": {
    "name": "get_weather",
    "description": "查询城市天气",
    "parameters": {
    "type": "object",
    "properties": {"city": {"type": "string"}},
    "required": ["city"]
    }
    }
    }
    ],
    "messages": [
    {"role": "system", "content": "你是天气助手"},
    {"role": "user", "content": "武汉今天天气怎么样?"},
    {"role": "assistant", "content": null,
    "tool_calls": [{"id": "call_123", "type": "function",
    "function": {"name": "get_weather", "arguments": "{\\"city\\":\\"武汉\\"}"}}]},
    {"role": "tool", "tool_call_id": "call_123",
    "content": "{\\"city\\":\\"武汉\\",\\"temp\\":24,\\"weather\\":\\"多云\\"}"}
    ]
    }

    对比第一轮请求,唯一的变化是 messages 多了两条:模型自己发起的调用,以及工具返回的结果。

    此时模型能读到全部历史:用户提问、自己发起的 tool_calls、工具返回的数据。它会继续推理,判断现在能不能回答,还是需要再调用工具。

    如果还需要,应用就重复上面的过程,直到模型不再发起调用。这个重复的过程,就是第 3 节要讲的循环。

    2.2 完整链路五步

    把上面的流程抽象一下,一个完整的 Tool Calling 链路包含五步:

    步骤做什么谁负责
    ① 工具注册与描述 用 JSON Schema 写清工具的名字、用途、参数 应用
    ② 工具选择 从工具列表里挑 0 个 / 1 个 / 多个 模型
    ③ 参数生成 输出结构化 JSON 参数 模型
    ④ 校验与执行 校验参数是否合法,真正调用函数 / API 应用
    ⑤ 结果回填与循环 把结果转成 tool 消息,回到第 ① 步 应用

    这张表,就是 Function Calling 和 Tool Calling 的分工全貌。

    Function Calling 只覆盖第 ②③ 步。剩下的 ①④⑤——工具怎么描述、参数能不能直接用、工具怎么执行、结果怎么回填、循环怎么继续——全部由应用侧实现。

    所以更准确的说法是:

    Function Calling 给了应用「动手的资格」,Tool Calling 才是「把整套动作做完的流程」。

    还有一个容易被忽略的细节:第 ① 步的工具描述虽然是应用写的,但它是 Function Calling 的输入。描述写得含糊,模型就会选错工具;参数 schema 写得不清,模型就会填错参数。所以这一步既属于 Tool Calling 的准备,也直接决定 Function Calling 的表现。

    2.3 为什么它叫「范式」而不是「能力」

    「能力」是模型自带的、开箱即用的东西;「范式」是开发者搭建的一整套约定和流程。

    范式、事实标准与正式规范的区别

    • Function Calling 回答的是:模型怎么表达它想调用工具?
    • Tool Calling 回答的是:从工具定义到拿到最终答案,整条链路怎么组织?

    前者是一个输出能力,后者是一个系统工程。这也是为什么:

    • 换一个支持 Function Calling 的模型,工具调用能力还在;
    • 但你的参数校验、执行逻辑、循环控制、错误处理,一样都不能少。

    还需要说明两个容易混淆的点。

    第一,Tool Calling 和 Function Calling 不是严格的包含关系。

    从范围上看,Tool Calling 覆盖了模型侧的那一环。但更准确地说,二者的关系是「使用」而不是「包含」:

    • 有原生 Function Calling 时,Tool Calling 用它作为模型侧基础;
    • 没有时,用 prompt 让模型输出 JSON 也能走完同样的流程,只是可靠性差得多。

    第二,Tool Calling 是跨厂商通用的流程范式,但不是统一规范。

    各家的链路逻辑一致:

    定义工具 → 模型选择 → 生成参数 → 执行 → 回填 → 循环

    但协议字段并不相同:

    OpenAIClaudeGemini
    工具定义 tools[].function tools[] + input_schema tools[].functionDeclarations
    模型返回 tool_calls tool_use functionCall
    结果回填 role: "tool" + tool_call_id tool_result + tool_use_id functionResponse

    所以更准确的说法是:范式通用,协议不统一。

    真正被标准化的是另外两样东西:参数描述格式 JSON Schema,以及工具接入协议 MCP。前者定义了工具长什么样,后者定义了工具如何被接入和发现。

    换句话说:模型厂商负责「开单」(输出调用请求),开发者负责「履约」(把请求执行到出结果)。

    3.Tool Loop:Tool Calling 的核心

    2.1 里我们只走了一轮。如果模型看完工具结果,发现信息还不够,又发起下一次调用呢?

    应用侧要做的,就是把这个过程重复下去。这个重复的过程,业界通常称为 Tool Loop(工具循环)。

    3.1 循环发生在应用侧,因为模型是无状态的

    一个很容易想错的地方:

    模型上一轮返回了 tool_calls,下一轮它肯定记得自己调过工具吧?

    不是的。模型完全无状态。

    每一次 API 调用,对模型来说都是一次全新的、毫无记忆的请求。它唯一能看到的东西,就是这次传进去的 messages 数组。

    换句话说:

    「记忆」不在模型里,而在应用每次重新拼好的 messages 里。

    我们能用两个例子直观感受一下。

    情况 A:正确回填

    "messages": [
    {"role": "user", "content": "武汉今天天气怎么样?"},
    {"role": "assistant", "content": null,
    "tool_calls": [{"id": "call_123", "type": "function",
    "function": {"name": "get_weather", "arguments": "{\\"city\\":\\"武汉\\"}"}}]},
    {"role": "tool", "tool_call_id": "call_123",
    "content": "{\\"city\\":\\"武汉\\",\\"temp\\":24,\\"weather\\":\\"多云\\"}"}
    ]

    模型看到这三条,才能推理出:“我已经查过武汉的天气,结果是 24 度多云,可以回答了。”

    情况 B:漏掉 assistant 消息

    "messages": [
    {"role": "user", "content": "武汉今天天气怎么样?"},
    {"role": "tool", "tool_call_id": "call_123",
    "content": "{\\"city\\":\\"武汉\\",\\"temp\\":24,\\"weather\\":\\"多云\\"}"}
    ]

    模型会懵:这个结果对应哪一次调用?我什么时候调过工具?——轻则报错,重则再调一次工具。

    所以循环的第一步不是"发请求",而是保证历史是完整的。

    3.2 循环的四个动作

    抛开细节,一个工具循环只有四个动作:

    步骤做什么
    ① 请求模型 带着完整的 messages 和 tools 发起请求
    ② 检查返回 看模型返回的消息里有没有 tool_calls
    ③ 执行并回填 有 tool_calls 就逐个执行,把结果追加进 messages,回到 ①
    ④ 输出结果 没有 tool_calls,说明模型认为信息够了,直接拿 content 作为最终回答

    用一个流程图表示:

    ┌──────────────────────────┐
    │ 请求模型(带 messages) │
    └────────────┬─────────────┘
    ▼
    ┌─────────────────┐
    │ 有 tool_calls? │
    └───────┬─────────┘
    有 │ │ 没有
    ▼ ▼
    ┌──────────────┐ ┌──────────────┐
    │ 执行工具 │ │ 输出最终回答 │
    │ 追加 tool 消息│ └──────────────┘
    └──────┬───────┘
    │
    └────────────► 回到请求模型

    3.3 终止条件:循环什么时候停

    正常情况下,终止条件是唯一的:

    模型返回的消息里,tool_calls 为空。

    但只有这一条不够。

    真实场景里,循环可能停不下来:

    • 工具一直报错,模型反复重试;
    • 模型陷入重复调用同一个工具的死循环;
    • 模型不断发起新的调用,永远不满足。

    所以应用侧必须再加一层兜底:最大循环轮数。

    超过轮数就强制终止,并返回一个明确的错误,而不是无限跑下去。

    这也是生产环境和 demo 的一个重要区别:demo 只考虑"成功路径",生产必须考虑"失败时怎么退出"。

    另外,循环每转一圈,messages 都会变长。轮数越多,上下文压力越大,这也是需要限制轮数的原因之一。工具结果过大的问题,我们会在下一节展开。

    最后提醒一句:上面说的是 Tool Loop 的终止条件——「模型不再请求工具」,而不是「任务完成」。这两者的区别,我们会在最后一节展开。

    3.4 把它写成代码

    把上面的四个动作写成代码,其实就这么短:

    def run_tool_loop(messages, tools, max_rounds=10):
    for _ in range(max_rounds):
    # ① 带着完整历史请求模型
    msg = llm.chat(model="gpt-4o", tools=tools, messages=messages)

    # ② 关键:把带 tool_calls 的 assistant 消息原样存回历史
    messages.append(msg)

    # ③ 终止条件:模型不再需要工具
    if not msg.tool_calls:
    return msg.content

    # ④ 逐个执行工具,并把结果回填
    for call in msg.tool_calls:
    result = execute(call.function.name, json.loads(call.function.arguments))
    messages.append({
    "role": "tool",
    "tool_call_id": call.id,
    "content": result,
    })

    # ⑤ 兜底:防止工具持续失败导致死循环
    raise RuntimeError("超出最大轮数")

    这段代码里有三处必须做对,否则链路会立刻断掉:

    • messages.append(msg):漏了这句,模型下一轮就看不到自己发起过调用,会重复调用;
    • tool_call_id:填错或漏填,模型匹配不到结果,整条记录就失效了;
    • for call in msg.tool_calls:模型可能一次返回多个调用,只处理一个会导致后续出错。

    说明:不同 SDK 对消息对象的序列化方式不一样(例如 OpenAI Python SDK 通常需要 model_dump() 转成 dict),上面的代码重点在流程,不是直接可运行的工程实现。

    到这里,循环就转起来了。

    但请注意它的出口:“模型觉得可以回答了。”

    如果把这个出口换成"事情做完了",循环的性质就变了。这个问题,我们留到最后一节再展开。

    4.四个实践中的坑

    机制和流程讲完了,但真正写代码时,下面四个坑几乎一定会踩到。

    4.1 tool_call_id 对不上

    这是最常见、也最容易排查的坑。要理解它,得先把 id 本身搞清楚。

    先搞清:这个 id 是谁生成的

    很多人的第一反应是:工具是我注册的,id 应该也是我定义的吧?

    不是。调用 id 由模型服务在返回响应时生成,不是开发者注册工具时定义的。

    这里要分清两样东西:

    东西例子谁定义
    工具名 name get_weather 开发者,写在 tools 里
    调用 id tool_calls[].id call_123 模型服务生成,开发者原样使用

    为什么这样设计?有三个原因。

    第一,id 标识的是「一次调用」,不是「一个工具」。

    开发者定义的是工具目录:有哪些工具、每个工具叫什么、需要什么参数。模型决定的是:这一次调用哪个工具、调用几次。

    同一件工具可能被调用多次。比如并行查询北京和武汉的天气,两次调用的工具名都是 get_weather,完全无法靠名字区分哪次调用对应哪个结果,必须给每次调用分配一个唯一的 id。

    可以类比商品编号和订单号:商品编号是商家定义的,订单号是下单系统生成的。同一件商品可以被下很多次单,每次都需要一个独立的订单号。

    第二,调用是模型发起的,id 只能由生成响应的一方分配。

    请求里只有 tools(工具定义),没有调用 id——因为在模型做出决定之前,根本不存在「这一次调用」。调用 id 是模型返回响应时才产生的信息,开发者没法提前定义它。

    第三,唯一性和匹配的需要。

    一次响应可能包含多个 tool_calls,多轮循环、并行执行又会产生更多调用实例。id 必须在当前响应内唯一,才能让每个工具结果准确归属到对应的调用。由生成响应的一方统一分配,是保证唯一性和双方理解一致的常规做法。

    id 的唯一性范围

    需要知道的是,id 只保证在「同一次响应」内不重复:

    • 模型一次返回多个调用时,它们的 id 一定不同;
    • 但不同响应之间,协议并不承诺全局唯一。

    实际实现通常用高熵随机值生成,碰撞概率低到可以忽略。所以真正需要保证的是:在同一个 messages 历史里能正确匹配,不要把它当成数据库主键那样的全局唯一标识。

    这也解释了一个常见疑问:很久没对话,又调用同一个工具,模型会复用旧 id 吗?

    不会。每次调用都是新生成的 id,历史里的旧 id 原样保留,两者互不影响,回填时用新 id 即可。

    为什么不能改写 id

    那么,可以把 id 改成一个自己更好认的值吗?

    不行。原因不是模型记住了原来的 id——模型本身没有任何记忆,它只看到你这次发过去的 messages。

    真正的原因是:你这次拼装的 messages 内部必须自洽。

    每条 tool 消息的 tool_call_id,必须能在前面某条 assistant 消息的 tool_calls[].id 里找到对应。

    如果把回填的 id 改掉:

    {
    "messages": [
    {"role": "user", "content": "武汉天气怎么样?"},
    {"role": "assistant", "content": null,
    "tool_calls": [{"id": "call_aaa", "type": "function",
    "function": {"name": "get_weather", "arguments": "{\\"city\\":\\"武汉\\"}"}}]},
    {"role": "tool", "tool_call_id": "my_own_id", "content": "{\\"temp\\":24}"}
    ]
    }

    这份输入本身就断了:assistant 声明的是 call_aaa,tool 却说自己对应 my_own_id。

    结果通常是:

    • API 服务端校验失败,直接返回错误;
    • 或者模型无法把结果关联到调用,推理出错。

    整个过程不需要模型有任何记忆——问题完全出在这一次请求的数据内部。

    所以最稳妥的做法就是:模型给什么 id,就原样保存、原样回填。

    三种常见错误

    ① 漏填

    {"role": "tool", "content": "{\\"temp\\":24}"}

    ② 写错

    {"role": "tool", "tool_call_id": "call_abc", "content": "…"}

    ③ 多个调用全用同一个 id

    {"role": "tool", "tool_call_id": "call_1", "content": "北京 24 度"},
    {"role": "tool", "tool_call_id": "call_1", "content": "武汉 26 度"}

    后果都一样:这次请求内部对不上,轻则重复调用工具,重则直接报错。

    做法

    把模型返回的 id 原样透传,不要自己生成、不要改写。

    另外还有两点:

    • 不要假设格式:OpenAI 常见 call_xxx,Claude 是 toolu_xxx,不同厂商并不相同;
    • 不要依赖全局唯一:id 只在同一次响应内有唯一性保证,实际碰撞概率极低,但不要拿它当主键。

    多个调用时,用一个 id → 结果 的映射逐条回填。

    如果发现新返回的 id 和历史里已有的 id 冲突,最安全的做法是报错,而不是自行修复:

    history_ids = {
    tc["id"]
    for m in messages
    if m.get("role") == "assistant"
    for tc in (m.get("tool_calls") or [])
    }

    for call in msg.tool_calls:
    if call.id in history_ids:
    raise RuntimeError(f"tool_call_id 与历史冲突:{call.id}")

    4.2 参数幻觉

    前文说过:Function Calling 保证了格式,但没有保证内容。

    模型返回的参数,可能长这样:

    {"city": "武汉", "unit": "c"} // unit 不在 schema 里
    {"city": 123} // 类型错误
    {"city": "武汉", "date": "今天"} // date 不在 schema 里
    {} // 缺必填

    更隐蔽的是语义错误:格式全对,但值是错的——城市名用了简称、日期时区错了、ID 是编造的、金额单位搞混。

    做法:拿到参数后,先校验,再执行。

    from jsonschema import validate

    args = json.loads(call.function.arguments)

    # 第一层:结构校验
    validate(instance=args, schema=tool.schema)

    # 第二层:业务校验(schema 查不出来的)
    if args["city"] not in known_cities:
    raise ValueError("未知城市")

    # 通过校验后,才允许执行
    execute(call.function.name, args)

    尤其当工具涉及写操作(转账、删数据、发消息)时,除了校验,通常还需要权限控制和人工确认。模型输出的参数,永远只能当作"用户意图的转述",不能当作可信输入。

    4.3 工具结果太大

    想象一个 read_file 工具,模型读了一个 2MB 的日志文件,你把结果原封不动塞进 tool 消息。

    后果很直接:

    • 上下文瞬间被撑满,后续推理质量下降;
    • token 成本飙升;
    • 超出窗口时直接报错。

    做法:在回填之前,先控制结果的大小。

    def truncate(content: str, max_chars: int = 4000) –> str:
    if len(content) <= max_chars:
    return content
    return content[:max_chars] + "\\n…[结果已截断]"

    几种常用手段:

    手段适用场景
    截断 日志、文件、长文本
    分页 列表、搜索结果、数据库查询
    只返回需要的字段 结构化数据
    摘要 长文档,但会多一次模型调用

    注意:截断后要明确告诉模型"结果不完整",否则它可能基于残缺信息给出错误结论。

    4.4 并行工具调用

    模型可能在一次返回里给出多个调用,比如同时查北京和武汉的天气:

    "tool_calls": [
    {"id": "call_1", "type": "function",
    "function": {"name": "get_weather", "arguments": "{\\"city\\":\\"北京\\"}"}},
    {"id": "call_2", "type": "function",
    "function": {"name": "get_weather", "arguments": "{\\"city\\":\\"武汉\\"}"}}
    ]

    错误做法:只处理第一个就继续请求。剩下的调用没有结果,下一轮对话会出错。

    正确做法:全部执行、逐个回填。

    这些调用之间通常没有依赖,可以并发执行来省时间:

    import asyncio

    async def execute_all(calls):
    tasks = [
    execute_async(call.function.name, json.loads(call.function.arguments))
    for call in calls
    ]
    return await asyncio.gather(*tasks)

    results = await execute_all(msg.tool_calls)

    for call, result in zip(msg.tool_calls, results):
    messages.append({
    "role": "tool",
    "tool_call_id": call.id,
    "content": result,
    })

    并发时要注意两点:

    • 控制并发数:不要一次打出去几十个请求;
    • 设置超时和失败处理:单个调用失败,不应该让整批结果都丢失。

    4.5 四个坑的共同点

    坑本质对策
    tool_call_id 对不上 数据对不上 原样透传、一一映射
    参数幻觉 数据不可信 结构校验 + 业务校验
    结果太大 规模失控 截断、分页、摘要
    并行调用 处理不完整 全部执行、并发控制

    一句话总结:

    模型负责"说",但说得对不对、全不全、大不大,应用都得兜住。

    这也再次说明:Function Calling 只是能力,真正决定系统能不能用的,是应用侧那一整套 Tool Calling 流程。

    5.一张表讲清区别

    前面几节,我们拆开了 Function Calling、Tool Calling 和 Tool Loop。现在把三者放在一起,用一张表收口。

    5.1 概念对比

    维度Function CallingTool Calling
    本质 模型能力 应用范式
    所在层级 模型侧 应用侧(包含模型)
    由谁提供 模型厂商 应用开发者
    输入 tools 定义 + messages 工具 + 模型 + 执行环境 + 循环
    产出 结构化调用请求(工具名 + 参数) 最终回答
    是否执行工具 否 是
    是否包含循环 否 是
    谁保证结果正确 —(不负责) 应用侧(校验、执行、回填)
    类比 只负责开单 负责履约

    5.2 三者的关系

    Tool Calling(完整范式)
    ├── 工具注册与描述
    ├── Function Calling
    │ ├── 工具选择
    │ └── 参数生成
    ├── 参数校验
    ├── 工具执行
    ├── 结果回填
    └── Tool Loop(循环)

    三条关系,可以分别用一句话记住:

    • Tool Calling 使用 Function Calling:FC 只是模型侧的那一环,不是 TC 的全部;
    • Tool Calling 包含 Tool Loop:循环是 TC 的运转机制;
    • Tool Loop 不等于 Agent Loop:前者的出口是「能回答了」,后者是「事做完了」。

    5.3 出问题时,该查哪一层

    这张表比概念对比更有用。实际开发中,先定位问题属于哪一层,能少走很多弯路。

    现象常见原因该查哪一层
    模型选错了工具 描述含糊 / 工具重名 / 工具太多 工具描述 + Function Calling
    参数格式不合法 模型输出问题 Function Calling
    参数语义错误 模型幻觉 / 缺少业务校验 Function Calling + 应用校验
    工具执行失败 业务代码或外部 API 问题 应用侧(Tool Calling)
    tool_call_id 对不上 回填逻辑错误 应用侧(Tool Calling)
    循环停不下来 终止条件或兜底缺失 应用侧(Tool Calling)
    上下文爆炸 工具结果太大 / 轮数过多 应用侧(Tool Calling)
    换模型后行为变化 协议字段差异 适配层(Tool Calling)

    规律很明显:

    • 模型「想错」 → 先查工具描述和 Function Calling;
    • 系统「做错」 → 查应用侧的校验、执行、回填和循环。

    5.4 一句话记住

    Function Calling 是模型「会调用」的能力,Tool Calling 是应用「能调用完」的系统。

    6.小结

    这篇博客想讲清的,其实就三句话:

  • Function Calling 是模型能力:模型只负责读懂工具、选择工具、生成参数,产出一段结构化 JSON。
  • Tool Calling 是应用范式:工具怎么定义、参数怎么校验、工具怎么执行、结果怎么回填、循环怎么继续,全部由应用侧负责。
  • Tool Loop 是循环机制:应用反复「请求模型 → 执行工具 → 回填结果」,直到模型不再请求工具。
  • 合起来就是:

    模型负责开单,应用负责履约。开单只是开始,履约才见真章。

    最后回到上一篇:Tool Loop 能自动转很多轮,但它的出口是「我能回答了」,不是「事情做完了」。

    能自动调用工具,不等于能自主完成任务。 Agent 的循环由目标驱动,直到完成判据满足才停止。

    赞(0)
    未经允许不得转载:171主机测评 » Function Calling 和 Tool Calling 到底有什么区别?一文讲透边界、循环与踩坑【2026最新】
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址