LLM 工程不是只有模型结构,数据、评测和失败处理同样是核心

项目目标

前面已经把模型内部和推理系统走了一遍。接下来进入更大的系统:稀疏模型、替代架构、数据管线、合成数据、scaling laws、后训练、量化、服务、评测、RAG、工具、Agent、多模态、可解释性和安全。

这部分看起来很散,但可以用一个问题串起来:模型进入真实应用后,哪些能力来自参数,哪些来自数据,哪些来自检索,哪些来自工具,哪些必须靠评测和安全边界兜住。

项目 17-18:MoE 与 sparse trade-offs

MoE 的动机是增加总参数量,但每个 token 只激活一部分 expert。

最小项目:实现一个 two-expert router。

token -> router -> expert A or expert B -> combine output

要记录:

  • 每个 expert 的 token 数。
  • router entropy。
  • 是否出现 expert collapse。
  • dense MLP 与 sparse MoE 的 FLOPs 对比。

破坏实验:人为给 router 加偏置,让它总选同一个 expert。观察另一个 expert 是否完全学不到东西。

最小 top-1 MoE 实现(两个 expert):

import torch
import torch.nn as nn
import torch.nn.functional as F

class TopKMoE(nn.Module):
    def __init__(self, n_embd, n_experts=4, top_k=1):
        super().__init__()
        self.experts = nn.ModuleList([
            nn.Sequential(nn.Linear(n_embd, 4*n_embd), nn.GELU(), nn.Linear(4*n_embd, n_embd))
            for _ in range(n_experts)
        ])
        self.router = nn.Linear(n_embd, n_experts, bias=False)
        self.top_k = top_k

    def forward(self, x):
        B, T, C = x.shape
        scores = self.router(x)                # (B, T, n_experts)
        top_val, top_idx = scores.topk(self.top_k, dim=-1)
        weights = F.softmax(top_val, dim=-1)   # 归一化权重

        out = torch.zeros_like(x)
        # 把每个 token 路由到选中的 expert
        for k in range(self.top_k):
            for i, expert in enumerate(self.experts):
                mask = top_idx[..., k] == i              # (B, T) 布尔掩码
                if not mask.any():
                    continue
                out[mask] += weights[..., k:k+1][mask] * expert(x[mask])

        # 记录 router 行为
        self.last_route = top_idx                       # 用于统计每 expert token 数
        self.last_entropy = -(F.softmax(scores, dim=-1) * F.log_softmax(scores, dim=-1)).sum(-1).mean()
        return out

# 统计 expert 利用率:跑完一个 batch 后看每 expert 处理了多少 token
moe = TopKMoE(n_embd=128, n_experts=4)
y = moe(torch.randn(2, 64, 128))
for i in range(4):
    used = (moe.last_route == i).sum().item()
    print(f"expert {i}: {used} tokens")
print(f"router entropy: {moe.last_entropy:.3f}")

expert collapse 怎么识别:理想情况 4 个 expert 各占 25%,entropy ≈ ln(4) ≈ 1.39;如果有一个 expert 拿到 70%+ 流量、entropy < 1.0,就是 collapse。生产 MoE 都会加 load balancing loss(鼓励均匀分配)来抑制 collapse。

项目 19-20:Transformer 之外的序列模型

Transformer 是主线,但不是唯一方案。

可以做两个 toy:

  • state-space / linear-attention 模型:观察线性时间序列处理。
  • diffusion-style language model:观察非自回归、mask-denoise 生成思路。

这里不追求复现大模型,而是建立判断力:当有人说“替代 Transformer”时,知道它到底是在改复杂度、改生成范式,还是改训练目标。

项目 21:预训练数据管线

数据质量常常比模型结构更早决定上限。

最小数据管线包括:

  1. 原始文本采集。
  2. 文档级去重。
  3. 低质量过滤。
  4. 语言检测。
  5. 敏感信息过滤。
  6. train/val/test 切分。
  7. tokenizer 统计。
  8. 数据卡片。

要做的图:

  • 文档长度分布。
  • token 长度分布。
  • 重复率。
  • 过滤前后样例。
  • 不同数据子集训练 loss 对比。

