Sunday 的面试指南

深入 Transformer:大模型为什么能理解上下文与生成内容

《Agent 大模型 0 到 1 系统课》 · 程序员 Sunday

前面几节课,咱们已经看到了很多大模型的原理内容了

咱们现在已经知道大模型可以根据上下文回答问题,可以根据 Prompt 调整输出,可以在 Context Window 里参考前面的内容,也会因为随机采样和上下文不足产生幻觉。

上一节,咱们又讲了普通模型、推理模型和多模态模型怎么选择。

那么现在,我请问大家几个问题,那就是:

  1. 模型为什么可以看到上下文?
  2. 为什么模型能知道一句话里面哪些词更重要?
  3. 为什么上下文窗口(Context Window)变长了之后,Token 成本和延迟会更高?
深入 Transformer:大模型为什么能理解上下文与生成内容 配图 1

那么想要理解这些问题,就需要涉及到一个 Agent 全新的知识点了,这就是 Transformer 架构

为什么要有 Transformer 架构

咱们先假设没有 Transformer。

大家想一下,模型要处理一句话,最直接的办法是什么?

很简单:一个字一个字的读就可以了,对不对。

这样理解应该还蛮简单的。

但是!如果一个字一个字的读,那么就会遇到两个问题:

  • 第一个问题:越往后读,前面的信息越容易被 “遗忘” 掉。 就和人看了后面的忘了前面的一个意思
  • 第二个问题:逐个往后读,很难并行。 举个例子:

假设模型要处理这句话:“订单 A1024 的退款金额是 3000 元,超过 2000 元时,需要人工审核。”

没有 Transformer 的前提下,模型就不能一下子读取整段话处理完,必须得 一个字一个字的读。

类似于 「订单」->「A1024」->「退款」 这样

必须得这样 按照顺序 才可以

这样效率就会特别低,比如 大模型要训练 1 亿 Token 的数据,这几年都读不完了。

所以,理想中的大模型架构就必须要解决两个问题才可以:

  • 长距离信息怎么保留下来? 模型不能读到后面以后,就把前面重要的信息给忘了
  • 大规模文本怎么高效计算? 也就是必须要并行处理

那么想要解决这两个问题,就需要用到 Transformer 架构 了

Transformer 架构到底是什么

Transformer 最早是在论文《Attention Is All You Need》里提出的。

原始 Transformer 其实是一个 Encoder-Decoder 架构,这玩意主要是用来做机器翻译这类序列到序列任务的。

而现在很多生成式大语言模型使用的其实是 Transformer 的 变体。比如常见文本生成模型,多数是 Decoder-only Transformer 路线。

Encoder-Decoder 和 Decoder-only-Transformer

你看,这里又有两个概念 Encoder-Decoder 和 Decoder-only-Transformer。

这两个东西大家不需要深入理解,咱们这里给大家 简单介绍一下,哪怕看不懂也没关系,完全不影响哈。

原始 Transformer 主要是为机器翻译设计的。

比如把英文翻译成中文:I want to refund this order. 变成 我想退款这个订单。

那么,此时这个场景下,这个任务就有两个阶段。

  • 第一阶段,模型先读取英文句子,理解输入内容。这个部分就叫做 Encoder。

  • 第二阶段,模型再根据理解到的内容,一个 Token 一个 Token 生成中文句子。这个部分叫 Decoder。

所以,Encoder-Decoder 可以简单理解成:Encoder:负责读懂输入,Decoder:负责生成输出

它比较适合翻译、摘要这种 “需要先读一段输入,然后再生成另一段输出” 的任务。

但是,咱们现在常见的聊天大模型,很多不是这种完整的 Encoder-Decoder 结构,而是 Decoder-only Transformer。

Decoder-only 的意思是:模型只保留“生成”这部分结构。

它的核心功能是根据已经看到的上下文,预测下一个 Token。

比如:订单 A1024 的退款金额超过 2000 元,所以需要____

模型要生成后面的:「人工审核」

这就是咱们之前在 03 - 探究大语言模型原理。大语言模型究竟是如何生成内容的? 提到了 “预测下一个 Token” 的原理

现在,很多生成式大语言模型都采用 Decoder-only Transformer 方案

差不多了解到这里就可以了。

针对这块,大家只需要记住以下两点就行:

  • 原始 Transformer 是 Encoder-Decoder 架构,适合序列到序列任务。
  • 现在很多聊天大模型使用 Decoder-only Transformer,核心是根据已有上下文不断预测下一个 Token。

下面是我用 GPT-Image-2 生成的一个详解图,感兴趣的可以看下,不感兴趣的直接跳过就行:

