Function Calling 机制与工具设计——模型其实没有调用任何函数

T3-2模块 3 · Agent 开发面试权重 更新于 2026-08-19
前置T3-1T1-2
关联题目Q3-03Q3-04Q3-05

这篇学完你能回答什么

  1. Function Calling 的完整机制是什么?模型真的「调用」函数了吗?
  2. 工具的 schema 和描述怎么设计,模型才能选得准、填得对?
  3. 并行工具调用什么时候用?工具结果太长塞爆上下文怎么办?

从一个真实故障讲起

一个电商客服 Agent,上线三个月,工具从 6 个长到 47 个——每来一个新需求就加一个工具,很自然。然后出现了两类越来越频繁的事故:

第一类:选错工具。 用户说"这单我不想要了",模型调用了 refund_order(直接退款)而不是 cancel_order(取消未发货订单)。两个工具的描述分别是"退款"和"取消订单"——在人看来区别很大,但它们的语义邻域高度重叠,而模型只能靠这两行字做判断。更糟的是还有 cancel_refundorder_cancel_apply 这类历史遗留命名。抽样统计,工具选择准确率只有 71%。

第二类:上下文被工具返回值撑爆。 get_order_detail 直接把订单的完整 JSON 返回给模型,包含 60 多个字段、物流轨迹全量节点、优惠券快照。单次返回 8000+ token。一个需要查三笔订单的会话,光工具返回就吃掉 25000 token,模型开始遗忘对话早期的用户诉求,答非所问。

修复动作没有换模型,只做了三件事:

  1. 工具收敛到 9 个,语义重叠的合并成一个带 action 枚举参数的工具;
  2. 把 list 型工具改成 search 型get_order_detail 增加 fields 参数,默认只返回 8 个核心字段;
  3. 给每个工具的描述加上"什么时候不要用我"cancel_order 的描述里明写"仅适用于未发货订单;已发货请使用 return_request"。

工具选择准确率回到 94%,平均会话 token 降了 62%。

结论:Agent 的效果,一半在模型,另一半在工具设计。而工具设计的本质是——你在给模型写一份它只能读一遍、且没法提问的说明书。

工具越加越多,模型越选越错
电商客服 Agent 上线三个月,工具从 6 个长到 47 个 —— 修复没换模型,只改了工具

  1. 工具三个月长到 47 个

    每来一个新需求就加一个,很自然

  2. 语义邻域高度重叠

    还有 cancel_refund 这类遗留命名

  3. 模型只有两行字可读断点

    「退款」和「取消订单」,人看区别很大

  4. 该取消的单被直接退款

    抽样统计,工具选择准确率只有 71%

工具选择准确率71%收敛到 9 个工具后回到 94%
get_order_detail 返回60 多个字段全量 JSONfields 参数,默认 8 个核心字段
平均会话 token查三笔订单就吃掉 25000降了 62%
描述里补的一句话「取消订单」「仅适用于未发货订单」
Agent 的效果,一半在模型,另一半在工具设计。而工具设计的本质是 —— 你在给模型写一份它只能读一遍、且没法向你提问的说明书。

核心概念:先打比方,再给定义

比喻:模型是点菜的客人,不是厨师。

你给它一份菜单(工具 schema),它说"我要一份宫保鸡丁,不要花生"(输出一个结构化的调用意向)。然后厨房是你的代码——真正去炒菜、去查数据库、去调第三方 API 的,从头到尾都是你。菜做好了端上桌(把结果塞回上下文),客人才知道这道菜长什么样。

所以那句面试高频反问的答案是:模型没有调用任何函数。它只是生成了一段符合你给定 schema 的结构化文本。所谓 Function Calling,全称应该念成"function call request generation"。

