从 20 行代码看语言模型的最小结构:它能说完整,但不算 LLM
先说结论
- 这份 Gist 里的“最小 LLM”实际上是一个一阶马尔可夫链,也叫 bigram 模型。它只统计相邻两个词之间的转移关系,生成时从当前词的后继候选里随机选一个。
- 它能产出表面上连续的短句,但完全没有语义理解、注意力机制、词嵌入或参数训练,和 GPT 类 LLM 有本质区别。
- 这个实现最重要的价值是展示了一个最小语言模型闭环:分词 → 统计上下文转移 → 随机采样 → 循环生成。
- 作者在评论里补充的 Python 版本更弱,只是从原文本里随机抽字符,连词序都不建模,严格说不能叫语言模型。
- 想搞懂 LLM 原理的人,可以把这份代码当作理解“下一个 token 预测”的入门教具,但不能把它当成现代 LLM 的微缩版。
证据与过程
代码在做什么
一手来源是作者 skorotkiewicz 发布在 GitHub Gist 的 tiny-llm.js。JS 版本完整代码如下:
const text = "mama dad mama cat dad mama dog";
const words = text.split(" ");
const model = {};
for (let i = 0; i < words.length - 1; i++) {
const a = words[i];
const b = words[i + 1];
model[a] ??= [];
model[a].push(b);
}
function next(word) {
const choices = model[word] ?? words;
return choices[Math.floor(Math.random() * choices.length)];
}
function generate(start, n = 10) {
let word = start;
const out = [word];
for (let i = 0; i < n; i++) {
word = next(word);
out.push(word);
}
return out.join(" ");
}
console.log(generate("mama"));
这段代码可以拆成四个动作。第一,split(" ") 把训练文本切成词。第二,for 循环遍历相邻词对,把每个词的后继词塞进数组,形成一个转移表。第三,next(word) 查询当前词的后继候选;如果当前词不在模型里,就回退到全体词表;然后随机选一个。第四,generate(start, n) 从起始词开始,循环调用 next,把结果拼接成句子。
从训练语料能推算出什么
训练文本只有 7 个 token:mama, dad, mama, cat, dad, mama, dog。根据源码逻辑,我手动推算了它的转移表,这些数字属于自己推算,不是作者提供的官方数据:
| 当前词 | 后继候选 | 下一词概率(自己推算) |
|---|---|---|
mama |
dad、cat、dog |
各 1/3 |
dad |
mama、mama |
mama 100% |
cat |
dad |
dad 100% |
dog |
全体词表 | mama 3/7、dad 2/7、cat 1/7、dog 1/7 |
这里有一个容易忽略的细节:dog 是最后一个词,没有后继,所以 model["dog"] 是 undefined。当生成过程走到 dog 时,next 函数里的 model[word] ?? words 会触发回退,从全体词表里随机选一个词。全体词频是 mama 3 次、dad 2 次、cat 1 次、dog 1 次,因此从 dog 跳回 mama 的概率是 3/7。这些数字不是官方数据,而是从代码和训练文本直接推导的。
另一个细节是 JavaScript 的 ?? 运算符。它只在左侧是 null 或 undefined 时才回退。如果 model[word] 是一个空数组,例如 [],?? 不会触发,因为空数组不是 null 或 undefined。但在这个例子里,dog 的键根本不存在,所以回退生效。这个行为对生成结果有实际影响。
它和主流 LLM 的差距在哪
这个模型只能看前一个词,属于 bigram。它没有现代 LLM 的关键组件:
- 上下文窗口:只看最近一个词,而不是 GPT 的多 token 上下文;
- 可学习参数:转移表是硬编码统计,没有权重,也没有梯度更新;
- 嵌入表示:词是字符串,没有向量语义;
- 训练目标:没有损失函数,没有反向传播;
- 采样策略:只有均匀随机,没有 top-k、top-p 或温度控制。
因此,它生成的“完整”只是表面完整:序列在词法上可能连续,但语义是随机游走。相比之下,GPT 类 LLM 通过 Transformer 建模长距离依赖,所有参数通过梯度下降学习,核心虽然也是预测下一个 token,但复杂度和能力完全不在一个量级。
这份 Gist 发布在 GitHub Gist 上,作者是 skorotkiewicz,仓库名为 tiny-llm.js。根据页面元数据,该 Gist 目前只有 1 个 revision,这些属于官方数据,来自 GitHub 页面本身。平台背景可以参见 GitHub 官网。
一个更小的 Python 版本
作者在 Gist 评论区还贴了一个 Python 类:
from random import choices
class TinyLLM:
def fit(self, text):
self.text = text
def generate(self, n):
return "".join(choices(self.text, k=n)) if self.text else ""
model = TinyLLM()
model.fit("hello world hello tiny language model")
print(model.generate(100))
这个版本没有任何词序建模。它只是从字符集合里有放回抽样 100 次,连空格和标点都作为普通字符处理。如果训练文本是 "hello world hello tiny language model",生成结果可能形如 o mrldo...,基本不可读。它说明:只有“生成”动作,没有“条件概率”,不足以构成语言模型。
自己的判断 / 清单
如果把“让 AI 说完整”这个目标拆成最小闭环,这份 Gist 给了一个可运行的骨架。我的判断是:
- 对于想搞懂原理的人,这个实现适合作为第一课:搞清“下一个 token 预测”的循环、转移表、采样三个基本动作。
- 但它不能称为严格意义的 LLM,更接近传统 n-gram 模型的单步版。它没有参数训练,没有表示学习。
- 如果要在教学上改进,可以加平滑(例如 +1 平滑避免未知词崩溃)、加 n-gram 的 n 参数、加温度控制,这样就能更接近 Karpathy 的 nanoGPT 或 minGPT 的最小实现。
- 生产级 LLM 的最小实现至少还需要:词嵌入矩阵、位置编码、自注意力层、前馈网络、softmax 输出、损失函数和优化器。这个清单是我自己总结的,不是来自该 Gist。
- 该 Gist 的 Python 版本属于反例:它把“随机生成”误当成语言模型,读者应避免把无条件的字符抽样和条件语言模型混为一谈。
这次没核实的
- 未能找到作者写这个 Gist 时的完整背景说明或配套博客。页面只有代码和一个评论,无法确认作者对“smallest LLM”的严肃定义。
- 未能核实同方向其他最小 LLM 实现与这个 Gist 的性能或结构差异,因为本次可用来源只有该 Gist 及其页面导航链接,没有额外论文或仓库正文可读。
- 未能核实该 JS 代码在 Node.js 下的实际输出分布。我们只从源码推导转移概率,没有运行多次采样统计。
参考来源
评论区
登录后可评论。