深入 Transformer:大模型为什么能理解上下文与生成内容 配图 2

回过头继续说 Transformer

刚才咱们其实就已经知道了:不同模型可能会采用不同的 Transformer 结构,但它们底层依然离不开 Transformer Block。

实现细节会一些变化,但核心模块差基本上都绕不开以下这几点:

  • Token Embedding
  • Position Encoding
  • Self-Attention
  • Multi-Head Attention
  • MLP / Feed Forward
  • Residual + LayerNorm
  • 多层 Transformer Block 堆叠

可能有点多,但是没关系,咱们一个个来说。

下面的每部分都会给大家生成一个手绘图,大家可以作为参考。

注意: 先看文字,再看手绘图。

第一步:Token Embedding

这是整个 Transformer 的第一步,也就是:文字会先变成 Token。

这一点其实在 第 04 节 也说过。模型不直接处理“文字”,而是处理 Token。

比如:“订单 A1024 需要人工审核”

进入模型前,会先被 Tokenizer 切成一段段 Token,再变成 Token ID。

但 Token ID 只是编号,编号本身是没有意义的。

所以还需要 Embedding,把每个 Token ID 映射成一串向量。

那么这样一套把 Token ID 映射成 向量的流程,就是 Token Embedding

深入 Transformer:大模型为什么能理解上下文与生成内容 配图 3
第二步:Position Encoding

映射好了之后,咱们得处理位置信息

因为 Self-Attention(第三步的) 本身并不知道顺序。

如果只把一堆 Token 向量丢进去,它知道有哪些 Token,但不知道谁在前、谁在后。

而在语言里面,文字的顺序肯定是非常重要的。比如说:用户退款 和 退款用户 ,就完全不是一个意思

因此,Position Encoding 的作用就是 让模型知道每个 Token 在序列里的位置(顺序)

深入 Transformer:大模型为什么能理解上下文与生成内容 配图 4
第三步: Self-Attention。

Self-Attention 是 Transformer 的核心步骤。

他做的事情,可以先用一句大白话理解,那就是:对于当前位置的 Token,模型会计算上下文里哪些 Token 更值得关注,然后把这些 Token 的信息汇总回来。

跟大家举个例子。

比如:模型正在处理 “人工审核” 这几个文字。但是这几个字并不是单独的。

想要处理好这几个字,他还需要关注前面的 “退款金额” “3000” “超过”“2000 ” “需要” 这些 Token。

那模型怎么知道应该关注谁呢?

这里就用到了 Q / K / V 了

Transformer 会把每个 Token 的向量,分别转换成三种向量:

  • Query:我现在想找什么信息
  • Key:我这里有什么信息可以被别人匹配
  • Value:如果别人关注我,我能提供什么内容

注意,这三者不是三个 Token。而是 同一个 Token 经过不同线性变换后得到的三种向量。

比如 “人工审核” 这个 Token,会生成自己的 Query、Key、Value。

“退款金额” “3000” “超过” 这些 Token,也都会生成自己的 Query、Key、Value。

当模型处理“人工审核”这个位置时,它会拿“人工审核”的 Query,去和上下文里每个 Token 的 Key 做匹配。

匹配分数越高,说明“人工审核”这个位置越需要关注那个 Token。

这些匹配分数会经过 softmax 变成一组权重。比如:“需要:0.30” 、 “超过:0.25” 这样的。

这个权重就表示:当前位置应该从每个 Token 那里拿多少信息。

接下来,模型就会按照刚才算出来的权重,把上下文里各个 Token 的 Value 加权汇总回来。

也就是说:Self-Attention 是在计算当前 Token 和上下文中其他 Token 的关系;相关性越高,对方的 Value 被拿回来的比例就越大。

深入 Transformer:大模型为什么能理解上下文与生成内容 配图 5
第四步: Multi-Head Attention。

刚才讲 Self-Attention 时,咱们说当前位置会根据 Q / K / V 计算一组注意力权重。

但这里有一个问题。

如果只计算一组注意力权重,那么模型就只有一套“关注方式”。

跟大家举个例子:

处理“人工审核”这个位置时,这一组权重可能主要关注:“退款金额” “3000” “超过”。

但是,一句话里往往不止一种关系。他可能还和订单号 “A1024” 有关系,和 商品类型也有关系。

所以 Transformer 不只做一组 Self-Attention,而是并行做多组 Self-Attention。

其中,每一组就叫一个 Attention Head,也就是一个注意力头。

而多组注意力头,就是 Multi-Head Attention 。

大家可以理解为:Multi-Head Attention 可以让多个注意力头同时工作,每个头都有自己的一组 Q / K / V 参数,因此可以算出不同的注意力分布。