定义与术语对照

  • Function Calling / Tool Use(函数调用 / 工具使用):模型根据工具定义,输出结构化的调用请求(工具名 + 参数),由宿主程序执行并把结果回填到上下文的机制。
  • Tool schema(工具契约):通常是 JSON Schema,描述工具名、用途、参数类型与约束。它会被拼进模型的上下文,是要花 token 的
  • tool_call_id(调用标识):一次请求与其结果的配对凭证。并行调用时靠它对齐,错配就会张冠李戴。
  • Constrained decoding / structured outputs(约束解码 / 结构化输出):在解码阶段用语法约束保证输出一定符合 schema 的技术,是"参数格式错误"这类问题的工程解法(见 T1-2)。
模型没有调用任何函数
它只生成了一段符合你给定 schema 的结构化文本,动手的从头到尾是你的代码

客人 · 只负责点菜

说一句「宫保鸡丁,不要花生」

从头到尾没碰过锅,菜端上桌才知道长什么样

对应
模型 · 输出调用意向

生成工具名 + 参数的结构化请求

到这一步为止,什么都还没有真正发生

Tool schemastop_reason=tool_usetool_call_id
厨房 · 真的在炒菜

查库、调 API、跑脚本,全在这儿

菜没端上桌之前,客人对这道菜一无所知

对应
宿主程序 · 唯一的执行方

解析、校验、执行、回填全归它

结果回填进上下文,模型才第一次「看见」这道菜

参数校验tool_result约束解码
所以 Function Calling 全称该念成 function call request generation。面试里先破这个误解,可信度立刻建立;紧接着补一句「模型输出的参数是不可信输入」,安全意识也一并交代了。

原理拆解

第 ③ 步是安全边界,不是格式检查
①发请求 → ②模型给意向 → ③校验 → ④执行 → ⑤回填 → ⑥再请求,缺一不可

你发请求
① messages + tools工具定义拼进上下文,占 token
模型输出
什么都还没发生② 结构化调用意向stop_reason=tool_use
安全边界③ 解析 + 校验参数类型、越权、注入都在这拦
④ 代码真正执行查库 / 调 API / 跑脚本
⑤ tool_result 回填携带同一个 call_a1
带完整历史再请求
⑥ 最终答案或再调再调工具就回到 ②
手算一遍:上下文是怎么被吃掉的
占位项算式结果读法
工具描述常驻20 个工具 × 平均 150 token3000 token每一轮都要重付一次
第 N 轮输入≈ 前 N-1 轮的全部内容超线性增长Agent 账单的真正根因
单次工具返回get_order_detail 全量 JSON8000+ token查三笔就吃掉 25000

第 ⑤ 步的措辞决定模型能不能自愈:工具报错返回 Error: 500 毫无用处;返回「订单号格式错误,应为 ORD- 开头的 16 位字符串,你传入的是 123」,模型下一轮就能自己改对。

并行工具调用省的是墙钟时间,不省 token。一次回复输出多个 tool_call,你的代码用 asyncio 或线程池并发执行,三个 tool_result 一起回填。只适合无依赖、无副作用的只读查询;模型有时会「乐观并行」,猜一个 id 就发出去,得靠参数校验拦住。

每一轮都要重发全部历史 —— 第 N 轮的输入 ≈ 前 N-1 轮的所有内容,这就是 Agent 成本随步数超线性增长的根因,也是 prompt 缓存(T1-4)对 Agent 特别关键的原因。

完整六步,缺一不可

原文示意
① 你发请求:messages + tools 定义(工具定义被拼进上下文,占 token)
        │
        ▼
② 模型输出:stop_reason=tool_use
   { "id":"call_a1", "name":"search_order",
     "arguments":{"user_id":"U123","status":"unshipped"} }
        │   ← 到这一步为止,什么都没有真正发生
        ▼
③ 你的代码解析 + 校验参数(类型、越权、注入)
        │
        ▼
④ 你的代码真正执行:查库 / 调 API / 跑脚本
        │
        ▼
⑤ 结果作为 tool_result 消息回填,携带同一个 call_a1
        │
        ▼
⑥ 带着完整历史再请求模型 → 它可能给最终答案,也可能再调工具(回到②)

