---
title: 模型架构:Dense 与 MoE
url: https://doc.liz6.com/ai/01-models-and-context/03-model-architecture-dense-vs-moe
locale: zh
area: ai
tags:
- ai
- 模型与上下文
date: 2026-06-30
modified: 2026-07-16
description: 几乎所有现代 LLM 都是 Transformer decoder。MoE 在过去几年成为最重要的架构折中——让模型"参数很大、但每个 token 只用一小部分",把容量和每 token 算力解耦。
---

# 模型架构:Dense 与 MoE

> 几乎所有现代 LLM 都是 Transformer decoder。MoE 在过去几年成为最重要的架构折中——让模型"参数很大、但每个 token 只用一小部分",把容量和每 token 算力解耦。

## 概述

几乎所有现代 LLM 都是 **Transformer decoder**。差异主要在**规模、以及怎么花这些规模**:参数越多通常越强,但每个 token 的推理成本、显存占用、延迟也随之涨。**MoE(Mixture of Experts)** 是过去几年最重要的架构折中——让模型"**参数很大、但每个 token 只用一小部分**",把"容量"和"每 token 算力"解耦。读懂 dense 与 MoE 的机制,直接关系到三件实事:本地部署选型与显存预算(见 [本地 LLM 部署](/homelab/local-llm.md))、推理吞吐、以及看懂像 Qwen-AgentWorld 那样的前沿模型卡。

先划清边界:**闭源模型(Claude、GPT 等)不公开架构**——是不是 MoE、多少层、多少参数,官方都没说。本文讲**通用机制 + 开源模型的可核实事实**;对闭源模型,能依赖的是 **Models API** 报告的能力,而不是臆测的内部结构。下文凡涉及 Claude,只谈能力查询,不谈结构。

## Transformer decoder 怎么工作

```mermaid
flowchart TD
    E["token embedding + 位置编码"] --> R["残差流 residual stream"]
    R --> L1["Layer 1"]
    subgraph L1["每一层 (×N)"]
        direction TB
        A["self-attention<br/>(Q·K → 权重, 加权 V)"] --> F["FFN<br/>(升维→激活→降维)"]
    end
    L1 --> LN["... Layer N"]
    LN --> O["输出 logits<br/>(下一个 token 分布)"]
```

要点(decoder-only,即 GPT 谱系):

- **残差流(residual stream)**:每层的输出 = 输入 + 该层增量。信息沿这条"主干"流动,每层往上读写。
- **self-attention**:每个 token 投影出 **Q/K/V** 三组向量;用 Q·K 算出"该关注谁"的权重,再对 V 加权求和。**multi-head** 是并行跑多组 Q/K/V 看不同关系。**因果掩码(causal mask)**保证一个 token 只能看它之前的——这才能自回归生成。注意力是 ~O(n²) 成本与 **KV cache** 的来源(见 [上下文工程](/ai/01-models-and-context/01-context-engineering.md))。
- **FFN(前馈网络)**:对每个位置独立地"升维→非线性→降维"(典型升 4×)。**模型的大部分参数在 FFN 里**——这正是 MoE 动刀的地方。

## Dense vs MoE:机制

- **Dense(稠密)**:每个 token 都流经**全部** FFN 参数。70B dense 模型,每个 token 都动用 70B。
- **MoE**:把每层的 FFN 换成 **N 个专家(expert)+ 一个路由器(router/gating)**。router 给每个 token 算一个"该用哪些专家"的分布,**只激活 top-k 个**专家,其余不参与计算。