最后,模型再把多个头的结果合并起来,这样模型就有机会从多个角度理解同一段上下文。

深入 Transformer:大模型为什么能理解上下文与生成内容 配图 6
第五步:MLP,也叫 Feed Forward。

Attention 可以帮当前 Token 从上下文里拿到了相关信息。

但是,拿到信息还不等于已经理解完了。

模型还需要继续加工这些信息。

比如它要把这些关系进一步组合起来,大概是下面这个意思:

退款金额是 3000
3000 超过 2000
超过 2000 所以需要人工审核

这个“继续加工”的过程,就是 MLP 负责的。

所以,大家可以把 MLP 理解成:负责把找到的信息继续加工成更有用的信息

深入 Transformer:大模型为什么能理解上下文与生成内容 配图 7
第六步: Residual 和 LayerNorm。

这俩东西有点类似于一个 规范 的东西。

根据上面的内容,咱们其实可以知道,Transformer 分成了很多层。层数一多,训练就容易不稳定,信息也可能在一层一层中丢失。

而 Residual 可以让输入绕过某些计算直接传到后面,LayerNorm 可以帮助每一层的数值保持稳定。

听起来有点复杂,不过没关系,这玩意不需要掌握的太深。

大家只需要知道:这两个东西可以让基于 Transformer 架构的模型更容易训练就可以了。

深入 Transformer:大模型为什么能理解上下文与生成内容 配图 8
第七步:Transformer Block

把以上所有的内容推到一起,就得到一个 Transformer Block。也就是一个 Transformer 块

你可以把一个 Transformer Block 理解成模型里的一次“加工”(每次加工都会走上面的流程)。

一段 Token 向量进来,经过这个 Block 以后,还是一段 Token 向量出去。只不过,出去的 Token 已经融合了上下文信息了。

深入 Transformer:大模型为什么能理解上下文与生成内容 配图 9

用 Node 模拟一次 Q/K/V Self-Attention

接下来咱们还是做个小案例吧。

在前面咱们一直在说 Q / K / V ,感觉这玩意还是挺难理解的。

所以说,想着用 Node 写一个小例子,把这个过程跑出来,让大家看看大致的流程,或许理解起来会更容易。

注意哈:代码本身并不重要,重要的是 通过代码验证流程

代码地址:https://github.com/lgd8981289/Agent--Code

创建 09-transformer-attention-demo 文件夹

创建 attention-demo.js:

// 模拟已经切好的 Token
const tokens = [
	'订单',
	'A1024',
	'退款金额',
	'3000',
	'元',
	'超过',
	'2000',
	'元',
	'需要',
	'人工审核'
]

// 模拟每个 Token 对应的向量
// 真实模型里的向量是训练出来的,这里是为了演示手动写的
const features = {
	订单: [0, 0, 0, 0, 1],
	A1024: [0, 0, 0, 0, 1],
	退款金额: [1, 0, 0, 0, 0],
	3000: [1, 0, 0, 0, 0],
	元: [0.4, 0.4, 0, 0, 0],
	超过: [0, 0, 1, 0, 0],
	2000: [0, 1, 0, 0, 0],
	需要: [0, 0, 1, 1, 0],
	人工审核: [0.5, 0.5, 1, 1, 0]
}

// 模拟一个 Attention Head
// 这个 Head 更关注“金额、阈值、超过、审核”这些信息
const head = {
	name: '金额审核关系头',
	queryWeights: [1, 1, 1, 1, 0],
	keyWeights: [1, 1, 1, 0.8, 0],
	valueWeights: [1, 1, 1, 1, 0]
}

// 简化版投影:用权重调整向量
function project(vector, weights) {
	return vector.map((value, index) => value * weights[index])
}

// 计算两个向量的相似度
function dot(a, b) {
	return a.reduce((sum, value, index) => sum + value * b[index], 0)
}

// 把分数转换成注意力权重
function softmax(scores) {
	const max = Math.max(...scores)
	const exps = scores.map((score) => Math.exp(score - max))
	const total = exps.reduce((sum, value) => sum + value, 0)

	return exps.map((value) => value / total)
}

// 按照注意力权重,把多个 Value 汇总成一个新向量
function weightedSum(values, weights) {
	const result = Array.from({ length: values[0].length }, () => 0)

	for (let i = 0; i < values.length; i++) {
		for (let j = 0; j < values[i].length; j++) {
			result[j] += values[i][j] * weights[i]
		}
	}

	return result
}