三个常被忽略但面试爱问的细节:

  • 第 ③ 步是安全边界。模型输出的参数是不可信输入,等同于用户输入。delete_file(path) 的 path 必须校验,不能因为"是模型生成的"就信任。提示词注入(T1-5)的实际落点就在这里。
  • 第 ⑤ 步的结果是纯文本(或结构化片段),模型对它的信任度完全取决于你怎么写。工具报错时返回 Error: 500 毫无用处,返回 订单号格式错误,应为 ORD- 开头的 16 位字符串,你传入的是 "123" 能让模型下一轮自我修正。
  • 每一轮都要重发全部历史。这是 Agent 成本随步数超线性增长的根因:第 N 轮的输入 ≈ 前 N-1 轮的所有内容。prompt 缓存(T1-4)之所以对 Agent 特别关键,原因就在这。

并行工具调用

模型可以在一次回复里输出多个 tool_call:

原文示意
一次回复 → [call_1: 查天气(北京), call_2: 查天气(上海), call_3: 查汇率]
             ↓ 并发执行(你的代码用 asyncio/线程池)
           三个 tool_result 一起回填 → 下一轮

适用:彼此无依赖、无副作用、可并发的只读查询。省的是墙钟时间(三次串行往返变一次),不省 token

不适用:有依赖关系(必须先查到 order_id 才能查物流)——模型有时会"乐观并行",猜一个 id 就并发发出去,这类幻觉参数在生产里很常见,要靠参数校验拦住;也不适用于写操作,并发写更难保证幂等。

工具返回值:Agent 上下文的头号杀手

工程上最容易被低估的一点:上下文很贵而磁盘很便宜,但很多工具是照着"给人看"的 API 设计的。传统程序可以流式扫描一万条记录逐条判断,模型必须把这一万条全部读进上下文——相当于为了找一个联系人,从头到尾读完整本通讯录。

Anthropic 在工具设计的工程实践中给出的默认约束值得记住:Claude Code 默认把单次工具返回截断在 25,000 token 上限。这不是模型限制,是产品侧主动设的护栏。你自己的 Agent 也应该有一个这样的数字。

工程实践(截至 2026-08)

工具设计七条硬规则

规则 反例 正例
搜索优于罗列 list_all_contacts() 返回全部 search_contacts(query, limit=10)
参数名消歧 user user_id(明确是 id 不是姓名)
命名加前缀分组 searchcreate crm_search_contactjira_create_issue
功能合并去重叠 5 个近义的取消/退款工具 1 个 order_action(action: enum)
返回值默认精简 整个订单 JSON 默认 8 字段,fields 参数按需展开
描述写清边界 "取消订单" "取消未发货订单;已发货请用 return_request"
错误信息可操作 Error 400 参数 date 需为 YYYY-MM-DD,你传入 "上周三"

参数经验值

  • 工具数量:单个 Agent 挂载 ≤ 15~20 个工具时选择准确率通常可接受;超过之后,改用分组 + 二级检索(先让模型选"域",再列出该域的工具)或渐进式披露(T3-12 的 Agent Skills 思路)。这是经验区间不是定律,务必用自己的评测集验证。
  • 单次工具返回:设一个硬上限(数千至 25,000 token 量级),超出必须分页或截断,且截断时要在返回里明写"结果被截断,请用 filter/offset 缩小范围"——引导模型改用更精确的检索,而不是让它以为数据就这么多。
  • 描述长度:工具描述每个字都占常驻上下文。20 个工具 × 平均 150 token 描述 = 3000 token 的固定开销,每一轮都要付。

迭代方法:先评测,再优化描述

工具优化不是拍脑袋,成熟做法是三段式循环:

原文示意
① 原型:先写最小可用的 3-5 个工具
        ↓
② 评测:造 20-50 条真实任务,跑起来,记录每次"选了哪个工具/参数对不对"
        ↓