<svg viewBox="0 0 720 340" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Dense 与 MoE 两种 token 流经 FFN 方式对比:Dense 全部参数都参与,MoE 只有 router 选中的 top-k 专家参与">
  <rect width="720" height="340" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Dense vs MoE:token 如何流经 FFN</text>
  <text x="48" y="90" font-size="13" font-weight="600" fill="#64748b">Dense</text>
  <text x="48" y="106" font-size="11" fill="#94a3b8">全部参数参与</text>
  <rect x="130" y="74" width="70" height="40" rx="5" fill="#e2e8f0"/>
  <text x="165" y="98" text-anchor="middle" font-size="12" fill="#334155">token</text>
  <line x1="204" y1="94" x2="234" y2="94" stroke="#475569" stroke-width="1.6" marker-end="url(#moeArrow1)"/>
  <rect x="238" y="74" width="250" height="40" rx="6" fill="#4f46e5"/>
  <text x="363" y="98" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">整块 FFN · 全部参数</text>
  <line x1="492" y1="94" x2="522" y2="94" stroke="#475569" stroke-width="1.6" marker-end="url(#moeArrow1)"/>
  <rect x="526" y="74" width="64" height="40" rx="5" fill="#e2e8f0"/>
  <text x="558" y="98" text-anchor="middle" font-size="12" fill="#334155">输出</text>
  <text x="602" y="98" font-size="11" fill="#64748b">总参 = 激活参</text>
  <line x1="48" y1="130" x2="672" y2="130" stroke="#e2e8f0" stroke-width="1"/>
  <text x="48" y="192" font-size="13" font-weight="600" fill="#0f766e">MoE</text>
  <text x="48" y="208" font-size="11" fill="#94a3b8">只让 top-k 参与</text>
  <rect x="130" y="177" width="64" height="40" rx="5" fill="#e2e8f0"/>
  <text x="162" y="201" text-anchor="middle" font-size="12" fill="#334155">token</text>
  <line x1="194" y1="197" x2="224" y2="197" stroke="#475569" stroke-width="1.6" marker-end="url(#moeArrow1)"/>
  <rect x="228" y="175" width="120" height="44" rx="6" fill="#0d9488"/>
  <text x="288" y="193" text-anchor="middle" font-size="11" font-weight="700" fill="#ffffff">router</text>
  <text x="288" y="209" text-anchor="middle" font-size="9" fill="#ccfbf1">softmax 打分 → top-k</text>
  <line x1="348" y1="187" x2="392" y2="167" stroke="#475569" stroke-width="1.6" marker-end="url(#moeArrow1)"/>
  <line x1="348" y1="207" x2="392" y2="227" stroke="#475569" stroke-width="1.6" marker-end="url(#moeArrow1)"/>
  <rect x="396" y="150" width="90" height="34" rx="5" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="441" y="171" text-anchor="middle" font-size="11" font-weight="700" fill="#0f766e">expert 7</text>
  <rect x="396" y="210" width="90" height="34" rx="5" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="441" y="231" text-anchor="middle" font-size="11" font-weight="700" fill="#0f766e">expert 23</text>
  <line x1="486" y1="167" x2="530" y2="187" stroke="#475569" stroke-width="1.6" marker-end="url(#moeArrow1)"/>
  <line x1="486" y1="227" x2="530" y2="207" stroke="#475569" stroke-width="1.6" marker-end="url(#moeArrow1)"/>
  <rect x="530" y="177" width="90" height="40" rx="6" fill="#0d9488"/>
  <text x="575" y="201" text-anchor="middle" font-size="11" font-weight="700" fill="#ffffff">加权求和</text>
  <line x1="620" y1="197" x2="650" y2="197" stroke="#475569" stroke-width="1.6" marker-end="url(#moeArrow1)"/>
  <rect x="654" y="177" width="54" height="40" rx="5" fill="#e2e8f0"/>
  <text x="681" y="201" text-anchor="middle" font-size="12" fill="#334155">输出</text>
  <text x="396" y="254" text-anchor="middle" font-size="11" fill="#94a3b8">其余 N−k 个专家本步不参与 —— 总参 ≫ 激活参</text>
  <defs>
    <marker id="moeArrow1" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect x="48" y="280" width="624" height="50" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="64" y="300" font-size="12.5" fill="#115e59">Dense 每 token 用全部参数;MoE 每 token 只让 router 选中的 top-k 专家参与,其余专家不计算——</text>
  <text x="64" y="320" font-size="12.5" fill="#115e59">这就是"总参 = 容量"与"激活参 = 每 token 算力"解耦的来源。</text>
</svg>

