芥末
发布于 2025-12-09 / 0 阅读
0
0

从输入到输出讲清大语言模型的工作原理

大语言模型看起来像是在“理解”用户的问题,然后直接给出一段完整回答。但从计算过程看,它做的是另一件事:把输入文本转成数字矩阵,经过多层神经网络计算,预测“下一个 token 最可能是什么”,再把预测出的 token 追加回上下文,继续预测下一个 token。

完整链路可以概括为:

flowchart LR
    A[系统提示词] --> E[组合上下文]
    B[历史对话] --> E
    C[工具描述] --> E
    D[用户最新问题] --> E

    E --> F[分词 Tokenization]
    F --> G[Token ID]
    G --> H[Embedding 向量]
    H --> I[Transformer 层堆叠]
    I --> J[Logits 词表得分]
    J --> K[Softmax 概率分布]
    K --> L[采样得到下一个 token]
    L --> M[追加到上下文]
    M --> I

这条链路里有几个关键点:

  • 模型输入不是“一个问题”,而是一段组合后的上下文。
  • 模型不会天然记住历史对话,历史是工程系统每次调用时重新塞进去的。
  • 模型内部处理的是向量和矩阵,不是中文、英文这些自然语言字符。
  • 生成回答时不是一次性写完,而是一个 token 一个 token 往外吐。
  • 上下文越长,计算成本越高,延迟也会变大。

输入:模型真正看到的是上下文

一次大模型调用通常包含四类信息。

输入部分作用示例
系统提示词规定模型身份、语气、边界和输出格式你是一个智能助手,回答要简洁
历史对话让模型知道前面聊过什么用户问过什么、模型答过什么
工具描述告诉模型可调用哪些函数或外部能力查询天气、搜索文档、调用数据库
最新问题用户当前真正想解决的问题今天北京天气怎么样

以常见 API(应用程序编程接口)调用形式表示,大致是这样:

const messages = [
  {
    role: "system",
    content: "你是一个智能助手,回答要简洁。"
  },
  {
    role: "user",
    content: "你好"
  },
  {
    role: "assistant",
    content: "你好,有什么可以帮你?"
  },
  {
    role: "user",
    content: "查询一下今天北京的天气"
  }
];

const tools = [
  {
    type: "function",
    function: {
      name: "get_weather",
      description: "获取指定城市的实时天气",
      parameters: {
        type: "object",
        properties: {
          city: { type: "string" }
        },
        required: ["city"]
      }
    }
  }
];

模型并不会因为上一轮聊过就“自动记得”。如果不把历史对话放进 messages,模型就只知道当前请求里的内容。这也是上下文会越聊越长的原因:每多一轮对话,工程系统往往会把新的问答继续拼到下一次输入里。

文本怎样变成模型能算的矩阵

大模型的核心计算是矩阵运算。自然语言要进入模型,必须经历两个步骤:分词和嵌入。

分词:把文本切成 token

Token 可以理解成模型处理文本的最小片段,但它不一定等于汉字、单词或字符。

例如:

文本可能的 token 切分
北京天气北京 / 天气
unhappyun / happy
GPT-4GPT / - / 4
今天 25℃今天 / 25 / ℃

不同模型的分词器不一样,同一句话在不同模型里可能切成不同数量的 token。中文里有的模型接近“一个汉字一个 token”,有的模型会把常见词合成一个 token;英文里常见词可能是一个 token,不常见词可能被拆成多个子词。

分词后,每个 token 会被映射成词表里的整数 ID。假设词表里有 50,000 个 token,那么每个 token 都有一个从 0 到 49,999 的编号。

输入文本:北京天气怎么样
分词结果:[北京, 天气, 怎么样]
Token ID:[1034, 8765, 2310]

嵌入:把 token ID 变成向量

Token ID 只是编号,本身没有语义。模型会通过一个可训练的嵌入矩阵,把每个 ID 转成固定维度的向量。

假设向量维度是 512,那么:

Token ID 1034 -> [0.12, -0.03, 0.48, ..., 0.07]
Token ID 8765 -> [0.09,  0.21, -0.11, ..., 0.33]
Token ID 2310 -> [-0.31, 0.08, 0.16, ..., -0.02]

如果输入有 n 个 token,每个 token 被转成 d 维向量,最终输入矩阵就是:

X ∈ R^(n × d)

这里的 n 就是上下文长度,d 是模型的隐藏维度。模型后续所有“理解”和“生成”,都建立在这个矩阵上。

上下文窗口为什么有限

每个大模型都有上下文窗口限制。窗口大小通常用 token 数表示,例如 32K、128K、1M。超过限制时,接口可能直接报错,也可能由上层系统截断旧内容。

需要特别注意:上下文窗口通常同时包含输入和输出。

如果模型窗口是 128K,接口允许最多输出 4K token,那么输入最好不要超过 124K token:

最大窗口:128K token
预留输出:4K token
可用输入:约 124K token

原因很简单:模型生成出的 token 也会追加进上下文。输出越长,占用的窗口越多。

Transformer:大语言模型的核心结构

大多数现代大语言模型都建立在 Transformer 架构上。它的核心能力来自自注意力机制,也就是让每个 token 在计算时关注上下文里的其他 token。

一个简化的 Transformer 层可以表示为:

flowchart LR
    A[输入向量 X] --> B[多头自注意力 Multi-Head Attention]
    B --> C[残差连接与归一化]
    C --> D[前馈网络 FFN]
    D --> E[残差连接与归一化]
    E --> F[输出到下一层]

真实模型会有更多工程细节,例如归一化位置、残差结构、激活函数、注意力优化、专家路由等。但理解主线时,抓住两个模块就够了:

模块作用
自注意力层让每个 token 聚合上下文信息
前馈网络层对聚合后的信息做非线性加工和特征提炼

自注意力:每个 token 怎样查找相关信息

自注意力机制会给每个 token 生成三组向量:

  • Query(查询向量):当前 token 想找什么信息。
  • Key(键向量):当前 token 能提供什么线索。
  • Value(值向量):当前 token 真正携带的内容。

计算方式可以写成:

Q = XWq
K = XWk
V = XWv

其中:

  • X 是输入矩阵;
  • WqWkWv 是训练得到的参数矩阵;
  • QKV 分别是查询、键和值矩阵。

注意力计算的核心公式是:

Attention(Q, K, V) = softmax((QK^T) / sqrt(dk) + mask) V

它可以拆成三步理解:

  1. 用当前 token 的 Query 和其他 token 的 Key 做相似度计算。
  2. 通过 Softmax 把相似度变成权重。
  3. 用这些权重对 Value 做加权求和,得到融合上下文的新向量。

对生成式语言模型来说,还会加入 causal mask(因果遮罩)。它保证某个位置只能看到自己和它之前的 token,不能偷看未来 token。否则训练时模型会直接看到答案,生成时就不成立了。

flowchart TD
    A[当前 token 的 Query] --> D[计算相关性分数]
    B[历史 token 的 Key] --> D
    D --> E[Softmax 得到注意力权重]
    C[历史 token 的 Value] --> F[加权求和]
    E --> F
    F --> G[融合上下文后的向量]

一个关键结论是:最后一个 token 对应的隐藏状态,已经融合了前面整段上下文的信息。模型生成下一个 token 时,主要就依赖这个位置的隐藏状态。

多头注意力:从多个角度看同一段文本

单个注意力头只能用一套 Wq/Wk/Wv 参数观察文本。多头注意力会并行使用多套参数,让不同注意力头关注不同关系。

例如同一句话:

小明把苹果放进书包,因为他下午要去学校。

不同注意力头可能分别关注:

注意力头可能关注的关系
头 A“他”指代“小明”
头 B“苹果”和“放进”的动作关系
头 C“书包”和“学校”的场景关系
头 D因果结构:“因为”后面解释原因

多个头的结果会拼接起来,再通过线性变换融合。这样模型能同时捕捉语法、指代、主题、因果、格式等多种信息。

前馈网络:把上下文信息进一步加工

