大模型基础原理:从架构到面试


目录

  • [第1章 大模型全景认知](#第1章 大模型全景认知)
  • [第2章 Transformer架构深度解析](#第2章 Transformer架构深度解析)
  • [第3章 注意力机制进阶](#第3章 注意力机制进阶)
  • [第4章 位置编码](#第4章 位置编码)
  • [第5章 预训练](#第5章 预训练)
  • [第6章 微调与对齐](#第6章 微调与对齐)
  • [第7章 开源LLM架构](#第7章 开源LLM架构)
  • [第8章 推理优化](#第8章 推理优化)
  • [第9章 RAG检索增强生成](#第9章 RAG检索增强生成)
  • [第10章 AI Agent](#第10章 AI Agent)
  • [第11章 评估与未来](#第11章 评估与未来)

第1章 大模型全景认知

1.1 什么是大语言模型

大语言模型(Large Language Model, LLM)是基于Transformer架构、在海量文本上通过Next-Token Prediction训练的高维条件概率分布模型。它的核心能力不是"记住"了知识,而是将语言规律、世界知识和推理模式压缩进了参数权重中。

python 复制代码
# 大模型的本质:条件概率预测
# P(下一个Token | 当前上下文)
import torch
import torch.nn.functional as F

# 模拟一个简单语言模型的输出
logits = torch.tensor([2.0, 1.5, 0.5, 0.1, -1.0])  # 5个token的原始分数
probs = F.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, 1)
print(f"概率分布: {probs.tolist()}")
print(f"选择的token: {next_token.item()}")

关键认知:大模型不是数据库。它虽然能回答知识性问题,但不能保证参数中的知识永远最新,也不能保证每句话都有事实依据。这就是为什么需要RAG、工具调用和评估体系来提升可靠性。

1.2 从RNN到Transformer的演进

RNN的困境

RNN(循环神经网络)按顺序处理序列,每个时间步依赖前一步的隐状态:

python 复制代码
# RNN的核心问题:顺序依赖
class SimpleRNN:
    def __init__(self, input_size, hidden_size):
        self.W_ih = torch.randn(hidden_size, input_size) * 0.01
        self.W_hh = torch.randn(hidden_size, hidden_size) * 0.01
    
    def forward(self, x_seq):
        h = torch.zeros(self.W_hh.shape[0])
        outputs = []
        for x_t in x_seq:  # 必须顺序处理!
            h = torch.tanh(self.W_ih @ x_t + self.W_hh @ h)
            outputs.append(h)
        return outputs

RNN三大缺陷:

  1. 无法并行:必须等前一步算完才能算下一步
  2. 长程遗忘:梯度在反向传播中指数衰减(梯度消失)
  3. 信息瓶颈:所有信息压缩到一个固定大小的隐状态

Transformer的突破

Transformer用Self-Attention替代循环,实现了:

  • 全并行:所有位置同时计算
  • 全局感受野:每个位置直接关注所有其他位置
  • 残差连接:梯度直接回传,不受序列长度影响
python 复制代码
# Transformer vs RNN的复杂度对比
# RNN: O(N) 串行 → 实际训练慢
# Transformer: O(N²) 并行 → GPU利用率高,实际训练快
# N=序列长度

import time

# 模拟RNN的串行处理
def rnn_process(seq_len, hidden_size=512):
    start = time.time()
    h = torch.zeros(hidden_size)
    for t in range(seq_len):
        h = torch.tanh(torch.randn(hidden_size))
    return time.time() - start

# 模拟Transformer的并行处理
def transformer_process(seq_len, hidden_size=512):
    start = time.time()
    x = torch.randn(1, seq_len, hidden_size)
    # 一次矩阵运算处理所有位置
    attn_output = F.softmax(x @ x.transpose(-2, -1) / (hidden_size ** 0.5), dim=-1) @ x
    return time.time() - start

# seq_len=512时,Transformer快10-100倍

1.3 预训练-微调范式

大模型训练遵循"预训练→微调→对齐"三阶段:

复制代码
预训练(Pre-training)
  目标:学习语言规律和世界知识
  数据:万亿级无标注文本
  任务:Next-Token Prediction
  产出:基座模型(Base Model)
      ↓
指令微调(SFT)
  目标:学会遵循人类指令
  数据:万级高质量指令-回答对
  任务:指令遵循
  产出:对话模型(Chat Model)
      ↓
偏好对齐(RLHF/DPO)
  目标:更安全、更有用、更诚实
  数据:人类偏好排序
  任务:对齐人类价值观
  产出:对齐模型(Aligned Model)
python 复制代码
# 三阶段的代码示意
# 预训练:预测下一个token
def pretrain_loss(model, input_ids):
    logits = model(input_ids[:, :-1])
    targets = input_ids[:, 1:]
    return F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))

# SFT:学习指令-回答模式
def sft_loss(model, instruction, response):
    input_ids = torch.cat([instruction, response], dim=-1)
    # 只对response部分计算loss
    logits = model(input_ids)
    response_logits = logits[:, instruction.size(1)-1:-1]
    return F.cross_entropy(response_logits.reshape(-1, response_logits.size(-1)), 
                           response.reshape(-1))

# DPO:偏好对齐
def dpo_loss(policy_chosen_logps, policy_rejected_logps,
             ref_chosen_logps, ref_rejected_logps, beta=0.1):
    chosen_rewards = beta * (policy_chosen_logps - ref_chosen_logps)
    rejected_rewards = beta * (policy_rejected_logps - ref_rejected_logps)
    return -F.logsigmoid(chosen_rewards - rejected_rewards).mean()

1.4 Scaling Law与涌现能力

Kaplan定律 vs Chinchilla定律

定律 核心结论 最优策略
Kaplan (2020) 性能随参数量幂律增长,数据量影响小 增大模型,数据够用就行
Chinchilla (2022) 参数量和数据量应等比例增长 模型和数据同步增大

Chinchilla定律的核心公式:最优token数 ≈ 20 × 参数量。即7B模型应训练140B token。

python 复制代码
# Chinchilla定律:计算最优分配
def chinchilla_optimal(params_billion):
    """给定参数量,计算最优训练token数"""
    optimal_tokens = 20 * params_billion  # 单位:B tokens
    compute_flops = 6 * params_billion * 1e9 * optimal_tokens * 1e9
    return {
        "params_B": params_billion,
        "optimal_tokens_B": optimal_tokens,
        "total_FLOPs": compute_flops,
        "GPU_hours_A100": compute_flops / (312e12 * 3600),  # A100=312TFLOPS
    }

print(chinchilla_optimal(7))   # 7B模型
print(chinchilla_optimal(70))  # 70B模型

涌现能力

涌现能力(Emergent Abilities)指模型在某个规模阈值后突然获得的新能力,如思维链推理、少样本学习等。

python 复制代码
# 涌现能力的两种解释
emergence_views = {
    "真实涌现": "模型确实在某个规模获得了新能力,如思维链推理",
    "度量假象": "只是评估指标的非线性导致看起来突然提升",
    "实践结论": "无论哪种解释,大模型在复杂任务上的表现确实随规模有质的飞跃",
}

过训练(Over-training)

LLaMA系列证明:小模型+更多数据 > 大模型+少数据。

  • LLaMA-7B用1T token训练(Chinchilla建议140B,过训练7倍)
  • 效果:7B模型在多数任务上接近GPT-3 175B
  • 原因:推理成本比训练成本重要得多------训练一次,推理无数次

1.5 Encoder-Only / Decoder-Only / Encoder-Decoder

架构 代表模型 特点 适用场景
Encoder-Only BERT 双向注意力,理解能力强 分类、NER、检索
Decoder-Only GPT, LLaMA 单向注意力,生成能力强 对话、创作、推理
Encoder-Decoder T5, BART 编码+解码,适合转换 翻译、摘要

为什么当前主流LLM选择Decoder-Only?

  1. 自回归一致性:训练和推理使用相同的Next-Token Prediction目标,没有训练-推理不一致
  2. 扩展效率:Decoder-Only架构在Scaling Law下表现更优
  3. 工程简洁:单一架构,无需维护Encoder-Decoder之间的交互
python 复制代码
# 三种架构的注意力模式对比
import torch

def encoder_attention_mask(seq_len):
    """Encoder: 所有位置互相可见"""
    return torch.ones(seq_len, seq_len)

def decoder_attention_mask(seq_len):
    """Decoder: 只能看到当前位置及之前(因果掩码)"""
    return torch.tril(torch.ones(seq_len, seq_len))

def enc_dec_attention_mask(enc_len, dec_len):
    """Encoder-Decoder: Decoder可以看到全部Encoder输出"""
    cross_mask = torch.ones(dec_len, enc_len)  # Cross-attention
    causal_mask = torch.tril(torch.ones(dec_len, dec_len))  # Self-attention
    return causal_mask, cross_mask

print("Encoder掩码(全可见):")
print(encoder_attention_mask(4).int())
print("\nDecoder掩码(因果):")
print(decoder_attention_mask(4).int())

1.6 大模型的本质与局限性

本质

大模型是一个高维条件概率分布模型

  • 微观:每次生成都在计算 P(next_token | context)
  • 中观:将语言规律和世界知识压缩进参数
  • 宏观:规模足够大时表现出推理、泛化等涌现能力

局限性

  1. 幻觉(Hallucination):生成看似合理但事实错误的内容
  2. 知识时效性:训练数据有截止日期,无法获取最新信息
  3. Lost in the Middle:对长文本中间部分的信息关注度下降
  4. 数学推理弱:复杂多步推理容易出错
  5. 无法自我验证:不知道自己不知道什么
python 复制代码
# 温度参数对生成的影响
logits = torch.tensor([2.0, 1.0, 0.5, 0.1])

for T in [0.1, 0.5, 1.0, 2.0]:
    probs = F.softmax(logits / T, dim=-1)
    print(f"T={T}: {probs.tolist()}")
# T=0.1: [0.9, 0.09, 0.01, 0.00] → 几乎确定选第一个(确定性输出)
# T=1.0: [0.56, 0.21, 0.13, 0.09] → 正常分布
# T=2.0: [0.35, 0.26, 0.21, 0.18] → 更随机(创造性输出)

1.7 本章面试题精讲

Q1(高频):Transformer为什么能取代RNN?核心优势在哪里?

:三大核心优势:

  1. 并行计算:RNN必须串行处理(O(N)步),Transformer所有位置同时计算(1步),GPU利用率从<10%提升到>80%
  2. 全局感受野:RNN需要N步才能让位置1的信息传到位置N,Transformer一步到位
  3. 梯度稳定:RNN存在梯度消失/爆炸,Transformer通过残差连接保证梯度直接回传

面试加分:Transformer的O(N²)复杂度看似比RNN的O(N)差,但GPU的并行能力使得实际训练速度远超RNN。

Q2(高频):什么是Scaling Law?Chinchilla定律和Kaplan定律有什么区别?

:Scaling Law描述模型性能随规模(参数量、数据量、计算量)的变化规律。

  • Kaplan定律:模型越大越好,数据量影响小 → 导致GPT-3用175B参数但数据不够
  • Chinchilla定律:参数量和数据量应等比例增长,最优token数≈20×参数量 → 促使LLaMA用小模型+多数据

Q3(高频):什么是涌现能力?它是否真实存在?

:涌现能力指模型在某个规模阈值后突然获得的新能力。存在两种观点:

  • 真实涌现:模型确实获得了新能力(如思维链推理在~100B参数时出现)
  • 度量假象:只是评估指标的非线性导致看起来突然

实践结论:无论哪种解释,大模型在复杂任务上的表现确实随规模有质的飞跃。面试时建议展示两种观点,然后给出自己的判断。

Q4(高频):Decoder-Only为什么成为主流?

:三个原因:

  1. 自回归一致性:训练目标(Next-Token Prediction)和推理目标完全一致
  2. 扩展效率:在相同计算预算下,Decoder-Only的Scaling Law更优
  3. 工程简洁:单一架构,无需维护Encoder-Decoder交互

面试加分:Encoder-Only(BERT类)在理解任务上仍有优势,但生成式任务已全面转向Decoder-Only。

Q5(中频):预训练-微调范式的核心意义是什么?

:预训练阶段用海量无标注数据学习通用知识("博学"),微调阶段用少量标注数据适配特定任务("专业")。核心意义是将NLP从"每个任务从头训练"转变为"一次预训练,多次微调",大幅降低任务适配成本。

Q6(高频):大模型为什么会产生幻觉?如何缓解?

:幻觉产生的根本原因:

  1. 模型本质是概率预测,不是知识检索
  2. 训练数据中的噪声和错误被模型学习
  3. 模型倾向于生成"看起来合理"而非"事实正确"的内容

缓解方案:

  • RAG(检索增强生成):用外部知识支撑回答
  • 约束生成:限制模型只能基于给定文档回答
  • 多模型验证:用另一个模型交叉验证
  • 置信度评估:让模型自评置信度

Q7(中频):什么是过训练?为什么LLaMA要过训练?

:过训练指用远超Chinchilla定律建议的token数训练模型。LLaMA-7B用1T token训练(建议140B,7倍过训练)。原因:推理成本远高于训练成本,小模型+多数据 = 推理便宜但效果接近大模型。

Q8(高频):GPT和BERT的核心区别是什么?

维度 GPT BERT
架构 Decoder-Only Encoder-Only
训练目标 Next-Token Prediction Masked Language Model
注意力 单向(因果) 双向
擅长 生成 理解
代表 ChatGPT, LLaMA BERT, RoBERTa

Q9(中频):什么是数据污染?如何检测和避免?

:数据污染指训练数据中包含了评测集的内容,导致模型"背到了题"而非真正学会。检测方法:①n-gram重叠检测;②用模型生成评测集题目看是否能直接复现;③对比污染前后的性能差异。避免方法:训练前严格去重、N-gram过滤。

Q10(中频):BF16和FP16有什么区别?为什么现代LLM训练用BF16?

  • FP16:5位指数+10位尾数,数值范围小(6e-8~65504),需要loss scaling防止下溢
  • BF16:8位指数+7位尾数,数值范围与FP32相同,不需要loss scaling
  • 现代LLM用BF16的原因:训练更稳定,不需要调loss scaling,A100/H100原生支持

面试加分:BF16精度比FP16低(7位尾数vs10位),但训练中精度损失可忽略,稳定性更重要。


第2章 Transformer架构深度解析

2.1 Self-Attention机制详解

Self-Attention是Transformer的核心,它让序列中每个位置都能直接关注所有其他位置。

计算流程

python 复制代码
import torch
import torch.nn as nn
import math

class SelfAttention(nn.Module):
    """完整的Self-Attention实现"""
    def __init__(self, d_model=512, n_heads=8):
        super().__init__()
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads
        
        self.W_q = nn.Linear(d_model, d_model, bias=False)
        self.W_k = nn.Linear(d_model, d_model, bias=False)
        self.W_v = nn.Linear(d_model, d_model, bias=False)
        self.W_o = nn.Linear(d_model, d_model, bias=False)
    
    def forward(self, x, mask=None):
        B, N, D = x.shape
        
        # 1. 线性投影
        Q = self.W_q(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        K = self.W_k(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        V = self.W_v(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        
        # 2. 计算注意力分数
        scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k)
        
        # 3. 应用掩码
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        
        # 4. Softmax归一化
        attn_weights = torch.softmax(scores, dim=-1)
        
        # 5. 加权求和
        context = attn_weights @ V
        
        # 6. 拼接多头输出
        context = context.transpose(1, 2).contiguous().view(B, N, D)
        return self.W_o(context)

# 使用示例
attn = SelfAttention(d_model=512, n_heads=8)
x = torch.randn(2, 10, 512)  # [batch, seq_len, d_model]
output = attn(x)
print(f"输入形状: {x.shape}, 输出形状: {output.shape}")

为什么除以√d_k?

python 复制代码
# 不除以√d_k的后果
d_k = 64
Q = torch.randn(1, 1, d_k)
K = torch.randn(1, 100, d_k)
scores = Q @ K.T  # 点积

print(f"点积均值: {scores.mean().item():.2f}")
print(f"点积标准差: {scores.std().item():.2f}")
print(f"理论标准差: {math.sqrt(d_k):.2f}")  # ≈8.0

# d_k=64时,点积标准差≈8,softmax输入范围很大
# 导致softmax输出接近one-hot,梯度极小(饱和)
# 除以√64=8后,标准差变为1,softmax梯度正常

2.2 多头注意力(MHA)

多头注意力的核心思想:让不同的头关注不同的表示子空间。

python 复制代码
# 多头注意力的"专业化"现象
# 实验发现不同头确实学到了不同的模式:
# - 有的头关注语法关系(主谓一致)
# - 有的头关注语义关系(同义词)
# - 有的头关注位置关系(相邻token)
# - 有的头关注长距离依赖(指代消解)

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads, n_kv_heads=None):
        super().__init__()
        self.n_heads = n_heads
        self.n_kv_heads = n_kv_heads or n_heads  # MHA: n_kv_heads = n_heads
        self.d_k = d_model // n_heads
        
        self.W_q = nn.Linear(d_model, n_heads * self.d_k, bias=False)
        self.W_k = nn.Linear(d_model, self.n_kv_heads * self.d_k, bias=False)
        self.W_v = nn.Linear(d_model, self.n_kv_heads * self.d_k, bias=False)
        self.W_o = nn.Linear(n_heads * self.d_k, d_model, bias=False)
    
    def forward(self, x, mask=None):
        B, N, _ = x.shape
        Q = self.W_q(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        K = self.W_k(x).view(B, N, self.n_kv_heads, self.d_k).transpose(1, 2)
        V = self.W_v(x).view(B, N, self.n_kv_heads, self.d_k).transpose(1, 2)
        
        # GQA: 扩展KV头数以匹配Q头数
        if self.n_kv_heads < self.n_heads:
            n_rep = self.n_heads // self.n_kv_heads
            K = K.unsqueeze(2).expand(-1, -1, n_rep, -1, -1).reshape(B, self.n_heads, N, self.d_k)
            V = V.unsqueeze(2).expand(-1, -1, n_rep, -1, -1).reshape(B, self.n_heads, N, self.d_k)
        
        scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        attn = torch.softmax(scores, dim=-1)
        out = (attn @ V).transpose(1, 2).contiguous().view(B, N, -1)
        return self.W_o(out)

2.3 前馈网络与SwiGLU

标准FFN

原始Transformer的FFN是两层全连接+ReLU:

python 复制代码
class StandardFFN(nn.Module):
    def __init__(self, d_model=512, d_ff=2048):
        super().__init__()
        self.fc1 = nn.Linear(d_model, d_ff)
        self.fc2 = nn.Linear(d_ff, d_model)
    
    def forward(self, x):
        return self.fc2(torch.relu(self.fc1(x)))

SwiGLU(LLaMA使用)

SwiGLU用门控机制替代ReLU,效果更好:

python 复制代码
class SwiGLUFFN(nn.Module):
    """SwiGLU FFN --- LLaMA/Qwen/DeepSeek的标配"""
    def __init__(self, d_model=4096, d_ff=14336):
        super().__init__()
        self.w1 = nn.Linear(d_model, d_ff, bias=False)  # gate
        self.w2 = nn.Linear(d_ff, d_model, bias=False)   # down
        self.w3 = nn.Linear(d_model, d_ff, bias=False)   # up
    
    def forward(self, x):
        # SwiGLU = (x @ W1) * SiLU(x @ W3) @ W2
        return self.w2(F.silu(self.w1(x)) * self.w3(x))

# SwiGLU vs ReLU对比
x = torch.randn(1, 10, 4096)
relu_out = torch.relu(x)
swiglu_out = F.silu(x) * x  # 简化示意
print(f"ReLU输出: 均值={relu_out.mean():.4f}, 非零比例={(relu_out > 0).float().mean():.2%}")
print(f"SiLU输出: 均值={swiglu_out.mean():.4f}, 非零比例=100%")  # SiLU没有完全归零

SwiGLU优势:

  1. 门控机制提供更丰富的非线性
  2. 没有ReLU的"死神经元"问题
  3. 梯度流更平滑

2.4 残差连接与层归一化

Pre-LN vs Post-LN

python 复制代码
class PostLNBlock(nn.Module):
    """Post-LN: 原始Transformer使用,训练不稳定"""
    def __init__(self, d_model):
        super().__init__()
        self.attn = SelfAttention(d_model)
        self.ln1 = nn.LayerNorm(d_model)
        self.ffn = SwiGLUFFN(d_model)
        self.ln2 = nn.LayerNorm(d_model)
    
    def forward(self, x):
        x = self.ln1(x + self.attn(x))   # 先残差,后归一化
        x = self.ln2(x + self.ffn(x))
        return x

class PreLNBlock(nn.Module):
    """Pre-LN: 现代LLM使用,训练更稳定"""
    def __init__(self, d_model):
        super().__init__()
        self.attn = SelfAttention(d_model)
        self.ln1 = nn.LayerNorm(d_model)
        self.ffn = SwiGLUFFN(d_model)
        self.ln2 = nn.LayerNorm(d_model)
    
    def forward(self, x):
        x = x + self.attn(self.ln1(x))   # 先归一化,后残差
        x = x + self.ffn(self.ln2(x))
        return x

Pre-LN更稳定的原因:残差路径上没有归一化操作,梯度可以无损回传。

RMSNorm

python 复制代码
class RMSNorm(nn.Module):
    """RMSNorm --- LLaMA/Qwen/DeepSeek使用,比LayerNorm快~10%"""
    def __init__(self, d_model, eps=1e-6):
        super().__init__()
        self.weight = nn.Parameter(torch.ones(d_model))
        self.eps = eps
    
    def forward(self, x):
        rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
        return self.weight * (x / rms)

# 对比LayerNorm和RMSNorm
# LayerNorm: 减均值 → 除标准差 → 缩放
# RMSNorm: 只除均方根 → 缩放(省去均值计算)

2.5 因果掩码与Padding掩码

python 复制代码
def create_padding_mask(seq, pad_idx=0):
    """Padding掩码: 屏蔽padding token"""
    # [batch, seq_len] → [batch, 1, 1, seq_len]
    return (seq != pad_idx).unsqueeze(1).unsqueeze(2)

def create_causal_mask(seq_len):
    """因果掩码: 防止看到未来token"""
    return torch.tril(torch.ones(seq_len, seq_len)).bool()

# 组合使用
seq = torch.tensor([[1, 2, 3, 0, 0], [4, 5, 6, 7, 8]])  # 0是padding
pad_mask = create_padding_mask(seq)
causal_mask = create_causal_mask(5)
combined_mask = pad_mask & causal_mask  # 同时满足两个条件

print("因果掩码:")
print(causal_mask.int())
# [[1,0,0,0,0],
#  [1,1,0,0,0],
#  [1,1,1,0,0],
#  [1,1,1,1,0],
#  [1,1,1,1,1]]

2.6 参数量与FLOPs计算

参数量计算

python 复制代码
def calc_llama_params(vocab_size=128256, d_model=4096, n_heads=32,
                      n_kv_heads=8, d_ff=14336, n_layers=32):
    """精确计算LLaMA3-8B参数量"""
    d_k = d_model // n_heads
    
    # 注意力层
    q_params = d_model * (n_heads * d_k)      # Q投影
    k_params = d_model * (n_kv_heads * d_k)   # K投影(GQA)
    v_params = d_model * (n_kv_heads * d_k)   # V投影(GQA)
    o_params = (n_heads * d_k) * d_model      # O投影
    attn_params = q_params + k_params + v_params + o_params
    
    # FFN层(SwiGLU: w1 + w2 + w3)
    ffn_params = d_model * d_ff * 3
    
    # 归一化层
    norm_params = d_model * 2  # 每层2个RMSNorm
    
    # 每层总参数
    per_layer = attn_params + ffn_params + norm_params
    
    # 全局参数
    embedding = vocab_size * d_model
    final_norm = d_model
    
    total = per_layer * n_layers + embedding + final_norm
    return {
        "total_B": total / 1e9,
        "attn_per_layer_M": attn_params / 1e6,
        "ffn_per_layer_M": ffn_params / 1e6,
        "embedding_M": embedding / 1e6,
    }

result = calc_llama_params()
print(f"LLaMA3-8B参数量: {result['total_B']:.2f}B")
# 输出约8.03B

FLOPs计算

python 复制代码
def calc_llama_flops(seq_len, d_model=4096, n_layers=32, vocab_size=128256):
    """估算LLaMA前向传播FLOPs"""
    # 注意力: 4*seq_len*d_model^2 (Q/K/V/O投影) + seq_len^2*d_model (注意力计算)
    attn_flops = 4 * seq_len * d_model**2 + 2 * seq_len**2 * d_model
    # FFN: 6*seq_len*d_model*d_ff (SwiGLU有3个矩阵)
    ffn_flops = 6 * seq_len * d_model * 14336
    # 每层
    per_layer = attn_flops + ffn_flops
    # 全部层 + 最终LM头
    total = n_layers * per_layer + 2 * seq_len * d_model * vocab_size
    return total

flops = calc_llama_flops(seq_len=2048)
print(f"LLaMA3-8B前向FLOPs: {flops/1e12:.2f}T")

2.7 本章面试题精讲

Q1(高频):请详细解释Self-Attention的计算流程,写出数学公式。

:Self-Attention计算分5步:

  1. 线性投影:Q=W_q·X, K=W_k·X, V=W_v·X
  2. 计算分数:scores = Q·K^T / √d_k
  3. 应用掩码:scores = mask(scores)
  4. Softmax:attn = softmax(scores)
  5. 加权求和:output = attn·V

数学公式:Attention(Q,K,V) = softmax(QK^T/√d_k)·V

Q2(高频):为什么注意力分数要除以√d_k?

:当d_k较大时,Q和K的点积方差为d_k(假设Q、K各元素独立,方差为1)。点积值过大会导致softmax进入饱和区,梯度接近0。除以√d_k将方差归一化为1,保证softmax梯度正常。

面试加分:可以手推方差。Var(Q·K^T) = d_k × Var(q_i × k_i) = d_k,所以除以√d_k后方差变为1。

Q3(高频):MHA、GQA、MQA的区别?LLaMA为什么选择GQA?

方案 Q头数 KV头数 KV Cache 质量 代表
MHA 32 32 1x 最好 GPT-3
GQA 32 8 1/4 接近MHA LLaMA3
MQA 32 1 1/32 略差 PaLM

GQA是MHA和MQA的折中:KV Cache减少4倍,质量损失几乎不可测。LLaMA选择GQA是因为在推理效率和模型质量之间取得了最佳平衡。

Q4(高频):Pre-LN和Post-LN哪个更好?为什么?

:Pre-LN更好。Post-LN在残差路径上有LayerNorm,导致梯度需要通过归一化层回传,深层网络训练不稳定。Pre-LN的残差路径是"干净"的(无归一化),梯度可以直接回传,训练更稳定。现代LLM(LLaMA/Qwen/DeepSeek)全部使用Pre-LN。

Q5(中频):RMSNorm相比LayerNorm有什么优势?

:RMSNorm省去了均值计算,只除均方根。优势:①计算量减少约10%;②效果与LayerNorm相当;③去均值操作对Transformer不是必须的(残差连接已保持均值稳定)。

Q6(高频):SwiGLU相比标准ReLU FFN有什么优势?

  1. 门控机制提供更丰富的非线性变换
  2. 没有ReLU的"死神经元"问题(SiLU不会完全归零)
  3. 梯度流更平滑,训练更稳定
  4. 实验证明在相同参数量下,SwiGLU比ReLU/GELU效果提升1-3%

代价:需要3个权重矩阵(w1/w2/w3)而非2个,参数量增加50%,但效果提升值得。

Q7(高频):残差连接的作用是什么?去掉会怎样?

:残差连接的核心作用:

  1. 梯度直通:梯度可以通过残差路径直接回传,不受中间层影响
  2. 恒等映射:最差情况下,残差块学成恒等映射(F(x)=0),网络不会退化
  3. 信息保留:每层的输出包含原始输入,信息不会丢失

去掉残差连接:深层网络(>10层)几乎无法训练,梯度消失导致底层参数不更新。

Q8(中频):FFN层的维度为什么是4倍d_model?

:原始Transformer设d_ff=4×d_model,这是经验值。更大的FFN提供更强的非线性表达能力,但参数量也线性增长。现代LLM的d_ff/d_model比例:LLaMA约3.5倍(14336/4096),这个比例在参数效率和表达能力之间取得了平衡。

Q9(高频):因果掩码(Causal Mask)的作用和实现?

:因果掩码防止Decoder在生成时"偷看"未来token。实现:用下三角矩阵(对角线及以下为1,以上为0),将上三角位置的注意力分数设为-inf,softmax后变为0。

Q10(高频):Transformer的参数量如何计算?

:以LLaMA3-8B为例:

  • 注意力层:Q/K/V/O四个投影矩阵,约d_model²×4(GQA时K/V更小)
  • FFN层:SwiGLU三个矩阵,约d_model×d_ff×3
  • 归一化层:每层2个RMSNorm,参数可忽略
  • 嵌入层:vocab_size×d_model
  • 总参数 ≈ n_layers × (attn + ffn + norm) + embedding + final_norm

Q11(中频):为什么现代LLM的注意力层不用偏置(bias=False)?

:①偏置在注意力层中效果不明显(Q/K/V投影的偏置会被softmax抵消);②去掉偏置减少参数量和计算量;③有利于量化(偏置在低精度下可能导致数值不稳定)。LLaMA/Qwen/DeepSeek全部使用bias=False。

Q12(中频):Transformer的推理流程分为哪两个阶段?

  1. Prefill阶段:处理完整输入prompt,计算所有token的KV Cache,计算密集型
  2. Decode阶段:逐个生成新token,每步只需计算1个新token的Q与已有KV Cache的注意力,显存带宽密集型

Prefill影响首token延迟(TTFT),Decode影响生成速度(tokens/sec)。


第3章 注意力机制进阶

3.1 GQA与MQA:减少KV Cache

为什么需要减少KV Cache?

python 复制代码
# KV Cache显存计算
def kv_cache_size(batch_size, seq_len, n_layers, n_kv_heads, d_k, dtype_bytes=2):
    """计算KV Cache显存占用"""
    per_token_per_layer = 2 * n_kv_heads * d_k * dtype_bytes  # K+V
    total = batch_size * seq_len * n_layers * per_token_per_layer
    return total

# LLaMA3-70B (80层, GQA: 8 KV头, d_k=128)
mha_cache = kv_cache_size(1, 128000, 80, 64, 128)  # MHA: 64 KV头
gqa_cache = kv_cache_size(1, 128000, 80, 8, 128)   # GQA: 8 KV头

print(f"MHA KV Cache: {mha_cache/1e9:.1f} GB")
print(f"GQA KV Cache: {gqa_cache/1e9:.1f} GB")
print(f"GQA节省: {(1 - gqa_cache/mha_cache)*100:.0f}%")

MHA / GQA / MQA对比

python 复制代码
class GroupedQueryAttention(nn.Module):
    """GQA实现 --- LLaMA3/Qwen2使用"""
    def __init__(self, d_model=4096, n_heads=32, n_kv_heads=8):
        super().__init__()
        self.n_heads = n_heads
        self.n_kv_heads = n_kv_heads
        self.d_k = d_model // n_heads
        self.n_rep = n_heads // n_kv_heads  # 每个KV头被几个Q头共享
        
        self.W_q = nn.Linear(d_model, n_heads * self.d_k, bias=False)
        self.W_k = nn.Linear(d_model, n_kv_heads * self.d_k, bias=False)
        self.W_v = nn.Linear(d_model, n_kv_heads * self.d_k, bias=False)
        self.W_o = nn.Linear(n_heads * self.d_k, d_model, bias=False)
    
    def forward(self, x, mask=None):
        B, N, _ = x.shape
        Q = self.W_q(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        K = self.W_k(x).view(B, N, self.n_kv_heads, self.d_k).transpose(1, 2)
        V = self.W_v(x).view(B, N, self.n_kv_heads, self.d_k).transpose(1, 2)
        
        # 扩展KV头以匹配Q头数
        K = K.unsqueeze(2).expand(-1, -1, self.n_rep, -1, -1).reshape(B, self.n_heads, N, self.d_k)
        V = V.unsqueeze(2).expand(-1, -1, self.n_rep, -1, -1).reshape(B, self.n_heads, N, self.d_k)
        
        scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        attn = torch.softmax(scores, dim=-1)
        out = (attn @ V).transpose(1, 2).contiguous().view(B, N, -1)
        return self.W_o(out)
方案 Q头数 KV头数 KV Cache 质量 代表模型
MHA 32 32 最好 GPT-3, BERT
GQA 32 8 1/4 接近MHA LLaMA3, Qwen2
MQA 32 1 1/32 略差 PaLM, StarCoder

3.2 MLA(DeepSeek多头潜在注意力)

MLA是DeepSeek-V2/V3的核心创新,将KV压缩到低维空间:

python 复制代码
class MultiHeadLatentAttention(nn.Module):
    """MLA简化实现 --- DeepSeek-V2/V3"""
    def __init__(self, d_model=4096, n_heads=128, kv_compress_dim=512):
        super().__init__()
        self.n_heads = n_heads
        self.d_k = d_model // n_heads
        self.kv_compress_dim = kv_compress_dim
        
        # Q投影(正常维度)
        self.W_q = nn.Linear(d_model, n_heads * self.d_k, bias=False)
        # KV压缩投影(降维!)
        self.W_kv_down = nn.Linear(d_model, kv_compress_dim, bias=False)
        # KV恢复投影
        self.W_k_up = nn.Linear(kv_compress_dim, n_heads * self.d_k, bias=False)
        self.W_v_up = nn.Linear(kv_compress_dim, n_heads * self.d_k, bias=False)
        self.W_o = nn.Linear(n_heads * self.d_k, d_model, bias=False)
    
    def forward(self, x, mask=None):
        B, N, _ = x.shape
        Q = self.W_q(x).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        
        # KV压缩:只缓存低维表示
        kv_latent = self.W_kv_down(x)  # [B, N, 512] 而非 [B, N, 4096]
        K = self.W_k_up(kv_latent).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        V = self.W_v_up(kv_latent).view(B, N, self.n_heads, self.d_k).transpose(1, 2)
        
        scores = Q @ K.transpose(-2, -1) / math.sqrt(self.d_k)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        attn = torch.softmax(scores, dim=-1)
        out = (attn @ V).transpose(1, 2).contiguous().view(B, N, -1)
        return self.W_o(out)

# MLA的KV Cache节省
# MHA: 缓存 [n_heads, d_k] = 128 * 32 = 4096 维
# MLA: 缓存 kv_compress_dim = 512 维
# 节省: 4096/512 = 8倍!

MLA优势:KV Cache减少5-8倍,质量接近MHA,远优于MQA。

3.3 FlashAttention原理与演进

核心思想

FlashAttention通过**分块计算(Tiling)**减少HBM(高带宽显存)访问:

python 复制代码
# 标准注意力 vs FlashAttention的内存访问对比
# 标准注意力:
#   1. QK^T → 写回HBM (N×N矩阵)
#   2. softmax → 读HBM → 写回HBM
#   3. attn@V → 读HBM → 写回HBM
#   总HBM访问: O(N²d) --- 注意力矩阵是瓶颈

# FlashAttention:
#   1. 将Q/K/V分成小块(适配SRAM大小)
#   2. 在SRAM中完成注意力计算(不写回HBM)
#   3. 只将最终结果写回HBM
#   总HBM访问: O(N²d²/M) --- M是SRAM大小,大幅减少

# 实际使用(PyTorch 2.0+自带)
import torch.nn.functional as F

# 自动选择最优注意力实现
output = F.scaled_dot_product_attention(
    query, key, value,
    attn_mask=None,
    is_causal=True,  # 因果掩码
)
# PyTorch会自动选择: FlashAttention > Memory-Efficient > 标准

FlashAttention演进

版本 核心改进 GPU利用率
FA1 分块计算+在线Softmax ~50%
FA2 优化warp分工+序列并行 ~72%
FA3 支持FP8+Hopper架构优化 ~90%

3.4 滑动窗口注意力(SWA)

python 复制代码
class SlidingWindowAttention(nn.Module):
    """滑动窗口注意力 --- Mistral使用"""
    def __init__(self, d_model, n_heads, window_size=4096):
        super().__init__()
        self.attn = MultiHeadAttention(d_model, n_heads)
        self.window_size = window_size
    
    def forward(self, x):
        B, N, D = x.shape
        # 创建滑动窗口掩码
        mask = torch.zeros(N, N, dtype=torch.bool)
        for i in range(N):
            start = max(0, i - self.window_size + 1)
            mask[i, start:i+1] = True
        
        mask = mask.unsqueeze(0).unsqueeze(0).expand(B, self.attn.n_heads, -1, -1)
        return self.attn(x, mask=mask)

# SWA的优势:
# 1. 注意力复杂度从O(N²)降到O(N×W)
# 2. 通过多层堆叠间接捕捉长距离依赖
#    - 第1层:窗口W内可见
#    - 第L层:感受野 = L × W
# 3. 推理时KV Cache只需保存窗口内的KV

3.5 线性注意力与长序列优化

标准注意力的O(N²)复杂度是长序列的瓶颈。线性注意力将复杂度降到O(N):

python 复制代码
# 线性注意力的核心思想
# 标准: softmax(QK^T)V → O(N²)
# 线性: φ(Q)(φ(K)^T V) → O(Nd²),先算K^T V (d×d),再乘Q

def linear_attention(Q, K, V, kernel_fn=None):
    """线性注意力简化实现"""
    if kernel_fn is None:
        # ELU+1核函数
        kernel_fn = lambda x: F.elu(x) + 1
    
    Q_prime = kernel_fn(Q)
    K_prime = kernel_fn(K)
    
    # 先算 K^T V: [d, d] --- 与序列长度无关!
    KV = K_prime.transpose(-2, -1) @ V  # [d, d]
    
    # 再算 Q (K^T V): [N, d]
    output = Q_prime @ KV
    
    # 归一化
    normalizer = Q_prime @ K_prime.transpose(-2, -1).sum(dim=-1, keepdim=True)
    output = output / normalizer
    
    return output

# 线性注意力为什么还没成为主流?
# 1. 核函数近似引入误差,质量不如标准注意力
# 2. softmax的指数运算难以精确用核函数近似
# 3. 实际加速受限于d²矩阵计算和内存访问模式

3.6 注意力机制选型决策

复制代码
选择注意力类型的决策流程:

1. 序列长度 < 8K?
   → 标准MHA(简单可靠)
   
2. 序列长度 8K-128K?
   → GQA + FlashAttention(主流选择)
   
3. 需要极长上下文 > 128K?
   → GQA + FlashAttention + KV Cache量化
   
4. 显存极度受限?
   → MLA(DeepSeek-V2/V3)或MQA
   
5. 需要极低延迟?
   → SWA(Mistral)+ 投机解码
   
6. 追求极致质量?
   → MHA + FlashAttention(不计成本)

3.7 本章面试题精讲

Q1(高频):Self-Attention的计算复杂度是多少?瓶颈在哪?

:时间复杂度O(N²d),空间复杂度O(N²+N×d)。瓶颈在QK^T的N×N注意力矩阵------序列长度翻倍,计算量翻4倍。这就是为什么长上下文(128K+)需要FlashAttention、GQA等优化。

Q2(高频):FlashAttention的原理是什么?为什么快?

:FlashAttention通过分块计算(Tiling)在GPU的SRAM中完成注意力计算,避免将中间结果写回HBM。快的原因不是减少FLOPs,而是减少HBM访问次数(从O(N²d)降到O(N²d²/M))。内存带宽才是瓶颈,不是计算。

Q3(高频):MHA、GQA、MQA的区别?如何选择?

:MHA每个Q头有独立KV头,GQA将KV头分组共享,MQA所有Q头共享1个KV头。选择:追求质量选MHA,追求效率选GQA(主流),极端效率选MQA。LLaMA3-8B用GQA(8 KV头/32 Q头)。

Q4(中频):MLA解决了什么问题?

:MLA(DeepSeek-V2/V3)将KV Cache压缩到低维空间,减少5-8倍KV Cache显存,同时保持接近MHA的质量。核心创新:只缓存压缩后的KV潜在向量,推理时再恢复到完整维度。

Q5(中频):滑动窗口注意力(SWA)的原理和适用场景?

:SWA限制每个token只关注窗口W内的token,复杂度O(N×W)。通过多层堆叠间接捕捉长距离依赖(L层感受野=L×W)。适合:需要低延迟推理且长距离依赖不强的场景(如Mistral-7B)。

Q6(高频):为什么注意力分数要除以√d_k?不除会怎样?

:d_k较大时点积方差为d_k,导致softmax饱和、梯度消失。除以√d_k后方差归一化为1。不除的后果:softmax输出接近one-hot,梯度极小,训练无法收敛。

Q7(中频):Padding Mask和Causal Mask的区别?

:Padding Mask屏蔽padding token(值为0的位置),防止模型关注无意义内容。Causal Mask防止看到未来token(下三角矩阵)。两者可以组合使用:combined = pad_mask & causal_mask。

Q8(低频):什么是线性注意力?为什么还没成为主流?

:线性注意力用核函数近似softmax,将复杂度从O(N²)降到O(Nd²)。未成为主流的原因:①核函数近似引入误差,质量不如标准注意力;②softmax的指数运算难以精确近似;③实际加速受限于d²矩阵计算。目前FlashAttention的工程优化更实用。

Q9(高频):FlashAttention-2相比FlashAttention-1有什么改进?

:三个改进:①减少非矩阵乘法运算(GPU上matmul效率远高于其他运算);②改善warp间工作分配(减少warp间同步等待);③支持序列维度并行(FA1只支持batch和head并行)。GPU利用率从50%提升到72%。

Q10(中频):KV Cache在GQA中如何工作?

:GQA中多个Q头共享同一组KV头,因此KV Cache只需存储n_kv_heads组KV而非n_heads组。LLaMA3-8B:32个Q头共享8个KV头,KV Cache减少4倍。推理时,共享同一KV头的Q头复用相同的KV Cache。

Q11(中频):DeepSeek-V2的MLA为什么能同时减少KV Cache和保持质量?

:MLA将KV压缩到低维潜在空间(如512维),推理时再恢复到完整维度。能保持质量的原因:①压缩是可学习的(通过训练优化压缩矩阵);②KV的大部分信息是冗余的,低维表示足以保留关键信息;③RoPE与压缩解耦,避免位置编码影响压缩效率。

Q12(低频):如何选择注意力类型?给出决策流程。

:见3.6节决策流程。核心原则:短序列用MHA,长序列用GQA+FlashAttention,显存受限用MLA/MQA,低延迟用SWA,追求质量不计成本用MHA。


第4章 位置编码

4.1 为什么需要位置编码

Transformer的自注意力是排列不变的------打乱输入顺序,输出不变。这意味着"猫吃鱼"和"鱼吃猫"对Transformer来说没有区别。位置编码就是给模型注入顺序信息。

python 复制代码
# 证明:Self-Attention是排列不变的
import torch

x = torch.randn(1, 4, 8)  # [batch, seq, dim]
perm = torch.tensor([2, 0, 3, 1])  # 打乱顺序

# 原始输入的注意力输出
attn_output_original = F.softmax(x @ x.transpose(-2, -1) / math.sqrt(8), dim=-1) @ x

# 打乱后的注意力输出
x_perm = x[:, perm, :]
attn_output_perm = F.softmax(x_perm @ x_perm.transpose(-2, -1) / math.sqrt(8), dim=-1) @ x_perm

# 如果恢复顺序,两者应该相同
attn_output_restored = attn_output_perm[:, torch.argsort(perm), :]
print(f"差异: {(attn_output_original - attn_output_restored).abs().max():.6f}")
# 输出约0.0 --- 证明排列不变

4.2 正弦位置编码

原始Transformer使用的固定位置编码:

python 复制代码
class SinusoidalPositionalEncoding(nn.Module):
    def __init__(self, d_model: int, max_len: int = 5000):
        super().__init__()
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1).float()
        div_term = torch.exp(
            torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
        )
        pe[:, 0::2] = torch.sin(position * div_term)  # 偶数维度
        pe[:, 1::2] = torch.cos(position * div_term)  # 奇数维度
        self.register_buffer('pe', pe.unsqueeze(0))
    
    def forward(self, x):
        return x + self.pe[:, :x.size(1)]

# 正弦编码的数学性质:PE(pos+k)可以表示为PE(pos)的线性变换
# 即存在矩阵M_k,使得 PE(pos+k) = M_k · PE(pos)
# 这意味着模型可以通过学习线性变换来捕捉相对位置关系

4.3 RoPE旋转位置编码详解

RoPE是目前主流LLM(LLaMA、Qwen、Mistral等)的标准选择。核心思想:通过旋转矩阵编码相对位置。

数学原理

RoPE将位置信息融入Q和K的旋转中,使得Q·K^T只依赖相对位置(m-n):

python 复制代码
class RotaryPositionalEmbedding(nn.Module):
    """RoPE --- 旋转位置编码"""
    def __init__(self, dim: int, max_seq_len: int = 8192, base: float = 10000.0):
        super().__init__()
        inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
        self.register_buffer('inv_freq', inv_freq)
    
    def forward(self, x: torch.Tensor, seq_len: int):
        t = torch.arange(seq_len, device=x.device, dtype=self.inv_freq.dtype)
        freqs = torch.outer(t, self.inv_freq)
        emb = torch.cat([freqs, freqs], dim=-1)
        return emb.cos(), emb.sin()

def apply_rotary_emb(x, cos, sin):
    """应用旋转位置编码"""
    d = x.shape[-1] // 2
    x1, x2 = x[..., :d], x[..., d:]
    # 旋转: [x1, x2] → [x1*cos - x2*sin, x1*sin + x2*cos]
    rotated = torch.cat([
        x1 * cos[..., :d] - x2 * sin[..., :d],
        x1 * sin[..., :d] + x2 * cos[..., :d]
    ], dim=-1)
    return rotated

# RoPE为什么能编码相对位置?
# 设位置m的向量为q_m,位置n的向量为k_n
# q_m · k_n = R_m * q · R_n * k = q · R_(m-n) * k
# 内积只依赖相对位置m-n!

RoPE的优势

  1. 显式相对位置:内积只依赖相对位置,无需额外学习
  2. 远程衰减:远距离token的注意力权重自然衰减
  3. 可扩展:通过NTK缩放支持更长上下文

4.4 ALiBi位置编码

ALiBi不使用位置嵌入,而是在注意力分数上添加线性偏置:

python 复制代码
def alibi_bias(n_heads: int, seq_len: int) -> torch.Tensor:
    """ALiBi线性偏置矩阵"""
    # 每个头的斜率:2^(-8i/n_heads)
    slopes = torch.tensor([2 ** (-8 * i / n_heads) for i in range(n_heads)])
    
    # 位置距离矩阵
    pos = torch.arange(seq_len)
    dist = pos.unsqueeze(0) - pos.unsqueeze(1)  # [seq, seq]
    
    # 偏置 = slope * distance
    bias = slopes.unsqueeze(1).unsqueeze(1) * dist.unsqueeze(0)
    return bias  # [n_heads, seq, seq]

# ALiBi的使用:在注意力分数上直接加偏置
# scores = Q @ K^T / √d_k + alibi_bias

ALiBi优势:天然支持长度外推(训练短序列也能推理长序列)。

4.5 上下文扩展:NTK缩放与YaRN

NTK-aware缩放

RoPE在超出训练长度后,注意力权重退化。NTK-aware缩放通过增大base频率解决:

python 复制代码
# NTK-aware缩放
def ntk_scaled_rope(base, scale_factor, dim):
    """计算NTK缩放后的新base"""
    # base_new = base × scale_factor^(dim/(dim-2))
    new_base = base * (scale_factor ** (dim / (dim - 2)))
    return new_base

# LLaMA3: base=10000, 支持8K → 扩展到128K
# scale_factor = 128000/8192 = 15.625
# new_base = 10000 × 15.625^(4096/4094) ≈ 500000
# LLaMA3实际使用rope_theta=500000

YaRN

YaRN(Yet another RoPE extensioN method)结合三种技术:

python 复制代码
# YaRN的核心思想:对不同频率分量使用不同缩放策略
# 高频分量(近距离):不缩放(保持精度)
# 低频分量(远距离):缩放(支持更长距离)
# 中频分量:混合缩放

def yarn_scale_factor(base_freq, scale_factor, dim, low_freq_factor=0.1, high_freq_factor=20.0):
    """YaRN的频率感知缩放"""
    wavelength = 2 * math.pi / base_freq
    
    if wavelength < low_freq_factor:  # 高频:不缩放
        return 1.0
    elif wavelength > high_freq_factor:  # 低频:全缩放
        return scale_factor
    else:  # 中频:平滑过渡
        smooth = (wavelength - low_freq_factor) / (high_freq_factor - low_freq_factor)
        return 1.0 / (1.0 + smooth * (scale_factor - 1.0))

4.6 位置编码对比与选型

编码方式 可扩展性 相对位置 代表模型 推荐度
正弦编码 隐式 原始Transformer ★★
可学习编码 GPT-2/3, BERT ★★
RoPE 显式 LLaMA, Qwen, Mistral ★★★★★
ALiBi 极好 显式 BLOOM, MPT ★★★

核心结论:RoPE已成为主流LLM的标配,它既编码了相对位置信息,又支持通过NTK缩放扩展上下文长度。


4.7 本章面试题精讲

Q1(高频):RoPE为什么能编码相对位置?数学原理是什么?

:RoPE通过旋转矩阵将位置m编码到Q中、位置n编码到K中。当计算Q·KT时,两个旋转矩阵的乘积等价于一个相对位置(m-n)的旋转,因此内积只依赖相对位置。数学上:R_mT · R_n = R_{n-m}。

Q2(高频):为什么RoPE比正弦位置编码更适合长上下文?

:三个原因:①RoPE显式编码相对位置(正弦编码只是隐式的线性关系);②RoPE有远程衰减特性(远距离token注意力自然减弱);③RoPE支持NTK缩放扩展上下文(正弦编码无法有效扩展)。

Q3(中频):ALiBi如何在训练短序列时实现长序列推理?

:ALiBi在注意力分数上添加线性偏置(slope × distance),这个偏置不依赖训练时的序列长度。推理时序列更长,偏置自然延伸,无需额外调整。代价是长距离token的注意力被强烈抑制。

Q4(高频):RoPE的NTK-aware缩放是什么?如何实现上下文扩展?

:NTK-aware缩放不缩小位置,而是增大RoPE的base频率。公式:base_new = base × scale_factor^(dim/(dim-2))。效果:高频分量(近距离)保持不变,低频分量(远距离)被拉伸。LLaMA3使用rope_theta=500000(原始10000),支持128K上下文。

Q5(中频):RoPE和ALiBi的核心区别?各自适合什么场景?

:RoPE将位置编码在Q/K向量中(旋转),ALiBi将位置编码在注意力分数中(加偏置)。RoPE适合追求质量的主流LLM(LLaMA/Qwen),ALiBi适合需要极长上下文且不想做额外缩放调整的场景(BLOOM/MPT)。

Q6(低频):什么是YaRN?它如何改进NTK-aware缩放?

:YaRN结合NTK-aware缩放+注意力温度调整+混合缩放。关键创新:对不同频率分量使用不同缩放策略(高频不缩放保持精度,低频缩放支持长距离)。在8K→128K扩展场景下比纯NTK缩放效果提升显著。

Q7(中频):位置编码在GQA/MQA中有什么特殊处理?

:RoPE只应用于Q和K,不应用于V(V不参与注意力分数计算)。DeepSeek-V2的MLA中,RoPE需要特殊处理------将RoPE从压缩的KV中分离出来,只对Q应用RoPE,避免位置编码影响KV的压缩效率。

Q8(中频):可学习位置编码和固定位置编码哪个更好?

:RoPE(当前主流)> 可学习 > 正弦。可学习位置编码灵活但受限于训练最大长度,无法外推。RoPE兼具可学习性和外推能力,通过旋转矩阵显式编码相对位置。

Q9(低频):为什么RoPE的base频率越大,支持的上下文越长?

:旋转角度θ = pos / base^(2i/dim)。base越大,相同位置的旋转角度越小,远距离位置不会"绕圈"太多导致注意力退化。代价是近距离位置的分辨率略有下降。

Q10(低频):多模态模型如何处理位置编码?

:文本用1D-RoPE,图像可用2D-RoPE(x/y维度分别旋转),视频可用3D-RoPE。实际中,多模态模型(LLaVA/Qwen-VL)通常将图像patch展平后用1D-RoPE,简化实现。


第5章 预训练

5.1 预训练目标:CLM与MLM

CLM(因果语言模型)

GPT系列使用的训练目标,预测下一个token:

python 复制代码
def clm_loss(logits, targets):
    """CLM: 给定前N-1个token,预测第N个token"""
    # logits: [batch, seq_len, vocab_size]
    # targets: [batch, seq_len] --- 右移一位
    shift_logits = logits[:, :-1, :].contiguous()
    shift_targets = targets[:, 1:].contiguous()
    loss = F.cross_entropy(
        shift_logits.view(-1, shift_logits.size(-1)),
        shift_targets.view(-1)
    )
    return loss

# CLM特点:
# - 单向注意力(因果掩码),只能看到当前及之前的token
# - 自回归生成:训练和推理目标完全一致
# - 适合生成任务

MLM(掩码语言模型)

BERT使用的训练目标,随机遮盖15%的token并预测:

python 复制代码
def mlm_loss(logits, targets, mask):
    """MLM: 预测被[MASK]遮盖的token"""
    # mask: 标记哪些位置被遮盖
    masked_logits = logits[mask]     # 只取被遮盖位置的logits
    masked_targets = targets[mask]   # 对应的真实token
    loss = F.cross_entropy(masked_logits, masked_targets)
    return loss

# MLM特点:
# - 双向注意力,可以看到上下文
# - 不适合自回归生成(训练和推理不一致)
# - 适合理解任务(分类、NER、检索)

为什么GPT选择CLM? 训练目标(Next-Token Prediction)和推理目标完全一致,没有训练-推理gap。MLM训练时看到双向上下文,推理时只能看到单向,存在不一致。

5.2 数据淬炼:质量、配比、去污染

数据质量决定模型上限

python 复制代码
# 数据质量的影响
data_quality_impact = {
    "垃圾进垃圾出": "低质量数据直接降低模型能力",
    "数据配比": "通用文本:代码:数学 ≈ 6:2:2 是常用配比",
    "代码数据": "显著提升推理能力(逻辑思维+结构化表达)",
    "去重": "重复数据导致模型记忆而非泛化",
    "去污染": "评测集混入训练集导致虚高分数",
}

# 数据清洗流水线
def data_pipeline(raw_text):
    """数据清洗流水线"""
    text = raw_text
    # 1. 去重(MinHash/LSH)
    text = deduplicate(text)
    # 2. 质量过滤(perplexity过滤、分类器过滤)
    text = quality_filter(text)
    # 3. 去PII(个人身份信息)
    text = remove_pii(text)
    # 4. 去污染(n-gram匹配评测集)
    text = decontaminate(text)
    # 5. 分词
    tokens = tokenize(text)
    return tokens

数据配比的重要性

python 复制代码
# 不同配比的实验结果(示意)
experiments = [
    {"config": "纯网页文本", "MMLU": 42, "GSM8K": 15, "HumanEval": 20},
    {"config": "网页+10%代码", "MMLU": 44, "GSM8K": 25, "HumanEval": 45},
    {"config": "网页+20%代码+10%数学", "MMLU": 48, "GSM8K": 40, "HumanEval": 55},
    {"config": "网页+30%代码+20%数学", "MMLU": 50, "GSM8K": 52, "HumanEval": 60},
]
# 结论:代码数据对推理能力提升最显著

5.3 分布式训练:3D并行

python 复制代码
# 3D并行 = 数据并行(DP) × 张量并行(TP) × 流水线并行(PP)

# 数据并行(DP): 每卡持完整模型副本,数据分片
# - 简单,但单卡必须放得下整个模型
# - 通信:每步AllReduce梯度

# 张量并行(TP): 单个矩阵切到多卡
# - 适合单节点内多GPU(NVLink高速互联)
# - 通信:每层2次AllReduce

# 流水线并行(PP): 不同层放不同卡
# - 适合跨节点训练
# - 问题:气泡(bubble)导致GPU空闲

# LLaMA-65B训练配置
llama_65b_config = {
    "DP": 8,   # 8路数据并行
    "TP": 8,   # 8路张量并行
    "PP": 2,   # 2路流水线并行
    "total_gpus": 8 * 8 * 2,  # 128张A100
    "model_size": "65B",
    "training_tokens": "1.4T",
}

FSDP vs DDP

python 复制代码
# DDP (DistributedDataParallel)
# - 每卡持完整模型副本+优化器状态
# - 显存: 单卡需装下完整模型
# - 适合: 小模型(<7B)

# FSDP (FullyShardedDataParallel)
# - 将模型参数、梯度、优化器状态分片到多卡
# - 显存: 单卡只需1/N的参数
# - 前向/反向时AllGather需要的参数
# - 适合: 大模型(7B+)

# 选择: 模型能单卡装下 → DDP; 装不下 → FSDP

5.4 训练稳定性

python 复制代码
# 大模型训练不稳定的常见表现和解决方案

training_issues = {
    "Loss突增": {
        "原因": "学习率过大或数据异常",
        "解决": "降低学习率、数据质量检查、梯度裁剪",
    },
    "梯度爆炸": {
        "原因": "深层网络梯度指数增长",
        "解决": "梯度裁剪(clip_grad_norm)、BF16训练",
    },
    "Loss NaN": {
        "原因": "数值溢出(FP16下溢/上溢)",
        "解决": "切换BF16、添加loss scaling、检查数据",
    },
    "训练发散": {
        "原因": "学习率调度不当",
        "解决": "Warmup策略、余弦退火",
    },
}

# 梯度裁剪
def clip_gradient(model, max_norm=1.0):
    """梯度裁剪 --- 防止梯度爆炸"""
    total_norm = torch.nn.utils.clip_grad_norm_(
        model.parameters(), max_norm
    )
    return total_norm

# Warmup + Cosine Decay学习率调度
class CosineWarmupScheduler:
    def __init__(self, optimizer, warmup_steps, total_steps, min_lr=1e-5):
        self.optimizer = optimizer
        self.warmup_steps = warmup_steps
        self.total_steps = total_steps
        self.min_lr = min_lr
        self.base_lr = optimizer.param_groups[0]['lr']
    
    def step(self, current_step):
        if current_step < self.warmup_steps:
            # Warmup: 线性增长
            lr = self.base_lr * current_step / self.warmup_steps
        else:
            # Cosine decay
            progress = (current_step - self.warmup_steps) / (self.total_steps - self.warmup_steps)
            lr = self.min_lr + 0.5 * (self.base_lr - self.min_lr) * (1 + math.cos(math.pi * progress))
        
        for param_group in self.optimizer.param_groups:
            param_group['lr'] = lr

5.5 Chinchilla定律与过训练

python 复制代码
# 计算训练所需资源
def estimate_training_resources(params_B, tokens_B, gpu_type="A100"):
    """估算训练所需GPU资源"""
    # FLOPs ≈ 6 × 参数量 × token数
    total_flops = 6 * params_B * 1e9 * tokens_B * 1e9
    
    # GPU算力
    gpu_flops = {"A100": 312e12, "H100": 990e12}  # BF16 FLOPS
    gpu_hours = total_flops / (gpu_flops[gpu_type] * 3600)
    
    # MFU (Model FLOPs Utilization) 通常40-50%
    gpu_hours_actual = gpu_hours / 0.45
    
    return {
        "total_FLOPs": f"{total_flops:.2e}",
        f"{gpu_type}_hours_theoretical": f"{gpu_hours:.0f}",
        f"{gpu_type}_hours_actual_MFU45": f"{gpu_hours_actual:.0f}",
        f"cost_1000GPU_{gpu_type}": f"{gpu_hours_actual/1000:.0f} hours",
    }

# LLaMA-7B: 1T tokens
print(estimate_training_resources(7, 1000))
# 约82,000 A100小时 ≈ 2048卡×40小时

# DeepSeek-V3: 14.8T tokens, 671B参数
print(estimate_training_resources(671, 14800))
# 约557万美元(2048×H800)

5.6 本章面试题精讲

Q1(高频):CLM和MLM各有什么优劣?为什么GPT选择CLM?

:CLM单向注意力,适合生成,训练-推理一致;MLM双向注意力,适合理解,训练-推理不一致。GPT选择CLM因为训练目标(Next-Token Prediction)和推理目标完全一致,没有gap。

Q2(高频):FSDP相比DDP有什么优势?在什么场景下必须用FSDP?

:FSDP将模型参数、梯度、优化器状态分片到多卡,单卡显存从完整模型降到1/N。DDP每卡需装完整模型。当模型>单卡显存(如7B模型FP16需14GB,加上优化器>28GB)时必须用FSDP。

Q3(中频):预训练1T token的7B模型需要多少计算量?如何估算?

:FLOPs ≈ 6 × 7B × 1T = 4.2×10²² FLOPs。A100 BF16算力312TFLOPS,MFU约45%,实际需约82,000 A100小时。2048张A100约需40小时。

Q4(高频):数据质量对模型有什么影响?为什么说"数据质量>数据数量>模型参数"?

:数据质量决定模型能力上限------垃圾进垃圾出。高质量数据让模型学到真实模式,低质量数据让模型学到噪声。Chinchilla定律证明数据量很重要,但LLaMA实践证明高质量数据+过训练比单纯增大参数更有效。

Q5(中频):什么是数据污染?如何检测?

:训练数据包含评测集内容,导致模型"背到了题"。检测方法:①n-gram重叠检测(13-gram);②用模型生成评测题目看能否复现;③对比污染前后的性能差异。避免:训练前严格去重和N-gram过滤。

Q6(高频):3D并行是什么?各解决什么问题?

:DP(数据并行)解决数据量大问题,TP(张量并行)解决单卡放不下模型的问题,PP(流水线并行)解决模型太深的问题。组合使用:LLaMA-65B用DP=8,TP=8,PP=2,共128张A100。

Q7(中频):什么是梯度累积?为什么需要?

:将多个小batch梯度累积后再更新参数,模拟大batch训练。原因:大模型训练需要大batch(如4M tokens),但单卡显存放不下。accumulation_steps=4等效batch扩大4倍。

Q8(中频):BF16和FP16在训练中如何配合?

:混合精度训练------前向用BF16(快、省显存),损失计算和梯度更新用FP32(精度高),优化器状态用FP32。BF16比FP16数值范围大,不需要loss scaling,训练更稳定。

Q9(低频):什么是Warmup?为什么大模型训练需要?

:Warmup是训练初期用较小学习率逐步增大到目标值。大模型参数随机初始化,初期梯度方向不稳定,大学习率可能导致训练发散。通常Warmup 2000-10000步。

Q10(中频):代码数据为什么能提升推理能力?

:代码具有强逻辑结构(条件分支、循环、函数调用),训练代码数据让模型学到:①逐步推理能力(类似Chain-of-Thought);②结构化表达能力;③调试和纠错能力。实验证明10-20%代码数据显著提升数学和推理基准。


第6章 微调与对齐

6.1 SFT监督微调

SFT(Supervised Fine-Tuning)将基座模型转化为对话模型,核心是高质量指令数据。

python 复制代码
# SFT数据格式
sft_example = {
    "instruction": "解释什么是梯度下降",
    "input": "",
    "output": "梯度下降是一种优化算法,通过沿梯度反方向迭代更新参数来最小化损失函数..."
}

# SFT训练代码
def sft_train(model, tokenizer, dataset, epochs=3, lr=2e-5):
    model.train()
    optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
    
    for epoch in range(epochs):
        for batch in dataset:
            # 构造输入: instruction + output
            input_text = f"### Instruction:\n{batch['instruction']}\n\n### Response:\n{batch['output']}"
            inputs = tokenizer(input_text, return_tensors="pt")
            
            # 只对output部分计算loss
            labels = inputs["input_ids"].clone()
            # 将instruction部分的label设为-100(忽略)
            instruction_len = len(tokenizer(f"### Instruction:\n{batch['instruction']}\n\n### Response:\n")["input_ids"])
            labels[:, :instruction_len] = -100
            
            outputs = model(**inputs, labels=labels)
            loss = outputs.loss
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()

SFT数据质量 > 数量

python 复制代码
# LIMA论文证明:1000条高质量SFT数据 > 50000条普通数据
# 关键:数据的多样性、正确性、格式一致性

# 高质量SFT数据的标准
sft_quality_criteria = {
    "正确性": "回答必须事实正确,无幻觉",
    "完整性": "回答完整覆盖问题,不遗漏关键点",
    "格式": "格式统一,Markdown/代码块等规范",
    "多样性": "覆盖不同任务类型和难度",
    "拒绝能力": "对无法回答的问题能正确拒绝",
}

6.2 LoRA与QLoRA

LoRA原理

LoRA(Low-Rank Adaptation)冻结原始权重,只训练低秩分解矩阵:

python 复制代码
class LoRALinear(nn.Module):
    """LoRA线性层实现"""
    def __init__(self, original_linear, r=8, alpha=16, dropout=0.0):
        super().__init__()
        self.original = original_linear
        self.original.weight.requires_grad = False  # 冻结原始权重
        
        d_in = original_linear.in_features
        d_out = original_linear.out_features
        
        self.lora_A = nn.Parameter(torch.randn(r, d_in) * 0.01)  # 降维
        self.lora_B = nn.Parameter(torch.zeros(d_out, r))         # 升维
        self.scaling = alpha / r
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, x):
        # 原始路径 + LoRA路径
        original_out = self.original(x)
        lora_out = (self.dropout(x) @ self.lora_A.T @ self.lora_B.T) * self.scaling
        return original_out + lora_out

# 参数量对比
def lora_param_count(d_model=4096, r=8):
    """计算LoRA参数量"""
    # 每个LoRA层: A(r×d) + B(d×r) = 2×r×d
    per_layer = 2 * r * d_model
    # LLaMA-7B: 32层,每层4个线性层(Q/K/V/O) + 3个FFN
    total = per_layer * 32 * 7  # 只对注意力层做LoRA
    full_params = 7e9
    return {
        "lora_params_M": total / 1e6,
        "full_params_M": full_params / 1e6,
        "ratio": f"{total/full_params*100:.2f}%",
    }

print(lora_param_count())
# LoRA参数量约7.2M,仅占0.1%

QLoRA

QLoRA在LoRA基础上将基座模型量化到4-bit:

python 复制代码
# QLoRA = 4-bit量化基座 + LoRA微调
# 核心:NF4量化 + 双重量化 + 分页优化器

qlora_config = {
    "base_model_precision": "NF4 (4-bit NormalFloat)",
    "lora_precision": "BF16",
    "compute_precision": "BF16",
    "lora_r": 64,
    "lora_alpha": 16,
    "target_modules": ["q_proj", "k_proj", "v_proj", "o_proj", 
                       "gate_proj", "up_proj", "down_proj"],
    "memory_saving": "7B模型: 28GB(FP16) → 5GB(NF4) + 0.5GB(LoRA)",
}

6.3 RLHF与PPO

RLHF(基于人类反馈的强化学习)三步流程:

python 复制代码
# Step 1: 训练奖励模型(RM)
# Step 2: 用PPO优化策略模型

class RLHFPipeline:
    def __init__(self, sft_model, reward_model, ref_model):
        self.policy = sft_model       # 待优化的策略模型
        self.reward = reward_model     # 奖励模型
        self.ref = ref_model           # 参考模型(SFT模型副本)
    
    def ppo_step(self, prompts):
        # 1. 策略模型生成回答
        responses = self.policy.generate(prompts)
        
        # 2. 奖励模型打分
        rewards = self.reward(prompts, responses)
        
        # 3. 计算KL散度惩罚(防止偏离参考模型太远)
        log_prob_policy = self.policy.log_prob(prompts, responses)
        log_prob_ref = self.ref.log_prob(prompts, responses)
        kl_penalty = 0.1 * (log_prob_policy - log_prob_ref)
        
        # 4. PPO目标函数
        advantage = rewards - kl_penalty
        ratio = torch.exp(log_prob_policy - log_prob_policy.detach())
        clipped_ratio = torch.clamp(ratio, 0.8, 1.2)
        ppo_loss = -torch.min(ratio * advantage, clipped_ratio * advantage).mean()
        
        return ppo_loss

6.4 DPO直接偏好优化

DPO绕过奖励模型,直接用偏好数据优化:

python 复制代码
def dpo_loss(policy_chosen_logps, policy_rejected_logps,
             ref_chosen_logps, ref_rejected_logps, beta=0.1):
    """DPO损失函数"""
    # 隐式奖励 = β × (log π_θ(y_w|x) - log π_ref(y_w|x))
    chosen_rewards = beta * (policy_chosen_logps - ref_chosen_logps)
    rejected_rewards = beta * (policy_rejected_logps - ref_rejected_logps)
    
    # Bradley-Terry模型下的损失
    loss = -F.logsigmoid(chosen_rewards - rejected_rewards).mean()
    return loss

# DPO vs RLHF对比
comparison = {
    "RLHF": {
        "需要奖励模型": True,
        "训练稳定性": "较差(PPO超参敏感)",
        "计算成本": "高(4个模型)",
        "数据格式": "偏好排序",
    },
    "DPO": {
        "需要奖励模型": False,
        "训练稳定性": "好(类似SFT)",
        "计算成本": "低(2个模型)",
        "数据格式": "偏好对(chosen, rejected)",
    },
}

6.5 灾难性遗忘与缓解

python 复制代码
# 灾难性遗忘:微调后模型丧失预训练能力

# 缓解方案
mitigation_strategies = {
    "LoRA": "只更新少量参数,原始能力保留在冻结权重中",
    "学习率控制": "微调用1/10~1/100的预训练学习率",
    "数据混合": "微调数据中混入10-20%通用数据",
    "EWC": "给重要参数加正则化,防止大幅修改",
    "多任务微调": "同时微调多个任务,避免偏向单一任务",
}

# 实际推荐:LoRA + 小学习率 + 数据混合

6.6 微调实战:数据构建与超参选择

python 复制代码
# 微调超参推荐
finetune_configs = {
    "Full Fine-tuning": {
        "lr": "1e-5 ~ 5e-5",
        "epochs": "2-5",
        "batch_size": "128-512",
        "适用": "数据充足(>100K), 需要最大性能",
    },
    "LoRA": {
        "lr": "1e-4 ~ 5e-4",
        "r": "8-64",
        "alpha": "16-32",
        "epochs": "3-10",
        "适用": "数据较少(<10K), 显存受限",
    },
    "QLoRA": {
        "lr": "1e-4 ~ 2e-4",
        "r": "64-256",
        "quantization": "NF4",
        "适用": "极小显存(单卡24GB微调70B)",
    },
}

# 数据构建最佳实践
data_best_practices = {
    "数量": "1K-10K高质量数据通常足够",
    "格式": "统一instruction-input-output格式",
    "多样性": "覆盖目标任务的各类场景",
    "质量": "人工审核 > GPT-4生成 > 网络爬取",
    "负样本": "包含拒绝回答的样本(防止过度顺从)",
}

6.7 本章面试题精讲

Q1(高频):LoRA的原理是什么?为什么能减少参数量?

:LoRA冻结原始权重W,只训练低秩分解矩阵A和B,输出=Wx + BAx。参数量从d×d降到2×r×d(r<<d)。有效的原因:微调时的权重变化ΔW是低秩的,不需要完整d×d矩阵来表示。

Q2(高频):LoRA的r和alpha如何选择?

:r控制秩(表达能力),alpha控制缩放(实际贡献=alpha/r×BAx)。经验:r=8-16适合简单任务,r=64-256适合复杂任务。alpha通常设为2×r。关键:r越大参数越多但可能过拟合,从小r开始逐步增大。

Q3(高频):QLoRA相比LoRA有什么改进?

:QLoRA将基座模型量化到4-bit(NF4),LoRA适配器仍用BF16。效果:7B模型显存从28GB降到5GB,单卡24GB可微调70B模型。三个创新:①NF4量化(正态分布最优量化);②双重量化(量化常数也量化);③分页优化器(防OOM)。

Q4(高频):RLHF和DPO的区别?各自优劣势?

:RLHF需要训练奖励模型+PPO优化,DPO直接用偏好数据优化。RLHF优势:奖励模型可复用、可在线学习。DPO优势:无需奖励模型、训练稳定、实现简单。当前趋势:DPO成为主流(简单有效),RLHF在需要在线学习的场景仍有价值。

Q5(中频):什么是灾难性遗忘?如何缓解?

:微调后模型丧失预训练的通用能力。缓解:①LoRA(只更新少量参数);②小学习率(1e-5~5e-5);③数据混合(混入10-20%通用数据);④EWC正则化。最实用:LoRA+小学习率+数据混合。

Q6(中频):SFT数据需要多少条?质量还是数量更重要?

:LIMA论文证明1000条高质量数据>50000条普通数据。质量远比数量重要。推荐:1K-10K高质量数据,覆盖目标任务的各类场景,人工审核确保正确性。

Q7(高频):LoRA应该应用在哪些层?

:原论文只对Q/V投影做LoRA,但实践证明对所有线性层(Q/K/V/O + gate/up/down)做LoRA效果更好。原因:FFN层也包含大量任务相关知识。QLoRA推荐target_modules="q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"

Q8(中频):DPO的beta参数如何选择?

:beta控制对偏好数据的"信任程度"。beta越大,模型越倾向于偏好数据;beta越小,模型越保守。推荐:beta=0.1(默认),简单任务可降到0.05,复杂任务可升到0.5。beta过大会导致模型过度优化偏好数据而丧失通用能力。

Q9(低频):PPO中的KL散度惩罚有什么作用?

:KL惩罚防止策略模型偏离参考模型太远。没有KL惩罚,策略模型可能找到奖励模型的漏洞(reward hacking),生成高奖励但无意义的内容。KL惩罚确保模型在获得高奖励的同时保持语言质量。

Q10(中频):如何评估微调效果?只看loss够吗?

:不够。loss下降不代表实际效果提升。评估方法:①在目标任务测试集上评估准确率;②人工评估输出质量;③检查通用能力是否退化(在MMLU等通用基准上测试);④检查是否产生新问题(幻觉增加、格式错误等)。

Q11(高频):全量微调和LoRA微调在效果上差多少?

:在数据充足(>100K)时,全量微调略优1-3%。在数据较少(<10K)时,LoRA可能更好(正则化效果防止过拟合)。大多数场景下LoRA的性价比远高于全量微调。

Q12(中频):什么是reward hacking?如何防止?

:Reward hacking指模型找到奖励模型的漏洞,生成高奖励但低质量的输出。例如:生成冗长但看似完整的回答。防止方法:①KL散度惩罚;②多奖励模型集成;③人工抽检;④DPO(绕过奖励模型)。


第7章 开源LLM架构

7.1 LLaMA系列演进(1→2→3)

python 复制代码
# LLaMA系列架构对比
llama_evolution = {
    "LLaMA-1 (2023.2)": {
        "参数": "7B/13B/33B/65B",
        "训练数据": "1.4T tokens",
        "注意力": "MHA",
        "归一化": "RMSNorm (Pre-LN)",
        "激活": "SwiGLU",
        "位置编码": "RoPE",
        "上下文": "2048",
        "创新": "证明小模型+多数据可行",
    },
    "LLaMA-2 (2023.7)": {
        "参数": "7B/13B/34B/70B",
        "训练数据": "2T tokens",
        "注意力": "GQA (70B)",
        "新增": "Chat版本 + RLHF对齐",
        "上下文": "4096",
        "创新": "GQA减少KV Cache",
    },
    "LLaMA-3 (2024.4)": {
        "参数": "8B/70B",
        "训练数据": "15T tokens",
        "注意力": "GQA (8B: 8 KV头/32 Q头)",
        "词表": "128K (tiktoken)",
        "上下文": "8192 → 128K (RoPE缩放)",
        "创新": "过训练+大词表+长上下文",
    },
    "LLaMA-3.1 (2024.7)": {
        "参数": "8B/70B/405B",
        "训练数据": "15T+ tokens",
        "新增": "405B旗舰模型",
        "上下文": "128K原生",
        "创新": "最大开源模型+多语言",
    },
}

# LLaMA架构代码(简化版)
class LLaMABlock(nn.Module):
    def __init__(self, d_model=4096, n_heads=32, n_kv_heads=8, d_ff=14336):
        super().__init__()
        self.ln1 = RMSNorm(d_model)
        self.attn = GroupedQueryAttention(d_model, n_heads, n_kv_heads)
        self.ln2 = RMSNorm(d_model)
        self.ffn = SwiGLUFFN(d_model, d_ff)
    
    def forward(self, x, mask=None):
        x = x + self.attn(self.ln1(x), mask)  # Pre-LN
        x = x + self.ffn(self.ln2(x))
        return x

class LLaMA(nn.Module):
    def __init__(self, vocab_size=128256, d_model=4096, n_layers=32, 
                 n_heads=32, n_kv_heads=8):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, d_model)
        self.layers = nn.ModuleList([
            LLaMABlock(d_model, n_heads, n_kv_heads) for _ in range(n_layers)
        ])
        self.ln_f = RMSNorm(d_model)
        self.lm_head = nn.Linear(d_model, vocab_size, bias=False)
        # 权重共享: embed.weight = lm_head.weight (可选)
    
    def forward(self, input_ids):
        x = self.embed(input_ids)
        for layer in self.layers:
            x = layer(x)
        x = self.ln_f(x)
        return self.lm_head(x)

7.2 Qwen架构与中文优化

python 复制代码
# Qwen2系列特点
qwen_features = {
    "中文优化": "中文词表效率高,中文编码更短",
    "GQA": "全系列使用GQA",
    "Tie Embedding": "嵌入层和LM头共享权重(减少参数)",
    "Dual Chunk Attention": "支持长上下文(128K+)",
    "多语言": "支持30+语言",
}

# Qwen2.5-72B配置
qwen2_72b_config = {
    "d_model": 8192,
    "n_layers": 80,
    "n_heads": 64,
    "n_kv_heads": 8,       # GQA: 8 KV头
    "d_ff": 29568,
    "vocab_size": 152064,
    "rope_theta": 1000000,  # 支持长上下文
    "max_position": 131072,
}

7.3 DeepSeek-V3创新:MLA + MoE + FP8

DeepSeek-V3是2024年最具创新性的开源模型:

python 复制代码
# DeepSeek-V3三大核心创新

# 创新1: MLA (多头潜在注意力)
# 已在第3章详解,KV Cache减少5-8倍

# 创新2: MoE (混合专家模型)
class DeepSeekMoE(nn.Module):
    """DeepSeek-V3的MoE FFN层"""
    def __init__(self, d_model=7168, n_routed_experts=256, 
                 n_shared_experts=1, top_k=8):
        super().__init__()
        self.n_routed = n_routed_experts
        self.n_shared = n_shared_experts
        self.top_k = top_k
        
        # 路由专家(只激活top_k个)
        self.routed_experts = nn.ModuleList([
            SwiGLUFFN(d_model, d_ff=18432) 
            for _ in range(n_routed_experts)
        ])
        # 共享专家(始终激活)
        self.shared_experts = nn.ModuleList([
            SwiGLUFFN(d_model, d_ff=18432)
            for _ in range(n_shared_experts)
        ])
        # 路由门控
        self.gate = nn.Linear(d_model, n_routed_experts, bias=False)
    
    def forward(self, x):
        B, N, D = x.shape
        # 路由决策
        logits = self.gate(x)  # [B, N, n_routed]
        topk_vals, topk_indices = logits.topk(self.top_k, dim=-1)
        topk_weights = F.softmax(topk_vals, dim=-1)
        
        # 只计算被选中的专家
        output = torch.zeros_like(x)
        for i in range(self.top_k):
            expert_idx = topk_indices[:, :, i]
            weight = topk_weights[:, :, i:i+1]
            for e in range(self.n_routed):
                mask = (expert_idx == e)
                if mask.any():
                    expert_input = x[mask]
                    expert_output = self.routed_experts[e](expert_input)
                    output[mask] += weight[mask] * expert_output
        
        # 加上共享专家输出
        for expert in self.shared_experts:
            output = output + expert(x)
        
        return output

# 创新3: FP8混合精度训练
# DeepSeek-V3首次在超大规模训练中验证FP8的可行性
# 关键:细粒度量化(tile/block级别)而非tensor级别
fp8_training = {
    "方法": "细粒度FP8量化",
    "策略": "前向FP8,反向FP8,关键层BF16",
    "效果": "训练速度提升40%,显存减少40%",
    "精度损失": "可忽略(通过细粒度量化和动态缩放补偿)",
}

# DeepSeek-V3整体架构
deepseek_v3_config = {
    "总参数": "671B (MoE)",
    "激活参数": "37B (每token)",
    "注意力": "MLA",
    "FFN": "MoE (256路由专家 + 1共享专家, top_k=8)",
    "训练精度": "FP8混合精度",
    "训练数据": "14.8T tokens",
    "训练成本": "557万美元 (2048×H800)",
    "性能": "接近GPT-4o水平",
}

7.4 Mistral与滑动窗口

python 复制代码
# Mistral-7B的创新
mistral_innovations = {
    "SWA": "滑动窗口注意力(W=4096),复杂度O(N×W)",
    "GQA": "8 KV头 / 32 Q头",
    "Rolling Buffer": "固定大小KV Cache,循环覆盖",
    "效果": "7B参数在多数基准上超过LLaMA-2-13B",
}

# 滑动窗口+Rolling Buffer
class RollingBufferKVCache:
    """Mistral的循环KV Cache"""
    def __init__(self, window_size, n_layers, n_kv_heads, d_k, batch_size):
        self.window_size = window_size
        # 固定大小的缓存,循环使用
        self.k_cache = torch.zeros(n_layers, batch_size, n_kv_heads, window_size, d_k)
        self.v_cache = torch.zeros(n_layers, batch_size, n_kv_heads, window_size, d_k)
    
    def update(self, layer_idx, k, v, pos):
        # pos % window_size: 循环覆盖旧位置
        cache_pos = pos % self.window_size
        self.k_cache[layer_idx, :, :, cache_pos] = k
        self.v_cache[layer_idx, :, :, cache_pos] = v
        return self.k_cache[layer_idx], self.v_cache[layer_idx]

7.5 开源vs闭源:策略与生态

python 复制代码
# 2024-2025年开源vs闭源格局
llm_landscape = {
    "闭源第一梯队": ["GPT-4o", "Claude 3.5 Sonnet", "Gemini Ultra"],
    "开源第一梯队": ["LLaMA-3.1-405B", "DeepSeek-V3", "Qwen2.5-72B"],
    "差距": "开源≈闭源的85-90%,且在快速缩小",
    "开源优势": "可私有部署、可定制、成本低、数据安全",
    "闭源优势": "综合能力最强、多模态、生态完善",
}

# 开源模型选型指南
def select_opensource_model(requirements):
    if requirements.get("max_quality"):
        return "DeepSeek-V3 / LLaMA-3.1-405B"
    elif requirements.get("chinese"):
        return "Qwen2.5-72B"
    elif requirements.get("low_resource"):
        return "Qwen2.5-7B / LLaMA-3-8B"
    elif requirements.get("coding"):
        return "DeepSeek-Coder-V2 / Qwen2.5-Coder-32B"
    elif requirements.get("edge_device"):
        return "Qwen2.5-1.5B / LLaMA-3.2-1B"
    else:
        return "Qwen2.5-7B (通用推荐)"

7.6 本章面试题精讲

Q1(高频):LLaMA系列有哪些关键架构改进?

:LLaMA-1→2:加入GQA(70B)、RLHF对齐。LLaMA-2→3:词表从32K扩到128K、GQA下放到8B模型、训练数据从2T到15T、上下文从4K到128K。核心趋势:过训练+大词表+长上下文。

Q2(高频):DeepSeek-V3有哪些核心创新?

:三大创新:①MLA(KV Cache减少5-8倍);②MoE(671B总参数/37B激活参数,效率极高);③FP8混合精度训练(首次在超大规模验证,训练成本仅557万美元)。综合效果接近GPT-4o。

Q3(中频):MoE的原理是什么?为什么DeepSeek用256个专家?

:MoE每个token只激活top_k个专家(DeepSeek-V3: top_k=8/256),总参数大但激活参数小。256个专家提供更细粒度的专业化:不同专家处理不同类型的知识(代码、数学、语言等)。共享专家确保通用知识始终可用。

Q4(中频):Mistral的滑动窗口注意力有什么优势?

:SWA限制每个token只看窗口W内的token,复杂度从O(N²)降到O(N×W)。通过多层堆叠间接捕捉长距离依赖(L层感受野=L×W)。Rolling Buffer使KV Cache固定大小,不受序列长度影响。

Q5(高频):为什么LLaMA-3把词表从32K扩到128K?

:大词表提升编码效率:①中文等非英语文本编码更短(节省30-50%token);②常见词/子词有独立token,减少推理步数;③训练数据中15T token的多样性需要更大词表覆盖。代价:嵌入层参数增加4倍,但总体值得。

Q6(中频):开源模型和闭源模型的差距有多大?趋势如何?

:2024-2025年,开源模型(DeepSeek-V3/LLaMA-3.1-405B)约为闭源(GPT-4o/Claude-3.5)的85-90%。趋势:差距在缩小,DeepSeek-V3已接近GPT-4o水平。开源优势在于可私有部署和定制,闭源优势在于综合能力和多模态。

Q7(中频):如何选择开源模型?

:追求质量选DeepSeek-V3/LLaMA-3.1-405B,中文场景选Qwen2.5,低资源选7B模型,代码选DeepSeek-Coder,边缘设备选1-3B模型。通用推荐Qwen2.5-7B(中文好、效率高)。

Q8(低频):MoE的负载均衡问题是什么?如何解决?

:MoE容易出现路由崩塌------所有token都被路由到少数几个专家。解决:①辅助损失(auxiliary loss)惩罚专家负载不均;②专家选择策略(token选专家→专家选token);③容量因子(限制每个专家处理的token数)。DeepSeek-V3使用无辅助损失的负载均衡策略。

Q9(中频):LLaMA-3的8B模型为什么也用GQA?

:GQA减少KV Cache,降低推理成本。8B模型用GQA(8 KV头/32 Q头),KV Cache减少4倍,推理速度提升显著。质量损失几乎不可测,但推理效率大幅提升。趋势:GQA已成为所有规模模型的标配。

Q10(低频):开源LLM的商业模式是什么?

:三种模式:①Meta模式(LLaMA免费开放,提升生态影响力);②DeepSeek模式(API收费,模型开源);③Qwen模式(云服务收费,模型开源)。核心逻辑:开源模型获客,云服务/API变现。


第8章 推理优化

8.1 KV Cache原理与显存计算

KV Cache原理

自回归生成时,每步生成1个新token,但需要与所有历史token做注意力。KV Cache缓存历史的K/V,避免重复计算:

python 复制代码
# 无KV Cache: 每步重新计算所有K/V
def generate_no_cache(model, prompt_ids, max_new_tokens):
    input_ids = prompt_ids
    for _ in range(max_new_tokens):
        # 每步都从头计算所有token的K/V --- 浪费!
        logits = model(input_ids)
        next_token = logits[:, -1, :].argmax(dim=-1, keepdim=True)
        input_ids = torch.cat([input_ids, next_token], dim=-1)
    return input_ids

# 有KV Cache: 只计算新token的K/V
def generate_with_cache(model, prompt_ids, max_new_tokens):
    # Prefill: 计算prompt的所有K/V
    logits, past_kv = model(prompt_ids, use_cache=True)
    next_token = logits[:, -1, :].argmax(dim=-1, keepdim=True)
    
    generated = [next_token]
    for _ in range(max_new_tokens - 1):
        # Decode: 只计算1个新token的K/V
        logits, past_kv = model(next_token, past_key_values=past_kv, use_cache=True)
        next_token = logits[:, -1, :].argmax(dim=-1, keepdim=True)
        generated.append(next_token)
    
    return torch.cat([prompt_ids] + generated, dim=-1)

KV Cache显存计算

python 复制代码
def kv_cache_memory(batch_size, seq_len, n_layers, n_kv_heads, d_k, 
                    dtype_bytes=2, n_gpus=1):
    """精确计算KV Cache显存"""
    # 每token每层: 2(K+V) × n_kv_heads × d_k × dtype_bytes
    per_token_per_layer = 2 * n_kv_heads * d_k * dtype_bytes
    total = batch_size * seq_len * n_layers * per_token_per_layer
    return {
        "total_GB": total / 1e9,
        "per_token_KB": per_token_per_layer / 1e3,
        "per_layer_MB": batch_size * seq_len * per_token_per_layer / 1e6,
    }

# LLaMA3-70B: 80层, GQA 8 KV头, d_k=128
print(kv_cache_memory(1, 128000, 80, 8, 128))
# 约 5.2 GB --- 仅KV Cache就占5GB!

# 对比MHA vs GQA
mha = kv_cache_memory(1, 128000, 80, 64, 128)  # MHA: 64 KV头
gqa = kv_cache_memory(1, 128000, 80, 8, 128)   # GQA: 8 KV头
print(f"MHA: {mha['total_GB']:.1f}GB, GQA: {gqa['total_GB']:.1f}GB")
# MHA: 41.6GB, GQA: 5.2GB --- GQA节省8倍!

8.2 量化:GPTQ、AWQ、FP8

量化原理

python 复制代码
# 量化 = 用更少位数表示权重
# FP16: 16位 → INT8: 8位 → INT4: 4位

# 对称量化
def symmetric_quantize(weight, n_bits=8):
    """对称量化: 零点=0"""
    max_val = weight.abs().max()
    scale = max_val / (2 ** (n_bits - 1) - 1)
    quantized = torch.round(weight / scale).clamp(-(2**(n_bits-1)), 2**(n_bits-1)-1)
    return quantized.to(torch.int8), scale

# 非对称量化
def asymmetric_quantize(weight, n_bits=8):
    """非对称量化: 有零点"""
    min_val = weight.min()
    max_val = weight.max()
    scale = (max_val - min_val) / (2**n_bits - 1)
    zero_point = torch.round(-min_val / scale)
    quantized = torch.round(weight / scale + zero_point).clamp(0, 2**n_bits - 1)
    return quantized.to(torch.uint8), scale, zero_point

# 量化对模型的影响
quantization_impact = {
    "FP16 → INT8": "精度损失<0.1%, 速度提升2x, 显存减半",
    "FP16 → INT4": "精度损失0.5-2%, 速度提升3x, 显存1/4",
    "FP16 → FP8": "精度损失<0.05%, 速度提升2x, 显存减半",
}

GPTQ vs AWQ

python 复制代码
# GPTQ: 基于Hessian的逐层量化
# 核心思想:利用二阶信息最小化量化误差
gptq_features = {
    "方法": "逐层量化,基于Hessian矩阵最小化重建误差",
    "优势": "INT4量化后精度保持最好",
    "劣势": "量化速度慢(需要校准数据)",
    "适用": "离线量化,追求最佳精度",
}

# AWQ: 激活感知权重量化
# 核心思想:保护重要权重通道(激活值大的通道)
awq_features = {
    "方法": "根据激活值大小缩放权重,保护重要通道",
    "优势": "量化速度快,不需要反向传播",
    "劣势": "精度略低于GPTQ",
    "适用": "快速量化,部署场景",
}

# FP8: 原生8位浮点
fp8_features = {
    "方法": "E4M3(前向) + E5M2(反向),硬件原生支持",
    "优势": "H100/H800原生支持,无需额外量化步骤",
    "劣势": "需要H系列GPU",
    "适用": "训练和推理",
}

8.3 FlashAttention与PagedAttention

PagedAttention(vLLM核心)

python 复制代码
# PagedAttention: 将KV Cache分页管理,解决显存碎片
# 类比操作系统的虚拟内存管理

class PagedKVCache:
    """PagedAttention简化示意"""
    def __init__(self, block_size=16, num_blocks=1024, 
                 n_kv_heads=8, d_k=128):
        self.block_size = block_size
        self.num_blocks = num_blocks
        # 预分配固定大小的block池
        self.k_cache = torch.zeros(num_blocks, n_kv_heads, block_size, d_k)
        self.v_cache = torch.zeros(num_blocks, n_kv_heads, block_size, d_k)
        self.free_blocks = list(range(num_blocks))  # 空闲block列表
        self.page_tables = {}  # 每个请求的页表
    
    def allocate(self, request_id, num_tokens):
        """为请求分配KV Cache块"""
        num_blocks_needed = (num_tokens + self.block_size - 1) // self.block_size
        if num_blocks_needed > len(self.free_blocks):
            raise OOMError("KV Cache显存不足")
        
        allocated = self.free_blocks[:num_blocks_needed]
        self.free_blocks = self.free_blocks[num_blocks_needed:]
        self.page_tables[request_id] = allocated
        return allocated

# PagedAttention vs 传统KV Cache
# 传统: 预分配最大长度的连续显存 → 30-60%浪费(碎片)
# Paged: 按需分配固定大小block → <5%浪费
# 效果: 同样显存可服务2-4倍并发请求

8.4 连续批处理与vLLM

python 复制代码
# 静态批处理 vs 连续批处理

# 静态批处理: 等最慢的请求完成才能处理下一批
# [请求A: 生成20token] [请求B: 生成5token]
# B完成后空等A → GPU利用率低

# 连续批处理(Continuous Batching): 请求完成立即替换新请求
# iteration 1: [A_1, B_1]
# iteration 2: [A_2, B_2]
# iteration 3: [A_3, B_3, C_1]  ← B完成,C加入
# iteration 4: [A_4, C_2, D_1]  ← A完成,D加入

# vLLM核心特性
vllm_features = {
    "PagedAttention": "KV Cache分页管理,显存利用率>95%",
    "Continuous Batching": "请求完成立即替换,GPU利用率>90%",
    "Prefix Caching": "共享prompt前缀只计算一次",
    "Speculative Decoding": "投机解码加速(可选)",
    "量化支持": "GPTQ/AWQ/FP8",
}

8.5 投机解码(Speculative Decoding)

python 复制代码
# 投机解码: 用小模型快速生成候选,大模型并行验证

def speculative_decoding(draft_model, target_model, prompt, 
                         max_tokens=100, gamma=5):
    """投机解码流程"""
    input_ids = prompt
    generated = []
    
    while len(generated) < max_tokens:
        # 1. 小模型快速生成gamma个候选token
        draft_tokens = []
        draft_input = input_ids
        for _ in range(gamma):
            logits = draft_model(draft_input)
            next_token = logits[:, -1, :].argmax(dim=-1, keepdim=True)
            draft_tokens.append(next_token)
            draft_input = torch.cat([draft_input, next_token], dim=-1)
        
        # 2. 大模型一次前向验证所有候选token
        target_logits = target_model(draft_input)
        
        # 3. 从左到右验证,找到第一个被拒绝的位置
        accepted = 0
        for i in range(gamma):
            target_prob = F.softmax(target_logits[:, prompt.size(1)-1+i, :], dim=-1)
            draft_prob = F.softmax(draft_model(draft_input[:, :prompt.size(1)+i])[:, -1, :], dim=-1)
            
            # 拒绝概率 = max(0, 1 - target_prob/draft_prob)
            if torch.rand(1) < 1 - target_prob[0, draft_tokens[i]] / draft_prob[0, draft_tokens[i]]:
                break
            accepted += 1
        
        # 4. 接受的token加入结果
        generated.extend(draft_tokens[:accepted])
        input_ids = draft_input[:, :prompt.size(1) + accepted]
    
    return generated

# 投机解码加速比
# 接受率90%时: 理论加速2-3x
# 接受率80%时: 理论加速1.5-2x
# 关键: 小模型和大模型的分布越接近,加速越明显

8.6 推理框架选型

python 复制代码
# 主流推理框架对比
inference_frameworks = {
    "vLLM": {
        "优势": "吞吐量最高、PagedAttention、生态完善",
        "劣势": "首token延迟略高",
        "适用": "高并发在线服务",
    },
    "TensorRT-LLM": {
        "优势": "NVIDIA官方、单请求延迟最低、FP8支持",
        "劣势": "编译时间长、灵活性差",
        "适用": "NVIDIA GPU、追求最低延迟",
    },
    "llama.cpp": {
        "优势": "CPU/GPU混合推理、跨平台、量化支持好",
        "劣势": "吞吐量不如vLLM",
        "适用": "本地部署、边缘设备",
    },
    "SGLang": {
        "优势": "结构化生成快、RadixAttention",
        "劣势": "生态较新",
        "适用": "结构化输出场景",
    },
}

# 选型建议
def select_framework(requirements):
    if requirements.get("high_throughput"):
        return "vLLM"
    elif requirements.get("lowest_latency"):
        return "TensorRT-LLM"
    elif requirements.get("local_deploy"):
        return "llama.cpp"
    elif requirements.get("structured_output"):
        return "SGLang"
    else:
        return "vLLM (通用推荐)"

8.7 本章面试题精讲

Q1(高频):KV Cache的作用和原理?显存如何计算?

:KV Cache缓存历史的K/V向量,避免自回归生成时重复计算。显存=2×batch×seq_len×n_layers×n_kv_heads×d_k×dtype_bytes。LLaMA3-70B在128K上下文下KV Cache约5.2GB(GQA),MHA则需41.6GB。

Q2(高频):vLLM为什么快?核心优化是什么?

:三大核心优化:①PagedAttention(KV Cache分页管理,显存利用率>95%);②Continuous Batching(请求完成立即替换,GPU利用率>90%);③Prefix Caching(共享前缀只算一次)。综合效果:吞吐量比HuggingFace高10-24倍。

Q3(高频):INT4/INT8量化对模型有什么影响?

:INT8精度损失<0.1%,INT4精度损失0.5-2%。量化主要影响:①困惑度轻微上升;②复杂推理任务(数学/代码)受影响更大;③生成多样性略降。实际部署中INT4通常可接受,配合GPTQ/AWQ可进一步减少损失。

Q4(中频):GPTQ和AWQ的区别?

:GPTQ基于Hessian矩阵逐层量化,精度最好但速度慢;AWQ根据激活值缩放权重保护重要通道,速度快但精度略低。选型:追求精度选GPTQ,追求部署速度选AWQ。

Q5(高频):投机解码的原理?加速比取决于什么?

:小模型快速生成候选token,大模型一次前向验证。加速比取决于小模型和大模型的分布一致性------越接近,接受率越高,加速越明显。典型加速1.5-3x。注意:投机解码不减少FLOPs,但减少大模型的推理步数。

Q6(中频):PagedAttention解决了什么问题?

:传统KV Cache预分配连续显存,导致30-60%碎片浪费。PagedAttention将KV Cache分页管理(类似OS虚拟内存),按需分配固定大小block,显存利用率>95%,同样显存可服务2-4倍并发请求。

Q7(中频):Continuous Batching和Static Batching的区别?

:Static Batching等最慢请求完成后才处理下一批,GPU空等。Continuous Batching请求完成立即替换新请求,GPU始终满载。效果:吞吐量提升2-3倍。

Q8(高频):推理的Prefill和Decode阶段有什么区别?

:Prefill处理完整输入prompt,计算密集型(矩阵乘法),影响首token延迟(TTFT)。Decode逐个生成token,显存带宽密集型(读取KV Cache),影响生成速度(tokens/sec)。优化方向不同:Prefill优化计算,Decode优化访存。

Q9(低频):FP8量化为什么在H100上特别有效?

:H100原生支持FP8(E4M3前向+E5M2反向),硬件层面加速2倍。FP8比INT8精度更好(浮点表示),比FP16显存减半。DeepSeek-V3首次在超大规模训练中验证FP8可行性。

Q10(中频):如何估算LLM推理的显存需求?

:总显存 = 模型权重 + KV Cache + 激活值。模型权重:7B FP16≈14GB,INT4≈3.5GB。KV Cache:见Q1公式。激活值:约为模型权重的10-20%。示例:7B模型INT4+4K上下文≈3.5GB+0.5GB+0.5GB≈4.5GB,单卡24GB可服务多并发。

Q11(低频):Prefix Caching如何优化共享前缀?

:多个请求共享相同system prompt时,只计算一次KV Cache并缓存。新请求复用已缓存的KV,只计算用户输入部分。vLLM的Automatic Prefix Caching可自动检测共享前缀,在多轮对话和RAG场景下效果显著。

Q12(中频):llama.cpp为什么能在CPU上运行?

:llama.cpp用C/C++重写推理逻辑,支持CPU/GPU混合推理。关键优化:①INT4/INT5量化减少内存带宽需求;②算子融合减少内存访问;③支持Apple Silicon的Metal加速。代价:吞吐量远低于GPU方案。


第9章 RAG检索增强生成

9.1 RAG核心流程

RAG(Retrieval-Augmented Generation)将外部知识检索与LLM生成结合,解决知识时效性和幻觉问题。

python 复制代码
# RAG基础流程
class SimpleRAG:
    def __init__(self, llm, embedder, vector_store):
        self.llm = llm
        self.embedder = embedder
        self.vector_store = vector_store
    
    def query(self, question, top_k=5):
        # 1. 检索: 将问题转为向量,搜索相关文档
        query_embedding = self.embedder.embed(question)
        docs = self.vector_store.search(query_embedding, top_k=top_k)
        
        # 2. 构建 prompt
        context = "\n\n".join([doc.text for doc in docs])
        prompt = f"""基于以下参考资料回答问题。如果资料中没有相关信息,请说明。

参考资料:
{context}

问题:{question}
回答:"""
        
        # 3. 生成
        answer = self.llm.generate(prompt)
        return answer, docs

# RAG vs 纯LLM
rag_vs_llm = {
    "纯LLM": {
        "知识来源": "训练数据(静态)",
        "时效性": "差(截止日期限制)",
        "幻觉": "严重",
        "私有数据": "不支持",
    },
    "RAG": {
        "知识来源": "外部知识库(动态更新)",
        "时效性": "好(实时检索)",
        "幻觉": "显著减少",
        "私有数据": "支持",
    },
}

9.2 向量检索与Embedding模型

python 复制代码
# Embedding模型选型
embedding_models = {
    "OpenAI text-embedding-3-large": {"维度": 3072, "多语言": True, "价格": "付费"},
    "BGE-M3 (BAAI)": {"维度": 1024, "多语言": True, "价格": "开源"},
    "GTE-Qwen2 (阿里)": {"维度": 1536, "多语言": True, "价格": "开源"},
    "E5-mistral-7b": {"维度": 4096, "多语言": True, "价格": "开源"},
}

# 向量相似度计算
def cosine_similarity(a, b):
    """余弦相似度 --- 最常用的向量相似度"""
    return F.normalize(a, dim=-1) @ F.normalize(b, dim=-1).T

def dot_product_similarity(a, b):
    """点积相似度 --- 归一化后等价于余弦"""
    return a @ b.T

# 向量数据库选型
vector_dbs = {
    "Milvus": "分布式、高性能、适合大规模",
    "Qdrant": "Rust实现、性能好、易部署",
    "Chroma": "轻量级、适合原型开发",
    "pgvector": "PostgreSQL扩展、适合已有PG的项目",
    "FAISS": "Meta开源、纯库、适合本地部署",
}

9.3 Chunking策略与混合检索

Chunking策略

python 复制代码
# 文档切分策略
class ChunkingStrategies:
    @staticmethod
    def fixed_size(text, chunk_size=512, overlap=50):
        """固定大小切分 --- 最简单"""
        chunks = []
        for i in range(0, len(text), chunk_size - overlap):
            chunks.append(text[i:i + chunk_size])
        return chunks
    
    @staticmethod
    def recursive_split(text, separators=["\n\n", "\n", "。", " ", ""],
                        chunk_size=512, overlap=50):
        """递归切分 --- LangChain默认策略"""
        if len(text) <= chunk_size:
            return [text]
        
        for sep in separators:
            if sep in text:
                parts = text.split(sep)
                chunks = []
                current = ""
                for part in parts:
                    if len(current) + len(part) + len(sep) > chunk_size:
                        if current:
                            chunks.append(current)
                        current = part
                    else:
                        current = current + sep + part if current else part
                if current:
                    chunks.append(current)
                return chunks
        return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
    
    @staticmethod
    def semantic_chunking(text, embedder, threshold=0.8):
        """语义切分 --- 相似度低于阈值时切分"""
        sentences = text.split("。")
        embeddings = [embedder.embed(s) for s in sentences]
        
        chunks = []
        current_chunk = [sentences[0]]
        for i in range(1, len(sentences)):
            sim = cosine_similarity(embeddings[i-1], embeddings[i])
            if sim < threshold:
                chunks.append("。".join(current_chunk))
                current_chunk = [sentences[i]]
            else:
                current_chunk.append(sentences[i])
        
        if current_chunk:
            chunks.append("。".join(current_chunk))
        return chunks

混合检索

python 复制代码
class HybridRetriever:
    """混合检索 = 向量检索 + 关键词检索"""
    def __init__(self, vector_store, bm25_store, alpha=0.7):
        self.vector_store = vector_store
        self.bm25_store = bm25_store
        self.alpha = alpha  # 向量检索权重
    
    def search(self, query, top_k=10):
        # 向量检索
        vector_results = self.vector_store.search(query, top_k=top_k*2)
        # BM25关键词检索
        bm25_results = self.bm25_store.search(query, top_k=top_k*2)
        
        # RRF (Reciprocal Rank Fusion) 融合
        scores = {}
        for rank, doc in enumerate(vector_results):
            scores[doc.id] = scores.get(doc.id, 0) + self.alpha / (rank + 1)
        for rank, doc in enumerate(bm25_results):
            scores[doc.id] = scores.get(doc.id, 0) + (1 - self.alpha) / (rank + 1)
        
        # 按融合分数排序
        sorted_docs = sorted(scores.items(), key=lambda x: x[1], reverse=True)
        return sorted_docs[:top_k]

9.4 Rerank重排序

python 复制代码
# Rerank: 对检索结果进行精排
class Reranker:
    """交叉编码器重排序"""
    def __init__(self, model_name="BAAI/bge-reranker-v2-m3"):
        self.model = AutoModelForSequenceClassification.from_pretrained(model_name)
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
    
    def rerank(self, query, documents, top_k=5):
        # 对每个文档计算query-doc相关性分数
        scores = []
        for doc in documents:
            inputs = self.tokenizer(query, doc.text, return_tensors="pt", 
                                   truncation=True, max_length=512)
            with torch.no_grad():
                score = self.model(**inputs).logits[0][0].item()
            scores.append((doc, score))
        
        # 按分数排序
        scores.sort(key=lambda x: x[1], reverse=True)
        return [doc for doc, _ in scores[:top_k]]

# Rerank在RAG流程中的位置
# 检索(top_k=20) → Rerank(top_k=5) → 生成
# 粗排用向量检索(快),精排用交叉编码器(准)

9.5 GraphRAG与Agentic RAG

GraphRAG

python 复制代码
# GraphRAG: 用知识图谱增强RAG
# 微软2024年提出

class GraphRAG:
    def __init__(self, llm, graph_store, vector_store):
        self.llm = llm
        self.graph = graph_store
        self.vector = vector_store
    
    def build_graph(self, documents):
        """从文档构建知识图谱"""
        for doc in documents:
            # 1. LLM提取实体和关系
            prompt = f"""从以下文本中提取实体和关系:
{doc.text}

输出格式:
实体: [实体1, 实体2, ...]
关系: [(实体1, 关系, 实体2), ...]"""
            result = self.llm.generate(prompt)
            # 2. 存入图数据库
            self.graph.add_entities_and_relations(result)
    
    def query(self, question):
        # 1. 从图谱中检索相关子图
        subgraph = self.graph.search(question)
        # 2. 结合向量检索
        vector_docs = self.vector.search(question)
        # 3. 融合生成
        context = f"知识图谱: {subgraph}\n文档: {vector_docs}"
        return self.llm.generate(f"基于以下信息回答:\n{context}\n问题: {question}")

# GraphRAG vs 传统RAG
# 传统RAG: 检索局部文档片段 → 缺乏全局理解
# GraphRAG: 检索知识图谱子图 → 理解实体间关系,支持全局问答

Agentic RAG

python 复制代码
# Agentic RAG: 让Agent自主决定检索策略

class AgenticRAG:
    def __init__(self, llm, retriever, tools):
        self.llm = llm
        self.retriever = retriever
        self.tools = tools  # 可用工具列表
    
    def query(self, question, max_iterations=5):
        messages = [{"role": "user", "content": question}]
        
        for _ in range(max_iterations):
            response = self.llm.chat(messages, tools=self.tools)
            
            if not response.tool_calls:
                return response.content  # 直接回答
            
            # 执行工具调用
            for tool_call in response.tool_calls:
                if tool_call.name == "search":
                    result = self.retriever.search(tool_call.args["query"])
                elif tool_call.name == "search_web":
                    result = web_search(tool_call.args["query"])
                
                messages.append({"role": "tool", "content": result})
        
        return "无法找到足够信息回答该问题"

9.6 RAG评估与优化

python 复制代码
# RAG评估框架: RAGAS
rag_metrics = {
    "Faithfulness": "回答是否基于检索文档(无幻觉)",
    "Answer Relevancy": "回答是否切题",
    "Context Precision": "检索文档中相关内容的比例",
    "Context Recall": "回答所需信息是否都被检索到",
}

# 常见RAG问题与优化
rag_optimization = {
    "检索不到相关文档": {
        "原因": "Chunking粒度不当、Embedding模型差",
        "优化": "调整chunk_size、换更好的Embedding、混合检索",
    },
    "检索到但没用": {
        "原因": "LLM没正确利用检索内容",
        "优化": "改进prompt、增加Rerank、减少无关文档干扰",
    },
    "回答有幻觉": {
        "原因": "LLM编造了检索文档中没有的信息",
        "优化": "约束生成(只基于给定文档回答)、Faithfulness评估",
    },
    "多跳推理失败": {
        "原因": "需要综合多个文档的信息",
        "优化": "GraphRAG、Agentic RAG、多轮检索",
    },
}

9.7 本章面试题精讲

Q1(高频):RAG的原理和流程?解决了什么问题?

:RAG流程:用户问题→向量化→检索相关文档→构建prompt(文档+问题)→LLM生成。解决三大问题:①知识时效性(检索实时数据);②幻觉(基于文档生成);③私有数据(检索企业知识库)。

Q2(高频):Chunking策略有哪些?如何选择?

:固定大小(简单但可能切断语义)、递归切分(LangChain默认,按分隔符层级切分)、语义切分(按语义相似度切分,最准但最慢)。推荐:先用递归切分(chunk_size=512, overlap=50),效果不好再试语义切分。

Q3(中频):混合检索为什么比纯向量检索好?

:向量检索擅长语义匹配但可能遗漏关键词精确匹配(如专有名词、编号);BM25擅长关键词匹配但不理解语义。混合检索(向量+BM25+RRF融合)结合两者优势,召回率提升10-30%。

Q4(高频):Rerank的作用?为什么需要?

:向量检索是双编码器(query和doc独立编码),速度快但精度有限。Rerank用交叉编码器(query和doc联合编码),精度高但速度慢。流程:检索top_k=20→Rerank top_k=5→生成。粗排用快方法,精排用准方法。

Q5(中频):GraphRAG和传统RAG的区别?

:传统RAG检索文档片段,缺乏全局理解。GraphRAG先构建知识图谱,检索时获取相关子图,理解实体间关系。优势:支持全局性问答(如"总结所有文档的主要观点"),传统RAG只能回答局部问题。

Q6(高频):如何评估RAG系统?

:四大指标(RAGAS框架):Faithfulness(回答是否基于文档)、Answer Relevancy(回答是否切题)、Context Precision(检索精度)、Context Recall(检索召回)。最关键的是Faithfulness------防止幻觉。

Q7(中频):Agentic RAG和传统RAG的区别?

:传统RAG是固定流程(检索→生成),Agentic RAG让Agent自主决定:是否需要检索、检索什么、是否需要多轮检索、是否需要调用其他工具。优势:更灵活,能处理复杂多跳问题。

Q8(中频):Embedding模型如何选型?

:开源首选BGE-M3或GTE-Qwen2(多语言、效果好)。付费选OpenAI text-embedding-3-large。关键指标:MTEB排行榜分数、推理速度、维度大小(维度越高精度越好但存储成本越大)。

Q9(低频):RAG中的Lost in the Middle问题如何解决?

:LLM对长上下文中间部分关注度下降。解决:①Rerank将最相关文档放在首尾位置;②减少输入文档数量(top_k=3-5);③LongContext模型(128K上下文)缓解但未完全解决。

Q10(中频):如何处理RAG中的表格和图片?

:表格:①用LLM将表格转为自然语言描述;②保留Markdown表格格式;③按行切分并保留表头。图片:①用多模态模型生成描述;②OCR提取文字;③将图表转为结构化数据。关键是让信息变为LLM可理解的文本形式。

Q11(低频):Parent-Child Chunking是什么?

:用小块(Child)检索以保证精度,检索到后返回对应的大块(Parent)给LLM以保证上下文完整。例如:Child=128 token用于检索,Parent=512 token用于生成。兼顾检索精度和生成质量。

Q12(中频):RAG和微调如何选择?

:RAG适合:知识频繁更新、需要引用来源、私有数据。微调适合:改变模型行为/风格、特定领域术语、任务格式固定。最佳实践:先RAG(快速见效),效果不够再微调,两者可结合(微调后模型+RAG)。


第10章 AI Agent

10.1 Agent核心架构:感知-规划-行动-记忆

AI Agent是能自主感知环境、制定计划、执行行动并利用记忆的系统。

python 复制代码
# Agent核心架构
class Agent:
    def __init__(self, llm, tools, memory):
        self.llm = llm          # 大脑:推理和决策
        self.tools = tools      # 手:执行行动
        self.memory = memory    # 记忆:存储经验
    
    def run(self, task):
        """Agent执行循环"""
        while not self.is_done(task):
            # 1. 感知:收集当前状态
            observation = self.observe(task)
            
            # 2. 规划:决定下一步行动
            action = self.plan(observation)
            
            # 3. 行动:执行工具调用
            result = self.act(action)
            
            # 4. 记忆:存储经验
            self.memory.add(observation, action, result)
        
        return self.get_final_answer()

# Agent vs 纯LLM
agent_vs_llm = {
    "纯LLM": "只能生成文本,无法与外部世界交互",
    "Agent": "LLM + 工具 + 记忆 + 规划 = 自主行动",
    "关键区别": "Agent能执行动作(搜索、计算、调用API),不只是生成文本",
}

10.2 ReAct与Plan-and-Execute

ReAct(Reasoning + Acting)

python 复制代码
class ReActAgent:
    """ReAct: 交替推理和行动"""
    def __init__(self, llm, tools):
        self.llm = llm
        self.tools = {t.name: t for t in tools}
    
    def run(self, question, max_steps=10):
        prompt = f"""回答问题: {question}

你可以使用以下工具:
{self._format_tools()}

按以下格式回答:
Thought: 思考下一步该做什么
Action: 工具名(参数)
Observation: 工具返回结果
... (重复Thought/Action/Observation)
Thought: 我现在知道最终答案了
Answer: 最终答案
"""
        messages = [{"role": "user", "content": prompt}]
        
        for _ in range(max_steps):
            response = self.llm.chat(messages)
            messages.append({"role": "assistant", "content": response})
            
            # 解析Action
            action = self._parse_action(response)
            if action is None:
                break  # 没有Action,说明已经得出答案
            
            # 执行工具
            tool_name, tool_args = action
            observation = self.tools[tool_name].run(**tool_args)
            messages.append({"role": "user", "content": f"Observation: {observation}"})
        
        return self._parse_answer(messages[-1]["content"])

# ReAct示例
# Question: 特斯拉2024年Q1的营收是多少?
# Thought: 我需要搜索特斯拉2024年Q1财报
# Action: search("特斯拉 2024 Q1 营收")
# Observation: 特斯拉2024年Q1营收为213亿美元
# Thought: 我现在知道答案了
# Answer: 特斯拉2024年Q1营收为213亿美元

Plan-and-Execute

python 复制代码
class PlanExecuteAgent:
    """Plan-and-Execute: 先制定完整计划,再逐步执行"""
    def __init__(self, llm, tools):
        self.llm = llm
        self.tools = {t.name: t for t in tools}
    
    def run(self, task):
        # 1. 制定计划
        plan = self._create_plan(task)
        
        # 2. 逐步执行
        results = []
        for step in plan:
            result = self._execute_step(step)
            results.append(result)
            
            # 3. 可选:根据结果重新规划
            if self._need_replan(results):
                plan = self._replan(task, results)
        
        return self._synthesize(results)

# ReAct vs Plan-and-Execute
# ReAct: 每步即时决策,灵活但可能走弯路
# Plan-and-Execute: 先规划再执行,高效但计划可能需要调整
# 推荐: 简单任务用ReAct,复杂任务用Plan-and-Execute

10.3 Function Calling与工具设计

Function Calling

python 复制代码
# OpenAI Function Calling格式
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "搜索互联网获取最新信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "搜索关键词"
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "execute_code",
            "description": "执行Python代码并返回结果",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {
                        "type": "string",
                        "description": "要执行的Python代码"
                    }
                },
                "required": ["code"]
            }
        }
    }
]

# 工具设计原则
tool_design_principles = {
    "单一职责": "每个工具只做一件事",
    "描述清晰": "LLM靠描述理解工具用途,描述要精确",
    "参数简单": "参数越少越好,复杂逻辑放工具内部",
    "幂等性": "相同输入应返回相同结果(查询类)",
    "错误处理": "返回有意义的错误信息,而非抛异常",
    "安全边界": "限制工具的权限范围(如文件访问路径)",
}

10.4 记忆系统设计

python 复制代码
class AgentMemory:
    """Agent记忆系统"""
    def __init__(self, llm, vector_store):
        self.llm = llm
        self.vector_store = vector_store
        self.short_term = []  # 短期记忆(当前对话)
        self.long_term = vector_store  # 长期记忆(向量存储)
    
    def add_short_term(self, message):
        """添加短期记忆"""
        self.short_term.append(message)
        # 短期记忆有长度限制
        if len(self.short_term) > 20:
            # 将旧记忆摘要后移入长期记忆
            summary = self.llm.generate(
                f"请摘要以下对话:\n{self.short_term[:10]}"
            )
            self.add_long_term(summary)
            self.short_term = self.short_term[10:]
    
    def add_long_term(self, content):
        """添加长期记忆"""
        embedding = self.vector_store.embed(content)
        self.vector_store.add(embedding, content)
    
    def recall(self, query, top_k=5):
        """回忆相关记忆"""
        # 检索长期记忆
        long_term_results = self.vector_store.search(query, top_k=top_k)
        # 短期记忆直接可用
        return {
            "short_term": self.short_term[-5:],
            "long_term": long_term_results,
        }

# 记忆类型
memory_types = {
    "短期记忆": "当前对话上下文,有限长度,类似工作记忆",
    "长期记忆": "历史经验,向量存储,按相关性检索",
    "情景记忆": "特定事件的完整记录",
    "语义记忆": "抽象知识和规则",
}

10.5 Multi-Agent协作

python 复制代码
class MultiAgentSystem:
    """多Agent协作系统"""
    def __init__(self):
        self.agents = {
            "researcher": Agent(role="研究员", task="搜索和整理信息"),
            "writer": Agent(role="写作者", task="撰写内容"),
            "reviewer": Agent(role="审校者", task="检查和改进"),
        }
    
    def run(self, task):
        # 流水线协作
        research = self.agents["researcher"].run(f"研究: {task}")
        draft = self.agents["writer"].run(f"基于以下研究写文章:\n{research}")
        final = self.agents["reviewer"].run(f"审校以下文章:\n{draft}")
        return final

# 协作模式
collaboration_patterns = {
    "流水线": "Agent A → B → C,顺序执行",
    "辩论": "多个Agent讨论,投票决策",
    "层级": "Manager Agent分配任务给Worker Agent",
    "混合": "根据任务动态选择协作模式",
}

10.6 MCP协议与工具生态

MCP(Model Context Protocol)是Anthropic提出的Agent工具调用标准协议:

python 复制代码
# MCP核心概念
mcp_concepts = {
    "Server": "提供工具的服务端(如文件系统、数据库、搜索引擎)",
    "Client": "调用工具的客户端(如Claude Desktop、IDE)",
    "Tool": "Server暴露的可调用函数",
    "Resource": "Server提供的可读数据(如文件内容)",
    "Prompt": "Server提供的提示词模板",
    "Transport": "通信方式(stdio / SSE)",
}

# MCP工具定义示例
mcp_tool_example = """
@mcp.tool()
def search_documents(query: str, top_k: int = 5) -> str:
    \"\"\"搜索文档库中的相关内容
    
    Args:
        query: 搜索关键词
        top_k: 返回结果数量
    \"\"\"
    results = vector_store.search(query, top_k)
    return json.dumps(results)
"""

# MCP为什么重要?
mcp_importance = {
    "标准化": "统一的工具调用协议,不同Agent框架可复用工具",
    "生态": "一次开发,多处使用(Claude/VSCode/Cursor等)",
    "安全": "客户端控制工具权限,Server无法直接访问用户数据",
    "可组合": "多个MCP Server可组合使用",
}

10.7 本章面试题精讲

Q1(高频):Agent和纯LLM的区别?Agent的核心组件是什么?

:Agent = LLM + 工具 + 记忆 + 规划。纯LLM只能生成文本,Agent能执行动作(搜索、计算、调用API)。四大组件:感知(收集信息)、规划(制定策略)、行动(执行工具)、记忆(存储经验)。

Q2(高频):ReAct和Plan-and-Execute的区别?

:ReAct每步即时决策(Thought→Action→Observation循环),灵活但可能走弯路。Plan-and-Execute先制定完整计划再逐步执行,高效但计划可能需要动态调整。简单任务用ReAct,复杂任务用Plan-and-Execute。

Q3(中频):Function Calling的工作原理?

:LLM根据工具描述和用户问题,生成结构化的工具调用请求(工具名+参数),由客户端解析并执行,结果返回LLM继续推理。关键:LLM不直接执行工具,只生成调用意图,客户端负责执行和安全校验。

Q4(中频):Agent的记忆系统如何设计?

:短期记忆(当前对话上下文,有限长度)+ 长期记忆(向量存储,按相关性检索)。短期记忆满时,将旧记忆摘要后移入长期记忆。设计要点:记忆检索的相关性、摘要的信息保留、记忆容量管理。

Q5(高频):MCP协议是什么?解决了什么问题?

:MCP(Model Context Protocol)是Anthropic提出的Agent工具调用标准协议。解决工具碎片化问题------不同Agent框架各自实现工具集成,MCP提供统一标准,一次开发多处使用。核心概念:Server提供工具,Client调用工具,标准化通信协议。

Q6(中频):如何设计好的Agent工具?

:六大原则:①单一职责(每个工具只做一件事);②描述清晰(LLM靠描述理解用途);③参数简单(越少越好);④幂等性(查询类工具相同输入相同输出);⑤错误处理(返回有意义的错误信息);⑥安全边界(限制权限范围)。

Q7(中频):Multi-Agent有哪些协作模式?

:四种模式:①流水线(A→B→C顺序执行,适合创作场景);②辩论(多Agent讨论投票,适合决策场景);③层级(Manager分配Worker,适合复杂任务);④混合(动态选择协作模式)。

Q8(低频):Agent的规划能力如何提升?

:①Tree of Thought(多分支探索);②Reflection(自我反思和修正);③LATS(语言Agent树搜索);④使用更强的LLM做规划;⑤将规划步骤显式化(强制输出结构化计划)。

Q9(中频):Agent的安全性如何保障?

:①工具权限控制(限制可访问的文件/API);②沙箱执行(代码执行在隔离环境);③人工确认(关键操作需用户确认);④输入验证(检查工具参数合法性);⑤审计日志(记录所有工具调用)。

Q10(低频):Agent在哪些场景下表现不好?

:①需要精确计算(LLM数学能力弱);②长链推理(错误累积);③需要实时感知(LLM无法直接感知环境);④高可靠性要求(Agent行为不确定);⑤安全敏感场景(无法保证不产生有害输出)。

Q11(中频):如何评估Agent的效果?

:①任务完成率(能否完成目标);②步骤效率(最少步骤完成);③工具使用准确率(是否调用了正确的工具);④成本效率(token消耗vs效果);⑤鲁棒性(面对异常输入的表现)。SWE-bench是代码Agent的标准评测。

Q12(低频):Agent的未来发展趋势?

:①多模态Agent(视觉+语言+行动);②Self-improving Agent(从经验中学习改进);③Agent操作系统(统一管理多个Agent);④更安全的Agent(形式化验证+约束生成);⑤Agent-to-Agent协议(标准化Agent间通信)。


第11章 评估与未来

11.1 LLM评估体系

通用能力评估

python 复制代码
# 主流LLM评测基准
llm_benchmarks = {
    "MMLU": {
        "全称": "Massive Multitask Language Understanding",
        "内容": "57个学科的多选题,涵盖STEM/人文/社科",
        "评估": "通用知识广度",
        "满分": "100%",
        "GPT-4o": "88.7%",
        "DeepSeek-V3": "87.1%",
        "LLaMA-3.1-405B": "85.2%",
    },
    "HumanEval": {
        "内容": "164道Python编程题",
        "评估": "代码生成能力",
        "指标": "pass@1 (一次通过率)",
        "GPT-4o": "90.2%",
        "DeepSeek-V3": "82.6%",
    },
    "GSM8K": {
        "内容": "8500道小学数学应用题",
        "评估": "数学推理能力",
        "GPT-4o": "95.8%",
        "DeepSeek-V3": "93.2%",
    },
    "MATH": {
        "内容": "竞赛级数学题",
        "评估": "高难度数学推理",
        "GPT-4o": "76.6%",
        "DeepSeek-V3": "73.8%",
    },
    "GPQA": {
        "内容": "研究生级别专家问答",
        "评估": "专家级知识深度",
        "GPT-4o": "53.6%",
        "说明": "连人类专家也只有65%准确率",
    },
}

# 评测的注意事项
evaluation_caveats = {
    "数据污染": "评测集可能泄露到训练数据中",
    "基准过时": "基准更新速度跟不上模型进步",
    "单一指标": "MMLU高分不代表实际应用好",
    "采样偏差": "少量样本评测结果方差大",
    "提示敏感": "不同prompt格式结果差异大",
}

评测最佳实践

python 复制代码
def evaluate_llm(model, benchmarks):
    """LLM评测最佳实践"""
    results = {}
    
    for bench_name, bench_data in benchmarks.items():
        # 1. 多次采样减少方差
        scores = []
        for seed in range(5):
            score = run_benchmark(model, bench_data, seed=seed)
            scores.append(score)
        
        # 2. 报告均值和标准差
        results[bench_name] = {
            "mean": np.mean(scores),
            "std": np.std(scores),
            "min": min(scores),
            "max": max(scores),
        }
    
    # 3. 检查数据污染
    contamination = check_contamination(model, benchmarks)
    
    # 4. 人工抽检
    human_eval = sample_and_annotate(model, n=100)
    
    return {
        "benchmark_results": results,
        "contamination_check": contamination,
        "human_evaluation": human_eval,
    }

11.2 LLM-as-Judge

用LLM评估LLM的输出质量:

python 复制代码
# LLM-as-Judge评估框架
class LLMJudge:
    def __init__(self, judge_model="gpt-4o"):
        self.judge = judge_model
    
    def evaluate(self, question, answer, reference=None):
        """评估单个回答"""
        prompt = f"""请评估以下回答的质量。

问题: {question}
回答: {answer}
{"参考答案: " + reference if reference else ""}

请从以下维度评分(1-5分):
1. 准确性: 事实是否正确
2. 完整性: 是否完整回答了问题
3. 清晰性: 表达是否清晰易懂
4. 相关性: 是否切题

输出JSON格式:
{{"accuracy": X, "completeness": X, "clarity": X, "relevance": X, "overall": X, "reason": "..."}}"""
        
        result = self.judge.generate(prompt)
        return parse_json(result)
    
    def compare(self, question, answer_a, answer_b):
        """比较两个回答(A/B测试)"""
        prompt = f"""请比较以下两个回答,选择更好的一个。

问题: {question}
回答A: {answer_a}
回答B: {answer_b}

输出: {{"winner": "A"或"B", "reason": "..."}}"""
        
        result = self.judge.generate(prompt)
        return parse_json(result)

# LLM-as-Judge的注意事项
judge_caveats = {
    "位置偏差": "倾向选择第一个展示的回答",
    "长度偏差": "倾向选择更长的回答",
    "自我偏好": "某些模型偏好自己风格的回答",
    "缓解": "随机化顺序、控制长度、多Judge投票",
}

11.3 幻觉检测与缓解

python 复制代码
# 幻觉类型
hallucination_types = {
    "事实性幻觉": "编造不存在的事实(如虚构论文引用)",
    "推理幻觉": "推理过程有逻辑错误",
    "指令幻觉": "忽略用户指令,自行发挥",
    "自相矛盾": "前后回答不一致",
}

# 幻觉检测方法
class HallucinationDetector:
    def __init__(self, llm, retriever=None):
        self.llm = llm
        self.retriever = retriever
    
    def detect_by_verification(self, claim):
        """通过验证检测幻觉"""
        # 1. 将声明分解为可验证的子声明
        subclaims = self.decompose(claim)
        
        # 2. 逐个验证
        results = []
        for subclaim in subclaims:
            if self.retriever:
                evidence = self.retriever.search(subclaim)
                verified = self.verify_with_evidence(subclaim, evidence)
            else:
                verified = self.verify_by_contradiction(subclaim)
            results.append(verified)
        
        return {
            "hallucination_rate": 1 - sum(results) / len(results),
            "details": list(zip(subclaims, results)),
        }
    
    def detect_by_consistency(self, question, n_samples=5):
        """通过一致性检测幻觉"""
        answers = [self.llm.generate(question, temperature=0.7) for _ in range(n_samples)]
        # 多数投票
        consistency = max(set(answers), key=answers.count) / n_samples
        return {"consistency_score": consistency, "answers": answers}

# 幻觉缓解策略
hallucination_mitigation = {
    "RAG": "基于检索文档生成,减少编造",
    "约束生成": "限制只能基于给定上下文回答",
    "自我反思": "让模型检查自己的回答",
    "多模型验证": "用另一个模型交叉验证",
    "置信度评估": "让模型自评置信度,低置信度时拒绝回答",
    "事实核查": "对关键声明进行自动事实核查",
}

11.4 长上下文评估

python 复制代码
# 长上下文评估基准
long_context_benchmarks = {
    "Needle in a Haystack": "在长文本中插入一个关键信息,测试模型能否找到",
    "LongBench": "长文本理解任务集合",
    "InfiniteBench": "超长上下文(100K+)评估",
    "RULER": "可配置长度的检索和推理任务",
}

# Needle in a Haystack测试
def needle_in_haystack_test(model, context_length, needle_position, needle="披萨的秘诀是使用新鲜的马苏里拉奶酪"):
    """在长文本中找针测试"""
    # 1. 生成填充文本
    haystack = generate_fill_text(context_length)
    
    # 2. 在指定位置插入"针"
    position = int(len(haystack) * needle_position)
    text_with_needle = haystack[:position] + f"\n{needle}\n" + haystack[position:]
    
    # 3. 让模型找针
    question = "文本中提到了什么关于披萨的秘诀?"
    answer = model.generate(f"{text_with_needle}\n\n问题: {question}")
    
    # 4. 评估是否找到
    found = needle_key_info in answer
    return {"found": found, "position": needle_position, "context_length": context_length}

11.5 大模型未来趋势

python 复制代码
# 2025-2026年大模型趋势
future_trends = {
    "1. 推理能力突破": {
        "代表": "OpenAI o1/o3, DeepSeek-R1",
        "核心": "思维链+强化学习,数学/代码推理接近人类专家",
        "影响": "AI在复杂推理任务上达到新高度",
    },
    "2. 多模态统一": {
        "代表": "GPT-4o, Gemini, Qwen-VL",
        "核心": "文本+图像+音频+视频统一理解生成",
        "影响": "AI从"语言模型"进化为"世界模型"",
    },
    "3. Agent化": {
        "代表": "MCP协议, Computer Use, AutoGPT",
        "核心": "从对话助手到自主Agent",
        "影响": "AI能独立完成复杂工作流",
    },
    "4. 小模型崛起": {
        "代表": "Qwen2.5-1.5B, LLaMA-3.2-1B, Phi-3",
        "核心": "高质量数据+蒸馏,小模型能力逼近大模型",
        "影响": "端侧AI普及,隐私计算",
    },
    "5. 训练成本持续下降": {
        "代表": "DeepSeek-V3 ($557万), FP8训练",
        "核心": "MoE+量化+分布式优化,训练成本每年降50%",
        "影响": "更多组织能训练自己的模型",
    },
    "6. 长上下文标配": {
        "代表": "128K-1M上下文",
        "核心": "RoPE缩放+KV Cache优化+FlashAttention",
        "影响": "RAG可能被长上下文替代(部分场景)",
    },
    "7. 安全与对齐": {
        "代表": "Constitutional AI, Red Teaming",
        "核心": "更强的安全对齐和可控性",
        "影响": "AI更安全可靠,监管框架完善",
    },
}

11.6 本章面试题精讲

Q1(高频):MMLU、HumanEval、GSM8K分别评估什么能力?

:MMLU评估通用知识广度(57学科多选题),HumanEval评估代码生成能力(164道Python题),GSM8K评估数学推理(8500道小学数学题)。三者互补:MMLU测知识,HumanEval测编程,GSM8K测推理。

Q2(高频):什么是LLM-as-Judge?有什么偏差?

:用LLM评估LLM输出质量。常见偏差:①位置偏差(偏好第一个展示的回答);②长度偏差(偏好更长的回答);③自我偏好(偏好自己风格的回答)。缓解:随机化顺序、控制长度、多Judge投票。

Q3(中频):如何检测和缓解幻觉?

:检测:①声明分解+逐个验证;②多次采样检查一致性;③与检索文档对比。缓解:①RAG(基于文档生成);②约束生成(只基于给定上下文);③自我反思(让模型检查自己);④多模型验证;⑤置信度评估(低置信度时拒绝)。

Q4(中频):Needle in a Haystack测试是什么?

:在长文本中随机位置插入一个关键信息("针"),测试模型能否找到。通过在不同长度和位置组合测试,绘制"找到率热力图",直观展示模型的长上下文能力。LLM普遍存在Lost in the Middle问题------中间位置的"针"最难找到。

Q5(高频):评测数据污染是什么?如何避免?

:训练数据包含评测集内容,导致模型"背到了题"而非真正学会。避免:①训练前严格N-gram去重过滤;②使用动态评测集(定期更新);③检测方法:用模型生成评测题目看能否直接复现。

Q6(中频):大模型的未来趋势是什么?

:七大趋势:①推理能力突破(o1/o3的思维链);②多模态统一(文本+图像+音频);③Agent化(从助手到自主Agent);④小模型崛起(端侧AI);⑤训练成本下降(MoE+量化);⑥长上下文标配(128K+);⑦安全对齐加强。

Q7(低频):如何设计一个公平的LLM评测?

:①使用多个评测基准(避免单一指标);②多次采样减少方差;③检查数据污染;④统一prompt格式;⑤人工抽检验证;⑥报告均值和标准差;⑦区分few-shot和zero-shot结果。

Q8(中频):RAG和长上下文模型哪个更好?

:各有优势。长上下文(128K+)适合:文档一次性输入、不需要外部知识库。RAG适合:知识库超大(>1M token)、需要实时更新、需要引用来源。趋势:长上下文在部分场景替代RAG,但RAG在大规模知识库场景仍有优势。

Q9(低频):什么是Constitutional AI?

:Anthropic提出的安全对齐方法。核心思想:用一组"宪法原则"指导AI自我修正,而非仅依赖人类反馈。流程:AI生成回答→用宪法原则评估→AI根据评估修正→迭代优化。减少对人类标注的依赖。

Q10(中频):如何评估Agent的效果?

:五个维度:①任务完成率(能否完成目标);②步骤效率(最少步骤完成);③工具使用准确率(是否调用了正确的工具);④成本效率(token消耗vs效果);⑤鲁棒性(面对异常输入的表现)。SWE-bench是代码Agent的标准评测,WebArena是Web Agent的标准评测。