router 的机制(以现代 MoE 为例):对每个 token,gating 网络输出对 N 个专家的打分,softmax 后取 **top-k**,按门控权重加权合并被选专家的输出。两个工程关键:

- **load balancing(负载均衡)**:不加约束的话 router 会偏爱少数专家("赢家通吃"),其余专家训练不足。训练时加**负载均衡辅助损失**鼓励均匀路由。
- **shared expert(共享专家)**:部分设计(DeepSeekMoE、Qwen)留一个**始终激活**的共享专家承接通用能力,其余路由专家负责专精——这就是 "8 routed + 1 shared" 这类描述的由来。

命名里的 `35B-A3B` 就是这个意思:**35B 总参,每 token 只激活约 3B**。

## 为什么 MoE:容量/算力解耦(及其代价)

| | Dense | MoE |
|---|---|---|
| 每 token 计算 | 全部参数 | 只激活的 top-k 专家 → **由激活参数决定** |
| 推理速度/吞吐 | 随总参数变慢 | **快**(算力 ∝ 激活参) |
| 显存 | 放下总参数 | **仍需放下全部专家**(任一都可能被选) |
| 扩容方式 | 加参数=加算力 | **加专家≈加容量但几乎不加每 token 算力** |
| 代价 | 简单 | 路由不均、训练更复杂、显存吃紧、batch 内专家命中分散 |

一句话:**MoE 用"全专家常驻显存"换"每 token 只算一小部分"**——大而省(算力/吞吐),但**不省显存**。

### 一道显存/算力的账(本地视角)

以本机常驻的 Qwen 35B-A3B 为例([本地 LLM 部署](/homelab/local-llm.md)):

- **显存**:35B 全部专家都要常驻。FP16 要 ~70GB,塞不进 24GB 卡 → 必须量化:Q3_K_XL 把权重压到 ~17GB,加 64k 的 Q8_0 KV cache ~2.85GB,再加 ~1.7GB 运行时计算缓冲,合计 ~21.5GB,给桌面留 ~3GB 余量。**显存看的是"总参 + KV cache",MoE 一点没省这块。**
- **算力**:每 token 只激活 ~3B,所以吞吐接近一个 3B dense 模型——配上 MTP 草稿头实测 ~111 t/s。**速度看的是"激活参",这才是 MoE 的红利。**

这就是为什么本地选 MoE:用得起的显存里塞下高容量模型,还跑得快——前提是用量化把"总参"压进卡里。**但代价是每 token 推理深度由激活参数决定**(见下节),agent/工具调用等需要多步推理链的任务,选型时把这个代价算进去。

### MoE 的隐性代价:每 token 推理深度

上面只算了显存和吞吐,但还有第三个维度:**每 token 的推理深度由激活参数决定,不由总参数决定。**

回想 Transformer 的结构:

<svg viewBox="0 0 480 210" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="每层拆解为 self-attention 加 FFN,注意力参数完整,MoE 只激活 FFN 的 top-k 专家">
  <rect width="480" height="210" fill="#ffffff"/>
  <text x="16" y="55" font-size="15" font-weight="600" fill="#1f2933">每层 =</text>
  <rect x="80" y="34" width="170" height="42" rx="6" fill="#4f46e5"/>
  <text x="165" y="60" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">self-attention</text>
  <text x="264" y="60" text-anchor="middle" font-size="16" font-weight="700" fill="#64748b">+</text>
  <rect x="284" y="34" width="110" height="42" rx="6" fill="#0d9488"/>
  <text x="339" y="60" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">FFN</text>
  <line x1="165" y1="164" x2="165" y2="80" stroke="#94a3b8" stroke-width="1.6" marker-end="url(#attnArrow)"/>
  <text x="165" y="180" text-anchor="middle" font-size="12" fill="#334155">注意力(看哪里)</text>
  <text x="165" y="196" text-anchor="middle" font-size="11" fill="#94a3b8">参数固定,完整</text>
  <line x1="339" y1="164" x2="339" y2="80" stroke="#94a3b8" stroke-width="1.6" marker-end="url(#attnArrow)"/>
  <text x="339" y="180" text-anchor="middle" font-size="12" fill="#334155">前馈网络(加工信息)</text>
  <text x="339" y="196" text-anchor="middle" font-size="11" fill="#0d9488">MoE 只激活 top-k 专家</text>
  <defs>
    <marker id="attnArrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#94a3b8"/></marker>
  </defs>