自注意力负责“从上下文拿信息”,前馈网络负责“对拿到的信息做变换”。它通常由两层或多层线性变换加激活函数组成。

可以把它理解成:

自注意力:这个 token 应该参考哪些上下文?
前馈网络:参考完以后,把信息加工成更有用的内部表示。

Transformer 层会堆叠很多层。浅层可能更关注局部词法和句法,深层会逐渐形成更抽象的语义、推理和任务表示。

大模型到底“大”在哪里

“大”主要体现在三个地方:参数量大、训练数据量大、计算量大。

以 DeepSeek V3 这类模型为例,可以看到现代大模型的规模特征:

维度含义
参数量模型内部可训练权重的数量,决定模型容量上限
层数Transformer 层堆叠数量,层数越多表达链路越深
注意力头数多头注意力中的并行头数量
专家数量MoE(Mixture of Experts,专家混合)结构中可选择的前馈网络数量
激活参数一次推理实际参与计算的参数,不一定等于总参数

DeepSeek V3 公开信息中包含几个典型设计:

项目说明
总参数量约 671B
单次激活参数约 37B
Transformer 层数约 61 层
注意力机制MLA(Multi-head Latent Attention,潜在多头注意力),用于减少 KV 缓存占用
专家结构MoE,每次只激活部分专家
预训练数据量约 14.8T token

MoE 的核心思想是:模型里有很多“专家网络”,但每个 token 只路由到少数几个专家计算。这样总容量很大,单次推理成本又不会等同于把所有参数全算一遍。

输出:模型怎样把向量变回文字

Transformer 输出的仍然是隐藏状态向量。要生成文字,还需要把隐藏状态映射回词表。

Logits:每个 token 的原始得分

如果词表大小是 50,000,模型会通过一个线性层,把最后一个位置的隐藏向量转换成 50,000 维数组。

hidden_state -> [2.1, -0.3, 1.8, ..., 0.02]

这个数组叫 logits。每个位置对应词表里的一个 token,数值越大,表示模型越倾向于选择它。

Softmax:把得分变成概率

Logits 不是概率,数值可能为负,总和也不等于 1。Softmax 会把它们转换成概率分布:

logits: [2.1, -0.3, 1.8]
softmax: [0.54, 0.05, 0.41]

转换后,每个 token 都有一个被选中的概率。

自回归生成:一次只生成一个 token

大语言模型生成回答的方式叫自回归生成。流程是:

sequenceDiagram
    participant U as 用户输入
    participant M as 大语言模型
    participant C as 上下文

    U->>C: 写入系统提示词、历史对话、最新问题
    C->>M: 输入完整上下文
    M-->>C: 生成第 1 个 token,追加到上下文
    C->>M: 带着新 token 再次推理
    M-->>C: 生成第 2 个 token,追加到上下文
    C->>M: 持续循环
    M-->>U: 遇到结束符或长度上限后返回完整回答

也就是说,模型不是直接生成一整段,而是不断重复:

根据当前上下文预测下一个 token
把这个 token 加进上下文
继续预测下一个 token

这也解释了为什么输出长度会影响总耗时:输出越长,循环次数越多。

Temperature 和 Top-p:为什么同一个问题会有不同回答

从概率分布里选 token,有多种策略。常见参数是 temperature 和 top-p。

参数作用值较小时值较大时
temperature调整概率分布的尖锐程度更稳定,更容易选高概率 token更随机,更容易出现多样表达
top-p只在累计概率达到 p 的候选集合里采样候选范围更小候选范围更大

举个简化例子,模型对下一个 token 的概率是:

token概率
北京0.55
上海0.20
广州0.10
天气0.08
苹果0.02
其他0.05

如果 top-p = 0.8,模型会从概率从高到低累加,选择覆盖 80% 概率的最小集合,也就是大概率只在“北京、上海、广州”附近采样。低概率但不太相关的 token 会被排除。

需要稳定输出时,可以降低 temperature,并配合较小的 top-p;需要创意写作时,可以适当提高随机性。

位置编码:为什么模型知道词语顺序

