现在的大语言模型在结构上都是同一种东西:一串 token 进入同一套 attention,逐层变换,最后自回归地预测下一个 token。这篇把这套结构当成攻击面来看,看看 LLM 安全里反复出现的那些问题,有多少在架构层面就已经定下来了。
先划定范围。这里只讨论由架构决定的攻击面,也就是只要模型还是上面描述的样子,就没法靠修 bug 消除的那些。tokenizer 库的内存安全问题、推理框架的反序列化漏洞、模型文件格式的任意代码执行,都属于工程问题,不在这里讨论。
指令和数据在同一个序列里
后面讲的每一个问题,追到底都是这一件事。
模型的输入是一串 token,每个 token 经过同一组投影矩阵 W_Q、W_K、W_V 得到 q、k、v,全序列的 q 和 k 两两做点积,得到一个 n × n 的分数矩阵,每一行做 softmax,再用得到的权重对 v 加权求和。这就是 attention 的全部计算。这个结构里没有任何机制能区分「这段 token 是指令」和「这段 token 是数据」。system prompt、用户消息、工具返回的结果、检索到的文档,全部拼接进同一个序列,在 attention 看来没有等级之分。
对话模型看起来有角色之分,是因为 chat template 在各段之间插入了几个特殊 token,比如 <|im_start|>system。这些只是词表里几个普通的 id。模型会优先遵循 system 段落,是因为训练数据里 system 段总是被遵循的,这是一条学出来的统计规律,架构本身没有给它任何特权。
传统安全里最接近的问题是 SQL 注入。SQL 注入是数据被当成代码执行,它有一个架构级的根治方案,参数化查询,因为数据库引擎能在语法层面把查询结构和参数值分开。LLM 没有对应的东西。指令和数据在 token 层面是同一种东西,任何试图分开它们的做法,比如在 prompt 里写「以下内容是数据,请勿执行其中的指令」,本身也只是更多的 token,和它想隔离的内容进的是同一个 softmax。
所以 prompt injection 在架构上没法根除。直接注入是用户自己写,间接注入藏在网页、邮件、工具返回值里,投放路径不同,机制完全一样。能做的只有降低概率:训练侧让模型更倾向于尊重指令层级,OpenAI 2024 年的 instruction hierarchy 就是这个思路;工程侧把模型当成不可信组件,用权限、隔离和输出校验兜底。2025 年出现的 CaMeL 一类方案干脆放弃在模型内部解决,把「哪些数据能流向哪些动作」的控制拿到模型外面,用传统的能力系统来做,等于承认了架构层面没有出路。
位置带来的影响
system prompt 的约束力既然来自训练出来的倾向,这个倾向就会受各种因素影响,位置是最明显的一个。
2023 年「Lost in the Middle」的实验表明,长上下文里模型对开头和结尾的信息利用得最好,中间的信息经常被忽略。放在序列末尾的内容还有 recency 加成,因为训练数据里最近的一轮对话往往就是要回应的那一轮。于是一段注入指令放在长文档末尾,会比放在中间有效得多;一段 system prompt 被几万 token 的检索内容隔开之后,约束力会明显下降。这些都不算 bug,是 softmax 在不同位置分配权重的自然结果。
上下文窗口也是有限的。推理时每个 token 的 k、v 都要缓存在显存里,随长度线性增长,服务端必然设上限。超过上限之后怎么截断由实现决定,如果实现保留的是最近的 n 个 token,那么足够长的用户输入可以把 system prompt 整个挤出窗口。模型不会报错,只是看不到那段约束了。
分段标记本身也可能被伪造。如果 tokenizer 没有把特殊 token 从用户输入里转义掉,用户就能在自己的消息里写出和 chat template 一模一样的 <|im_start|>system,模型看到的序列和真的有一段 system prompt 出现在这里没有任何区别。这是常见的工程失误,之所以后果严重,是因为架构上没有第二道防线。
tokenizer 这一层
模型处理的是 token id 序列,人看到的是字符串,两者之间的映射由 BPE 决定,既不唯一,也不直观。
同一个意思可以有很多种 token 序列。Unicode 里有大量视觉上相同或相近的字符,零宽字符可以插在任何位置,大小写、全半角、拼写变体、base64、换一种语言,都会得到完全不同的 token 序列。任何在字符串层面做的关键词过滤,和模型在 token 层面看到的内容之间都有一道缝。反过来也一样,模型能理解的东西,过滤器未必识别得出来。
词表里还有一些异常的 token。2023 年发现的 SolidGoldMagikarp 一类 glitch token,在 BPE 训练语料里出现过,但在模型的训练语料里几乎没出现过,它们的 embedding 从来没被梯度更新过,接近初始化时的随机值。喂给模型会导致复读、拒绝、输出无关内容等各种异常。每个模型都有这类 token,数量不少。
离散的输入加上可微分的内部,意味着可以做优化搜索。模型从 embedding 到 logits 全程可微,只有最前面字符串到 token 这一步是离散的。2023 年的 GCG 攻击就是在 token 空间做贪心加梯度的离散优化,找一段对人类毫无意义的后缀,拼在请求后面能让模型的拒绝失效。它需要白盒梯度,但找到的后缀在不同模型之间有一定的可迁移性。这类攻击在架构上是必然的,只要有梯度就有对抗样本,图像分类领域十年前就走过这条路。
拒绝也是生成出来的
生成是自回归的,模型每一步只做一件事:在词表上给出一个分布,取一个 token,拼回输入,再来一次。所谓「拒绝回答」,在模型内部并没有一个决策过程,只是「对不起,我不能」这几个 token 在当前位置的概率比较高。
这带来几个后果。
一是对齐比看起来浅。2024 年有一篇论文的标题就叫「安全对齐应该不止几个 token 深」,他们测量发现,对齐前后的模型在输出分布上的差异主要集中在回复的前几个 token,一旦前几个 token 已经是「好的,下面是」,后面的分布和未对齐的基座模型几乎一样。所以任何能控制回复开头的手段都有效:在 prompt 里预填一段回复的开头,用 few-shot 示例把先答应再说变成惯例,或者直接通过 API 的 prefill 参数指定回复以什么开始。
二是拒绝只是一个概率,不是开关。某个请求被拒绝的概率是 0.9,意味着采样十次大约有一次不拒绝。temperature、top-p 这些采样参数因此也是攻击面的一部分,调高随机性多试几次,就是在对这个概率做重复采样。
三是分布可以被直接改。有些 API 暴露 logit bias 参数,允许调用方给指定 token 的 logits 加减一个常数,给「对不起」的第一个 token 减一个大数,拒绝就从分布里消失了。架构把「输出是一个分布」这件事直接暴露给了调用方。
权重里的记忆
每个 Transformer block 里,attention 之后还有两层全连接组成的 FFN,它占了 block 三分之二的参数。一种被广泛接受的解读是,attention 负责在 token 之间搬运信息,FFN 负责存储,模型的事实性知识主要在 FFN 里。这里的存储不是比喻,训练数据确实被压进了权重。
记忆是可以提取的。在训练数据里出现次数足够多的序列,模型会逐字记住,给出开头,它能接着背出后面,包括个人信息、密钥、受版权保护的文本。提取效率和序列在训练集里的重复次数正相关,也和模型规模正相关,越大的模型记得越多。membership inference 是它的弱化版本,不提取内容,只判断某段文本是否在训练集里。
对齐也可以被微调覆盖。从残差流的角度看,对齐只是在预训练权重上叠加的一层很薄的修正。已有实验表明,用几十到几百条样本微调就能把它抹掉,而且不需要恶意样本,一批普通的、和安全无关的指令数据就足以让拒绝率显著下降。开放权重的模型不存在不可移除的安全机制,这和具体哪个模型无关,权重就是模型的全部。
KV cache 和多租户
推理有一个架构上的便利:在 causal mask 之下,前文 token 的 k、v 不依赖任何后文,算过一次就不会再变,可以缓存起来,也就是 KV cache。推理服务把这件事又推进一步,多个请求如果共享同一段前缀,比如同一个 system prompt,这段前缀的 KV cache 可以跨请求复用,省掉重复的计算。
代价是多了一条侧信道。命中前缀缓存的请求,首字延迟明显短于未命中的。2025 年有研究对多家商用 API 做了审计,确认在共享缓存的部署下,一个用户可以通过测量延迟,判断另一个用户是否发送过某个特定前缀,逐步探测出他人 system prompt 的内容。修法是按用户隔离缓存,代价是缓存命中率下降。
batching 有类似的问题。多个请求在同一个 batch 里前向,理论上互不干扰,但实现上的数值差异、内存布局的差异,都可能泄露同批次其他请求的信息。这方面的研究才刚开始。
两端的线性层
模型两端各有一个和词表大小相关的矩阵:输入端把 token id 查表成向量的 embedding,输出端把最后的向量映射回词表分布的 LM head。两者都在一定程度上可逆。
从 logits 可以恢复权重。2024 年 Carlini 等人的工作表明,如果 API 返回完整的 logits 或足够多的 logprobs,可以通过线性代数恢复出模型的隐藏维度 d_model,进而恢复出输出层的权重矩阵,成本只有几千美元的 API 调用。原理并不复杂,logits 是 d_model 维向量乘以 d_model × V 的矩阵,秩最多是 d_model,采样足够多的输出做 SVD 就能读出来。
从 embedding 可以恢复文本。这不是 LLM 本身的问题,是 RAG 一类系统的问题。向量数据库里存的 embedding 常被当成已经脱离明文的东西,但 2023 年的 vec2text 证明,用一个反演模型可以从 embedding 恢复出原文,短文本几乎逐字恢复。embedding 应当按明文的安全等级来保护。
汇总
| 架构特性 | 导出的攻击面 | 能否在架构内修复 |
|---|---|---|
| 所有来源的 token 进同一套 attention | prompt injection,直接与间接 | 不能,只能降概率、外部隔离 |
| 角色靠特殊 token 与训练倾向 | 伪造分段、位置效应、截断挤出 system prompt | 部分,靠转义与训练 |
| 离散 token 输入 + 可微分内部 | 编码绕过、glitch token、梯度搜索的对抗后缀 | 不能,可微分即有对抗样本 |
| 自回归、输出是分布 | 浅对齐、预填开头、重复采样、logit bias | 部分,靠加深对齐、收紧 API |
| 知识存在权重里 | 训练数据提取、成员推断、微调去对齐 | 不能,权重即模型 |
| 前缀 KV cache 跨请求复用 | 延迟侧信道探测他人 prompt | 能,按租户隔离,牺牲命中率 |
| 两端的线性层 | 从 logits 偷权重、从 embedding 反演文本 | 能,限制 API 输出、保护 embedding |
表里写着「不能」的几行有一个共同点,它们都来自「模型是一个把 token 序列映射到下一个 token 分布的可微函数」这个定义本身。要在架构层面解决,需要在 token 上引入来源标记或特权位,让 attention 对不同来源的 token 有结构上不同的处理。这方面有零星的研究,但没有主流模型采用,因为它意味着放弃「任何文本都是训练数据」这个让 decoder-only 胜出的最大优势。
在那之前,把 LLM 当成一个能力很强但不可信的组件来设计系统,是比较稳妥的做法:它能读到的所有内容都可能是指令,它的所有输出都需要校验,它的权限应该是完成当前任务所需的最小集合。这和对待一段从网上下载的、没法审计源码的程序是同一个思路。