// 对指定 Token 执行一次 Attention 计算
function runAttention(targetToken) {
	const targetIndex = tokens.indexOf(targetToken)

	// 只看当前 Token 以及它前面的内容
	const visibleTokens = tokens.slice(0, targetIndex + 1)

	// 当前 Token 生成 Query
	const query = project(features[targetToken], head.queryWeights)

	// 前文 Token 分别生成 Key 和 Value
	const keys = visibleTokens.map((token) =>
		project(features[token], head.keyWeights)
	)
	const values = visibleTokens.map((token) =>
		project(features[token], head.valueWeights)
	)

	// 计算当前 Token 和前文每个 Token 的相关性
	const scores = keys.map((key) => dot(query, key) / Math.sqrt(query.length))

	// 将相关性分数转换成注意力权重
	const weights = softmax(scores)

	// 根据注意力权重汇总 Value,得到新的表示
	const newRepresentation = weightedSum(values, weights)

	// 整理注意力权重,方便查看
	const attentionList = visibleTokens
		.map((token, index) => ({ token, weight: weights[index] }))
		.sort((a, b) => b.weight - a.weight)

	return { query, attentionList, newRepresentation }
}

// 观察“人工审核”这个 Token 会关注哪些前文信息
const result = runAttention('人工审核')

console.log(`Attention Head:${head.name}`)
console.log('目标 Token:人工审核')
console.log(
	'Query:',
	result.query.map((value) => value.toFixed(2))
)

console.log('\n注意力权重 Top 6:')
for (const item of result.attentionList.slice(0, 6)) {
	console.log(`${item.token}: ${(item.weight * 100).toFixed(2)}%`)
}

console.log('\n加权汇总 Value 后,得到的新表示:')
console.log(result.newRepresentation.map((value) => value.toFixed(3)))

运行:

node attention-demo.js

你会看到类似这样的结果:

深入 Transformer:大模型为什么能理解上下文与生成内容 配图 10

这段输出要分两层看。

第一,看注意力权重。

当模型处理“人工审核”这个位置时,它不是平均看前面的所有 Token,而是更关注 “需要” “超过” “退款金额” “3000” “2000” 这些和审核判断有关的内容。

第二,看最后的新表示。

新表示是按照注意力权重,把这些 Token 的 Value 向量加权汇总,得到“人工审核”这个位置的新向量表示。

把这个案例结合前面的内容来看,应该会更好理解一些

总结

最后,咱们回到开头的三个问题。

第一个问题:模型为什么可以看到上下文?

这是因为文本进入模型以后,会先变成一段 Token 向量。

这些 Token 向量会进入 Transformer Block,在 Self-Attention 里互相计算关系。

所以,模型所谓“看到上下文”,更准确地说是:当前位置的 Token 表示,可以在可见范围内和其他 Token 表示发生计算,并把相关信息融合进自己的新表示里。

这也是为什么前面讲 Context Window 时,咱们一直说模型只能参考当前窗口里能看到的信息。

如果关键信息不在窗口里,Attention 就没有办法和它建立关系。

第二个问题:为什么模型能知道一句话里面哪些词更重要?

这就和 Self-Attention 里的 Q / K / V 有关。

当前 Token 会生成自己的 Query。上下文里的每个 Token 会提供自己的 Key 和 Value。

模型用当前 Token 的 Query 去匹配其他 Token 的 Key,得到一组相关性分数。

分数再经过 softmax,就变成注意力权重。

权重越高,说明当前 Token 越应该从那个位置拿更多信息。

最后,模型按照这些权重,把各个位置的 Value 向量加权汇总回来,形成当前 Token 的新表示。

第三个问题:为什么上下文窗口变长以后,Token 成本和延迟会更高?

原因也在 Transformer 这里。

上下文窗口变长,意味着一次请求里会有更多 Token 进入模型。

Token 越多,模型要处理的向量就越多。

更重要的是,Self-Attention 不是只处理单个 Token。它要计算 Token 之间的关系。

也就是说,窗口越长,需要计算的 Token 关系就越多。调用成本也就越大了

这就是为什么后面做 Agent 时,不能把所有历史消息、所有文档、所有工具结果都一股脑塞进上下文。

理解到这里,再回头看 Prompt、RAG、Memory、模型选择和成本控制,就会更清楚了。

添加作者微信 · 购买完整课程

解锁完整课程 ¥499

《Agent 大模型 0 到 1 系统课》
扫码添加微信,备注「Agent 课程」,购买后由 Sunday 提供完整内容的学习方式。

扫码添加作者微信 LGD_Sunday,购买 499 元 Agent 课程

微信昵称:LGD_Sunday
手机上可长按保存二维码,再用微信扫一扫识别。

保存微信二维码