自注意力本身只看 token 之间的相关性。如果没有位置信息,“我咬狗”和“狗咬我”会包含同样的词,模型很难区分语义。

位置编码就是为了解决顺序问题。

类型思路问题
绝对位置编码给每个位置一个固定编号或向量超过训练长度时,模型没见过更远位置
相对位置编码关注两个 token 之间的距离更适合泛化到更长上下文
RoPE用旋转方式把位置信息注入 Query 和 Key长距离仍可能衰减或失真

RoPE(Rotary Position Embedding,旋转位置编码)是目前常见方案之一。它的思路是:把位置信息变成向量空间里的旋转角度,在计算 Query 和 Key 的相关性前,先按位置旋转它们。

这样做有一个好处:注意力分数天然包含相对距离信息。两个 token 距离越远,它们之间的注意力关系通常会更弱,模型也更容易关注局部上下文。

长上下文为什么难

很多模型支持 32K、128K,甚至更长上下文。但“能输入这么长”和“在这么长上下文里保持同等效果”不是一回事。

难点主要有两个。

计算成本随长度快速增长

标准自注意力需要计算 token 两两之间的关系。上下文长度是 n 时,注意力矩阵规模接近:

n × n

所以计算复杂度约为:

O(n²)

上下文从 4K 增加到 32K,长度变成 8 倍,注意力相关计算可能接近 64 倍。实际系统会用 KV Cache(Key-Value Cache,键值缓存)、稀疏注意力、分页缓存等优化,但长上下文仍然昂贵。

长文本训练数据更少

模型如果主要在 4K 或 8K 长度上训练,就算学会了相对位置规律,遇到 128K 的超长上下文时也可能不稳定。原因不是位置编码完全不能扩展,而是模型缺少足够多的长距离依赖训练。

主流训练路线通常是:

flowchart LR
    A[大量短文本预训练] --> B[学会语言、知识、推理和基础指令能力]
    B --> C[位置外推或窗口扩展]
    C --> D[少量长文本微调]
    D --> E[获得更长上下文能力]

这种方式比从零开始用长文本训练便宜得多,但也意味着:长上下文能力通常不如短上下文稳定。

常见长上下文扩展策略

策略核心思路代价
位置插值把更长距离压缩到模型熟悉的位置范围内可能损失距离分辨率
YaRN 等改进插值对不同距离段使用不同缩放策略实现更复杂,需要调参和训练配合
滑动窗口注意力每个 token 只关注附近窗口全局信息可能丢失
稀疏/选择性注意力只挑选部分重要区间参与计算选择策略错误会影响结果
长文本微调用长上下文数据继续训练高质量长文本数据稀缺,成本高

长上下文不是免费能力。它适合处理长文档、代码仓库、多轮复杂任务,但如果任务本身只需要几百 token,硬塞大量历史和规则只会增加延迟,并可能降低稳定性。

工程实践:怎样用这些机制改进系统

理解底层机制后,很多工程问题会变得清晰。

不要把所有历史都塞进上下文

历史对话越多,上下文越长,模型越慢,也越容易被无关内容干扰。更合理的方式是把历史记录存储起来,在新问题到来时只检索相关片段。

flowchart LR
    A[用户新问题] --> B[检索历史记录]
    B --> C{是否相关}
    C -- 是 --> D[加入上下文]
    C -- 否 --> E[丢弃或只保留摘要]
    D --> F[调用大模型]
    E --> F

可选策略:

策略适合场景
最近 N 轮简单聊天、上下文依赖近几轮
历史摘要长时间会话,需要保留整体目标
向量检索历史很多,只需要相关片段
结构化记忆用户偏好、项目配置、长期事实

Prompt 和工具描述要克制

系统提示词和工具描述也是上下文的一部分。规则写得越多,模型越容易在多条规则之间冲突;工具列表越长,模型越容易选错工具或输出不符合格式。

更稳的做法是:

1. 把规则写成明确、可验证的约束。
2. 工具描述只保留调用条件、参数含义和返回格式。
3. 对工具调用结果做程序校验,不要完全依赖模型自觉。
4. 输出 JSON 时使用 schema 校验,失败后带错误信息重试。

例如要求结构化输出,可以直接给出格式:

{
  "action": "get_weather",
  "arguments": {
    "city": "北京"
  }
}

并在服务端校验:

def validate_tool_call(data: dict) -> bool:
    if data.get("action") != "get_weather":
        return False

    arguments = data.get("arguments")
    if not isinstance(arguments, dict):
        return False

    city = arguments.get("city")
    return isinstance(city, str) and len(city) > 0

模型负责生成候选结果,程序负责兜底校验,这是构建稳定 Agent 系统的基本原则。

延迟由输入长度和输出长度共同决定

一次调用的耗时大致分成两段:

阶段做什么主要受什么影响
Prefill处理完整输入上下文,算出首个 token 前的状态输入 token 数
Decode一个 token 一个 token 生成输出输出 token 数、当前上下文长度

可以粗略理解为:

总耗时 ≈ a × 输入长度² + b × 输出长度 × 上下文长度

实际推理框架会用 KV Cache 降低重复计算,但规律仍然成立:

  • 输入越长,首 token 等待越久。
  • 输出越长,总等待越久。
  • 长上下文下,每生成一个 token 的成本也更高。

优化手段很直接:

问题处理方式
首 token 慢缩短输入上下文、减少工具描述、压缩历史
总输出慢限制最大输出 token,要求回答简洁
多轮任务慢把不相关任务拆开,避免每次带全量上下文
格式错误导致重试使用结构化输出和程序校验

多 Agent 拆分可以降低单次上下文长度

如果一个 Agent 既要懂需求分析,又要会查数据、写代码、生成报告、做审核,它的提示词和工具列表会非常长。更好的结构是把任务拆成多个子 Agent,每个子 Agent 只负责一类能力。

flowchart TD
    U[用户请求] --> M[主 Agent:理解任务并拆分]
    M --> A[检索 Agent]
    M --> B[代码 Agent]
    M --> C[审核 Agent]
    A --> M
    B --> M
    C --> M
    M --> U

主 Agent 不需要知道每个子 Agent 的完整规则,只需要知道它们能做什么。这样可以让单次调用的上下文变短。

假设原来一个大 Agent 上下文是 12K token。拆成 4 个子 Agent,每个 3K token。只看注意力复杂度的数量级:

单 Agent:12² = 144
多 Agent:4 × 3² = 36

虽然调用次数增加了,但因为自注意力成本与长度平方相关,总耗时可能反而下降。这个估算不包含网络开销、排队时间和工具调用耗时,但足以说明拆分为什么有意义。

多模态输入要分清两种实现

图片、音频、视频等非文本输入通常有两类处理方式。

类型实现方式特点
文本中转先用 OCR、图像识别或语音识别转成文本,再交给语言模型工程简单,但会丢失视觉细节
原生多模态用视觉编码器把图像转成向量,与文本 token 一起输入模型能利用图像特征,但模型和训练更复杂

如果系统底层主要是文本语言模型,那么图片理解能力往往依赖外部视觉模块。视觉模块识别错了,语言模型后续推理也会跟着错。即使是原生多模态模型,也仍然可能因为概率生成机制出现幻觉,所以关键业务场景不能只看模型回答,还要结合检测、校验和人工确认。

关键结论

大语言模型的工作方式可以压缩成一句话:把上下文转成 token 向量,用 Transformer 聚合上下文信息,再通过概率分布逐 token 生成输出。

工程上最有用的几个判断是:

  • 上下文不是越长越好,短而相关的上下文更稳定。
  • 历史对话要检索、摘要或裁剪,不能无限追加。
  • 工具描述和系统规则要清晰克制,复杂任务适合拆成多个 Agent。
  • 输出不稳定是概率生成的自然结果,关键流程必须加校验。
  • 长上下文会增加成本,也可能降低效果,只有任务确实需要时才使用。
  • 控制输入长度和输出长度,是降低延迟最直接的办法。

评论