想象你走进一家餐厅,新来的服务员小张笑容满面,能跟你聊天气、聊菜品的典故,可当你说“来一碗牛肉面,不要辣”时,他只把这句话原封不动复述一遍,然后站在原地。半小时过去了,面没来。问题出在哪?小张只会“说话”,不会“下单”——他没能把你的需求写成一张标准订单送到后厨。对小语言模型(SLM)来说,函数调用就是“下单”这项关键技能。本文用“服务员学下单”这条主线,讲清楚如何让 SLM 学会调用工具:先读懂“菜单”(函数描述格式),再学会“写订单”(调用解析逻辑),经过“师徒带教”(训练数据构造)和“上岗集训”(微调策略),最终能“统筹宴席”(多工具编排),并在小小的“街边厨房”(端侧设备)里游刃有余。

岗位关联

学会函数调用,SLM 才能从“会聊天”升级为“能办事”。这是端侧 Agent 工程师的核心技能,也是小模型在手机、车载、家居等场景真正落地的关键一环。

目录
  1. 可视化演示
  2. 函数调用概述
  3. 函数描述格式
  4. 调用解析逻辑
  5. 训练数据构造
  6. 微调训练策略
  7. 多工具编排
  8. 端侧落地优化
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态展示2PC协议的Prepare和Commit两阶段执行流程,呈现协调者与参与者之间的投票与提交消息交互机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

2PC - PREPARE阶段 协调者发送PREPARE消息给所有参与者 参与者回复YES/NO投票 C→P:PREPARE 阶段1准备投票

函数调用概述

餐厅里新来的服务员小张,口才极好,客人说“来碗牛肉面,不要辣”,他能一字不差地复述,然后站在原地。客人等了半小时,面没来。问题出在哪?小张只会“说话”,不会“下单”——他没有把需求写成一张标准订单送到后厨。对 SLM 来说,函数调用就是“下单”这项技能。没有这项技能,模型再能聊,也改变不了现实世界的一分一毫。

纯文本聊天就像复述,能理解却无法行动。要让 SLM 成为真正的 Agent(智能体),它必须学会输出一种结构化指令,也就是函数调用:一个函数名加一组参数。例如用户问“北京天气怎么样”,模型输出 get_weather("北京"),系统便调用真实天气 API 拿回数据。这样一来,模型的能力就从“嘴甜”升级为“手勤”,从聊天机器人变成能办事的助手。

那么,函数调用在数学上是什么?本质上是一个条件概率问题:给定用户请求 $x$ 和当前可用的函数列表 $F$,模型选择调用第 $i$ 个函数的概率:

P(fi∣x,F)\displaystyle P(f_i \mid x, F)

这个概率越大,说明模型越擅长“在合适的时机点合适的菜”。训练函数调用能力,就是要最大化这个概率——让模型在“该点菜”时准确报出菜名和口味,在“不用点菜”时安静地陪聊。

1. 条件概率符号:$P(f_i \mid x, F)$ 表示在“用户请求为 $x$、函数列表为 $F$”这两个条件下,模型认为应该调用第 $i$ 个函数的概率。

P(fi∣x,F)\displaystyle P(f_i \mid x, F)

2. 用户请求:$x$ 是用户输入的自然语言,比如“帮我查一下北京的天气”,相当于客人说的话。

x=用户自然语言输入x = \text{用户自然语言输入}

3. 函数列表:$F$ 是所有可用工具的集合,相当于餐厅的菜单,列出了后厨能做的所有菜。

F={f1,f2,…,fn}F = \{f_1, f_2, \dots, f_n\}

4. 选择含义:$f_i$ 是第 $i$ 个函数,$P$ 值最高的那个函数就是模型准备“下单”的目标。

fi=待调用的第 i 个函数f_i = \text{待调用的第 } i \text{ 个函数}

就像新服务员上岗必须先认识后厨有哪些灶台、能做什么菜,SLM 也要先“知道”有哪些工具、各自负责什么。认识工具、理解需求、生成标准订单,这就是函数调用能力的三板斧。接下来我们逐个拆解,先从最重要的“菜单”讲起。

函数描述格式

小张上岗第一天,老板丢给他一本厚厚的菜单。如果菜单上只写“菜一”“菜二”,不写口味、不写食材,小张永远不知道“菜一”是红烧肉还是青菜豆腐。菜单必须写明:菜名、做法、用什么料。函数的“菜单”也是同理——模型只能根据你给它的描述来理解每个工具是干什么的。

