跳转至

小米大模型一面

自我介绍

GPT、LLaMA、Qwen 注意力机制的区别

这三大家族模型都基于 Transformer 解码器架构,但在注意力机制的具体实现和优化上存在显著差异,主要体现在位置编码、注意力变体和长文本扩展策略上。

GPT 系列:

  • 位置编码:早期的 GPT-1/GPT-2 使用可学习的位置嵌入(Learned Positional Embedding),每个位置有一个独立的嵌入向量,与词嵌入相加。缺点是位置编码的长度在训练时固定,无法直接外推到更长的序列。

  • 注意力机制:标准的多头自注意力(MHA),每个头有独立的 Q、K、V 投影。在自回归生成时使用因果掩码,确保当前位置只能看到过去的信息。

  • 长文本扩展:GPT-3 及以后的部分版本仍使用可学习位置嵌入。OpenAI 在 GPT-3.5/GPT-4 的细节未完全公开,但已知 GPT-4 支持超长上下文(128K),可能采用了类似于 RoPE 或改进的位置编码以及稀疏注意力等技术,但未明确说明。

  • 优化:FlashAttention 等加速实现被广泛使用。GPT 系列的注意力结构相对传统,更侧重于通过数据和规模取胜,而非精巧的注意力变体。

LLaMA 系列:

  • 位置编码:RoPE(旋转位置编码)。通过旋转矩阵将相对位置信息注入 Q 和 K 的点积中,使模型能够自然地捕捉相对位置关系。RoPE 具有良好的理论性质和外推潜力。

  • 注意力机制:LLaMA 1 使用标准多头注意力。从 LLaMA 2 开始,引入了 GQA(分组查询注意力)。在较大的模型中(如 70B),Q 的头数保持不变,但 K 和 V 的头数减少(分组共享),在速度和效果之间取得平衡,大幅降低推理时的 KV 缓存。

  • 长文本扩展:基于 RoPE,LLaMA 社区通过位置插值(PI)、NTK-Aware 缩放、YaRN 等方法,在不重新训练或少量微调的情况下将上下文窗口扩展到 32K、128K 甚至更长。LLaMA 3 原生支持 8K 上下文。

  • 其他优化:LLaMA 使用 Pre-Norm + RMSNorm 结构,激活函数为 SwiGLU,注意力部分并未做过多复杂变体,保持简洁高效。

Qwen 系列:

  • 位置编码:同样采用 RoPE,与 LLaMA 一致,具备良好的外推能力。

  • 注意力机制:Qwen 模型中采用了多头注意力(MHA)或分组查询注意力(GQA)的混合策略。Qwen-7B 使用标准 MHA,更大的模型可能采用 GQA 来降低推理成本。此外,Qwen 在注意力计算中引入了LogN 缩放(LogN-Scaling),对注意力 logits 进行基于层索引的缩放,以稳定深层网络的训练。

  • 长文本扩展:Qwen 支持动态 NTK 插值和窗口注意力的组合。它可以根据输入序列的长度动态调整 RoPE 的频率,使模型能够平滑地处理比训练时更长的上下文。同时,Qwen 提出了 Dual Chunk Attention 等机制来高效处理超长序列,兼顾了全局和局部注意力。

  • 独特设计:Qwen 在多模态版本(Qwen-VL)中,引入了视觉语言适配器,其注意力机制需要处理额外的视觉 token,因此在注意力掩码和位置编码分配上有专门设计。Qwen 还使用了FlashAttention v2 进行加速。

核心区别总结表:

维度 GPT(早期) LLaMA Qwen
位置编码 可学习位置嵌入 RoPE RoPE
注意力变体 MHA MHA / GQA MHA / GQA
长文本方案 固定窗口/未公开 PI, NTK, YaRN 插值 动态NTK, 窗口注意力, LogN缩放
推理加速 FlashAttention FlashAttention, GQA FlashAttention v2, GQA
特色 传统结构,重规模 简洁高效,社区生态丰富 训练稳定,原生长文本支持好

Agent 项目的数据形式和 Agent-as-Tool 实现细节

数据形式:

我们的 Agent 项目涉及多种数据类型,在训练和推理中以统一格式流转:

  • 文本:用户输入、模型思考、工具调用指令和结果、最终回答。所有文本采用 ChatML 格式(<|im_start|>role\ncontent<|im_end|>)组织多轮对话。

  • 工具调用:采用 JSON 格式嵌入在 assistant 消息中,例如:

<|im_start|>assistant
{"action": "search", "params": {"query": "2024年GDP"}}
<|im_end|>
  • 工具返回结果以 <|im_start|>tool\n{...}<|im_end|> 格式追加。

  • 多模态数据:图片以 base64 编码或本地路径形式嵌入 <img>path</img> 标签,在送入模型前由视觉编码器处理为视觉 token,并插入到文本 token 序列中。

  • 训练数据:由多轮 agent 轨迹构成。每一条轨迹包含完整的 ReAct 循环:用户指令 → 模型思考 → 工具调用 → 工具返回 → ... → 最终答案。这些轨迹被切分为多个训练样本,loss 只在模型输出的部分计算(assistant 的思考、动作、最终答案),系统提示、用户消息和工具返回不计算 loss。

  • 评估数据:包含输入、期望的工具调用序列、期望的最终答案,以及人工评判的质量标签。用于离线评测和奖励建模。

Agent-as-Tool 如何实现:

“Agent-as-Tool” 是指将一个子 Agent 封装成一个可被父 Agent 调用的工具。这实现了层次化任务分解。具体实现:

  • 接口标准化:每个子 Agent 被包装成一个符合 MCP(或自定义)标准的工具。它具有标准的 JSON Schema 描述其功能、输入参数(如任务描述、上下文)和输出格式(如任务结果、状态)。

  • 独立性:子 Agent 拥有自己独立的上下文、记忆空间和工具集。它可以是专门解决某类问题的专家(如代码解释 Agent、数据分析 Agent)。

  • 调用流程:

  • 父 Agent 规划任务时,决定将某个子任务委派给子 Agent。
  • 父 Agent 生成一个 Function Call,工具名即为子 Agent 的名字,参数为对子 Agent 的任务描述和必要上下文。
  • 调度器接收到该调用后,启动一个新的 Agent 会话(或复用已有会话),将参数作为初始指令传递给子 Agent。
  • 子 Agent 独立完成其 ReAct 循环,最终返回结果(文本、文件、结构化数据)。
  • 父 Agent 收到子 Agent 的结果作为 Observation,继续其推理。

  • 权限与沙箱:子 Agent 通常运行在受限的环境中,拥有比父 Agent 更少的权限。父 Agent 只允许调用指定的子 Agent,且子 Agent 不能反向调用父 Agent,以防止死循环和权限逃逸。

  • 状态传递:父 Agent 可以传递部分上下文给子 Agent(如用户原始问题),但子 Agent 无法访问父 Agent 的完整记忆,保证隔离性。

  • 实现方式:在 LangGraph 中,父 Agent 的图上有一个节点专门处理“调用子 Agent”的动作。该节点创建一个新的线程/子图来运行子 Agent,并将最终结果作为该节点的输出返回。

通过 Agent-as-Tool,我们可以构建分层的多智能体系统,将复杂任务逐层分解,每个层级专注于自己的职责,极大提升了系统的可扩展性和能力上限。


微调项目:SFT 数据组织、训练轮次、超参设置及最优参数搜索

SFT 数据组织形式:

  • 采用 ChatML 格式 统一表示多轮对话和工具调用。

  • 每条样本是一个完整的对话历史,包含 systemuserassistant 角色。对于多轮工具调用,还包括 tool 角色。

  • 系统提示(system prompt)在每条样本的开头,定义模型的角色、行为规范和输出格式。

  • 对于多轮对话,通过滑动窗口切分。确保每条样本的上下文长度不超过模型的最大长度(如 2048 token),且不会截断在某轮对话的中间。同一个会话的所有片段划分为训练集或验证集,不会跨集合泄漏。

  • 数据格式示例:

<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
请帮我查询天气<|im_end|>
<|im_start|>assistant
好的,我需要调用天气工具。{"action": "get_weather", ...}<|im_end|>
<|im_start|>tool
{"temperature": 25, "condition": "晴"}<|im_end|>
<|im_start|>assistant
当前天气晴朗,气温25℃。<|im_end|>
  • Loss 只计算 assistant 角色输出的部分,systemusertool 的内容不参与 loss 计算。

训练轮次(Epochs)选择:

  • 对于 SFT,通常训练 2~4 个 epoch。过少则欠拟合,过多则过拟合且灾难性遗忘加重。

  • 根据数据量:若数据量较小(数千条),可训练 4-5 epoch;数据量大(数万条以上),2-3 epoch 即可。

  • 通过监控验证集上的 loss 和下游任务指标,当验证 loss 不再下降或开始上升时提前停止(early stopping)。