</svg>

attention 层是完整的——模型知道该关注输入的哪个 token。但 FFN 负责对关注到的信息做加工推理,而 MoE 每 token 只激活一小部分 FFN 参数。**35B 总参 / 3B 激活的模型,每一步推理的"思考深度"约等于一个 3B dense 模型。** 换言之:**注意力决定"看哪里",激活 FFN 决定"看懂了什么、接下来做什么"——MoE 的眼睛不瞎,但每步干活的脑细胞只有激活参数那么多。**

这对单步任务(分类、摘要、简单问答)影响不大——3B 的加工能力够用。但对**多步串行推理**——典型如 agent 工具调用链:

<svg viewBox="0 0 760 150" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Agent 多步工具调用链:理解任务到决定下一步,循环往复">
  <rect width="760" height="150" fill="#ffffff"/>
  <rect x="20" y="30" width="96" height="40" rx="6" fill="#4f46e5"/>
  <text x="68" y="55" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">理解任务</text>
  <line x1="116" y1="50" x2="146" y2="50" stroke="#475569" stroke-width="1.6" marker-end="url(#chainArrow)"/>
  <rect x="150" y="30" width="86" height="40" rx="6" fill="#0d9488"/>
  <text x="193" y="55" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">选工具</text>
  <line x1="236" y1="50" x2="266" y2="50" stroke="#475569" stroke-width="1.6" marker-end="url(#chainArrow)"/>
  <rect x="270" y="30" width="86" height="40" rx="6" fill="#0d9488"/>
  <text x="313" y="55" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">填参数</text>
  <line x1="356" y1="50" x2="386" y2="50" stroke="#475569" stroke-width="1.6" marker-end="url(#chainArrow)"/>
  <rect x="390" y="30" width="86" height="40" rx="6" fill="#0d9488"/>
  <text x="433" y="55" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">读结果</text>
  <line x1="476" y1="50" x2="506" y2="50" stroke="#475569" stroke-width="1.6" marker-end="url(#chainArrow)"/>
  <rect x="510" y="30" width="112" height="40" rx="6" fill="#4f46e5"/>
  <text x="566" y="55" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">决定下一步</text>
  <path d="M622,60 C660,90 120,110 68,72" fill="none" stroke="#94a3b8" stroke-width="1.6" stroke-dasharray="4,3" marker-end="url(#chainArrowGray)"/>
  <text x="345" y="122" text-anchor="middle" font-size="11" fill="#94a3b8">每一步都在消费 FFN 的推理预算 —— 循环直到任务完成或链路断裂</text>
  <defs>
    <marker id="chainArrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
    <marker id="chainArrowGray" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#94a3b8"/></marker>
  </defs>
</svg>

每一步都在消费 FFN 的推理预算。3B 激活参数走到第二步就开始"忘"了第一步的意图,表现就是:输出了 `<think>` 文本,但在应该产出 tool call token 的时候注意力散了,继续当作文本往下编——思考了,但没行动。

**经验阈值**:稳定单步工具调用需要 ~7–10B 激活参数;稳定多步 agent 循环需要 ~20B+ 激活参数。这也是为什么 DeepSeek V3(MoE,671B 总参但激活 37B)的工具调用很稳——它的激活参数够大。在 24GB 显存约束下,做 agent 任务优先选 dense 模型(如 Qwen 2.5 14B/32B、Qwen 3 32B),它们的激活参数 = 总参数,多步推理链比同体量的低激活 MoE 可靠得多。

## 实例:从本机到前沿