在工程实践中,我们通常用 JSON Schema 来写这份“菜单”。每个函数包含三要素:函数名 name、用途描述 description、参数定义 parameters。比如 get_weather 的 description 写“查询指定城市的实时天气”,模型看到“北京天气怎么样”,自然就联想到它;而 set_alarm 的描述写“设定某个时间的提醒闹钟”,模型听到“明早 7 点叫我”就会找到它。

形式上,整个函数列表是一个结构化集合,每个函数又是一个包含名字、描述和参数的三元组:

F={f1,f2,…,fn},fi=(namei, desci, paramsi)\displaystyle F = \{f_1, f_2, \dots, f_n\},\qquad f_i = (\mathrm{name}_i,\ \mathrm{desc}_i,\ \mathrm{params}_i)

其中 $\mathrm{params}_i$ 内部还要用 JSON Schema 定义每个参数的类型、是否必填、取值范围。写 description 时要用“模型的话”:写清楚触发场景和输入含义,少用“等等”“之类”这种模糊词,否则模型会在两个相似工具之间摇摆,就像客人说“来份荤菜”,服务员不知道是点红烧肉还是点烤鸭。

1. 函数集合:$F$ 是所有可用工具的清单,集合里装着 $n$ 个函数,相当于餐厅里可供点选的 $n$ 道菜。

F={f1,f2,…,fn}\displaystyle F = \{f_1, f_2, \dots, f_n\}

2. 单个函数:$f_i$ 是第 $i$ 个工具,每个工具对应一段可执行的真实代码,就像一道菜对应一个灶台流程。

fi=第 i 个可调用工具f_i = \text{第 } i \text{ 个可调用工具}

3. 函数名:$\mathrm{name}_i$ 是函数名,模型生成的调用里必须用它来指路,相当于点菜单上的菜名编号。

namei=函数唯一标识符\mathrm{name}_i = \text{函数唯一标识符}

4. 描述与参数:$\mathrm{desc}_i$ 说明工具的使用场景,$\mathrm{params}_i$ 用 JSON Schema 约束参数类型与必填项。

paramsi=JSON Schema 参数定义\mathrm{params}_i = \text{JSON Schema 参数定义}

一份好菜单,是服务员准确点单的前提;一份好的函数描述,是模型准确调用的前提。菜单写得好,前台和后厨都省心。写完菜单,下一步就是教模型读懂客人的话、写出标准订单——这就是调用解析逻辑要解决的问题。

调用解析逻辑

小张记性不错,但习惯用自己发明的“密语”记单:“那碗面,那个啥,辣的不要,加个蛋。”后厨看了直翻白眼。于是餐厅规定:所有订单必须写成统一格式——菜名、辣度、数量,逐项填写。对模型输出也是如此:如果模型随意发挥,系统就没法稳定地执行。所以我们要训练模型输出一种“标准订单”,再用解析器去读懂它。

SLM 输出的是一段文本,比如 <tool_call>{"name":"get_weather","arguments":{"city":"北京"}}</tool_call>。解析器要做的,就像收银员核对订单:先定位 <tool_call> 标签包围的区域,再从中抽取函数名和参数对象,最后把参数传给对应的工具执行。这一步看似简单,却是工程里最容易翻车的地方——模型偶尔会多打一个空格、把一个引号写弯、甚至参数值里混进多余字符。

从概率角度看,模型生成调用文本的过程,就是在所有可能的输出序列中挑选概率最高的那个:

y^=arg⁡max⁡y∈Y Pθ(y∣x,F)\displaystyle \hat{y} = \arg\max_{y \in \mathcal{Y}}\, P_\theta(y \mid x, F)

在实际解析时,我们要做“格式规范化”:容忍多余空白,尝试修复不完整的 JSON,对参数值做类型强制转换(比如把数字字符串转成整数),校验必填参数是否齐全。如果解析失败,系统应返回“无法解析”并触发重试或询问用户,而不是直接崩溃——就像收银员看不清订单时会再问客人一遍,而不是随便报个菜名进后厨。

1. 候选输出空间:$\mathcal{Y}$ 是所有可能输出文本的集合,范围极大,包含普通回答和函数调用。

y∈Yy \in \mathcal{Y}

