从 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 dadcatdog 各 1/3
dad mamamama 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 的 ?? 运算符。它只在左侧是 nullundefined 时才回退。如果 model[word] 是一个空数组,例如 []?? 不会触发,因为空数组不是 nullundefined。但在这个例子里,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 给了一个可运行的骨架。我的判断是:

  1. 对于想搞懂原理的人,这个实现适合作为第一课:搞清“下一个 token 预测”的循环、转移表、采样三个基本动作。
  2. 但它不能称为严格意义的 LLM,更接近传统 n-gram 模型的单步版。它没有参数训练,没有表示学习。
  3. 如果要在教学上改进,可以加平滑(例如 +1 平滑避免未知词崩溃)、加 n-gram 的 n 参数、加温度控制,这样就能更接近 Karpathy 的 nanoGPT 或 minGPT 的最小实现。
  4. 生产级 LLM 的最小实现至少还需要:词嵌入矩阵、位置编码、自注意力层、前馈网络、softmax 输出、损失函数和优化器。这个清单是我自己总结的,不是来自该 Gist。
  5. 该 Gist 的 Python 版本属于反例:它把“随机生成”误当成语言模型,读者应避免把无条件的字符抽样和条件语言模型混为一谈。

这次没核实的

  • 未能找到作者写这个 Gist 时的完整背景说明或配套博客。页面只有代码和一个评论,无法确认作者对“smallest LLM”的严肃定义。
  • 未能核实同方向其他最小 LLM 实现与这个 Gist 的性能或结构差异,因为本次可用来源只有该 Gist 及其页面导航链接,没有额外论文或仓库正文可读。
  • 未能核实该 JS 代码在 Node.js 下的实际输出分布。我们只从源码推导转移概率,没有运行多次采样统计。

参考来源

  1. The Smallest LLM – skorotkiewicz/tiny-llm.js(一手代码)
  2. 作者 skorotkiewicz 的 Gist 主页
  3. GitHub 官网(平台背景)
  4. 该 Gist 页面锚点(start-of-content)

评论区

0 条评论

登录后可评论。

木木夕 46 阅读