③ 优化:只改指标最差的那个工具的描述与 schema,重跑对比
        ↑____________________________________________|

真正有效的杠杆常常是改描述而不是改代码——因为描述就是喂给模型的 prompt,它属于提示词工程的范畴。这也是 2026 年很多团队把工具描述纳入版本管理和回归测试的原因。

进阶技术三段式:并行工具调用

  • 解决什么:多个独立只读查询的墙钟延迟,从 N 次串行往返压缩到 1 次。
  • 代价是什么:不省 token;并发放大下游压力(限流风险);模型可能凭空猜参数发起"乐观并行";错误处理复杂化——部分成功部分失败时,回填的上下文会让模型困惑,你得明确标注哪个失败了、为什么。
  • 什么时候不该用:工具间有数据依赖;任何带副作用的写操作;下游 API 有严格 QPS 限制;以及——单次查询就够的时候,别为了并行而并行。

避坑清单

  • 别把内部 API 直接 1:1 包成工具。内部 API 是给程序调的,工具是给模型读的,受众不同。
  • 别在工具描述里写"如果用户很生气就调用这个"这类主观条件,模型判断不稳。写可验证的客观条件。
  • 工具返回里别夹带原始 HTML/日志噪音,它们会污染注意力(context rot,见 T1-12)。
  • 高危工具单独走确认流程,不要靠在描述里写"请谨慎使用"来防(T3-10)。
  • 工具多了之后,先做减法再做检索。很多团队直接上"工具检索",其实合并同类项就能解决大半。
工具是给模型读的,不是给程序调的
反例正例对着看;工具多了之后,先做减法,再谈工具检索

工具设计硬规则
规则反例正例
搜索优于罗列list_all_contacts() 返回全部search_contacts(query, limit=10)
命名消歧并分组usersearchuser_idcrm_search_contactjira_create_issue
功能合并去重叠先做减法5 个近义的取消 / 退款工具1 个 order_action(action: enum)
返回值默认精简整个订单 JSON默认 8 字段,fields 参数按需展开
描述写清边界「取消订单」「取消未发货订单;已发货请用 return_request」
错误信息可操作Error 400「参数 date 需为 YYYY-MM-DD,你传入 上周三」
经验值与迭代方法
工具数量
单个 Agent 挂载 ≤ 15~20 个时准确率通常可接受;超过就分组 + 二级检索,或渐进式披露(T3-12)
单次返回上限
Claude Code 默认截断在 25,000 token;截断时要明写「结果被截断,请用 filter/offset 缩小范围」
描述的常驻成本
20 个工具 × 平均 150 token 描述 = 3000 token 固定开销,每一轮都要付一次
迭代三段式
① 原型:3-5 个最小可用工具 → ② 评测:20-50 条真实任务,记录选错 / 填错 → ③ 优化:只改最差那个工具的描述,重跑对比
最有效的杠杆
改描述往往比改代码有效 —— 描述就是喂给模型的 prompt,所以要纳入版本管理与回归测试
别踩这三条
内部 API 别 1:1 包成工具;描述里别写「用户很生气就调用」这类主观条件;返回里别夹带 HTML / 日志噪音(T1-12)
上下文很贵而磁盘很便宜,可很多工具是照着「给人看」的 API 设计的。传统程序能流式扫一万条逐条判断,模型必须把这一万条全读进上下文 —— 相当于为了找一个联系人,从头读完整本通讯录。

面试视角

Q3-03 是入场券,拉开差距的是后两题
追问路径:机制 → 选错工具 → 50 个工具 → 返回 5 万字 → 并行失败怎么处理

  1. 先破一个常见误解「模型没调用任何函数,执行方是宿主程序」
  2. 完整讲六步点名 ③ 是安全边界、⑤ 的错误信息要可操作
  3. 把话题引向工具设计「坑不在机制,在工具设计」+ 47 砍到 9 的故事
  4. 给数字准确率 71% → 94%,会话 token 降 62%