2. 模型概率:$P_\theta(y \mid x, F)$ 中的 $P_\theta$ 表示模型在参数 $\theta$ 下生成文本 $y$ 的概率。

Pθ(y∣x,F)P_\theta(y \mid x, F)

3. 条件输入:$x$ 是用户请求,$F$ 是函数列表,两者共同决定输出分布,就像客人和菜单共同决定订单内容。

x, F=条件输入x,\ F = \text{条件输入}

4. 最优输出:$\hat{y}$ 是概率最高的输出序列,也就是模型最终“写”下的标准订单。

y^=arg⁡max⁡y∈Y Pθ(y∣x,F)\hat{y} = \arg\max_{y \in \mathcal{Y}}\, P_\theta(y \mid x, F)

一张写错格式的订单会毁掉一桌好菜;一段格式不规范的输出也会让 Agent 空转甚至报错。因此训练时我们会刻意把模型“调教”成稳定输出标准格式的服务员。而“调教”需要的教材,就是我们下一节要讲的训练数据。

训练数据构造

要让小张学会写标准订单,光背菜谱不够,还得给他看大量“客人说了什么 → 服务员写了什么”的师徒案例。这些案例就是训练数据。案例越真实,小张面对陌生客人时越从容;案例里有各种刁钻场景,他才不会在高峰期手忙脚乱。

每组训练数据包含三部分:用户请求 $x$、可用的函数列表 $F$、标准答案 $y$。注意 $y$ 有两种形态:需要工具时,它是一个标准函数调用;不需要工具时(比如用户只是问好),它就是一句普通回复。数据来源主要有三个:清洗后的真实用户日志、让大模型(如 GPT 系列)蒸馏生成的样例、以及人工编写的高质量模板。三者混合,既保证真实度,又保证多样性。

训练时,我们用监督学习的方式最大化标准答案的概率,也就是最小化交叉熵损失:

L=−1N∑i=1Nlog⁡Pθ(yi∣xi,Fi)\displaystyle \mathcal{L} = -\frac{1}{N} \sum_{i=1}^{N} \log P_\theta(y_i \mid x_i, F_i)

这里特别要强调负样本的设计。如果训练集里每个问题都调用工具,模型就会变成“乱下单”的服务员——用户问“你好”,它也调个搜索工具。所以必须混入大量“不需要调用工具”的样本,标准答案就是自然语言回复,让模型学会判断边界:能直接答的就直接答,答不了才下单。

1. 训练集规模:$N$ 是训练样本总数,样本由“用户请求、函数列表、标准答案”三元组构成。

N=样本总数\displaystyle N = \text{样本总数}

2. 单样本对数似然:$\log P_\theta(y_i \mid x_i, F_i)$ 量化模型对第 $i$ 个样本答对的自信程度。

log⁡Pθ(yi∣xi,Fi)\log P_\theta(y_i \mid x_i, F_i)

3. 平均求和:$\frac{1}{N} \sum_{i=1}^{N}$ 把所有样本的损失求平均,让每个样本公平贡献。

1N∑i=1N(⋅)\frac{1}{N} \sum_{i=1}^{N} (\cdot)

4. 负号求最小:$-$ 号把“最大化概率”翻转为“最小化损失”,方便梯度下降优化。

L=−1N∑i=1Nlog⁡Pθ(yi∣xi,Fi)\mathcal{L} = -\frac{1}{N} \sum_{i=1}^{N} \log P_\theta(y_i \mid x_i, F_i)

师徒案例越真实、越多样,小张面对陌生客人时越从容。数据质量直接决定模型调用工具有多“靠谱”。教材备齐之后,接下来就是正式的“上岗集训”——微调。

微调训练策略

小张在厨师学校已经学会了听说读写,上岗集训不需要把学校重读一遍,只需要重点练两件事:认菜单、写订单。对 SLM 来说,这就是微调——在预训练的基础上,用小而精的数据“集训”函数调用技能。全量微调要更新所有参数,对端侧小模型来说成本太高,于是我们常用 LoRA。

LoRA 的核心思想是“冻结旧知识,只学新增量”。具体做法是:把原始权重矩阵 $W_0$ 冻住不动,在旁边加一个低秩的增量矩阵 $\Delta W$,训练时只更新这个增量。这就像老服务员已经会说话、会走路,集训时只需要换一本点单手册,不用重新培训整个人。