最常被忽略的一步是文档级去重——预训练数据里 30~50% 是近似重复很常见,不去重会让模型反复看到同样内容、过拟合特定句式。最小 MinHash 去重实现:

from datasketch import MinHash, MinHashLSH

def minhash(text, num_perm=128):
    """把文本变成一个可以快速比较的指纹"""
    m = MinHash(num_perm=num_perm)
    # 用 5-gram 字符片段作为特征
    for i in range(len(text) - 4):
        m.update(text[i:i+5].encode())
    return m

# 阈值 0.8 = 文档之间 Jaccard 相似度 ≥ 80% 视为重复
lsh = MinHashLSH(threshold=0.8, num_perm=128)
deduped = []

for doc_id, doc in enumerate(docs):
    sig = minhash(doc)
    # 查询是否有近似副本
    if not lsh.query(sig):
        lsh.insert(f"doc_{doc_id}", sig)
        deduped.append(doc)

print(f"原始 {len(docs)} 篇 → 去重后 {len(deduped)} 篇,重复率 {1 - len(deduped)/len(docs):.1%}")

MinHash + LSH 是工业标准做法(C4 / RefinedWeb / The Pile 都用过),近似检测 O(N) 时间,准确度足够预训练场景。

项目 22:合成数据

合成数据不能只看“生成了多少”。它必须回答三个问题:

  • 是否覆盖真实数据缺口。
  • 是否被过滤和去重。
  • 是否在独立评测集上真的提升。

最小实验:

  1. 用一个强模型生成某类任务数据。
  2. 加入噪声样例和重复样例。
  3. 做过滤、去重、质量打分。
  4. 分别训练 real-only、synthetic-only、mixed。
  5. 在干净 eval split 上比较。

如果 mixed 只提升训练集、不提升验证集,它很可能只是让模型学会了生成器的口癖。

项目 23:scaling curves

训练 tiny/small/medium 三个模型,固定数据与训练设置,画 loss vs 参数量、loss vs token 数、训练时间 vs 参数量。

这个项目的目的不是推导理想化 scaling law,而是形成直觉:

  • 模型太小会欠拟合。
  • 数据太少会过拟合。
  • compute 固定时,参数和 token 要一起考虑。
  • 更大不自动等于更好,尤其在数据和训练预算不足时。

项目 24-25:后训练与偏好优化

后训练把 base model 变成 assistant。

先做 SFT:把指令、输入、答案整理成 supervised dataset。

再做 LoRA/QLoRA:只训练低秩 adapter,降低显存成本。

再做偏好优化:DPO 是最容易上手的一类,RLHF/PPO/GRPO/RLVR 更接近 reasoning 模型训练讨论。

最小交付:

  • 一个 SFT 数据集。
  • 一个 LoRA adapter。
  • 一组 before/after 输出对比。
  • 一个偏好数据小样本。
  • DPO 或 toy PPO 实验记录。

LoRA 最小实现(一段就能看清"低秩 adapter 是什么"):

import torch.nn as nn

class LoRALinear(nn.Module):
    """在原 Linear 旁边加一个低秩旁路:W' = W + (B A) * scale"""
    def __init__(self, base: nn.Linear, rank=8, alpha=16):
        super().__init__()
        self.base = base
        for p in self.base.parameters():
            p.requires_grad = False         # 冻结原权重
        in_f, out_f = base.in_features, base.out_features
        self.lora_A = nn.Linear(in_f, rank, bias=False)
        self.lora_B = nn.Linear(rank, out_f, bias=False)
        nn.init.zeros_(self.lora_B.weight)  # 初始化为 0 → 初始等价于原模型
        self.scale = alpha / rank

    def forward(self, x):
        return self.base(x) + self.scale * self.lora_B(self.lora_A(x))

# 用法:把模型里所有 nn.Linear(或只挑 q_proj / v_proj)替换成 LoRALinear
# 训练时只有 lora_A / lora_B 的参数会更新,参数量从几亿降到几百万

DPO 损失(直接从偏好对学习,不需要 PPO 那套 RL 复杂度):

