JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

位置编码与上下文窗口

注意力会一次性读完所有词——这也意味着,它本身根本分不清谁先谁后。本指南将讲清 Transformer 如何把顺序重新注入进去、上下文窗口究竟是什么,以及为什么「直接把它做长一点」远没有听起来那么简单。

注意力内部的盲点

到现在你已经看过 自注意力如何让每个词元去看其他所有词元,并决定彼此的重要程度。但请停下来想一想它*怎么*去看。每个词元都会变成一组 查询、键和值,注意力用缩放点积把查询和键作比较。这个比较本质上是一次求和——而求和并不在意顺序。把词打乱,出来的还是同一组值,只是重新排了排。注意力本身是置换不变的:在它眼里,「狗咬了人」和「人咬了狗」长得一模一样。

这正是 Transformer 为它那记漂亮的把戏所付出的代价。它所取代的循环网络是一个词一个词地读句子,所以顺序天然地嵌在「阅读」这个动作里。Transformer 把这一点抛掉了,改成并行地读完所有内容——快,但对顺序失明。于是顺序必须靠人手重新放回去,而且要在注意力开跑之前。

把名牌发下去:位置编码

每个词元进来时本就带着一个嵌入——一个表示它*是什么意思*的向量。位置编码再加上第二个向量,表示它*坐在哪里*。两者直接相加,于是一个词元最终的表示里同时带着它的含义和它的位置。真正巧妙的地方,在于那个位置向量长什么样。

最初的 Transformer 用的是一组固定的正弦和余弦波,波长各不相同。位置 0 拿到一组波值的组合,位置 1 拿到稍微平移过的一组,依此类推。由于波是平滑重复的,相邻位置拿到相近的编码、相距很远的位置拿到区分度高的编码——而且关键在于,两个位置之间的*相对*距离会表现为一个稳定的偏移,而这恰恰是注意力能加以利用的东西。这里没有任何东西是学出来的;它纯粹是几何,算一次就够了。

# meaning + place, summed before attention
for pos in range(sequence_length):
    token_vec[pos] = embedding[pos] + position_code(pos)
# position_code(pos): a vector of sin/cos waves
#   wave_k(pos) = sin( pos / 10000^(k/d) )   # many wavelengths k
# nearby pos -> similar code; relative offset -> consistent shift
在注意力看到任何东西之前,逐个词元地把位置加到含义上。

还有别的方案。有些模型不是把位置固定下来,而是为每个槽位*学*一个位置向量。今天大多数大模型用的是旋转式编码(常称 RoPE):它把查询和键向量旋转一个随位置增大的角度——于是两个词元之间的点积自然就取决于它们相隔多远。要记住的关键是它们共同的目标:给注意力一种可靠的*相对*距离感,而不只是一个绝对的下标。

上下文窗口究竟是什么

上下文窗口是模型在某一刻能同时纳入视野的词元上限——你的提示词,加上它到目前为止生成的全部内容,统统算在一起。当人们说某个模型有「128K 上下文」时,指的就是这个上下文长度:大致相当于大家说话的那个房间有多大。一旦超过它,最早的那些词元就掉出了边界;模型根本无法去注意那些已经放不下的东西。

两点要老实说清。其一,窗口是按*词元*算的,不是按词算——由于分词,一个长词或生僻词可能被拆成好几个词元,所以「到底能装多少文字」要比那个标称数字模糊得多。其二,窗口不是不同对话之间的记忆。一段对话一旦结束,什么都不会延续下来;每个新请求都要从零把整个上下文重新搭一遍。模型没有日记本。

那为什么窗口非得有限不可?因为注意力的开销随序列长度的*平方*增长:词元翻一倍,比较的次数大约翻两番,因为每个词元仍要去注意其他每一个词元。更长的窗口不是某人忘了拨的一个开关——它是一笔实打实的算力与内存账单,每一次前向传播都得有人来付。

KV 缓存:为什么第一个词之后生成会加速

模型在写文字时,靠的是自回归解码:预测一个词元,把它接上去,再预测下一个。若是天真地做,每个新词元都得对整段历史从头重跑一遍注意力——而历史只会越来越长,于是每多写一个词就越来越慢。解决办法就是 KV 缓存

关键的观察在于:一旦某个词元的向量算出来了,它们就再也不变。第 5 个词元的键,无论这句话是 6 个词元还是 600 个词元,都是同一个。于是模型把每个词元的键和值只算一次、存起来,往后永远复用。要生成下一个词元,它只需算*一个*新的查询,拿它去和所有缓存的键作比较,再把缓存的值组合起来。昂贵的那段历史只付一次费,而不是每一步都付。

长上下文:远不止「直接做大一点」

把窗口拉长,会同时撞上三堵墙。注意力的平方级开销让纯算力爆炸式增长。KV 缓存在内存里急剧膨胀。而在短序列上训练出来的位置编码,往往*外推*得很差——喂给模型一些远超它训练时见过的位置,它的距离感会悄悄地崩掉。

研究者们逐堵墙地去啃。FlashAttention 重新组织了计算方式,把内存用得高效得多,让长序列在不改变数学本质的前提下变得可行。另一些做法把注意力稀疏化或近似化,让每个词元只注意一个挑选过的子集,而非所有人。RoPE 一类的编码可以经过重新缩放,伸到超出其训练长度的地方。这些没有一个是免费午餐——每一个都在用精确性、通用性或实现复杂度去换取触及的范围。

下面这部分是宣传里被跳过的。大窗口意味着模型*能*读很多——并不意味着它读得*好*。在「大海捞针」式的测试里,模型常常能取回埋在长上下文最开头或最末尾的事实,却漏掉停在中间的内容。一个长窗口是一种容量,而不是注意力的保证。请把「100 万词元」看作模型能吞下多少的上限,而不是它会忠实使用多少的承诺。

具体说来为什么要在意?因为窗口大小和 KV 缓存一起决定了推理成本——每一次回复背后的延迟与花费。把它们想通,就能把模糊的烦躁(「长文档的回答又差又慢」)变成一个你可以动手处理的诊断;它也解释了为什么哪怕是一个带着巨大窗口的大语言模型,喂给它*相关的*文字,仍然比把你手头*所有东西*都塞进去要好。