数学上,LoRA 把增量分解成两个小矩阵的乘积:

W′=W0+ΔW=W0+BA,A∈Rr×d, B∈Rd×r, r≪d\displaystyle W' = W_0 + \Delta W = W_0 + B A,\qquad A \in \mathbb{R}^{r \times d},\ B \in \mathbb{R}^{d \times r},\ r \ll d

参数量从 $d \times d$ 骤降到 $2 \times r \times d$,训练开销大幅降低,普通消费级显卡甚至 CPU 都能跑。推理时把 $BA$ 合并回 $W_0$,不增加任何延迟。除 LoRA 外,解码参数也很关键:函数调用追求确定性,温度 $\tau$ 应设低(0.1~0.3),避免模型同一个订单每次写出来的格式都不一样。

1. 原始权重:$W_0$ 是预训练得到的基础权重矩阵,训练中保持冻结,相当于服务员原有的语言功底。

W0=冻结的原始权重\displaystyle W_0 = \text{冻结的原始权重}

2. 低秩增量:$\Delta W = BA$ 是两个小矩阵相乘的结果,代表本次集训学到的“新技能”。

ΔW=BA\Delta W = B A

3. 维度约束:$A \in \mathbb{R}^{r \times d}$、$B \in \mathbb{R}^{d \times r}$,$r \ll d$ 说明增量矩阵非常“瘦”。

r≪dr \ll d

4. 合并推理:$W' = W_0 + BA$,推理时先合并再使用,速度与原始模型完全一致。

W′=W0+ΔWW' = W_0 + \Delta W

集训结束,小张出师:认得菜单、写得规范、不乱点。LoRA 让这场集训又快又省,小本经营的餐厅(端侧设备)也请得起。不过,真正的大场面还在后面——客人办婚宴时,要学会统筹多个档口。

多工具编排

客人要办婚宴,需求一串:先看天气定场地,再订蛋糕,最后设提醒。小张不可能一个灶台跑到底,他要统筹凉菜、热菜、甜品多个档口,还得讲究先后顺序,遇到某道菜卖完了,要立刻换个备选方案。真实世界里的 Agent 也是如此——很少有任务只调一个工具就能完成。

多工具编排的第一步是“路由”:模型根据用户意图,把请求分发给正确的工具。第二步是“多步调用”:把上一步的结果作为下一步的输入,形成一条调用链。比如“导航到最近的充电站,顺便看看那边天气”就需要先调定位工具,再调导航工具和天气工具。模型要学会安排顺序,而不是一股脑全调一遍。

从概率上,一整条调用链可以看成每一步条件概率的乘积:

P(c1,c2,…,ck∣x,F)=∏j=1kP(cj∣x,c<j,F)\displaystyle P(c_1, c_2, \dots, c_k \mid x, F) = \prod_{j=1}^{k} P(c_j \mid x, c_{<j}, F)

工程上还要重点处理异常:工具返回错误时先重试一次;重试失败就切换到功能类似的备用工具;实在不行就向用户说明并请求补充信息。同时要给整条链设定步数上限和超时时间,防止 Agent 陷入“死循环”——就像服务员不能因为甜品档口排队,就让整场婚宴的客人都干等着。

1. 调用序列:$(c_1, c_2, \dots, c_k)$ 是一个 Agent 在一轮任务中依次发起的 $k$ 次工具调用。

c1,c2,…,ck\displaystyle c_1, c_2, \dots, c_k

2. 链式法则:$\prod_{j=1}^{k}$ 表示把每一步的条件概率相乘,得到整条调用链出现的概率。

∏j=1k(⋅)\prod_{j=1}^{k} (\cdot)

3. 历史依赖:$P(c_j \mid x, c_{<j}, F)$ 说明第 $j$ 步的选择依赖于之前所有调用的结果。

c<j={c1,c2,…,cj−1}c_{<j} = \{c_1, c_2, \dots, c_{j-1}\}

4. 整体决策:整条链的概率越高,说明编排越合理,模型越有可能成功完成任务。

P(c1,c2,…,ck∣x,F)P(c_1, c_2, \dots, c_k \mid x, F)

优秀的服务员不是闷头跑腿,而是眼里有整场宴席;优秀的 Agent 也不只会调一个工具,而是懂得统筹规划。编排能力,决定了它能接多大的“活”。宴席方案定好了,最后还得看厨房装不装得下——这就是端侧落地的问题。