import torch
import torch.nn.functional as F

def dpo_loss(policy_logp_chosen, policy_logp_rejected,
             ref_logp_chosen, ref_logp_rejected, beta=0.1):
    """
    每条偏好样本由 (prompt, chosen_response, rejected_response) 组成。
    logp_* 是模型在对应回答上的对数概率求和。
    """
    # 当前策略相对于参考模型的对数比
    policy_ratio = policy_logp_chosen - policy_logp_rejected
    ref_ratio = ref_logp_chosen - ref_logp_rejected
    # DPO 目标:让 policy 更倾向 chosen,与 ref 拉开距离
    logits = beta * (policy_ratio - ref_ratio)
    loss = -F.logsigmoid(logits).mean()
    return loss

beta 控制偏离 ref 模型的程度——大 beta 让 policy 离 ref 更近(保守),小 beta 让 policy 更激进地拟合偏好数据。DPO 比 PPO 简单的核心点:不需要 reward model,不需要 rollout,监督学习级别的训练成本就能做偏好对齐

项目 26-27:量化与 serving stack

量化不是“把模型变小”这么简单。它改变数值行为、显存占用、带宽压力和有时的输出质量。

要比较:

  • FP16/BF16。
  • INT8。
  • INT4。
  • GGUF/AWQ/GPTQ 等格式。

serving stack 选择:

  • llama.cpp:本地、CPU/GPU 混合、GGUF 生态。
  • vLLM:高吞吐、PagedAttention、OpenAI compatible server。
  • TensorRT-LLM:NVIDIA 生态深度优化。
  • SGLang:结构化生成、调度、prefix cache。

记录首 token 延迟、吞吐、显存、并发、失败模式。

项目 28:evaluation harness

如果不能测,就不能改。

最小评测体系:

层次示例
单元测试tokenizer round-trip、mask 不看未来
模型评测loss、perplexity、小任务准确率
RAG 评测recall、faithfulness、answer relevance
Agent 评测success rate、tool error、step count
安全评测prompt injection、越权工具调用、敏感输出

评测集要版本化。每次改 prompt、模型、检索、rerank、工具 schema,都要能回放。

项目 29-30:RAG、工具与 Agent

RAG 不是把 PDF 塞进向量库这么简单。它至少包含:切分、embedding、索引、召回、rerank、上下文构造、引用、答案验证。

工具调用也不是“让模型调用函数”这么简单。它包含 schema 设计、权限、幂等、超时、错误恢复、审计。

Agent loop 要在理解模型、检索和工具之后再做。否则框架会掩盖失败来源。

项目 31-33:多模态、可解释性与安全

多模态最小项目:接一个 vision encoder,把图像特征映射到 LLM 可消费的 token 空间。重点是理解 adapter,而不是追求效果。

可解释性最小项目:观察 attention、训练 probes、尝试 sparse autoencoder 的概念实验。

安全最小项目:建立红队样例库。包括 prompt injection、越权工具调用、数据泄露、RAG 污染、代码执行危险、内容安全边界。

本项目交付物

  • two-expert MoE router 和 expert utilization 图。
  • 数据管线报告。
  • synthetic data 对比实验。
  • scaling curve。
  • LoRA/QLoRA 微调记录。
  • 量化损伤表。
  • serving stack benchmark。
  • evaluation harness。
  • RAG/Agent 失败样例库。
  • 安全红队用例集。

和本站已有内容的连接

  • ml-basics/混合专家模型
  • fine-tuning/document-processing-training-data
  • inference-opt/04-LoRA与QLoRA微调
  • vector-db/01-向量数据库到底是什么
  • llm-app/RAG检索增强生成
  • ai-agent/01-Agent是什么
  • agent-eval/01-评判一个Agent
  • code-agent-harness/15-代码Agent安全边界

延伸阅读

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:项目 17-33:MoE、数据、后训练、评测、RAG、Agent 与安全

本文链接:https://www.sshipanoo.com/blog/ai/llm-roadmap/05-数据后训练评测与应用系统/

本文最后一次更新为 天前,文章中的某些内容可能已过时!