大语言模型工作原理:从 Token 预测到多轮对话的系统性解析

本文系统解析大语言模型实现多轮对话的底层机制,指出模型本身无状态,依赖客户端重放全部历史并按Chat Template拼接为长序列,通过自回归预测逐token生成回复。核心环节包括分词器处理、模板渲染一致性、RoPE位置编码、KV cache跨轮复用、prefill/decode计算分离及上下文窗口管理。强调训练与推理模板错位、lost-in-the-middle、前缀缓存命中率等关键工程问题。

0 · 导读:一次对话的九个瞬间

把「用户问『中国的首都是哪里?』,模型答『中国的首都是北京。』」这件事放到慢镜头上,它实际是九个前后相扣的工序:

# 工序 输入 → 输出 本文章节
① 客户端组装消息 用户键入的文本 → messages 数组(含角色) §1.2
② 模板渲染 messages 数组 → 一条带特殊 token 的纯文本 §3
③ 分词 文本 → token id 序列 §2
④ 预填充 prefill token 序列 → 全部位置的 K/V 缓存 + 末位 logits §4、§5.2
⑤ 解码循环 decode 末位 logits → 采样 → 新 token → 拼回 → 再前向,直到 EOS §5
⑥ 流式吐出 逐个新 token → detokenize → SSE 推给前端 §5.3
⑦ 收尾拼装 完整回答文本 → assistant 消息,追加进 messages §1.2
⑧ 下一轮 全量历史 + 新问题 → 重新走 ②–⑥ §6
⑨ 上下文管理 历史超窗时:截断 / 滑窗 / 摘要 / 外挂检索 §7

一句话概括全文的核心洞见(也是理解多轮对话的钥匙):

模型没有「对话状态」。每一次生成,都是把「系统提示 + 全部历史 + 新问题」按模板拼成一条长序列,从头做一次完整的下一词预测。所谓「记忆」,是客户端每轮把历史重放一遍;所谓「上下文窗口」,是这条序列的长度上限。

章节逻辑链:① 全景(无状态架构)→ ②③ 输入侧(token 与模板,多轮对话的两个格式地基)→ ④⑤ 单次生成(前向与采样)→ ⑥ 多轮机制(序列变长、KV cache 跨轮复用)→ ⑦ 窗口工程(有限记忆的管理)→ ⑧⑨ 训练与服务两侧(模型怎么学会、系统怎么支撑)→ ⑩ 端到端 trace(把一切放进一个两轮对话的具体数字里)→ ⑪ 误区与验收清单。


1 · 系统全景:从按键到回复

1.1 无状态服务 + 有状态客户端:主流架构

今天所有主流 LLM API(OpenAI、Anthropic、通义、DeepSeek……)都是无状态 HTTP 服务:服务端不保存你的对话(计费与审计流水除外),每次请求自带全量消息历史。一次 chat.completions 调用的请求体就是全部「记忆」:

{
   
   
  "model": "qwen-max",
  "messages": [
    {
   
   "role": "system",    "content": "你是一个乐于助人的助手。"},
    {
   
   "role": "user",      "content": "中国的首都是哪里?"},
    {
   
   "role": "assistant", "content": "中国的首都是北京。"},
    {
   
   "role": "user",      "content": "它有多少人口?"}
  ],
  "temperature": 0.7,
  "stream": true
}

三个直接后果,解释了初学者大部分的困惑:

  1. token 计费随轮次超线性增长:第 nnn 轮要为全部前 n−1n-1n−1 轮的历史重新付费(prompt tokens),不是只付新问题;
  2. 改系统提示词立即生效,无需「重启对话」:下一轮请求带上新 system 消息即可——因为它本来就在序列里;
  3. 「失忆」有两大类:一类是工程失忆(历史没传 / 被窗口截断),一类是模型失忆(历史传了,但注意力没能有效利用,见 §7.3 的 lost-in-the-middle)。两者治法完全不同,先分清再修。

1.2 消息数组:三角色与它们的序列命运