端侧落地优化

婚宴方案再完美,也得看厨房实力。端侧设备就像一个小厨房:灶台少、储物柜小、出菜要快。SLM 要在这里安家,必须精打细算。好在有三个优化方向:量化、延迟优化、离线部署。

量化是最立竿见影的手段。把权重从 FP16(16 位浮点)压缩成 INT8 甚至 INT4(整数),就像把大块食材提前切成小块,更省空间。延迟优化则靠缓存和预加载:重复的请求缓存结果,KV Cache 复用,模型提前预热,相当于后厨提前备好半成品,高峰期才能快速出菜。离线部署让模型和工具全部在本地运行,数据不出设备,既保护隐私又避免网络抖动。

量化带来的内存节省可以用一个简单的公式估算:

Msaved=Pparams×(1−bqb0)\displaystyle M_{\text{saved}} = P_{\text{params}} \times \left(1 - \frac{b_q}{b_0}\right)

以 1B 参数的模型为例:FP16 存储约需 2GB,量化到 INT4 只需约 500MB,内存直降 75%,手机、耳机、车载盒子都能跑。需要注意的是,量化可能带来少量精度损失,通常要在量化后用小规模函数调用数据做校准,把损失补回来——就像后厨换了新灶台,要先试炒几道菜调整火候。

1. 参数量:$P_{\text{params}}$ 是模型的总参数量,决定了内存占用的基本盘。

Pparams=模型参数量\displaystyle P_{\text{params}} = \text{模型参数量}

2. 原始位宽:$b_0$ 是量化前每个权重占用的比特数,通常为 16(FP16)。

b0=16, bq=8 或 4b_0 = 16,\ b_q = 8\ \text{或}\ 4

3. 比例缩减:$\frac{b_q}{b_0}$ 是量化后权重体积占原始体积的比例,越小越省空间。

bqb0∈(0,1)\frac{b_q}{b_0} \in (0, 1)

4. 节省总量:$M_{\text{saved}}$ 是减少的内存字节数,量化到 INT4 时约为原始内存的 75%。

Msaved=Pparams×(1−bqb0)M_{\text{saved}} = P_{\text{params}} \times \left(1 - \frac{b_q}{b_0}\right)

小厨房也能端出满汉全席——只要菜谱精简、备菜到位、流程顺畅。端侧 SLM 加上函数调用,就是那个在手机、手表、车上随时待命的“贴身服务员”:反应快、不联网、懂规矩、能办事。

AI 应用场景

端侧语音助手

阶段:模型微调与端侧推理部署

解决的问题:传统语音助手依赖云端解析意图,延迟高、断网即失灵,而且用户的日程、位置等隐私数据要上传服务器。通过函数调用微调,SLM 在本地直接解析用户指令并生成结构化调用,由系统调用本地能力(设置闹钟、读取日程、控制智能家居),全程数据不出设备,响应更快、隐私更安全。

具体案例:用户对手机说“明早七点提醒我开会”,端侧 SLM 输出 <tool_call>{"name":"set_alarm","arguments":{"time":"07:00","note":"开会"}}</tool_call>,解析器抽出参数后写入本地闹钟应用。整个过程就像服务员在店内下单,不用把客人需求喊到店外。

智能座舱车载助手

阶段:多工具编排与系统工程

解决的问题:车载环境网络不稳定,隧道、山区经常断网,云端 Agent 不可靠。车载助手需要 SLM 在本地编排多个车控工具,把“打开空调、导航到最近的充电站”这类复合指令拆成多个函数调用依次执行。

具体案例:司机说“太热了,顺便找地方充电”,SLM 先调用 set_ac(26) 调节空调,再调用 search_poi("充电站") 搜索位置,最后调用 navigate(poi) 发起导航。三个工具像三个档口依次出菜,环环相扣。

工业巡检助手

阶段:工具函数设计与离线部署

解决的问题:工厂设备间网络受限,巡检人员需要随身助手实时读取传感器数据并对照安全阈值。SLM 通过函数调用读取本地传感器接口,返回结构化结果,还能在多步巡检中把上一步的读数传给下一步的比对工具。

具体案例:巡检员问“三号机组温度正常吗”,SLM 调用 read_sensor("temp") 拿到数值,再调用 compare_threshold(65) 判断是否超限,最终回复“当前 58 度,正常”。模型和工具全部离线部署在手持终端上,断网也能工作。