只读过:这些回答会暴露你
  • 说「模型调用了函数并拿到了结果」—— 机制根本没讲清
  • 只会说「工具描述要写清楚」,给不出任何一条具体规则
  • 认为工具越多能力越强,加工具是唯一的迭代方式
  • 谈并行调用只说「更快」,说不出不省 token,也说不出失败处理
  • 从没提过工具定义本身要占常驻上下文
真做过:这些细节骗不了人
  • 说得出执行方是宿主程序,还顺手带上 tool_call_id 配对
  • 给得出「搜索优于罗列」「返回默认裁剪」这类规则,并配自己的数字
  • 先做减法再做检索 —— 合并同类项就能解决大半
  • 并行省墙钟不省 token,部分失败要在回填里标清是哪一个失败了
  • 算得出 20 个工具 × 150 token = 3000 的每轮固定开销
三个只有踩过坑才会顺口带出来的说法:把模型输出的参数当不可信输入校验、幂等与重试、以及「工具描述属于 prompt,要进回归测试」。Q3-04 和 Q3-05 就是靠这些筛人的。

面试官怎么问

Q3-03(FC 完整机制)是一面必问,几乎是 AI 应用开发岗的入场券。真正拉开差距的是 Q3-04(schema 与描述怎么设计)和 Q3-05(并行调用与上下文爆炸)——这两题只有真在生产里踩过坑的人答得出细节,是 2026 年有生产经验的面试官最爱用的筛子。

常见的连环追问路径:机制 → "那模型选错工具怎么办" → "工具有 50 个怎么办" → "工具返回 5 万字怎么办" → "并行调用的失败怎么处理"。

答题结构建议

  1. 先破一个常见误解:"严格说模型没有调用任何函数,它只输出了一个结构化的调用意向,执行方是宿主程序。"——这句话能立刻建立可信度。
  2. 完整讲六步,并点名第 ③ 步是安全边界、第 ⑤ 步的错误信息要可操作。
  3. 主动把话题引向工具设计:"我们踩过的坑其实不在机制,而在工具设计"——然后给你那个"从 47 个砍到 9 个"级别的具体故事。
  4. 给数字:准确率从多少到多少,token 降了多少。

分水岭信号

只读过资料的回答

  • 说"模型调用了函数并拿到了结果"——机制理解不清。
  • 只会说"工具描述要写清楚",给不出任何具体规则。
  • 认为工具越多能力越强。
  • 谈并行调用只说"更快",说不出不省 token、也说不出失败处理。
  • 从没提过工具定义本身要占常驻上下文。

真做过的回答

  • 提到 tool_call_id 配对、部分失败的回填处理。
  • 说得出搜索优于罗列返回值默认字段裁剪这类具体设计原则,并给了自己的数字。
  • 把模型输出的参数当作不可信输入来校验——这是安全意识的直接体现。
  • 提到幂等与重试(模型重试是常态)。
  • 说得出自己是怎么评测工具的:多少条任务、看哪些指标、改了描述后回归对比。
  • 意识到工具描述属于 prompt,会被纳入回归测试。

小结与延伸

  • Function Calling 的本质是"模型生成结构化调用意向,宿主程序执行",模型从不真的调用函数。
  • 六步循环里,参数校验是安全边界,错误信息是模型的自我修正燃料。
  • 工具设计是 Agent 效果的一半:搜索优于罗列、合并语义重叠、默认裁剪返回、描述写清边界。
  • 并行调用省墙钟时间不省 token,只适合无依赖的只读操作。
  • 工具优化靠评测驱动,改描述往往比改代码更有效。

延伸:把这套工具机制放进循环里,就是下一篇 T3-3 的 ReAct。工具太多导致的上下文压力,最终的系统性解法是渐进式披露(T3-12)。工具的来源标准化——从"你自己写"变成"接别人的 server"——见 T3-4 的 MCP。

继续深入

本篇归属第 3 章「Agent 开发」,去做这一章的题