关于今天 LLM 如何从资料中中学习出“关系”，其核心思想来自最经典的 word2vec。

## 共现矩阵

我们使用一批处理好的语料，可以通过简单地统计一个单词和周围单词的概率关系，来预测下一个词是什么。下面使用 CS224N 中的例子：

> CS224N 的授课老师就是 GloVe 的作者。

<img src="./images/截屏2026-09-17 11.47.38.png" alt="截屏2026-09-17 11.47.38" style="zoom:50%;" />

假设我们的语料库中只有如下三句话：

- I like deep learning.
- I like NLP.
- I enjoy flying.

去重之后只有 8 个词（包含一个符号`.`）：

``` 
{I, like, enjoy, deep, learning, NLP, flying, .}
```

我们假设窗口为 1（也就是只看周围的 1 个单词）：

- I 后面：like 出现 2 次，enjoy 出现 1 次，总共 3 次。也就是说，下一个单词是 like 的概率是 66.7%，enjoy 则为 33.3%；
- like 后面：deep 出现 1 次，NLP 出现 1 次，总共 2 次。也就是说，下一个单词是 deep 的概率是 50%，NLP 则为 50%；
- enjoy 后面：flying 出现 1 次，总共 1 次。也就是说，下一个单词是 flying 的概率是 100%；
- deep 后面：learning 出现 1 次，总共 1 次。也就是说，下一个单词是 learning 的概率是 100%；
- learning 后面："." 出现 1 次，总共 1 次。也就是说，下一个单词是句号的概率是 100%；
- NLP 后面："." 出现 1 次，总共 1 次。也就是说，下一个单词是句号的概率是 100%；
- flying 后面："." 出现 1 次，总共 1 次。也就是说，下一个单词是句号的概率是 100%；