Python 演示

python77 行
import jsonimport re TOOLS = [    {        "name": "get_weather",        "description": "查询指定城市的实时天气",        "parameters": {"city": {"type": "string", "required": True}},    },    {        "name": "set_alarm",        "description": "设定某个时间的提醒闹钟",        "parameters": {            "time": {"type": "string", "required": True},            "note": {"type": "string"},        },    },    {        "name": "search_web",        "description": "搜索互联网上的最新信息",        "parameters": {"query": {"type": "string", "required": True}},    },] CALL_RE = re.compile(r"<tool_call>\s*(.*?)\s*</tool_call>", re.S)  def parse_tool_call(text):    """从模型输出中提取函数名与参数,返回 dict 或 None"""    m = CALL_RE.search(text)    if not m:        return None    try:        data = json.loads(m.group(1))        return {"name": data["name"], "arguments": data.get("arguments", {})}    except (json.JSONDecodeError, KeyError):        return None  def execute_tool(call):    """根据函数名分发到对应的具体实现"""    name = call["name"]    args = call["arguments"]    if name == "get_weather":        return {"result": "{} 晴 气温 25 度".format(args["city"])}    if name == "set_alarm":        return {"result": "已设定 {} 闹钟 {}".format(args["time"], args.get("note", ""))}    if name == "search_web":        return {"result": "找到关于 {} 的最新资讯 3 条".format(args["query"])}    return {"error": "未知工具 {}".format(name)}  def mock_slm(user_msg):    """模拟微调后 SLM 的输出,真实场景中由模型推理生成"""    if "天气" in user_msg:        return '<tool_call>{"name": "get_weather", "arguments": {"city": "北京"}}</tool_call>'    if "提醒" in user_msg or "闹钟" in user_msg:        return '<tool_call>{"name": "set_alarm", "arguments": {"time": "07:00", "note": "起床"}}</tool_call>'    return '<tool_call>{"name": "search_web", "arguments": {"query": "端侧大模型"}}</tool_call>'  def agent_loop(user_msg):    """一次完整的工具调用流程:生成 -> 解析 -> 执行 -> 返回"""    raw = mock_slm(user_msg)    call = parse_tool_call(raw)    if call is None:        return "模型未生成合法调用,直接回复:我无法完成这个任务。"    result = execute_tool(call)    if "error" in result:        return "调用失败:{}".format(result["error"])    return "已调用 {},{}".format(call["name"], result["result"])  if __name__ == "__main__":    for msg in ["帮我查一下北京的天气", "明早七点提醒我起床"]:        print("用户:{}".format(msg))        print("助手:{}".format(agent_loop(msg)))
目的:演示 SLM 函数调用全流程——模型输出标准调用文本,解析器提取函数名与参数,执行器分发到具体实现,最终把结果返回给用户。
  1. CALL_RE = re.compile(r"<tool_call>\s*(.*?)\s*</tool_call>", re.S)用正则定位 <tool_call> 标签包裹的调用区域,相当于收银员先找到订单上“点菜栏”在哪。
  2. data = json.loads(m.group(1))把调用文本解析成 Python 字典,这一步必须容错,捕获 JSON 解析异常。
  3. data.get("arguments", {})参数缺省时给空字典,避免取不到参数直接崩溃,体现“订单少写一行也别掀桌”的容错思想。
  4. def execute_tool(call)工具分发器,按函数名把请求送到对应的“档口”,对应真实系统的 API 路由。
  5. raw = mock_slm(user_msg)模拟微调后 SLM 的输出,真实场景中这一行替换为模型推理代码。
  6. if call is None:解析失败时的兜底分支,直接回复用户而不是抛异常,就像看不清订单时礼貌地再问一遍。
  7. if __name__ == "__main__":程序入口,遍历两条测试指令,打印完整的“用户 → 助手”对话链路。

常见误区

误区我以为函数调用就像搜索引擎,模型在函数列表里找一条最像的现成答案,然后把对应代码拿过来执行。既然函数都定义好了,模型只是“挑一个”而已。

正解实际上,函数调用是生成式的。模型不是“挑选”现成文本,而是逐字逐词地生成一段结构化文本(函数名和参数)。就像服务员不是从柜台上拿起一张写好的订单,而是根据客人的话现场写出订单内容。一个函数可能有无数种参数组合,只有生成式模型才能处理这种开放性。