messages 里每个元素有三要素:role(system / user / assistant)、content(文本或多模态块)、可选 name。渲染成序列时,三角色的命运截然不同:

角色 职责 在序列中的位置 是否计入训练损失(SFT)
system 行为规约、人设、格式要求 序列最前端,全局可见 通常不计(因模板而异)
user 用户输入 交替出现 不计——它是条件不是目标
assistant 模型输出 交替出现,止于 EOS 计入——这才是学习目标
(第四角色 tool) 工具返回 夹在 assistant 与下一 user 之间 视任务而定

「prompt 是条件、completion 才是目标」这条损失掩码纪律,从预训练到 SFT 到多轮对话一脉相承(与 next-token-prediction-deep-dive §4.3 的三种 mask 同一逻辑)。

1.3 全景数据流

用户键入 ──①──► messages[] ──②──► 渲染文本 ──③──► token ids
                                                        │
前端 ◄──⑥── SSE 流 ◄──⑤── detokenize ◄── 采样 ◄── logits ◄──④── prefill/decode
  │                                                              (Transformer
  └──⑦── 完整回答追加进 messages[],等待下一轮 ──⑧── 历史+新问题 ──┘  前向 + KV cache)

每一层只做一件事、只认一种数据格式,这是整个系统可拆解、可替换(换模型 / 换模板 / 换推理框架)的原因。


2 · 文本 → Token:分词器是第一道工序

2.1 为什么需要 token:三个不可兼得

  • 按字:词表小(中文常用字约 2 千–1 万),序列被切碎,「巴黎」要模型自己学组合;
  • 按词:语义完整,但词表爆炸 + OOV(未登录词直接瞎);
  • subword:高频模式整体成词、低频拆成可复用碎片,词表大小一个旋钮同时控制序列长度、显存与多语言覆盖(推导与词表账详见 llm-deep-dive Q2)。

2.2 一次对话的 token 账

语料 经验换算 来源
英文 1 token ≈ 0.75 词 ≈ 4 字符 OpenAI 经验口径
中文(128K 词表,如 LLaMA-3 / Qwen) 常见字 ≈ 1 token/字 【估算】
中文(50K 词表,GPT-2 风格) 1–3 token/字 【估算】
代码 / 数字 高度依赖词表;长数字常被切碎 BPE 按频合并的副产物

据此估算 §10 的两轮示例对话(system 18 字 + 问 8 字 + 答 9 字 + 追问 6 字 + 答 20 字)约 61 token;一次十轮的技术对话轻松过 3–5 千 token。多轮对话的上下文不是被「聊」长的,是被「历史重放」撑大的——每一轮都把此前全部内容重新计费。

2.3 特殊 token:对话的标点符号

词表里有一批不出现在正常文本里的「控制字符」,是多轮对话的结构骨架:

特殊 token 作用 典型写法
序列终止 / 文档分隔 预训练时分隔文档;对话中 = 生成结束 <|endoftext|>
轮次开始 / 结束 标记一段消息的边界 /
角色开始 / 结束 标记这段消息属于谁 system / user / assistant
填充 / 思考标记 对齐训练、推理模型的思考段 /

它们的作用有两层:给模型划边界(哪里是系统提示、哪里是历史、哪里该我说话),给推理循环发信号(见到 EOS 就停)。注意这些 token 在渲染后的文本里是不可见字符——打印聊天记录时你看不到它们,但它们实实在在地占着词表位置与上下文长度(ChatML 每条消息的 4 个控制 token 在多轮里会重复几十次,交互 W2 实时算这笔账)。


3 · Chat Template:多轮对话的序列格式

3.1 从消息数组到一条序列:渲染规则

模板 = 一组字符串拼接规则,把 messages 渲染成一条纯文本。以 OpenAI 的 ChatML 为例:

system
{system content}
user
{user content}
assistant
{assistant content}
user
{user content}
assistant

真实渲染结果(§10 示例的前两轮):

system
你是一个乐于助人的助手。
user
中国的首都是哪里?
assistant
中国的首都是北京。
user
它有多少人口?
assistant