- **本地常驻**:Qwen 35B-A3B(35B 总 / 3B 活)——标准 MoE,见上账。
- **前沿变体**:[Qwen-AgentWorld-35B-A3B](https://huggingface.co/Qwen/Qwen-AgentWorld-35B-A3B) 同为 35B-A3B,但更激进:**256 个专家,每 token 激活 8 routed + 1 shared**;且是**混合架构**——40 层里大量用 **Gated DeltaNet**(一种**线性注意力**变体,把长序列注意力的 ~O(n²) 降到近线性,省长上下文开销),仅每个重复块的最后一层用 Gated Attention(40 层中只 10 层),从而支撑 **256K 上下文**。它把"MoE 容量解耦"和"线性注意力长上下文"两条线叠在一起,服务于它的 world-model 目标(模拟 agent 环境需要长上下文 + 强容量,见 [Agent 循环与工具使用](/ai/02-Agent/01-the-agent-loop-and-tool-use.md) 的前沿小节)。

> 这些数字来自可核实的开源模型卡。对 Claude 不要套用——官方未公开其是否 MoE、参数规模或注意力变体。

## 选型:看能力,别猜结构

对应用方,真正该问的不是"它是不是 MoE",而是**能力与成本**。闭源模型用 Models API 实时查,别猜:

```python
m = client.models.retrieve("claude-opus-4-8")
m.max_input_tokens   # 上下文窗口
m.max_tokens         # 最大输出
m.capabilities       # image_input / thinking / effort / structured_outputs ...
# 也可 client.models.list() 按能力过滤
```

开源/本地模型则读模型卡的**总参 / 激活参 / 层数 / 上下文 / 注意力类型**,结合显存预算([本地 LLM 部署](/homelab/local-llm.md) 的量化与显存博弈)做选型。

## 最佳实践

- **闭源模型查能力,别猜结构。** 用 Models API 实时取 `max_input_tokens`/`max_tokens`/`capabilities`,别臆测它是不是 MoE、多少参数。
- **MoE 显存按"总参 + KV cache"估。** 激活参小不等于显存小——全部专家都要常驻,用量化把总参压进卡。
- **MoE 吞吐看激活参,不看总参。** 35B-A3B 跑得像 3B,这才是 MoE 的红利。
- **按激活参评 agent 适配性,不看总参。** 稳定单步工具调用需 ~7–10B 激活,稳定多步 agent 需 ~20B+;24GB 下做 agent 优先 dense 或高激活 MoE。
- **开源选型读模型卡的四件套。** 总参 / 激活参 / 层数 / 注意力类型,结合显存预算([本地部署](/homelab/local-llm.md))定档。

## 权衡与失败模式

- **把"激活参小"当"显存小"**:MoE 显存看总参,会塞爆卡 → 按总参 + KV cache 估显存,用量化压。
- **拿 dense 直觉估 MoE 吞吐**:MoE 快是因为激活参小,不是因为它"小" → 看激活参。
- **batch 内专家分散**:并发请求路由到不同专家,实际算力利用率可能不如理论 → 服务端调度/专家并行的工程问题。
- **用 MoE 跑 agent 工具调用却频繁断链**:激活参数太小(如 3B),多步推理中意图漂移,产出 think 文本但不产出真正的 tool call → agent 场景选 dense 或高激活 MoE,按激活参数而非总参数评估。
- **臆测闭源架构**:无依据 → 只用 Models API 的能力字段。

## 参考

- **论文**: "Attention Is All You Need"(Vaswani 2017)、"Switch Transformers"(Fedus 2021)、"DeepSeekMoE"(2024, shared expert)、"GShard"(load balancing)
- **模型卡**: [Qwen-AgentWorld-35B-A3B](https://huggingface.co/Qwen/Qwen-AgentWorld-35B-A3B)
- **闭源能力查询**: Anthropic Models API(platform.claude.com)

*Keywords: Transformer, decoder-only, residual stream, self-attention, QKV, multi-head, causal mask, FFN, KV cache, dense, MoE, Mixture of Experts, router, gating, top-k, shared expert, load balancing, 总参数, 激活参数, active parameters, 35B-A3B, throughput, Gated DeltaNet, linear attention, Qwen-AgentWorld, Models API, 量化, 闭源架构*