这其实就是输入法的算法核心，或者叫马尔可夫链。你可以在 Claude 的这个教程中看到一个交互界面，可以更好的帮助你理解[给朋友发短信：马尔可夫版 - Claude Academy](https://support.typoraio.cn/zh/Markdown-Reference/#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5)。

不过这样是远远实现不了今天使用的 LLM 的程度的：

- LLM 拥有更大的窗口（可以读取之前所有的内容，而不是周围的一个单词）；

  > 需要注意，LLM 是 Decoder-only，只能读到之前的内容，没有办法读到之后的。
  >
  > 你可能会好奇什么叫“读到之后的”。Transformer 衍生出来的各种模型可以完成的工作其实就两种：续写和完形填空。完形填空就是你需要看上下文，而不只是上文来做的。
  >
  > Encoder 可以读上下文，而 Decoder 只能读上文。这部分我打算在新博客里详细说说。

- 使用更加复杂的算法来拟合更复杂的情况（数十亿的参数、注意力机制、embedding、残差连接等等，而非简单的查表操作）。

不过二者相同之处都是预测下一个单词（或者应该叫 token）。

你可能会觉得这个例子很突兀，和 embedding 毫无关系，也和今天的 LLM 毫无关系，讲它的意义是什么呢？

embedding 是将一句话或者一个单词转换成一个向量表示，这样方便我们计算相似度。但这里，每一行去掉自己后，不也可以当作一个向量吗？

比如"like"和"enjoy"这两行：

| 单词  | 向量                     |
| ----- | ------------------------ |
| like  | [2, 0, 0, 1, 0, 1, 0, 0] |
| enjoy | [1, 0, 0, 0, 0, 0, 1, 0] |

这两个向量的余弦相似度大约为 0.5774。不过在这里没有什么用，因为语料太少了，就三句话，很多信息学不到。可以使用 SVD 降维（因为你看，向量大部分都是 0，拼接成矩阵之后大部分也是零，可以降维成更小的矩阵来计算），这里降成 2 维（注意，此时没有一个单词和另一个单词之间的关系表示了）：

| 词       | 第 1 维 | 第 2 维 |
| -------- | ------- | ------- |
| I        | -1.445  | -1.534  |
| like     | -1.639  | 1.688   |
| enjoy    | -0.707  | 0.734   |
| deep     | -0.788  | -0.664  |
| learning | -0.533  | 0.091   |
| NLP      | -0.841  | -0.787  |
| flying   | -0.503  | -0.431  |
| .        | -0.681  | 0.421   |

由于是 2 维，我们使用欧几里得距离（就是常说的平面距离公式）即可。两两计算后如下：

|              | I     | like  | enjoy | deep  | learning | NLP   | flying | .     |
| ------------ | ----- | ----- | ----- | ----- | -------- | ----- | ------ | ----- |
| **I**        | 0     | 3.228 | 2.385 | 1.091 | 1.864    | 0.960 | 1.450  | 2.100 |
| **like**     | 3.228 | 0     | 1.334 | 2.501 | 1.943    | 2.600 | 2.404  | 1.588 |
| **enjoy**    | 2.385 | 1.334 | 0     | 1.400 | 0.666    | 1.527 | 1.183  | 0.314 |
| **deep**     | 1.091 | 2.501 | 1.400 | 0     | 0.797    | 0.135 | 0.367  | 1.090 |
| **learning** | 1.864 | 1.943 | 0.666 | 0.797 | 0        | 0.931 | 0.523  | 0.362 |
| **NLP**      | 0.960 | 2.600 | 1.527 | 0.135 | 0.931    | 0     | 0.491  | 1.219 |
| **flying**   | 1.450 | 2.404 | 1.183 | 0.367 | 0.523    | 0.491 | 0      | 0.871 |
| **.**        | 2.100 | 1.588 | 0.314 | 1.090 | 0.362    | 1.219 | 0.871  | 0     |

这里值越小的，说明语义和含义越接近。

例如这里的 like 和 enjoy 都是谓语动词，所以接近一些。不过你可以找到反例，例如 enjoy 和 learning 的距离更近，一个是谓语动词，一个是非谓语动词，导致这个的原因是二者在语料中经常一起出现。

所以还是那个问题：语料太少，而且 2 维里的距离本来就不稳。这里我们只看个大概，主要是知道如何将文本转换成向量（周围词的频次只是一种方案，但是逻辑都是用各种向量来表示一个单词）。

> 其实这也是模型训练的一个问题：语料需要足够好、足够大。虽然现如今数据清洗的已经非常不错，甚至可以用模型自动清洗。但是在早期，语料质量对模型的效果非常大。
>
> CS224N 课程中，Glove 的作者 Chris Manning 提到，他们当时很高兴训练出来的模型远超 Word2Vec，认为自己的设计要更好，但是多年后发现，很可能是语料改进带来进步。
> 位置在 [Stanford CS224N NLP with Deep Learning | Winter 2021 | Lecture 2 - Neural Classifiers](https://youtu.be/gqaHkPEZAew?si=Y0OT0gEcGp-rMn8T) 中的 **52:43**。

**此外，马尔可夫链还可以让你感受到概率的奇妙之处**。概率是大语言模型的根本，更是信息论的根本之一，但是我很难想到其他的例子，让你如此直观的看到，概率论是如何完成 LLM 这一奇观的。必须建立对概率论力量的直觉。

此外，embedding 虽然很多人告诉你是向量（也就是线性代数的内容），但是如何分布、如何选择结果却是概率论相关的。

>LLM 是人类智慧实现的一大奇观，虽然它是数字化的，但是它第一次让人类的智慧和逻辑外化了，这样就可以像机器外化力量后一样，不断扩展它，拥有人类肉体无法达到的力量。
>
>但是它的根本却是概率。概率这个东西在大众的眼中，一直和赌博、算命、玄学沾边，哪怕是在教材中，也都是以骰子、牌来举例，很难让人意识到，概率论能拥有实现人类奇观的力量（拉斯维加斯或许也算吧）。



## embedding

接下来，我们来聊聊 embedding 操作是如何让“关系”涌现出来的。

我们直接从头训练一个 embedding 模型，这样是最直观能看到“关系”涌现的过程，不过我这里不会列大量代码，因为我们核心是思想，而非实现。（再说你知道思想之后，让 ai 去写就行了）

这里我选择了 Text8 数据集的前 20MB 做实验（因为我要在 MacBook Air M5 上跑，全跑时间有点久）。需要说明的是，项目的正式训练用的是前 60MB；本节的统计数字和下文的 AB 对比实验都是 20MB 下的结果。

>Text8 是 2006 年 3 月英文维基百科 Matt Mahoney 清洗版（去标签/标点、全小写、空格分词，非常适合研究使用）
>前 20 MB 共 3,416,066 个原始 Tokens，丢弃频次少于 5 的词（min_count=5）后剩 3,299,364 Tokens，词表为 29428 词。
>



### 预处理
首先，我们需要预处理一下数据集（尽管这个数据集清洗的非常好，但是我们为了训练，还需要进一步处理）。

#### 下采样

首先我们需要去掉一部分高频词（例如“the”、“of”、“and”），因为高频词会占用训练次数，可能导致低频词没有被训练到。

下采样（subsampling）是处理高频词常见的操作，也就是按概率删掉一部分高频词。具体做法不是固定比例，而是按词频决定每个词的保留概率：

$$
P(\text{保留}) = \sqrt{t / f} \qquad t = 10^{-4} \text{（阈值）},\ f = \text{该词的词频}
$$

词频越高，保留概率越低。例如 "the" 的保留率只有约 4%（每出现 100 次随机留下 4 次），而 "king" 这类中频词几乎全部保留。

在我的训练中，

| 项           | 数量          |
| ------------ | ------------- |
| token 总数（已丢弃低频词） | 3,299,364 |
| 下采样后保留 | 1,614,418 |
| 被删掉的高频词 token | 1,684,946（占比 51.1%） |

你可以看到这些高频词占比有多大，超过一半都是，也就是说产生的单词对大部分都是高频词相关的。大部分训练次数都在训练这种内容，去掉高频词要么可以在同样的时间训练次数多一倍，要么降低一半的训练时间。


##### 实验发现：高频词并不会对分布产生致命影响

由于英文中频次最高的单词“the”后面经常跟着名词，所以我一开始猜测如果保留高频词，可能能学到最后，几乎所有的名词都围着“the”，这样很难区分名词之间的关系了。换言之，我一开始认为，**这些高频词会影响其他的词之间的关系。**

> 原始论文中有这样一个脚注，有类似的说法：
>
> A notable exception is the case of syntactic similarity. For example, all verbs share a very strong negative association with being preceded by determiners, and past tense verbs have a very strong negative association to be preceded by “be” verbs and modals.
>
> 句法相似性是一个显著的例外。例如，所有动词都与前面出现限定词存在极强的负相关；而过去式动词，若前面是系动词（be 动词）或情态动词，同样存在极强的负相关。

于是让 ai 写了个测试跑一下，对比去掉和保留高频词的情况。结果出人意料，几乎没有区别：

![subsample_ab](./docs/subsample_ab.png)

你看这里的结果和我的预测是反的：

1. 下采样的相似度要比不下采样还要高（不论是训练中还是结束训练的时候，都是如此）。
2. 下采样和不下采样的分布差不多（不过下采样后效果稍好一些，没有单词离得很近）。

那么下采样的意义是什么呢？

1. **速度更快**：例如在训练中，下采样去掉了 51% 的填充词，并且结果更好一些。这意味着节约了一半的时间。

2. **有效滑动窗口“变大”**：之前一些单词可能被“the”隔离开了，没有通过滑动窗口建立相应的 pair，去掉“the”后，这样的对就变多了。这相当于变相提高滑动窗口的大小

3. **提升低频词的表示质量**：这是因为高频次在大量样本训练后变化就很小了，继续训练是一种浪费。而训练次数是固定的，可以把省下来的训练次数给低频词。可以看到下图中，对应类型的词在下采样后聚在一起了。

   ![rare_clusters_ab](./images/rare_clusters_ab.png)

#### 滑动窗口配对

按照原始论文中的设计：滑动窗口的半径为 5，距离越远的邻居保留概率越低。

具体流程如下：

**输入**：一串词（已经过完下采样的 id 序列，所有句子拼成一个超长文本），窗口半径 `window = 5`。

```
语料:  ... once in the land the king ruled his kingdom and the queen wore her crown ...
```

窗口每次以一个词为中心，左右各最多看 5 个词（共 10 个候选邻居）。但不是 10 个都配：每个中心词会先随机抽一个"实际半径" $r$（1~5 等概率），只和距离 $\le r$ 的邻居配对，距离 $> r$ 的直接跳过。

![skipgram_sliding_window](./images/skipgram_sliding_window.png)

图中第一步，窗口到 `king`，抽到 r = 3，配出 6 个对：

```
(king, the) (king, land) (king, the) (king, ruled) (king, his) (king, kingdom)
```

第二步，窗口向右滑一格，中心词变成 `ruled`，重新抽 r，这次抽到 r = 2，配出 4 个对：

```
(ruled, the) (ruled, king) (ruled, his) (ruled, kingdom)
```

不断这样滑一格，直到滑完整条语料。r 的取值决定了配对数的范围：

```
r = 1 → 2 个对    
r = 2 → 4 个对    
r = 3 → 6 个对
r = 4 → 8 个对    
r = 5 → 10 个对   
```

此外，距离 $d$ 的邻居被保留的概率是 $\dfrac{W - d + 1}{W}$（由 $r$ 均匀取 1~$W$ 推出，和 word2vec 的实现一致），以 $W = 5$ 为例：

```
距离 1（相邻）:  100% 保留
距离 3:           60% 保留
距离 5（最远）:   20% 保留
```

所以每个中心词实际配到 2~10 个对（偶数），平均 6 个。也就是说，本次实验 161 万 token（下采样后）的语料通过滑动窗口处理完毕后，得到约 970 万个训练对。

后续的训练只使用这些对，之前的文本文件和抽取的语料没有用了。

至此预处理阶段结束，下面就是进入训练模型的阶段了。


## 训练

预处理结束后，我们得到了大约 970 万个 `(中心词, 周围词)` 对（pair）。我们训练只需要这些对，之前的语料已经不需要了（我们已经将其转换成“对”这种结构了）。

### `W_in` 和 `W_out`：被训练的参数权重

训练中，我们为每个词准备了两个对应向量，这就是 embedding 训练的参数权重。

一个是它当"中心词"时用的（`W_in`），一个是它当"周围词"时用的（`W_out`）。训练结束后，`W_out` 直接扔掉，只留 `W_in` 当最终的词向量，这时候，我们就知道不同词语之间的关系（也就是它作为中心词的向量之间的关系）。

> 这里的思想其实很常见。不论是计算机还是人类社会。
>
> 比如，在数据库中，你可以创建一个 UUID 来表示一条数据，也可以使用这条数据的各种属性来确定。
>
> 或者，作为一个人类，你可以用身份证号来表示自己，或者你的人际关系（谁的孩子、谁的爱人、谁的学生/老师等等）。



那么为什么要准备两个向量呢？

`W_in` 就类似人类的“身份证号”，`W_out` 则是“关系”。

举个例子。给一堆人发身份编号 `1～100`，编号是随机发放的——不代表高低、不代表亲疏，而且是一次性的，发完就不能改。所以现实中看两个人的编号，看不出任何关系。

但是 embedding 里的“编号”不一样：**在最终定下来之前，我们可以反复调整它**。我发现一些人是亲人，就把他们的编号挪近一点；发现一些人是同事，也挪近一点。几千万次调整之后，原本随机的编号就不再随机了：编号挨得近的，往往就是关系近的。

这就是训练的过程：`W_in` 一开始就是随机发放的“编号”（见下面的初始化），训练就是不断调整这些编号，最后编号本身就承载了关系。

参数在训练前需要初始化，好的初始化可以带来更好的结果和更快的训练速度：

- `W_out` 初始化为**全 0**——一开始模型对任何词对的打分都是 0，也就是"完全没把握"，是个中立的起点；
- `W_in` 初始化为 **$-0.05 \sim 0.05$** 之间的小随机数。不能全 0（所有词完全对称，梯度也一样，永远学不出区别），但也不能太小。

> 太小会发生什么？`config.py` 里记录了一次实测（30MB 语料，3 轮）：
>
> | 初始尺度               | 覆盖 90% 方差需要的维度 | 任意两词平均余弦 |
> | ---------------------- | ----------------------- | ---------------- |
> | 0.005（word2vec 原版） | 17 维                   | 0.385            |
> | 0.05（本项目默认）     | 47 维                   | 0.320            |
> | 0.1                    | 67 维                   | 0.301            |
>
> 初始值太小时，100 维里只有十几维在干活，多维向量的区分能力严重下降，导致所有词挤在一起，余弦相似度全在 0.99 附近。
>
> 而且这个和语料尺寸有关。如果语料够大、轮数够多时这个问题会有一定程度的缓解。这也是为什么论文用 0.005，而我们使用的较小语料就需要变成 0.05。

### 负采样

负采样就是随机抽取几个和中心词无关的词，拿来当反面例子。这里使用的 $f^{0.75}$ 负采样是按词频的 0.75 次方来抽。举个具体例子。假设词表里只有 3 个词，语料共 100 万 token：

| 词    | 出现次数 | 词频 $f$ | 均匀抽（$f^0$） | 按词频抽（$f^1$） | $f^{0.75}$ |
| ----- | -------- | -------- | --------------- | ---------------- | ---------- |
| the   | 60,000   | 0.06   | 33.3%        | 98.5%          | 96.3%  |
| king  | 600      | 0.0006 | 33.3%        | 0.99%          | 3.0%   |
| zebra | 100      | 0.0001 | 33.3%        | 0.16%          | 0.8%   |

$f^{0.75}$ 那一列是怎么算的：先给每个词算"权重" $= f^{0.75}$，再归一化。

- the:$0.06^{0.75} \approx 0.121$
- king:$0.0006^{0.75} \approx 0.00382$
- zebra:$0.0001^{0.75} = 0.001$

总和 $\approx 0.1258$，各自除以总和就得到抽样概率：the ≈ 96.3%，king ≈ 3.0%，zebra ≈ 0.8%。

对比三列就能看出 0.75 次方的作用：

- $f^1$（按原始词频）：the 占 98.5%，无关词几乎全是 the/of/and——模型轻松就能把它们推远，学不到东西；
- $f^0$（均匀）：zebra 这种生僻词占 1/3，它们跟中心词八竿子打不着，推远它们也太容易，同样学不到东西；
- $f^{0.75}$：the 仍然最常被抽到（96.3%），但低频词被明显"提拔"了——zebra 从 0.16% 提到 0.8%（5 倍），king 从约 1% 提到 3%。

当然这里“看中心词和周围词的关系”的方法是考虑到训练时间的，你可能会觉得每次就这么几个词，会不会不够全面。其实我们可以使用 softmax，让一个词和所有词看一下关系，但是这样的训练时间就非常大了。不过我后面做了个对比实验，让你看看区别。

### 如何训练参数权重

前面提到了：每个训练对是（中心词，周围词），训练时再配上负采样抽到的 5 个无关词。分别记作中心词 $c$、周围词 $o$、无关词 $w_1 \dots w_5$，训练步骤如下：

$$
\begin{aligned}
\text{打分} \quad & s = v_c \cdot u_x \\
\text{概率} \quad & \sigma(s) = \frac{1}{1 + e^{-s}} \\
\text{误差} \quad & g = \sigma(s) - y
\end{aligned}
$$

打分就是两个向量点乘，越像分越高；sigmoid 把任意分数压进 0~1，变成"是不是一对"的把握；$y = 1$ 是真的周围词，$y = 0$ 是无关词。

训练过程就是通过 $g$ 来判断拉近还是推远。如果 $g$ 是负的就把两个向量拉近，正的就推远。整个训练就是这一步重复几千万次。

例如，5 个词 `[king, queen, the, banana, war]`，训练对 `(king, queen)`，当前各词和 king 的点积已经算好，无关词抽到了 the 和 banana：

| 题              | $y$  | $s$  | $\sigma(s)$ | $g = \sigma(s) - y$ | 解读               |
| --------------- | ---- | ---- | ----------- | ------------------- | ------------------ |
| queen 是一对吗  | 1    | +2.0 | 0.881 | −0.119     | 已经不错，轻轻拉近 |
| the 是一对吗    | 0    | +1.0 | 0.731 | +0.731     | 太像了，重重推远   |
| banana 是一对吗 | 0    | 0.0  | 0.500 | +0.500     | 没把握，中等推远   |

war 没被抽中进行对比，它的向量这一步不更新。

在几千万次这样的相互作用之后，king 和 queen 的相对位置会趋于稳定，我们就可以看到二者之间的关系了。

> embeddingTest 项目里的 `trace.py` 可以把真实训练中的某一步这样摊开打印（每个中间量都对得上上面的公式），看到的数字和真实训练完全一致。

### 训练循环中的一些细节

- **每一轮重新下采样一次**：每轮被丢掉的高频词位置不同，相当于每轮看到的语料不同，这算是一种数据增强；
- **学习率线性衰减**：从 0.025 一路降到接近 0；

实际跑起来是什么样？正式训练（60MB、2 轮）的日志：

```
下采样 : 9,971,789 -> 5,019,569 token（丢掉 50% 的高频词）
训练对 : 30,119,260 对 / 轮（60MB 语料；开头的 970 万对是 20MB 实验的口径）
速度   : 约 23 万对/秒，总共 273 秒（M 系列 MacBook Air，纯 numpy）
loss   : 3.69 -> 2.32
```

理论起点可以对一下：瞎猜时 $\sigma = 0.5$，每道题的损失是 $\ln 2 \approx 0.693$，6 道题 ≈ 4.16。`loss` 从 3.69 降到 2.32，说明模型确实学进去了——当然 `loss` 只是副产品，真正的成果在向量里。

### 实验：负采样 vs 完整 softmax

既然负采样只是"完整 softmax 太贵"的妥协，那妥协的代价是什么？我让 ai 写了个对比实验（`compare.py`）：同一个 skip-gram，只换输出层，一边负采样，一边对全词表做 softmax。

先说我原来的预测：我以为二者只是速度差别，质量应该差不多——毕竟"抽得多了总会轮到"。

结果只对了一半。**词表小、语料也小（3MB）的时候，负采样直接崩了**：无论怎么调学习率、批大小、训练步数，词向量都挤成一团，任意两词余弦高达 0.94，最近邻全是噪声；而 softmax 在同样数据上好端端的。把语料加大到 30MB（词表不变），负采样立刻恢复正常。

机制上的解释：

- **softmax 是全量推远**：每一步对词表里所有非答案词都施加推力，一个不漏，不靠运气。还是上面那 5 个打分（+2.0, +1.0, 0, −1.0, −2.0），换成 softmax 会算出概率 $P(\text{queen}) = 0.636$、$P(\text{the}) = 0.234$、$P(\text{banana}) = 0.086$、$P(\text{war}) = 0.032$、$P(\text{king}) = 0.012$，然后每个词按"概率 − 应该是的值"来推：queen 被拉近 0.364，the 被推 0.234，war 只被推 0.032——推力自动正比于当前错误程度；
- **负采样是采样推远**：每步只推 5 个无关词，这种稀疏的斥力需要大量不重复的数据才能在统计上生效。小语料里"轮到"的次数太少，推不开，就塌缩了。

这也解释了负采样为什么是为网络级语料发明的：数据足够大时，采样推远的统计平均才成立，而它省掉的 $O(V)$ 开销才值回票价。

> 还有个细节很有意思：两边不能用同一个学习率。softmax 的梯度被摊薄到全词表，用负采样的 0.025 几乎学不动，要调到 0.2 才正常，调到 1.0 就发散。需要不同的学习率，本身就是"这是两个不同的优化问题"的直接证据。

## 总结

单词之间的关系是“学习”中自然涌现的，而非设计好的（不像人类教推理、逻辑一样）。也就是说我们没有指定某一维度的含义是什么，表示人际关系、身份等等，这都是在训练的过程中涌现出来的。

> 所谓的“学习”是指使用大量训练统计、概率模型的过程，并不是他真的会自己学习。



## 扩展：如何计算句子的相似度？

最简单的办法：**把句子里每个词的向量平均一下，当作这句话的向量**（叫 `mean pooling`），然后再用余弦相似度两两比较。

$$
\text{句向量} = \frac{v_{the} + v_{king} + v_{ruled} + v_{his} + v_{kingdom}}{5}
$$

项目里 `demo.py` 就是这么干的，比如这两句的相似度就很高：

- the king ruled his kingdom
- the queen governed her empire

因为 king/queen、ruled/governed、kingdom/empire 的向量本来就挨得近，平均之后两句自然也挨得近。

但这个方法有两个明显的窟窿：

1. **不记录位置信息**。`I love you` 和 `you love I` 的词完全一样，平均出来的向量一模一样，相似度是 1——但这两句话的意思显然不同（`dog bites man` 和 `man bites dog` 也是，一个是新闻一个不是）。
2. **不认识的词直接扔掉**。句子里不在词表中的词会被直接跳过，当作不存在。

不过 BERT、SBERT 这类句向量模型，核心解决的问题就是这些。

## 参考资料

[给朋友发短信：马尔可夫版 - Claude Academy](https://support.typoraio.cn/zh/Markdown-Reference/#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5)：互动性很不错，辅助了解。