跳到正文
Atomind原子思维
菜单
全部文章

大模型是怎样「一个字一个字」生成文本的

Atomind4 分钟阅读--- 次阅读

和大模型聊天时,回复是一点点「流」出来的。这不是前端特效,而是模型真实的工作方式:每一步只预测下一个 token,再把它拼回输入,继续预测下一个。这个过程叫自回归生成(autoregressive generation)。

第一步:文本变成 token

模型看不到汉字,只看得到整数。Tokenizer 负责把文本切成 token,再映射为词表里的编号:

python
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
ids = tok.encode("原子思维")
print(ids) # 一串整数
print([tok.decode([i]) for i in ids]) # 每个整数对应的片段

一个汉字可能是一个 token,也可能被拆成多个字节级片段;常见词组也可能合并成一个 token。计费、上下文长度,统计的都是 token 数,而不是字数。

第二步:模型输出 logits

把 token 序列送进模型,最后一层会为词表中的每一个 token 打一个分数,叫 logits。词表通常有十几万项,所以每一步的输出就是一个十几万维的向量。

python
import torch
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
inputs = torch.tensor([tok.encode("天空是")])
with torch.no_grad():
logits = model(inputs).logits[0, -1] # 只看最后一个位置
print(logits.shape) # torch.Size([词表大小])

第三步:从 logits 采样下一个 token

logits 经过 softmax 变成概率分布,再从中选出一个 token。选法决定了生成的「性格」:

参数作用直觉
temperature先把 logits 除以它再做 softmax越小越保守,越大越发散;趋近 0 时等价于每次选最大值(贪心)
top_k只在概率最高的 k 个里采样砍掉长尾的离谱选项
top_p只在累计概率达到 p 的最小集合里采样根据分布形状自适应地截断
python
def sample(logits, temperature=0.7, top_p=0.9):
probs = torch.softmax(logits / temperature, dim=-1)
sorted_probs, idx = probs.sort(descending=True)
cutoff = sorted_probs.cumsum(-1) > top_p
cutoff[1:] = cutoff[:-1].clone() # 保留刚好越过阈值的那一个
cutoff[0] = False
sorted_probs[cutoff] = 0
choice = torch.multinomial(sorted_probs / sorted_probs.sum(), 1)
return idx[choice].item()

第四步:循环

把选出的 token 追加到输入末尾,重复上面的过程,直到生成结束符或达到长度上限:

python
ids = tok.encode("用一句话解释什么是大模型:")
for _ in range(60):
with torch.no_grad():
logits = model(torch.tensor([ids])).logits[0, -1]
next_id = sample(logits)
if next_id == tok.eos_token_id:
break
ids.append(next_id)
print(tok.decode(ids))

这就是一个完整的(虽然很慢的)生成循环。实际的推理框架会用 KV Cache 把前面算过的注意力结果缓存起来,每一步只计算新 token,速度能快上一个数量级——这是后续文章的主题。

小结

  • 模型每一步只做一件事:给下一个 token 的所有候选打分。
  • 「创造力」主要来自采样策略,而不是模型本身在「思考」。
  • 理解了这个循环,流式输出、停止词、最大长度、KV Cache 这些概念就都有了落脚点。