最后那个开口的 assistant 是生成起点:模型从它的下一个位置开始续写,续写到 <tool_call>(结束标记)为止。多轮对话的全部秘密都在这条渲染规则里:历史不是存进某个「记忆模块」,而是每次原样拼回序列开头。

3.2 主流模板家族对比

不同模型家族用不同模板,同一份 messages 渲染出的文本不同——这是跨模型迁移对话数据的第一坑:

模板家族 代表模型 结构特征 生成起点
ChatML GPT-3.5/4、Qwen \n 包裹,无内嵌角色名行 末尾开口 ``
Llama-2 Llama-2-Chat [INST] ... [/INST] 包裹,system 单独 <<SYS>> 末尾 [/INST] 之后
Llama-3 Llama-3-Instruct <|start_header_id|>role<|end_header_id|> 头格式 <|start_header_id|>assistant 之后
Vicuna/v1 早期开源微调 USER: ... ASSISTANT: ... 明文 ASSISTANT: 之后
ChatGLM GLM 系 [Round N] 问:... 答: 答: 之后

渲染逻辑由模型卡目录里的 chat_template.jinja(Jinja2 模板)定义,HuggingFace apply_chat_template 与各家 SDK 都按它执行。选型铁律:模板必须与模型训练时严格一致——用 Llama-2 模板喂 Qwen,相当于用一套「标点符号」去考一个只学过另一套标点的学生,轻则格式错乱,重则完全不遵循指令(§3.4)。

3.3 一个反直觉的事实:模板是「代码」不是「配置」

模板里有循环、条件、变量(Jinja2 全家桶),能表达「奇数轮加一段免责声明」「工具调用轮插入 <tool_response> 块」这类逻辑。这意味着:同一个模型在不同产品里可以有不同的「人格边界」,因为渲染层可以注入了产品特有的指令。工程上的推论——排查对话异常(突然不听话、格式漂移)时,第一嫌疑人是模板渲染层,而不是模型权重。

3.4 训练-推理一致性:模板错位是线上事故高发区

SFT 阶段,训练数据就是按模板渲染后的序列(§8)。推理时若用了不同模板,等于分布外输入:模型见过的 system 从来长另一个样子。典型事故清单:

  • 漏掉系统消息的闭合标记,导致模型把用户消息当成系统指令的一部分;
  • 多轮数据里 assistant 消息忘了拼 EOS,模型学不到「什么时候停」;
  • 手写字符串拼接替代标准模板,升级模型卡后静默漂移;
  • 把 \n 写成字面量 \\n,角色边界消失,整段糊成一个 token 序列。

验收方法(交互 W2):把同一份 messages 用模型卡模板渲染,与训练数据里的样本逐字符对比——不一致就是 bug,没有「差不多」。


4 · 模型内部:一次前向的完整旅程

4.1 单层解剖(现代 LLM 口径)

x ──► RMSNorm ──► GQA 注意力(RoPE + 因果掩码)──► + 残差
                                                      │
x ──► RMSNorm ──► SwiGLU FFN ──────────────────────► + 残差

与 2017 年原版 Transformer 的逐项差异及动机(归一化、位置编码、注意力、FFN、正则五项)已在 llm-deep-dive Q3 列表给出,此处不重复。从「对话生命周期」视角,只需记住三件事:

  1. 因果掩码是自回归的地基:第 ttt 个位置只能看到 ≤t\le t≤t 的 token——所以模型生成第 t+1t+1t+1 个 token 时,前 ttt 个(含全部历史)都是它的合法条件;
  2. 注意力是「历史检索」的唯一机制:多轮对话里,模型回答第二轮时「想起」第一轮的内容,靠的就是每个新 query 对历史 K/V 的打分加权,没有任何独立的记忆模块;
  3. FFN 常被当作「知识存储器」(学界主流假说之一,Meichanizer et al. 2022 的思路):事实性知识大量存于前馈层,注意力负责「按需取用」。

4.2 参数化与输出:logits → 分布

模型主体 fθf_\thetafθ​ 把序列映射成每个位置的

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

reset2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值