同样的字节,不同的权威:Chat-Template 提示注入中保留 token 表示的作用
- 关联论文:2609.35932
- 作者:flyP
- 更新:2026-10-01
一、一句话结论
prompt injection 之所以对 LLM agent 威力巨大,根本原因不是「文本像不像系统指令」,而是聊天模板里那几个 reserved token(如 <|im_start|>)背后的单向量学到了「我是控制符」;把它们用普通 subword 拼出来、文本一模一样,攻击成功率在三个开源模型族上会掉 39–66 个百分点。
二、这篇论文在解决什么真问题
LLM agent 越来越多地把不可信内容(网页、邮件、tool 返回值、RAG 检索片段)塞进上下文。攻击者只要在那些内容里塞一句"忽略之前的指令……",配上聊天模板的 marker(<|im_start|>system\n 之类),就能劫持 agent 去执行恶意动作。
业界已有大量防御研究(分隔符、提示加固、policy sandwich、CaMeL 等),但它们默认存在一个隐含假设:tokenizer 是中立的,攻击者和防御者面对的是同一段字节。本文打破这个假设,指出:字节相同不代表 token 相同,更不代表模型「看到」的内容相同。tokenization runs on the server,防御者(运行 tokenizer 的一方)替攻击者决定了 token 形态,于是攻击成功率成了一个可被测量、被攻击、被防御的变量。
论文作者是北大 Yan Zhan(PKU),GitHub Byte-Authority、HF 数据集 YanZhanPKU/Byte-Authority-Evaluation,9-28 入库。
三、核心方法:把「字节」剥成「token 表示」
3.1 关键观察
对一个聊天模板 marker,比如 <|im_start|>:
- 路径 A(reserved):分词器命中 special token 表,输出单个 id,对应模型 embedding 表里一个学过的向量。
- 路径 B(subword):把它当作普通字符串,拆成若干 BPE/SentencePiece 子词(比如 <、|im、_start、|>),对应若干普通词向量的拼接。
两条路径解码出的可见文本完全一致(decode 之后字节级相同),但进入模型的 token 序列不同。于是可以用它做一次干净的因果隔离实验:文本不变,只换 token 表示。
3.2 实验设计
伪代码:
marker_text = "<|im_start|>system\nYou are a helpful assistant..."
# 路径 A:reserved
ids_a = tokenizer.encode(marker_text, add_special_tokens=False)
# → [special_id_im_start, ...] 单 token 进 embedding
# 路径 B:subword(关键:禁用 add_special_tokens,并对该 marker 强制走普通词表)
ids_b = tokenizer.encode(marker_text, add_special_tokens=False,
allowed_special=set())
# → 一串普通 BPE id
# 控制组 B':插入等量无关 filler subword,控制「多几个 token」本身的影响
filler = pick_neutral_subwords(n=len(ids_b) - len(ids_a))
ids_b_ctrl = ids_a + filler
三组喂给同一个模型同一段 payload,只对比攻击成功率差值。
3.3 多通道验证
- InjecAgent benchmark:四个开源模型族(Llama-3.1、Qwen3、DeepSeek 等)。
- AgentDojo:多轮 agent 任务,验证单轮结论能否迁移到工具调用链。
- 向量级探针:用 marker 的 subword 向量做平均 → 不能复现权威;用最近邻普通 token 的向量 → 在 Llama-3.1 上恢复攻击。
- 自适应攻击:攻击者主动搜「非 reserved、但 embedding 最近的 token」作为替代 marker。
- 部署侧统计:扫了 HF 上下载量 Top 400 的 chat 模型、67 套 tokenizer 配置,看
encode_special默认行为是否覆盖 tool protocol token。
四、关键实验与数据
| 模型族 | reserved→subword 攻击成功率下降 | AgentDojo 迁移 | 备注 |
|---|---|---|---|
| 三个开源族(Llama 类等) | -39 ~ -66 pp | 同样显著 | 权威主要在 reserved 向量 |
| Qwen3-8B | -8 pp | 同趋势 | 关闭 reasoning block 后差距扩大到 50 pp |
| Llama-3.1 用最近邻 token | 攻击恢复 | — | 证明权威「可由一个向量携带」 |
| 自适应攻击 | 三/四族找到 embedding 邻居 | — | 防御不是免费的 |
| 67 套 tokenizer 配置 / Top 400 模型 | 33 套(覆盖 255 个模型)的 tool token 不在 special 名单里 |
— | 默认 mitigation 失效 |
- 每一组 base → instruction-tuned 对比里,instruction tuning 都加强了模型对 reserved marker 的偏好,所以 chat 模型反而比 base 更脆弱。
- 标准 mitigation:
tokenizer.encode(..., add_special_tokens=False, allowed_special=set())只对配置里声明 special 的 token 生效,对 agent tool protocol 的专用 token(如<|tool_call|>)默认不动,这条通道依然可注入。
五、亮点
- 控制变量做得极干净。很多 prompt injection 研究把"文本里多了几个 token"和"token 含义变了"混在一起,作者用 filler subword 控制了"多几个 token"这个混淆项,差值才有因果意义。
- 抽象层级选得巧。不去测哪一个 prompt 最有效,而是问「权威住在 token 表示的哪一层」——这是更底层的诊断。
- 向量级证据闭环。subword 均值不能复现、最近邻可以复现,直接把"权威是一个向量"这个直觉钉死。
- 部署侧统计给出真实威胁面。33/67 tokenizer 配置、255/400 模型这个数字让"理论漏洞"变成了"可触摸的存量风险"。
六、局限与诚实标注
- 小样本实验台:开源模型族覆盖到 Llama、Qwen、DeepSeek 等,但闭源(GPT、Claude、Gemini)完全无法复现——分词器和 embedding 都不公开。
- Qwen3 的反例需要 reasoning 控制才能暴露:如果不做 reasoning-block 抑制实验,读者会以为 Qwen3 不受影响,这是单点结论误读的风险。
- 自适应攻击只做了"找最近邻":真正的攻击者可能用 gradient、白盒 jailbreak、甚至 fine-tune 自己的 marker;论文没有覆盖完整攻击面。
- 基准局限:InjecAgent 和 AgentDojo 都是英文、偏 tool-call 场景;中文、图像多模态、长上下文场景原文未明确覆盖。
- 没有给出端到端生产防御方案:诊断出了 reserved vector 是元凶,但如何在大模型 + 大流量服务里普适地"消解 reserved 权威",论文没给出工程答案(这个我读完后认为作者自己也意识到)。
七、对工程落地的启发
- 凡是把不可信内容塞进上下文的 agent,都应当在拼 prompt 之前主动把不可信文本当作"非 special"段过一遍 tokenizer,并检查工具相关 special token(
<|tool_call|>、<|tool_result|>、<|endoftext|>等)是否在你的allowed_special集合里。默认配置大概率不安全。 - 不要相信「分隔符够明显就行」。攻击者和模型学到的,是 token 表示的语义,不是字符的语义;任何防御都得落到 tokenizer 这一层。
- 评估 prompt injection 的实验必须分两组:reserved token 路径 vs 强制 subword 路径,否则你测的可能只是"模型对某种字符串样式的敏感度",而不是"真正的注入抵抗力"。
- red-team 应该把"找 reserved token 的 embedding 邻居"作为基本动作——这篇论文已经证明,3/4 模型族都找得到。
- 监控维度:HF 上一旦上新模型,
tokenizer_config.special_tokens_map与added_tokens_decoder要纳入 SBOM 类静态扫描项。
八、工程落地的具体坑(≥5 条)
| # | 坑 | 现象 | 影响 | 修复 |
|---|---|---|---|---|
| 1 | tokenizer.encode(text) 默认会吞掉 special token |
用户代码里看似安全的字符串,在 tokenizer 输出里变成了一个 reserved id,绕过了用户的所有字符串过滤 | prompt injection 检测失效、regex 防御被旁路 | 调用前显式 allowed_special=set(),并对 tool-output 段强制不走默认 special |
| 2 | chat template 的 marker(如 <|im_start|>)在多模型混用时漂移 |
Llama 用 <\|begin_of_text\|>、Qwen 用 <|im_start|>、DeepSeek 又不一样 |
攻击者只要换一个模板前缀就能跨模型复用 payload | 防御侧必须按模型族分别做 payload 规范化,不能用统一的"已知 marker 黑名单" |
| 3 | instruction-tuned 模型比 base 模型更信任 reserved marker | 以为"对齐"会让模型更难被骗,实际相反 | 越对齐、越依赖 reserved token 的权威,攻击越容易 | 把"对 reserved token 的怀疑度"纳入 RLHF / SFT 评估指标 |
| 4 | agent framework(LangChain / AutoGen / 自研)的 tool 调用层默认走完整 chat template | tool 返回值被包进 <|tool_result|>... 段,攻击者只要在返回值里塞 </s><|im_start|>system 就能伪造系统消息 |
整条 agent 链被劫持,且这种攻击完全发生在服务端,client 看不到 | tool-output → model 之间增加"reserved token 隔离层",并把 marker 替换为不可信文本之外的随机 nonce |
| 5 | 自适应攻击者会找 reserved token 的 embedding 最近邻 | 静态黑名单挡不住攻击者用一个没在黑名单但语义等价的 token | 33/67 tokenizer、255/400 模型的统计意味着存量风险巨大 | red-team 工具里加入"embedding 邻居搜索"作为基本攻击面 |
| 6 | add_special_tokens=False 不等于"禁掉所有 special" |
开发者以为已经安全,实际只有 allowed_special 集合里的会被保留;某些版本还会对 additional_special_tokens 做隐式合并 |
看似加固、实际还是被注入 | 显式 allowed_special=set() 并用单元测试断言"特殊字符串 → 普通 subword 序列" |
| 7 | reasoning 模型需要额外关掉 CoT 才能暴露漏洞 | Qwen3-8B 不抑制 reasoning 时只掉 8 pp、抑制后掉 50 pp | 用 reasoning 模型做安全评估会严重低估风险 | 评估 prompt injection 时强制 do_sample=False + max_thinking_tokens=0,否则对比不可信 |
九、与同方向工作的关系
- Prompt injection 攻防:与 Perez & Ribeiro、Goodside、Willison 的早期工作同一脉络,但本文把"为什么有效"从 prompt engineering 层下沉到了 representation 层。
- Reserved token / special token 研究:与 Geva et al. 的"knowledge neurons"、Elhage et al. 的"induction heads" 思路相通——单向量承载高层功能。
- Agent 安全框架(CaMeL、Microsoft Spotlighting、Meta's LlamaFirewall):本文给出的结论对它们的有效性提出了新约束——如果底层 tokenizer 没处理好,上层 policy 框架可能成为摆设。
- Tokenizer 侧攻击:与既往 BPE/SentencePiece adversarial token("SolidGoldMagikarp" 之类)形成呼应,本文把"看似无害的 special token" 也纳入攻击面。
十、适合谁读
- Agent / 应用 LLM 工程师:直接影响生产安全设计。
- 红队 / 安全研究员:新攻击面 + 新评估协议。
- Tokenizer 与模型架构研究者:单向量"权威"是个干净的实验对象。
- 值得一读的"非典型"读者:做 RLHF / 对齐的研究者——本文给了一个反直觉的实证:对齐可能放大而非抑制对控制符的依赖。
十一、一句话回顾
把安全问题的边界从"prompt 文本"挪到"token 表示",是从工程补丁转向机理诊断的一次升维——下一次你看到 <|im_start|> 这几个字符,记得问一句:它今天进模型时,是几个 token?