往期回顾
如何速成LLM以伪装成一个AI研究者(1)——循环,卷积,编解码器,注意力,Transformer
如何速成LLM以伪装成一个AI研究者(2)——Pre-LN,KV-Cache优化,MoE
如何速成LLM以伪装成一个AI研究者(3)——预训练,监督微调,强化学习RLHF/DPO
如何速成LLM以伪装成一个AI研究者(4)——PPO,GRPO,DAPO,GSPO
如何速成LLM以伪装成一个AI研究者(5)——显存估算,显卡选择
如何速成LLM以伪装成一个AI研究者(6)——LoRA,Adapter,P-tuning,量化,QLoRA
免责声明:作者也是伪装的,有错漏属于正常现象,欢迎评论指正。
从“伪装”迈向实践
计算机科学是工程学科,人们关心什么问题、为什么关心?有哪些想法看着很美好,实践起来困难重重?哪些公司在无依无据地吹牛逼,谁又在脚踏实地地做实事?这些往往需要积累才能判断。
关于如何入门LLM,有看到一句说得很在理的话:
首先看完Attention is all you need,然后找一份实习。
呃,但是遇到下面的情况怎么办?

这不就是伪装的意义吗!
这种伪装倒不是让你骗人,有实践经验的地方你可以阐述得更详细,没有经验的地方可以承认自己实践经验不足,但依然可以阐述自己对相关问题的一定见解,让面试官看到你不是大脑空空。最终,当你真的有了经验,有了自己的research taste,有自己的实践经验和技术价值观判断后,也就不再需要花时间琢磨怎么伪装了。
那么接下来我们来做一些面试测试题(俗称面试八股),也算是“伪装课程”的一个阶段性测试。参考答案是作者的视角答案,不一定是最好的,并且主要集中在RL/Agent方向。作者也会持续性收集一些问题更新博文。
面试八股暨阶段性测试
架构
简述RMSNorm和LayerNorm的区别?
如何速成LLM以伪装成一个AI研究者(2)——Pre-LN,KV-Cache优化,MoE
LayerNorm同时调整可学习缩放参数 γ \gamma γ(方差)和可学习平移参数 β \beta β(均值),RMSNorm只调整 γ \gamma γ。RMSNorm 的原始论文通过实验证明了 β \beta β的重要性远不如 γ \gamma γ,所以RMSNorm可以在相似性能的情况下,因为减少可学习参数而计算量更小、更稳定,是现在主流架构的选择。
简述pre-norm(pre-LN)和post-norm(post-LN)的区别?
如何速成LLM以伪装成一个AI研究者(2)——Pre-LN,KV-Cache优化,MoE
在训练初期或者数值不稳定时,post-norm的整个链式求导的谱范数可能出现指数级增长,因此刚需warm-up流程。pre-norm对残差连接保留得更好,它的梯度和训练效果更加稳定,是现在的主流选择。
RL
RL训练中一般需要关注哪些指标?
- 奖励(Reward):平均奖励及其趋势,判断模型是否在优化目标。
- KL 散度(KL Divergence):当前策略与参考模型间的差异,防止偏离过远。
- 响应长度(Response Length):生成序列的平均/最大长度,监控是否出现长度崩塌或不合理增长。
- 策略熵(Policy Entropy):反映输出的多样性,熵过低易导致重复或退化。
RL训练中,出现重复输出可能是什么原因导致的?
- Reward Hacking:模型发现重复某些高奖励片段(如特定格式、关键词)能以低成本骗取更高回报,属于对奖励函数的过度优化。
- KL约束失效:KL散度惩罚过弱或系数衰减过快,策略严重偏离原始SFT模型,丧失自然语言多样性。
- 熵正则化不足:策略熵过低,采样趋于贪婪,迅速陷入局部最优循环。
RL训练中,出现乱码输出可能是什么原因导致的?
- 数据分布问题:预训练/RL训练/测试数据的分布不一致,RL训练数据的词表覆盖率不足,部分token的logprob出现异常。
- 训练不稳定:学习率过大、PPO的clip范围过宽或梯度累积不当,导致策略网络权重更新失控。
RL训练为什么会让seq_len变长?
模型为了获取更高奖励,会倾向于生成更详细、更完整的内容(比如添加解释、推理步骤、安全声明等)。如果奖励函数无意中偏好长回答(如避免信息缺失或格式要求),模型就会学到“输出更多 token 能提高得分”,从而导致生成序列长度自动增长。
如何控制entropy?
这里我推荐一篇论文:Addressing Performance Saturation for LLM RL via Precise Entropy Curve Control,对这个问题分析得比较全面。以下是小结:
熵崩塌原因:正样本导致熵降,负样本导致熵增。因为训练后期正样本越来越多,模型熵就崩了,只会输出少数“安全”答案。
控熵办法:损失正则化(加入熵惩罚项),裁剪技术(限制更新幅度),正负样本解耦(分别处理正负样本)
本文方法:使用拒绝采样(Reject Sampling),控制正负样本数量来控熵,初始目标(0.6,0.7),最终目标(0.1,0.2),退火方式线性衰减
GAE是什么? γ \gamma γ一般怎么设置?
如何速成LLM以伪装成一个AI研究者(4)——PPO,GRPO,DAPO,GSPO
GAE是一种平衡优势函数的偏差和方差的技巧。 γ \gamma γ越大,越低偏差、高方差,模型越考虑长期回报。 γ \gamma γ越小,越高偏差、低方差,模型越不考虑长期回报。为了平衡方差和长期回报,常见任务一般取0.95-0.99。
KL估计器是什么?K1、K2、K3估计器有什么区别?
推荐博客:KL散度近似方法介绍:从John Schulman的博客到DeepSeek GRPO的应用
KL估计器是PPO作者John Schulman在其博客中提出的,Monte Carlo估计KL散度的时候减小方差的近似估计方法。其中, K1简单无偏,但方差大,K2有偏,减少方差,K3无偏但是减少方差。实践中多用K3。
正向KL和反向KL是什么?在RL应用中有什么不同?
如何速成LLM以伪装成一个AI研究者(3)——预训练,监督微调,强化学习RLHF/DPO
设真实分布(客观分布)为 P P P,拟合分布(主观分布)为 Q Q Q,正向KL(fKL)为 K L ( P ∥ Q ) = ∑ x P ( x ) log P ( x ) Q ( x ) KL(P \parallel Q)=\sum_x P(x)\log \frac{P(x)}{Q(x)} KL(P∥Q)=∑xP(x)logQ(x)P(x),反向KL(rKL)为 K L ( Q ∥ P ) = ∑ x Q ( x ) log Q ( x ) P ( x ) KL(Q \parallel P)=\sum_x Q(x)\log \frac{Q(x)}{P(x)} KL(Q∥P)=∑xQ(x)logP(x)Q(x)。
fKL是mode-covering(覆盖模式)——会狠狠惩罚 P ( x ) P(x) P(x)大,而 Q Q Q没有覆盖到的地方,因此Q会倾向于把峰铺开。
rKL是mode-seeking(追逐主峰)——关注 Q ( x ) Q(x) Q(x)大的地方,是否和 P P P分布一致。故Q会倾向于抓住P最主要的峰。
fKL 常用于 模仿学习 / 行为克隆,而rKL一般用于策略优化(PPO / TRPO / RLHF),以约束新策略和旧策略的主峰不偏离。
PPO的loss包含哪些?
PPO 的 loss 包含三部分:
策略损失(clipped surrogate):核心项,通过 clip 限制更新幅度。
价值损失(value loss):通常为价值网络输出的均方误差(MSE)。
熵正则项(entropy bonus):鼓励探索,增加策略随机性。
PPO的clip有什么用?
用于限制策略更新的幅度,防止单步更新过大导致训练不稳定或性能崩塌。
Policy的top_P,top_K是什么?
在策略模型(生成)中,top_p 和 top_k 是两种截断采样策略,用于控制输出的随机性与多样性:
top_k:仅从概率最高的 k 个 token 中采样(例如 top_k=50),其余 token 概率置零并重新归一化。可防止低概率 token 被选中。
top_p(核采样):从累积概率和刚好超过 p(如 0.9)的最小 token 集合中采样,动态调整候选集大小。比 top_k 更自适应。
两者常结合使用:先按 top_k 过滤,再按 top_p 截断。
大模型RL训练中,loss初始值为0正常吗?
PPO的情况:不正常。PPO的优势函数一般非0,并且value loss一般非0。
GRPO的情况:正常。
max θ J G R P O ( θ ) = E [ 1 G ∑ i = 1 G 1 ∣ o i ∣ ∑ t = 1 ∣ o i ∣ min ( r t ( θ ) A ^ i , t , c l i p ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A ^ i , t ) ] \begin{equation} \max_{\theta} J_{GRPO}(\theta)=E[\frac{1}{G}\sum_{i=1}^G \frac{1}{|o_i|}\sum_{t=1}^{|o_i|}\min (r_t(\theta) \hat{A}_{i,t}, clip(r_t(\theta), 1-\epsilon, 1+\epsilon)\hat{A}_{i,t})] \end{equation} θmaxJGRPO(θ)=E[G1i=1∑G∣oi∣1t=1∑∣oi∣min(rt(θ)A^i,t,clip(rt(θ),1−ϵ,1+ϵ)A^i,t)]
初始 π θ = π o l d \pi_{\theta}=\pi_{old} πθ=πold,故KL散度项为0,并且 r t ( θ ) = 1 r_t(\theta)=1 rt(θ)=1。而 A ^ i , t \hat{A}_{i,t} A^i,t有做过归一化,在 r t ( θ ) = 1 r_t(\theta)=1 rt(θ)=1的情况下, ∑ A ^ i , t = 0 \sum \hat{A}_{i,t}=0 ∑A^i,t=0。故GRPO的loss为0是正常的。
已经有奖励函数了,为什么强化学习(GRPO)还需要优势呢?
奖励函数给出的是绝对得分,但不同 prompt 的难度不同,奖励尺度难以直接比较。优势函数通过组内标准化将奖励转为相对好坏(均值为0),让模型专注于同一 prompt 下回答间的优劣差异,从而稳定训练、避免跨 prompt 的尺度干扰。
为什么计算优势时要乘以重要性比率( r t ( θ ) r_t(\theta) rt(θ))?
用于纠正数据分布偏移。我们只能从旧策略采样的数据中计算优势,但更新的是新策略。乘以该比率后,损失函数的梯度等价于从新策略采样下的期望梯度,从而允许安全地重复使用旧数据进行多轮更新。
简述GSPO相对GRPO做了什么改进?
GRPO (Token级):每个Token计算一次重要性权重,进而导致权重剧烈波动、方差大。
GSPO (序列级):整个序列计算一次总重要性比率,信号平滑稳定,有效避免了噪声累积。
Agent
Thinking主要提升了agent哪方面的能力?
planning的能力,状态理解和资源规划的能力,反思和纠错能力。
为什么有些时候,Thinking反而破坏了agent的性能?
我认为主要是训练错位问题,思考模型经常在数学题之类的高反思场景训练,没有与agent适配,导致Thinking模型容易内耗(倾向于内部模拟而轻视与外部环境的交互),降低效率。但是“agent不需要数学题一样的thinking”不等于“agent不需要thinking”,我认为thinking依旧可以提升agent的能力上限。
实现长上下文Agent有什么主要挑战?
- 链式崩塌与纠错能力:Agent每一步都是概率性的,偏差容易在长期执行中被无限放大。
- 长上下文记忆
- 长期规划与planning、子任务划分能力
- 可观测性和调试:Agent的推理链路不可复现,无法日志打断点。
- 成本和延迟控制
multi-agent有哪些编排格式?
Orchestrator-Worker(主从):最常用、最可控、最适合并行
Sequential:分工按序列运行,一般用于处理大量相似事件。容易积累误差。
Hierarchical:对于复杂度较高的任务,可能需要分层管理结构合作
Group Chat:看起来酷,一用一个不吱声
手撕
MHA
import torch
import torch.nn as nn
import torch.nn.functional as F
class MultiHeadAttention(nn.Module):
"""简易多头注意力实现,展示主要功能"""
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0, "d_model 必须能被 num_heads 整除"
self.d_model = d_model # 输入/输出特征维度
self.num_heads = num_heads # 注意力头数
self.d_k = d_model // num_heads # 每个头的维度
# 线性变换层:生成 Q, K, V 和最终输出投影
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, d_model)
self.W_v = nn.Linear(d_model, d_model)
self.W_o = nn.Linear(d_model, d_model)
def _split_heads(self, x, batch_size):
"""将最后一维拆分成 (num_heads, d_k),并交换维度便于并行计算"""
# x: (batch, seq_len, d_model) -> (batch, seq_len, num_heads, d_k)
x = x.view(batch_size, -1, self.num_heads, self.d_k)
# 转换为 (batch, num_heads, seq_len, d_k)
return x.transpose(1, 2)
def _combine_heads(self, x, batch_size):
"""将多头结果合并回 (batch, seq_len, d_model)"""
# x: (batch, num_heads, seq_len, d_k) -> (batch, seq_len, num_heads, d_k)
x = x.transpose(1, 2).contiguous()
# -> (batch, seq_len, d_model)
return x.view(batch_size, -1, self.d_model)
def _scaled_dot_product_attention(self, Q, K, V, mask=None):
"""缩放点积注意力核心计算"""
# Q, K, V: (batch, num_heads, seq_len, d_k)
scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attn_weights = F.softmax(scores, dim=-1) # (batch, num_heads, seq_len, seq_len)
output = torch.matmul(attn_weights, V) # (batch, num_heads, seq_len, d_k)
return output, attn_weights
def forward(self, query, key, value, mask=None):
batch_size = query.size(0)
# 1. 线性变换得到 Q, K, V
Q = self.W_q(query)
K = self.W_k(key)
V = self.W_v(value)
# 2. 拆分为多个头
Q = self._split_heads(Q, batch_size)
K = self._split_heads(K, batch_size)
V = self._split_heads(V, batch_size)
# 3. 缩放点积注意力 (每个头独立计算)
attn_output, attn_weights = self._scaled_dot_product_attention(Q, K, V, mask)
# 4. 合并所有头
concat_output = self._combine_heads(attn_output, batch_size)
# 5. 最终线性变换
output = self.W_o(concat_output)
return output, attn_weights
# ========== 示例:展示 MHA 主要功能 ==========
if __name__ == "__main__":
# 超参数
d_model = 512 # 嵌入维度
num_heads = 8 # 多头数量
batch_size = 2
seq_len = 10
# 模拟输入 (batch, 序列长度, 特征维度)
query = torch.randn(batch_size, seq_len, d_model)
key = torch.randn(batch_size, seq_len, d_model)
value = torch.randn(batch_size, seq_len, d_model)
# 创建多头注意力模块
mha = MultiHeadAttention(d_model, num_heads)
# 前向传播,得到输出和注意力权重
output, attn_weights = mha(query, key, value)
print("输入 query shape: ", query.shape) # (2, 10, 512)
print("输出 shape: ", output.shape) # (2, 10, 512)
print("注意力权重 shape: ", attn_weights.shape) # (2, 8, 10, 10)
print("多头注意力机制运行完毕,每个头关注不同子空间的信息。")
GQA
import torch
import torch.nn as nn
import torch.nn.functional as F
class GroupedQueryAttention(nn.Module):
"""分组查询注意力 (GQA) - 简洁实现"""
def __init__(self, d_model, num_heads, num_kv_heads):
super().__init__()
assert d_model % num_heads == 0
assert num_heads % num_kv_heads == 0, "查询头数必须是KV头数的整数倍"
self.d_model = d_model
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads
self.d_k = d_model // num_heads # 每个头的维度
self.group_size = num_heads // num_kv_heads # 每组包含的查询头个数
# Q, K, V 线性投影 (K和V只投影 num_kv_heads 份)
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, num_kv_heads * self.d_k)
self.W_v = nn.Linear(d_model, num_kv_heads * self.d_k)
self.W_o = nn.Linear(d_model, d_model)
def forward(self, query, key, value, mask=None):
batch_size = query.size(0)
seq_len = query.size(1)
# 1. 线性投影 -> (batch, seq_len, num_heads*d_k) 和 (batch, seq_len, num_kv_heads*d_k)
Q = self.W_q(query).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
K = self.W_k(key).view(batch_size, -1, self.num_kv_heads, self.d_k).transpose(1, 2)
V = self.W_v(value).view(batch_size, -1, self.num_kv_heads, self.d_k).transpose(1, 2)
# 2. GQA 核心:将 K, V 按组重复,使头数与 Q 对齐
# 形状从 (batch, num_kv_heads, seq_len, d_k) 扩展到 (batch, num_heads, seq_len, d_k)
K = K.repeat_interleave(self.group_size, dim=1) # 沿着头维度重复
V = V.repeat_interleave(self.group_size, dim=1)
# 3. 缩放点积注意力 (标准计算)
scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attn_weights = F.softmax(scores, dim=-1)
attn_output = torch.matmul(attn_weights, V) # (batch, num_heads, seq_len, d_k)
# 4. 合并头并输出投影
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
return self.W_o(attn_output)
# ========== 演示 GQA 主要功能 ==========
if __name__ == "__main__":
d_model = 512
num_heads = 8
num_kv_heads = 2 # 2组KV头,每组服务4个查询头 (group_size=4)
batch, seq_len = 2, 10
query = torch.randn(batch, seq_len, d_model)
key = torch.randn(batch, seq_len, d_model)
value = torch.randn(batch, seq_len, d_model)
gqa = GroupedQueryAttention(d_model, num_heads, num_kv_heads)
out = gqa(query, key, value)
print("输入 shape:", query.shape) # (2, 10, 512)
print("输出 shape:", out.shape) # (2, 10, 512)
print(f"GQA 完成:{num_heads} 个查询头,仅 {num_kv_heads} 个KV头,减少 KV 缓存至原来的 {num_kv_heads/num_heads:.0%}")
GRPO Loss
import torch
import torch.nn.functional as F
def grpo_loss(policy_logps, ref_logps, old_logps, rewards, group_ids,
epsilon=0.2, beta=0.04):
"""
GRPO 损失函数 (简化版,核心计算流程)
Args:
policy_logps: (B,) 当前策略下生成序列的对数概率
ref_logps: (B,) 参考模型(通常是冻结的 SFT 模型)的对数概率
old_logps: (B,) 旧策略(更新前的策略)的对数概率
rewards: (B,) 每个序列的奖励值(原始环境奖励或结果奖励)
group_ids: (B,) 每个序列所属的组编号(每组对应同一 prompt 的多个采样)
epsilon: PPO clip 范围
beta: KL 惩罚系数
Returns:
损失标量
"""
# 1. 组内奖励标准化 → 优势 (advantage)
advantages = torch.zeros_like(rewards)
for gid in torch.unique(group_ids):
mask = (group_ids == gid)
group_rewards = rewards[mask]
mean = group_rewards.mean()
std = group_rewards.std() + 1e-8
advantages[mask] = (group_rewards - mean) / std # 组内相对优势
# 2. 概率比 r(θ) = exp(π_θ / π_old)
ratio = torch.exp(policy_logps - old_logps)
# 3. PPO clipped 策略损失 (仅针对被采样序列)
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - epsilon, 1 + epsilon) * advantages
policy_loss = -torch.min(surr1, surr2).mean()
# 4. KL 散度惩罚 (当前策略 vs 参考策略)
# 常用近似:KL(π_θ || π_ref) = exp(logπ_ref - logπ_θ) - (logπ_ref - logπ_θ) - 1
kl_penalty = (torch.exp(ref_logps - policy_logps) - (ref_logps - policy_logps) - 1).mean()
# 总损失
loss = policy_loss + beta * kl_penalty
return loss
# ========== 使用示例 ==========
if __name__ == "__main__":
# 假设 batch 中有 4 个序列,来自 2 个不同的 prompt(每组 2 个采样)
batch_size = 4
policy_logps = torch.randn(batch_size) # 当前策略对数概率
ref_logps = torch.randn(batch_size) # 参考模型对数概率
old_logps = torch.randn(batch_size) # 旧策略对数概率
rewards = torch.tensor([0.8, 0.3, 0.6, 0.9]) # 原始奖励
group_ids = torch.tensor([0, 0, 1, 1]) # 组 ID
loss = grpo_loss(policy_logps, ref_logps, old_logps, rewards, group_ids)
print(f"GRPO Loss: {loss.item():.4f}")
——面试八股【持续更新中】&spm=1001.2101.3001.5002&articleId=161196573&d=1&t=3&u=ad51d2053a8b405a8840ca5c8ecf0392)
207

被折叠的 条评论
为什么被折叠?



