Karpathy新實(shí)驗(yàn)火了!一個(gè)「表情」占53個(gè)token,DeepSeek-R1苦思10分解謎失敗
一個(gè)??,竟然要占用53個(gè)token?!
最近,AI大佬Karpathy在X上分享了這一有趣現(xiàn)象。
(UTF-8應(yīng)為Unicode)
為什么簡(jiǎn)單的一個(gè)笑臉表情包,卻占據(jù)53個(gè)token之多呢?
Karpathy揭示道:這背后,就隱藏著Unicode和隱藏字符的秘密!
在數(shù)字世界中,文字可不像看上去那么簡(jiǎn)單。你可能以為e和е看起來(lái)都一樣,但它們很可能來(lái)自不同的字符集。
比如,拉丁字母的「e」(U+0065)和西里爾字母的「е」(U+0435)在外觀上幾乎一模一樣,但它們的Unicode編碼是不同的。這類易混淆字符,就被稱為Confusables。
這樣,惡意攻擊者就可以利用它們偽造網(wǎng)址,把我們引導(dǎo)到釣魚(yú)網(wǎng)站。
更神奇的是,還有一種更隱蔽的方法可以在文字中藏入數(shù)據(jù),那就是變體選擇符(Variation Selectors)。
比如Karpathy舉的這個(gè)例子:正常來(lái)說(shuō),一個(gè)普通的??只會(huì)占用1-2個(gè)token。而如今的53個(gè)token,就意味著它正在偷偷傳遞信息,并且不會(huì)被肉眼察覺(jué)。
這樣,它就可以用于加密通信或隱藏信息,或者被惡意濫用,隱藏惡意代碼。
我們也嘗試?yán)霉ぞ邔⒁痪湓挷卦诹??????????????????????????????????????????????????????????????????????????這個(gè)表情里??梢钥吹剑膖oken數(shù)量一下子就飆升到了146個(gè)。
Karpathy表示,自己能用這種方法使用不可見(jiàn)字節(jié),進(jìn)行基本的提示注入(prompt injection),但如果沒(méi)有明確的解碼提示,這種方法就沒(méi)用。
而具有思維能力的模型,似乎就更容易受此方法的影響了,因?yàn)樗鼈兲焐矚g解謎,而且會(huì)因?yàn)樽⒁獾教砑拥淖止?jié)而表現(xiàn)出濃厚的興趣和好奇心。
比如Karpathy發(fā)現(xiàn),DeepSeek-R1花了整整10分鐘尋找其中的模式。
根據(jù)它的推斷,隱藏信息在說(shuō),「Onli!n37e27i4h4he3ingle7odlol」。雖然不是正確答案「lol」,但也算比較接近了。
不過(guò)最后,它認(rèn)為這是無(wú)意義的,從而放棄了嘗試。
但從理論上講,LLM確實(shí)很有可能發(fā)現(xiàn)隱藏在變體選擇符中的信息,并執(zhí)行相應(yīng)指令。
另外,這種編碼/解碼方法可能過(guò)于專門化,需要在提示中提供解釋和線索。
Karpathy指出,如果這篇文章被收錄到預(yù)訓(xùn)練數(shù)據(jù)中,這些知識(shí)可能會(huì)被整合到模型參數(shù)中,這樣模型就可能在沒(méi)有特定提示的情況下,自動(dòng)識(shí)別和解碼這種特殊編碼了。
有網(wǎng)友表示,自己也有同樣經(jīng)歷。對(duì)于這個(gè)笑臉表情包,Gemini無(wú)法解碼,但Claude和GPT都能解碼消息,而且還能執(zhí)行其中的操作。
不過(guò),他本人并未成功注入任何東西。
另有網(wǎng)友制作了一個(gè)「token炸彈」??,大幅超出GPT-4o上下文長(zhǎng)度,讓模型直接崩潰無(wú)法處理內(nèi)容。
表情符號(hào),暗藏任意數(shù)據(jù)?
Karpathy之所以得出如上觀點(diǎn),想法來(lái)自軟件工程師Paul Butler的一篇博客。
Butler表示,幾天前GuB-42在HK上的評(píng)論引發(fā)了自己的興趣:
理論上,通過(guò)使用 ZWJ(零寬連接符)序列,你可以在單個(gè)表情符號(hào)中編碼無(wú)限量的數(shù)據(jù)。
那么,真的是可以用一個(gè)表情符號(hào)來(lái)編碼任意數(shù)據(jù)嗎?
如下栗子中,作者不使用ZWJ,先把一句話「this is my hidden message」編碼成一個(gè)字母「t」。
你可以在任何Unicode 字符中編碼數(shù)據(jù),這個(gè)句子包含一個(gè)隱藏信息????????????????????????????????????????????????????????????????????????????????????????????????
要知道,若是在單個(gè)字符(表情包)嵌入極端數(shù)量的token,會(huì)導(dǎo)致LLM處理信息時(shí),計(jì)算量急劇上升。
而且,對(duì)于那些以token API計(jì)費(fèi)的開(kāi)發(fā)者而言,簡(jiǎn)直就是一場(chǎng)災(zāi)難。
到底如何把「隱藏信息」藏在一個(gè)表情包后面呢?
那就不得不提到一個(gè)關(guān)鍵技術(shù)了——變體選擇器。
變體選擇器
Unicode規(guī)定了256個(gè)碼位作為「變體選擇器」(variation selector),分別命名為VS-1到VS-256。這些變體選擇符本身不顯示任何內(nèi)容,但它們會(huì)改變緊跟在它們之前的那個(gè)字符的顯示樣式。
大多數(shù)Unicode字符都沒(méi)有與之相關(guān)的變體(不同的顯示樣式)。因?yàn)閁nicode是一個(gè)不斷發(fā)展的標(biāo)準(zhǔn),并且力求未來(lái)兼容,所以即使處理Unicode的代碼不知道變體選擇符的含義,在進(jìn)行轉(zhuǎn)換時(shí)也應(yīng)該保留它們。
舉個(gè)例子,字符 「g」(U+0067) 后面跟著一個(gè)VS-2 (U+FE01),顯示出來(lái)還是小寫(xiě)的「g」,和單獨(dú)的「g」一模一樣。但是,如果你復(fù)制粘貼它,變體選擇符會(huì)跟著一起被復(fù)制。
因?yàn)?56剛好足以表示一個(gè)字節(jié)(byte)的所有可能值,所以這給我們提供了一種方法,可以將一個(gè)字節(jié)的數(shù)據(jù)「隱藏」在任何一個(gè) Unicode 碼位后面。而且,Unicode規(guī)范并沒(méi)有明確說(shuō)明多個(gè)變體選擇符序列的情況,只是暗示在渲染(顯示)時(shí)應(yīng)該忽略它們。
你猜到要怎么干了吧?
我們可以將一系列變體選擇符連接起來(lái),以表示一個(gè)任意的字節(jié)串(byte string)。
舉個(gè)例子,假設(shè)我們要藏的數(shù)據(jù)是[0x68, 0x65, 0x6c, 0x6c, 0x6f] (就是「hello」這幾個(gè)字母)。我們可以把每個(gè)字節(jié)變成一個(gè)對(duì)應(yīng)的變體選擇符,然后把它們串起來(lái)。
變體選擇符的編號(hào)分兩塊:前面16個(gè)是U+FE00到U+FE0F,后面240個(gè)是U+E0100到U+E01EF。要將一個(gè)字節(jié)轉(zhuǎn)換為變體選擇符,我們可以使用類似這樣的Rust代碼:
fn byte_to_variation_selector(byte: u8) -> char {
if byte < 16 {
char::from_u32(0xFE00 + byte as u32).unwrap()
} else {
char::from_u32(0xE0100 + (byte - 16) as u32).unwrap()
}
}
如果要編碼一系列字節(jié),我們可以在基礎(chǔ)字符后面連接多個(gè)這樣的變體選擇器。
比如要編碼字節(jié)序列 [0x68, 0x65, 0x6c, 0x6c, 0x6f],我們可以執(zhí)行以下操作:
fn main() {
println!("{}", encode('??', &[0x68, 0x65, 0x6c, 0x6c, 0x6f]));
}
執(zhí)行后將輸出:
????????????
表面上看起來(lái)只是一個(gè)普通的表情符號(hào),但當(dāng)你將它粘貼到解碼器中時(shí),就能看到其中隱藏的信息。
如果我們使用調(diào)試模式(debug formatter)來(lái)查看,就能觀察到實(shí)際的編碼結(jié)構(gòu):
fn main() {
println!("{:?}", encode('??', &[0x68, 0x65, 0x6c, 0x6c, 0x6f]));
}
輸出結(jié)果為:
"??\u{e0158}\u{e0155}\u{e015c}\u{e015c}\u{e015f}"
這清楚地展示了在原始輸出中被「隱藏」的字符序列:[0x68, 0x65, 0x6c, 0x6c, 0x6f]。
解碼
解碼的過(guò)程也同樣簡(jiǎn)單直觀。代碼如下所示。
fn variation_selector_to_byte(variation_selector: char) -> Option<u8> {
let variation_selector = variation_selector as u32;
if (0xFE00..=0xFE0F).contains(&variation_selector) {
Some((variation_selector - 0xFE00) as u8)
} else if (0xE0100..=0xE01EF).contains(&variation_selector) {
Some((variation_selector - 0xE0100 + 16) as u8)
} else {
None
}
}
fn decode(variation_selectors: &str) -> Vec<u8> {
let mut result = Vec::new();
for variation_selector in variation_selectors.chars() {
if let Some(byte) = variation_selector_to_byte(variation_selector) {
result.push(byte);
} else if !result.is_empty() {
return result;
}
// note: we ignore non-variation selectors until we have
// encountered the first one, as a way of skipping the "base
// character".
}
result
}
具體使用方法:
use std::str::from_utf8;
fn main() {let result = encode('??', &[0x68, 0x65, 0x6c, 0x6c, 0x6f]);
println!("{:?}", from_utf8(&decode(&result)).unwrap()); // "hello"
}
請(qǐng)注意,基礎(chǔ)字符不一定要是表情符號(hào)——變體選擇符的處理與普通字符相同。只是用表情符號(hào)會(huì)更有趣。
這種技術(shù)會(huì)被濫用嗎?
需要特別指出的是,這種行為本質(zhì)上屬于對(duì)Unicode規(guī)范的不當(dāng)使用,如果你開(kāi)始考慮將此類技術(shù)用于實(shí)際場(chǎng)景,請(qǐng)立即停止這種想法。
不過(guò)從技術(shù)角度來(lái)說(shuō),確實(shí)有幾種潛在的惡意使用方式。
1. 繞過(guò)人工內(nèi)容審核:由于通過(guò)這種方式編碼的數(shù)據(jù)在顯示時(shí)是不可見(jiàn)的,人工審核員或?qū)彶檎邿o(wú)法發(fā)現(xiàn)它們的存在。
2. 為文本添加數(shù)字水?。耗壳耙延幸恍┘夹g(shù)可以通過(guò)在文本中添加細(xì)微變化來(lái)實(shí)現(xiàn)「數(shù)字水印」標(biāo)記,這樣當(dāng)文本被發(fā)送給多個(gè)接收者后發(fā)生泄露時(shí),就能追蹤到最初的接收者。變體選擇器序列是一種特殊方法,它不僅能在大多數(shù)復(fù)制/粘貼操作中保持不變,還支持任意密度的數(shù)據(jù)嵌入。
理論上,你甚至可以對(duì)文本中的每個(gè)字符都添加水印標(biāo)記。