超参设置及理由:

  • 学习率:5e-5 ~ 2e-4(全参微调时)或 1e-4 ~ 5e-4(LoRA 微调时)。LoRA 可训练参数少,需要稍大的学习率以有效更新。

  • 学习率调度:使用余弦退火(Cosine Annealing),搭配 warmup(占总步数的 10%)。Warmup 使训练初期稳定,余弦退火有助于收敛到更优的平坦区域。

  • 批次大小:全局批次大小 32~128。通过梯度累积实现,micro batch size 受限于显存(如 1~4)。更大的批次有助于训练稳定,但过大会降低泛化性。

  • 优化器:AdamW,权重衰减 0.01~0.1,betas=(0.9, 0.999)。AdamW 能有效解耦权重衰减和自适应学习率,提升泛化。

  • 最大序列长度:2048 或 4096。根据任务和显存选择,覆盖大部分训练样本而不浪费计算。

  • Dropout:通常设为 0 或极小值(如 0.05),因为大模型本身不易过拟合,且 LoRA 参数少,dropout 可能阻碍学习。

为什么这么设置:

这些设置借鉴了 LLaMA、Alpaca、Vicuna 等主流 SFT 工作的最佳实践,并在我们的数据和任务上通过小规模实验验证。LLM 微调属于迁移学习,基座模型已经很强,我们只需在适当的学习率下进行少量 epoch 的微调,配合 warmup 和余弦退火,即可快速适配下游任务而不致于遗忘通用能力。

如何寻找最优参数:

  • 搜索空间定义:选择关键超参(学习率、epoch、batch size),对每个超参设定候选值列表。

  • 小规模实验:在全部训练数据的一个子集(如 10%)上进行独立实验,使用网格搜索或贝叶斯优化(如 Optuna),以验证集上的 loss 和下游任务评分为目标。

  • 逐步细化:先粗粒度搜索,确定大致范围;再在较优区域进行细粒度搜索。

  • 交叉验证:对于小数据集,进行 k 折交叉验证,取平均最佳超参。

  • 监控指标:除了 loss,还关注模型在几个代表性评测集上的表现(如 MMLU、TruthfulQA),以及生成质量的自动评分。最终选择在这些指标上帕累托最优的参数组合。


LoRA、QLoRA 原理、区别,模型量化方法及原因

LoRA(Low-Rank Adaptation)原理:

image.png

QLoRA(Quantized LoRA)原理:

QLoRA 在 LoRA 的基础上,将基座模型 W0W0 量化为 4-bit(NF4 数据类型),进一步降低显存占用。它引入了三项关键技术:

  • NF4 数据类型:一种适用于正态分布权重的 4-bit 量化格式,比均匀量化信息损失更小。

  • 双重量化(Double Quantization):对量化所需的缩放因子也进行量化,节省更多内存。

  • 分页优化器(Paged Optimizers):利用 NVIDIA 的统一内存分页机制,在 GPU 内存不足时自动将优化器状态交换到 CPU 内存,避免 OOM。 训练时,量化的基座模型权重被冻结,只在前向传播时反量化为 16-bit 精度,梯度和优化器状态仅应用于 LoRA 的低秩矩阵。

LoRA 与 QLoRA 的区别:

维度 LoRA QLoRA
基座模型精度 保持原精度(如 FP16/BF16) 量化为 4-bit NF4
显存占用 相对全参微调大幅降低,但模型仍需 16-bit 存储 进一步降低约 4 倍,可在单张消费级 GPU 上微调 65B 模型
训练速度 较快,无量化反量化开销 略慢,因需反量化计算
性能 接近全参微调 通过先进量化技术,性能与 LoRA 几乎持平
适用场景 资源较充足时 显存极度受限时

模型量化的方法和原因:

  • 量化方法:将浮点权重和激活值映射到低比特整数(如 INT8、INT4)。常用方法有:
  • 训练后量化(PTQ):直接对已训练模型进行量化,无需重新训练。代表算法:GPTQ(基于近似二阶信息)、AWQ(激活感知权重量化)、GGUF(用于 CPU 推理)。
  • 量化感知训练(QAT):在训练中模拟量化误差,使模型适应量化后的分布,精度更高但成本大。

  • 为什么量化:

  • 降低内存占用:将模型从 16-bit 压缩到 4-bit,大小减少约 4 倍。
  • 加速推理:低比特计算可利用硬件(如 Tensor Cores 的 INT8 加速)获得更高吞吐,并降低内存带宽压力。
  • 降低能耗与成本:使大模型能够在边缘设备或低成本 GPU 上运行。
  • QLoRA 中的量化进一步使得在极有限的硬件上微调千亿大模型成为可能。

SFT 数据整理成 ChatML 格式(手撕代码)

任务:将原始的 SFT 对话数据(通常为包含 rolecontent 的 JSON 列表)转换为 ChatML 格式的字符串。

代码实现(Python):

