大语言模型看起来像是在“理解”用户的问题,然后直接给出一段完整回答。但从计算过程看,它做的是另一件事:把输入文本转成数字矩阵,经过多层神经网络计算,预测“下一个 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 切分 |
|---|---|
| 北京天气 | 北京 / 天气 |
| unhappy | un / happy |
| GPT-4 | GPT / - / 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是输入矩阵;Wq、Wk、Wv是训练得到的参数矩阵;Q、K、V分别是查询、键和值矩阵。
注意力计算的核心公式是:
Attention(Q, K, V) = softmax((QK^T) / sqrt(dk) + mask) V
它可以拆成三步理解:
- 用当前 token 的 Query 和其他 token 的 Key 做相似度计算。
- 通过 Softmax 把相似度变成权重。
- 用这些权重对 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。
- 输出不稳定是概率生成的自然结果,关键流程必须加校验。
- 长上下文会增加成本,也可能降低效果,只有任务确实需要时才使用。
- 控制输入长度和输出长度,是降低延迟最直接的办法。