为何常见:因为人类习惯“查表”思维,总觉得函数是固定的、调用就是匹配。但神经网络天生是概率生成器,理解这一点才能想明白:为什么训练目标是最大化生成概率,而不是优化检索排序。

误区函数能跑就行,name 和 parameters 写清楚就够了,description 多写少写无所谓,反正模型是“智能”的,应该能自己理解每个函数是干嘛的。

正解描述词恰恰是函数调用准确率的“命门”。SLM 参数量小,语义理解能力有限,它判断“该调哪个函数”几乎完全依赖 description。写得含糊,模型就会在两个相似工具之间摇摆;写清触发场景(比如“当用户询问天气时”),模型才能精准点单。好的描述就像菜单上“微辣”“多葱”的标注,直接影响出菜对不对。

为何常见:因为人类看函数名 get_weather 就能猜出功能,下意识觉得模型也能。但小模型没有这种“常识联想”,它只能依靠你写在 description 里的显式信息。

误区我写好了解析代码,要求模型每次输出都严格合法的 JSON。一旦解析失败,就认为是模型太笨,反复加大训练数据量。

正解SLM 生成的文本天然带噪声,完美 JSON 是理想不是常态。工程上必须做容错解析:容忍多余空格、修复引号、类型强制转换、缺字段时给默认值。这就像后厨不会因为客人写字潦草就把单子扔回去,而是会核对、询问、尽量理解。解析器设计得够“皮实”,远比苛求模型完美更实际。

为何常见:开发者习惯了机器之间的严格协议,容易把“模型输出”也当成“机器输出”。但模型是概率采样器,输出天然有随机性,必须用“人读手写单”的心态去设计解析层。

误区微调函数调用能力,数据当然是越多越好。我准备了十万条调用样本,全部灌进去训练,模型应该会更强。

正解数据质量比数量重要得多。十万条相似样本可能不如一万条多样样本:重复的“查天气”模板不但没帮助,还会让模型对“查询”类请求过度敏感。更重要的是负样本——如果所有样本都要求调用工具,模型就学不会“什么时候不调用”。正确做法是控制正负样本比例(通常 1:1 到 3:1),并保证场景、句式、参数组合的多样性。

为何常见:大模型时代“数据越多越好”的说法深入人心,但对微调而言,目标功能范围很窄,盲目堆量只会稀释有效信息,还挤占了负样本的空间。

误区既然大模型用提示词写清函数格式就能调用工具,小模型照葫芦画瓢应该也行?我写了个超长的 system prompt,把函数定义全塞进去,结果 SLM 输出格式还是飘忽不定。

正解提示词对 SLM 的约束力远弱于大模型。SLM 参数量小,指令遵循能力有限,很难仅靠提示词稳定输出 <tool_call> 这种严格格式。微调才是把“输出规范”内化到权重里的可靠手段——就像光靠口头叮嘱“记得写标准订单”,新服务员记不住,必须反复演练形成肌肉记忆。实际工程中,微调后再加提示词做引导,效果最佳。

为何常见:因为大模型(GPT、Claude)的提示词函数调用体验太好,让人误以为所有模型都具备同等的指令遵循能力,忽略了小模型的能力瓶颈。

面试题

Q1. 函数调用的本质是什么?

单选题 · easy

  • A.从预定义答案库中检索最佳文本
  • B.用条件语言模型生成结构化调用文本← 正确答案
  • C.通过搜索引擎定位工具所在位置
  • D.对用户文本做关键词规则匹配

答案:B。函数调用是生成式任务,模型在给定用户请求和函数列表的条件下,逐词生成函数名和参数,而不是检索现成答案或做关键词匹配。服务员是“现写订单”,不是“背答案”。

Q2. 描述函数时最常用的标准格式是?

单选题 · easy

  • A.JSON Schema 结构← 正确答案
  • B.XML 标签嵌套结构
  • C.YAML 配置文件结构
  • D.CSV 表格行列结构

答案:A。函数描述(名称、说明、参数类型与约束)工程上统一用 JSON Schema 表达,解析方便且生态成熟。XML、YAML、CSV 虽然也能存数据,但不是函数描述的事实标准,就像菜单统一用文字标注而不用表格画菜。

Q3. 解析模型输出 <tool_call>{"name":"get_weather","arguments":{"city":"北京"}}</tool_call> 时,第一步应该做什么?

