先说个我最近常被问到的问题:模型越做越大,推理成本越来越高,但业务的响应时延又卡在那里没法降。很多人第一反应是上量化、上剪枝、上各种推理加速框架,但这些手段大多治标不治本。真正能让一个7B、3B甚至1B的模型,在某些关键能力上逼近几十B大模型的,是知识蒸馏。
这词听起来学术味很重,实际干起来也很费功夫,但它解决的恰恰是工程上最朴素的需求——用更小的模型、更低的成本,把大模型在特定任务上的“手感”继承下来。尤其是当你要部署一个Agent(智能体)时,小模型的价值会被进一步放大:Agent需要多轮工具调用、需要快速响应、需要高并发支撑,这些场景下大模型的体积和速度往往是致命的,而蒸馏恰好能帮你把大模型的一部分能力抽出来,塞进一个小模型的身躯里。
这篇文章我不打算讲学院派的理论推演,只讲我在实际项目里怎么拆解蒸馏、怎么构造数据、怎么调训练细节、以及最后踩过哪些坑。如果你正准备把一个大模型能力迁移到一个小模型Agent上,这篇文章应该能帮你少走不少弯路。
1. 先搞清楚:知识蒸馏到底在“蒸”什么
1.1 从logit到soft label:蒸馏的基本逻辑
传统监督学习里,训练一个小模型用的是硬标签(hard label),比如一张图片是猫还是狗,答案就是“猫”或“狗”这种离散的、非0即1的标签。但人类认识事物从来不是这么武断的——一张图既像猫又有点像狐狸的时候,你希望模型知道“它更像猫,但也有一点点像狐狸”这种细腻的模糊信息。
知识蒸馏的核心逻辑,就是把这层“模糊信息”显式地传递给学生模型。我们通常把大模型叫作教师模型(Teacher),小模型叫作学生模型(Student)。教师模型在训练时不仅输出预测结果,还输出一个概率分布,这个分布里包含了它对每个候选类别的置信度。比如输入一句“帮我订明早8点去机场的出租车”,教师模型可能给出“调用打车API”的概率是0.82、“查询日历”的概率是0.11、“直接回答”的概率是0.07。这个概率分布远比硬标签“调用打车API”这一个结果要丰富得多,它隐含了教师模型对任务边界的理解。
实现时需要考虑的一个重要参数是温度系数(Temperature)。蒸馏时我们用温度T对logits做软化处理:
[ q_i = \frac{\exp(z_i / T)}{\sum_j \exp(z_j / T)} ]
T越大,概率分布越平滑,类别之间的差异被压缩,学生模型能学到教师模型在“接近但不确定”的样本上所表现出的犹豫感。T越接近0,分布就越趋近于硬标签。常规做法是训练时对教师logits和学生logits用同一个较大的T(比如4~8)计算KL散度,推理时再把T调回1。这个细节看起来轻描淡写,但实际很多第一次做蒸馏的人都会卡在这里:要么温度调太小导致蒸馏和普通训练没区别,要么温度调太大导致小模型学到一堆噪声。
1.2 蒸馏的三种模式:离线、在线与自蒸馏
蒸馏发展到现在,已经从最初的“拿一个训好的教师去教学生”不断演化了,不同场景下手段差异很大。
离线蒸馏(Offline KD):最经典的模式。先把教师模型完整训练好,冻结住,然后让它对大量样本生成soft label,再拿这些soft label去训练学生模型。好处是逻辑清晰、训练稳定;坏处是教师模型的推理开销在数据标注阶段是一次性的,但如果你有海量数据,这个阶段会非常漫长。
在线蒸馏(Online KD):教师模型和学生模型同时训练。教师模型可能是另一个初出茅庐的模型,或者和学生共享底层编码器。典型做法如Co-Distillation,两个模型互为师生、互相学习。这种模式适合你的场景里没有现成的强教师,或者数据规模太大、离线生成成本过高的情况。
自蒸馏(Self-Distillation):模型自己教自己。通常的做法是取模型深层的输出作为教师信号,浅层的输出作为学生输出,让浅层去拟合深层。这类方法在视觉模型里用得比较多,文本模型里偶尔也会用,主要解决的是深层特征利用率不足的问题。
对于Agent场景,我自己的经验是:如果你已经有一个跑得动、效果也不错的超大模型(比如几十B以上),优先选离线蒸馏;如果你的教师模型本身还在频繁迭代,那就选在线蒸馏,省得每次教师更新都重新生成一遍数据。
1.3 为什么Agent场景更需要蒸馏
很多人有个误解,觉得蒸馏只是“用小模型模仿大模型”——听起来不是刚需,毕竟大模型API也不是用不起。但放到Agent场景里,你的判断会完全改变。
Agent天然是延迟敏感型业务。用户说一句话,Agent要做意图识别、槽位抽取、决策规划、工具调用、结果生成,多个环节串行或并行地跑。如果每个环节背后都是一个大模型,端到端延迟几百毫秒是家常便饭,更关键的是成本——一次完整任务要调很多次大模型接口,按token计费的账单很快就会让你肉疼。
更重要的一个因素是高并发下的吞吐瓶颈。大模型的推理吞吐受限于显存带宽和KV Cache,一台8卡机器可能只能同时服务几十个并发Agent会话。但如果你把核心环节压缩成一个小模型Agent,同样的硬件可以轻松支撑几千路并发。在这种情况下,蒸馏不是“锦上添花”,而是“规模化落地的前提条件”。
2. Agent小模型的特殊性:蒸馏的“知识面”被撑大了
2.1 Agent和普通小模型的差异
传统意义上的蒸馏,目标是让一个小模型学会大模型的“输入到输出”映射。但Agent比这复杂得多。一个Agent小模型要承担的角色至少包括:
- 理解用户意图,判断当前该不该调用工具。
- 在多个工具之间做选择,并填充正确的参数。
- 根据工具返回的结果做下一步决策。
- 多轮对话中维护状态,不丢失上下文。
换句话说,普通小模型学的是“问答”,Agent小模型学的是“决策+操作”。后者的输出空间是开放式的——不是说一个答案对错就可以,而是要一连串动作都对,中间任何一步出错都会导致任务失败。
这也意味着,蒸馏Agent能力时,你不能只拿大模型的最终答案来当soft label,还要让它把中间推理过程和工具调用轨迹一起“教”给学生模型。
2.2 Agent知识蒸馏的三层知识:任务知识、工具知识、规划知识
我把Agent场景下的蒸馏知识拆成三层,分别对应不同的蒸馏目标和数据形式:
第一层:任务知识。这是最常见的一层,也就是训练数据里的“用户指令-标准答案”对。在Agent场景里,任务知识对应的是意图分类结果、从用户语言中抽取的槽位、以及对工具参数值的准确填充。比如用户说“帮我查一下北京到上海的航班”,模型需要正确识别出出发地=北京、目的地=上海、查询对象=航班。这部分知识通常用分类任务或抽取式任务来蒸馏,loss直接用教师模型在该任务上的logits蒸馏到学生模型即可。
第二层:工具知识。工具知识指的是模型对“有哪些工具可用,每个工具应该填什么参数,什么情况下该用什么工具”的理解。在函数调用(Function Calling)的语境下,这本质上是一个受限的生成任务:模型需要输出一个格式化严格的工具调用文本,比如search_flight(departure="北京", arrival="上海")。蒸馏时可以做序列级蒸馏(Sequence-Level KD),让学生模型在给定对话历史和工具定义的情况下,生成与教师模型一致的工具调用序列。
第三层:规划知识。这是最值钱但也最难蒸的一层。一个Agent在面对复杂指令时,可能需要先后调用几个工具才能完成任务。比如“帮我对比北京的天气和上海的天气,再帮我把更暖和的那个城市的机票订一张”,正确的做法是先调用两次天气查询,再调用一次机票预订。这种多步规划能力通常来自大模型在广泛语料中习得的推理链。要蒸馏这一层,常规做法是用大模型生成完整的思考链(Chain of Thought)或ReAct轨迹(Thought-Action-Observation),再让学生模型去模仿这条轨迹。
有一点要特别提醒:轨迹蒸馏很容易“学形式、不学逻辑”。学生模型如果只是机械记住了输出格式,但在工具结果变化后不能做出适应性决策,那就是失败的蒸馏。解决这个问题没有银弹,主要靠数据多样性——你必须让教师模型生成足够多“中间步骤不同但结果等价或相近”的轨迹,让学生模型真正学会规划而不是背答案。
2.3 蒸馏数据从哪来:合成、筛选与清洗
蒸馏Agent能力的第一大难题,不是模型训练,而是数据。
网上能拿到的开源Agent轨迹数据非常有限,而且大多是英文场景、工具集也很固定。如果你要在自己的业务里蒸馏,最靠谱的数据来源是“让大模型自己造数据”。具体做法大致是:
- 把你线上真实的用户请求(脱敏后)汇总起来,作为种子数据。
- 配上你定义的函数schema(工具的定义说明)。
- 让教师模型针对这些请求生成完整的多轮Agent轨迹,包括推理过程、工具调用、工具返回值分析、最终回复。
- 把这些轨迹统一整理成微调格式。
合成数据看起来省事,但质量参差不齐,必须做一轮筛选。我自己常用的过滤规则有三条:一是请求成功率,只有任务完成标记为成功的轨迹才保留;二是语法检查,工具调用的JSON必须能被解析且参数符合schema定义;三是相似度去重,如果一段轨迹和已有数据在语义上高度重复,直接丢弃,保持数据覆盖面。
清洗这一环尤其关键。一次Agent轨迹数据里往往包含多轮工具调用,如果其中某一步的工具参数填错了,整条数据就是毒药,不仅不能教会学生模型正确的行为,还会把错误模式固化下来。我的做法是先用规则自动过滤掉明显解析失败的数据,再抽样人工审核,最后把通过率低于某个阈值的批次整体丢掉。
3. 实操:一个可落地的蒸馏流程
3.1 准备工作与模型选型
在动工之前,先把两个模型定下来:教师模型和学生模型。
教师模型的选择标准很直接:在你自己的业务评测集上效果足够好。有些团队图省事直接拿通用大模型的API当教师,但你很快会发现,通用模型在你业务里那种特定工具格式下经常输出不规范的JSON,反而污染数据。相比之下,一个针对你业务微调过的模型往往更稳。基于我的经验,最佳实践是:先微调出一个业务专用的教师模型,再拿它去蒸馏。
学生模型的选型,我建议遵循一个原则:在你能接受的延迟和显存范围内,选最大的。蒸馏不是魔法,它不能无中生有——一个1B模型再怎么蒸,也很难继承70B模型复杂的多步推理能力,但一个7B模型通过蒸馏逼近13B~30B模型在特定任务上的表现是完全可能的。业内有很多团队报告过类似的结论:对于Agent任务,7B级别的学生模型配合高质量蒸馏数据,大致能继承教师模型70%到85%的能力,且推理速度提升数倍。
3.2 数据构造细节:格式、字段、配比
数据格式在整个蒸馏过程中是最容易被低估的一环。如果你的学生模型用的是AutoTokenizer和HuggingFace Trainer这套工具链,数据格式一般保持和训练教师模型时一致即可。但Agent场景有一条额外要求:工具定义是动态的、结构化信息很多,你需要把工具列表也放入上下文中。
以常用的对话格式为例,我通常这样组织一条Agent训练样本:
{ "messages": [ {"role": "system", "content": "你是智能助手,你有以下工具可用:<tool_schema>"}, {"role": "user", "content": "帮我订一张后天去上海的机票,最好下午两点前到达。"}, {"role": "assistant", "content": "我来帮你查一下航班信息。", "tool_calls": [{"name": "search_flight", "arguments": {"from": "北京", "to": "上海"}}]}, {"role": "tool", "name": "search_flight", "content": "以下航班符合要求:CA1501 到达14:30..."}, {"role": "assistant", "content": "找到合适航班:CA1501,14:30到达。"} ], "loss_mask": [0, 0, 1, 0, 1] }loss_mask这个字段是我强烈建议你加上的。在计算loss时,只对student生成的部分做反向传播,system prompt、user输入、工具返回的这些内容一律mask掉。如果忘了做mask,模型会花大量容量去记忆“预测用户说了什么”,而不是学会“怎么调用工具”。
数据配比上,我踩过的坑也不少。最合理的做法是混合多类数据:
- 纯对话数据(帮助模型保持基本对话能力)占比30%~40%。
- 单轮工具调用数据(帮助掌握工具格式)占比30%~40%。
- 多轮工具调用轨迹(帮助学习规划能力)占比20%~30%。
- 少量困难样本、负样本(不该调用工具的场景,模型要学会不调工具直接回复)占比5%~10%。
负样本极其重要。很多蒸馏出来的Agent模型会有乱调工具的病,核心原因就是训练数据里全是“需要调工具”的样本,模型没见过“不需要工具”的场景。这一点务必要在数据里刻意留出位置。
3.3 训练配置与损失函数组合
数据准备妥当之后进入训练环节。Agent蒸馏的loss一般不是单一的交叉熵loss,而是多个loss的组合。我常用的做法是:logits蒸馏loss加序列级CE loss加工具调用字段的重点加权。
import torch import torch.nn.functional as F def kd_loss(teacher_logits, student_logits, temperature=4.0): teacher_probs = F.softmax(teacher_logits / temperature, dim=-1) student_log_probs = F.log_softmax(student_logits / temperature, dim=-1) # 对教师概率做stop_gradient是关键 loss = F.kl_div(student_log_probs, teacher_probs.detach(), reduction="batchmean") return loss * (temperature ** 2) def ce_loss(student_logits, labels, loss_mask): # 只对有效token位置计算交叉熵 vocab_size = student_logits.size(-1) logits_flat = student_logits.view(-1, vocab_size) labels_flat = labels.view(-1) mask_flat = loss_mask.view(-1).float() losses = F.cross_entropy(logits_flat, labels_flat, reduction="none") return (losses * mask_flat).sum() / mask_flat.sum().clamp(min=1.0)训练时几个关键参数我的默认值是这样的:
- temperature:4.0到8.0之间,小模型容量低时用偏高的温度,让软化后的分布更平滑;但温度也不能太高,否则小模型会连教师模型的“错误自信”也一起学了。
- KD loss权重:0.5到1.0。如果教师模型和学生模型的能力差距太大(比如70B蒸馏到1B),建议把KD权重调低到0.3,否则学生会拟合不动,loss震荡严重。
- 学习率:1e-5到2e-5,比普通SFT要低一些。因为soft target本身已经包含了很多信息,过大的学习率容易让学生模型在一个batch内被某几个异常样本带跑偏。
- Epoch数:2到3个就够了,多轮重复训练Agent轨迹数据极易过拟合,表现为模型开始”背题“而不是学会工具调用的泛化逻辑。
另外一个经常被人忽略的细节:冻结部分参数。如果学生模型的backbone是基座模型,在训练早期冻结前几层,只更新后面几层和输出头,可以大幅减少灾难性遗忘的风险,训练也更稳定。等loss降到一定程度后再解冻全部参数做第二段训练。
3.4 评估体系:不能只用准确率
Agent蒸馏的评估比普通模型评估复杂得多,因为单个指标很难描述这个模型到底好不好用。我强烈建议搭建一套分层评估体系:
第一层:工具调用格式正确率。让模型针对一批测试指令只输出一次工具调用,用JSON Schema校验工具名和参数格式是否正确。这一项不过关就没有继续评估的必要了。
第二层:单步任务成功率。针对需要单次工具调用的指令,端到端执行一遍流程,看模型能否正确选择工具、填入参数、并根据工具结果生成合理回复。
第三层:多轮轨迹任务成功率。拿复杂的、需要调用多个工具的任务去测,标准是任务最终完成且关键路径(工具调用顺序、参数)与大模型教师给出的参考轨迹一致。
我自己习惯用tool_call_acc(工具调用准确率)和plan_accuracy(多步规划准确率)作为两个核心的北极星指标。还有一个额外建议:评价时一定要对比学生模型蒸馏前后、以及教师模型本身的表现,三组数字放一起才能看出蒸馏到底带来多少增益。否则你很容易被“蒸馏后效果不错”这种幻觉误导——很可能只是任务太简单,基座模型本来就能做。
4. 踩坑记录:我见过最多的五类翻车现场
4.1 温度系数调不好,学生模型“学歪了”
温度对蒸馏效果影响非常大,但这个参数很多人都是随手填的。我在一个Agent蒸馏项目里,最开始用T=2.0,结果学生模型在工具调用参数上频繁出现边界模糊的问题:教师模型对参数置信度是0.9和0.1的区别,但T=2.0的softmax拉平后变成0.7和0.3,学生模型的参数选择明显不如教师果断。后来把T调到6.0,软化后的分布拉开了一些,参数准确率提升明显。
如果你发现学生模型的输出变得“模棱两可”——不确定该填什么参数、不确定该不该调工具——优先怀疑温度没有调好。
4.2 数据全是“简单题”,能力迁移了个寂寞
我第一次做Agent蒸馏时,合成数据来自大模型对种子请求的回应,但种子请求大多只有一种标准解法。这导致学生模型面对新指令时,一旦遇到稍长的上下文或多步工具需求就直接崩掉。核心原因是数据覆盖不够:教师模型在简单任务上的表现本来就不需要蒸馏就能逼近,真正拉开差距的是复杂任务。
解决办法是刻意在合成数据时加扰动:给同一用户指令生成多个合法变体,变换措辞、变换槽位值、变换工具调用顺序。教师模型能力允许的情况下,让它在一条指令上生成多条成功的轨迹,再随机抽取进入训练集。
4.3 灾难性遗忘:蒸馏后连基础指令都听不懂
蒸馏Agent数据占比过高、而且全是工具调用格式数据时,学生模型很可能会出现“工具调用能力上来了,但基础对话能力崩了”的现象。你问它“你好”,它回你一段JSON调用。
两个解决思路:一是按我之前提到的配比,混入足够多的通用对话数据,比例不要低于30%;二是在训练过程中间做增量评测,每几百个step在通用指令集上测一次。一旦发现通用能力掉的幅度超出你设定的阈值,马上回滚到之前合适权重存档,而不是硬着头皮把整个训练流程跑完。
4.4 评估指标的安慰剂效应
有的团队做蒸馏蒸馏到一半,发现准确率很高,上线之后却被用户反馈说“变笨了”。我后来复盘发现,评测集和训练数据高度重合,说白了模型已经把测试集答案背下来了。更隐蔽的问题是,评测集里的工具调用场景过于单一,模型只要记住了几个特定格式的模板,就能拿到极高的通过率。
建议评估集和训练集在数据来源上严格隔离,而且评测数据要保持新鲜——线上真实用户请求中定期抽取一批新样本加入测试集,才能持续反映模型的真实能力。
4.5 小模型容量不够,硬蒸馏反而掉点
蒸馏不是在所有情况下都能带来正向收益。如果你的学生模型太小(比如1B以下),而教师模型的能力高到“完全超出学生容量的表达上限”,强行蒸馏时学生模型会处于一个很尴尬的境地:它既没法完全拟合教师输出,又被soft label干扰了原本在硬标签上的学习。
这种情况的典型表现是:蒸馏后的模型在训练集上loss降不下去,同时基线能力还在掉。我在某个对比实验中见过,一个0.5B的模型做普通SFT时工具调用能力是70分,拿一个大模型蒸馏它反而降到了55分。后来换成能力更接近的教师模型,或者减少KD loss权重、加大CE loss权重,情况才好转。
5. 一点私人经验
做了这么多蒸馏项目,我最深的体会是:知识蒸馏的瓶颈不在于算法,而在于数据质量与任务边界的设计。同样的学生模型,喂给它高质量的教师轨迹数据和不加筛选的合成数据,最终效果可能差出一倍。与其花时间去调那些花哨的蒸馏loss,不如把数据链路夯实——种子指令的覆盖面、教师轨迹的筛选规则、负样本的配比,这些才是真正决定Agent蒸馏成败的地方。
另外一个小技巧收尾分享:项目初期不要急着蒸馏完整能力,先挑一个最核心的单工具调用场景,把一条“教师产出轨迹->筛选清洗->SFT训练->评测”的流水线跑通。这段流程跑顺了,后面再扩展到多工具、多轮场景,就只是数据的横向扩展问题,而不是重新摸索。先小步快跑,再逐步放大,这是我在Agent蒸馏项目里一以贯之的节奏。