def convert_to_chatml(messages):
    """
    messages: list of dict, each with 'role' and 'content'
    returns: ChatML formatted string
    """
    chatml_parts = []
    for msg in messages:
        role = msg['role']
        content = msg['content']
        # 拼接单个消息
        chatml_parts.append(f"<|im_start|>{role}\n{content}<|im_end|>")
    # 拼接所有消息,并在末尾添加 assistant 起始标记(如果最后一轮是 assistant 回答)
    # 对于训练,通常最后是 assistant 的回答,结束时加上 <|im_end|>
    return "\n".join(chatml_parts)

# 示例
messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "你好,请帮我查天气。"},
    {"role": "assistant", "content": "好的,正在查询..."},
    {"role": "tool", "content": '{"temp": 25}'},
    {"role": "assistant", "content": "当前气温25℃。"}
]
print(convert_to_chatml(messages))

输出:

<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
你好,请帮我查天气。<|im_end|>
<|im_start|>assistant
好的,正在查询...<|im_end|>
<|im_start|>tool
{"temp": 25}<|im_end|>
<|im_start|>assistant
当前气温25℃。<|im_end|>

关键点:

  • 使用特殊的起止标记 <|im_start|><|im_end|>,这是 ChatML 的核心。

  • 每个角色占一行,内容紧随其后,末尾明确标记结束。

  • 训练时只对 assistant 角色的内容计算 loss,因此 tokenization 后需要生成相应的 mask。

  • 确保 content 不包含这些特殊标记,否则需转义。


场景题:SFT 数据准备好了,如何做一个意图识别?完整流程

意图识别任务定义:根据用户输入的自然语言,判断其属于哪个预定义类别(如“查询订单”、“退款申请”、“咨询产品”等),并提取相关槽位(如订单号、产品名)。

完整流程:

  1. 数据准备与标注

  2. 收集大量真实或模拟的用户查询,覆盖各种意图类别和表述变体。

  3. 为每条查询标注:

  4. 意图类别(多分类):intent(如 query_order, refund, product_inquiry)。
  5. 关键槽位(命名实体识别/信息抽取):如 order_idproduct_namedate

  6. 确保数据平衡,每个意图类别样本量足够。对于少样本类别,进行上采样或数据增强(回译、同义改写)。

  7. 数据格式转换(SFT 格式)

  8. 将标注数据整理为指令微调格式,用于训练大模型。格式示例:

{
  "messages": [
    {"role": "system", "content": "你是一个意图识别助手。根据用户输入,识别意图并抽取槽位。意图类别包括:query_order, refund, product_inquiry。"},
    {"role": "user", "content": "我想查一下我的订单,单号是A12345"},
    {"role": "assistant", "content": "{\"intent\": \"query_order\", \"slots\": {\"order_id\": \"A12345\"}}"}
  ]
}
  • 要求输出为结构化 JSON,便于下游解析和评测。

  • 模型选择与微调

  • 基座模型:选择一个较小但强大的 LLM(如 Qwen-7B、LLaMA-3-8B),以平衡效果和推理延迟。

  • 微调方法:使用 LoRA(或全参,若资源允许)进行监督微调。训练时只计算 assistant 回复的 loss。

  • 超参设置:学习率 2e-4,batch size 64(通过梯度累积),2-3 epochs,余弦退火+warmup。

  • 评测与迭代

  • 构建独立的测试集,包含各类意图以及难以判断的边缘 case。

  • 评测指标:

  • 意图准确率:模型输出的 intent 是否与标注一致。
  • 槽位 F1 分数:槽位抽取的精确率、召回率和 F1。
  • 端到端准确率:意图和所有槽位完全正确的比例。

  • 分析 bad case,针对性地增加训练数据(如易混淆意图的对比样本、少样本类别的增强)。

  • 若模型在某些意图上偏弱,可尝试构造专门的对比微调数据或使用 DPO 偏好对齐。

  • 部署与优化

  • 将微调后的模型部署为在线服务,使用 vLLM 或 TGI 提高吞吐。

  • 推理约束:采用 guided generation 确保输出必为合法 JSON,并使用预定义的 intent 值域进行约束解码,杜绝无效输出。

  • 不确定性处理:若模型输出置信度低(可通过 logits 或多次采样评估),则启动多轮澄清或转接人工。

  • 持续迭代:收集线上日志中的误判案例,定期重新标注并微调模型,形成闭环优化。

流程总结:数据标注 → SFT 格式构造 → 基座模型 LoRA 微调 → 多指标评测 → 基于 bad case 的数据增强与再训练 → 上线部署 + 推理约束 → 线上反馈持续迭代。通过这一套流程,可以快速构建一个高准确率、稳定输出的意图识别系统。