单选题 · easy

  • A.直接调用外部工具执行
  • B.把调用文本原样返回给用户
  • C.抽取函数名与参数并校验← 正确答案
  • D.丢弃输出重新生成一次

答案:C。正确的流程是先用正则或解析器定位调用区域,抽取函数名和参数对象并做校验,全部通过后才交给工具执行。直接执行或原样返回都跳过了关键的“收银核对”环节,遇到格式瑕疵就会出错。

Q4. 训练数据中加入“不需要调用工具”的负样本,其主要作用是?

单选题 · mid

  • A.显著减小训练集的数据规模
  • B.让模型学会区分何时不该调用← 正确答案
  • C.提高模型参数的稀疏程度
  • D.压缩端侧模型的存储体积

答案:B。负样本教会模型判断边界:能直接回答的问题就正常回复,不要动不动“下单”。没有负样本,模型会变成乱调用工具的服务员,问“你好”也去调搜索接口。它不影响参数稀疏性,也不压缩模型体积。

Q5. LoRA 微调的核心思想是?

单选题 · mid

  • A.用梯度更新全部原始参数
  • B.在每次推理前修改输入文本
  • C.冻结原权重并训练低秩增量← 正确答案
  • D.用更大规模语料重新预训练

答案:C。LoRA 冻结预训练权重 $W_0$,只训练低秩矩阵 $A$、$B$,用 $W' = W_0 + BA$ 得到微调结果。它不更新全部参数,不修改输入,也不做预训练,而是像在旧书上贴便签一样做“小修补”。

Q6. 端侧部署 SLM 时,降低模型内存占用最有效的手段是?

单选题 · mid

  • A.增大推理时的批处理大小
  • B.提高解码时的温度参数数值
  • C.增加训练阶段的数据增强次数
  • D.把权重量化到 INT8 或 INT4← 正确答案

答案:D。量化把每个权重从 16 位降到 8 位或 4 位,内存直接缩减一半以上,是端侧部署最立竿见影的手段。批处理、温度、数据增强都不改变模型权重本身,对内存占用几乎没有帮助。

Q7. 函数描述中 description 写得含糊不清时,最可能造成的后果是?

单选题 · hard

  • A.模型参数数量快速下降
  • B.模型选错工具或误填参数← 正确答案
  • C.端侧推理延迟明显缩短
  • D.模型在加载时直接崩溃报错

答案:B。SLM 判断调用哪个工具几乎完全依赖 description。写得不清楚,模型会在相似工具间摇摆,或者把“查天气”误路由到“搜索网页”。描述质量不影响参数量、推理速度和加载过程,只影响“点单准不准”。

Q8. 构造函数调用训练数据时,合理的数据来源包括哪些?

多选题 · mid

  • A.清洗后的真实用户日志← 正确答案
  • B.大模型蒸馏生成的样例← 正确答案
  • C.人工编写的高质量模板← 正确答案
  • D.随机堆砌的无关文本段落

答案:ABC。真实日志保证场景真实性,大模型蒸馏快速扩充覆盖面,人工模板控制格式和多样性,三者结合是业界主流做法。随机无关文本没有“请求-调用”对应关系,对训练毫无帮助,就像给服务员看小说学不会写订单。

Q9. 多工具编排中合理的异常处理手段包括哪些?

多选题 · mid

  • A.失败后重试相同调用← 正确答案
  • B.切换到备用等价工具← 正确答案
  • C.向用户说明并请求补充← 正确答案
  • D.直接静默忽略错误结果

答案:ABC。工具调用失败时,先重试解决偶发错误;仍失败则切换备用工具;都不行就告知用户并请求补充信息。静默忽略错误会让 Agent 假装成功,用户拿到错误结果还以为任务完成了,是工程大忌。

Q10. 以下哪些措施能提高 SLM 函数调用的稳定性?

多选题 · hard

  • A.使用统一的调用输出格式约定← 正确答案
  • B.微调时加入格式错误负样本← 正确答案
  • C.解码时采用较低的温度参数← 正确答案
  • D.删除所有函数描述仅留函数名

答案:ABC。统一输出格式(如 <tool_call> 包裹)让解析器有据可依;格式错误负样本让模型知道“写错订单会被退单”;低温度让生成更确定。删除函数描述等于让服务员对着光秃秃的菜名猜口味,稳定性必然崩盘。