はじめに
最近(2025年2月12日)、AI研究者として名高いAndrej Karpathy氏がX(旧Twitter)上で行った実験が話題となりました。
わずか1つの絵文字(見た目は普通の「😀」など)が、なんと53トークンにもなり得るというのです。
大規模言語モデル(LLM)を扱う上で「トークン数」は非常に重要な要素ですが、いったい何が原因でこんなことが起こるのでしょうか?
本記事では、そのカラクリとして【Unicode変体選択子(Variation Selectors)】を利用した「不可視文字の仕組み」を解説します。さらに、セキュリティリスクや悪用例、そして簡単な対策方法をまとめてご紹介します。
目次
- KarpathyがXで発見した「53トークン現象」とは
- Unicode変体選択子とは:不可視文字の仕組み
- LLMを無力化し得るトークン爆発のメカニズム
- 簡単な実装例:1つの絵文字にデータを埋め込む
- 悪用シナリオとセキュリティ対策
- まとめ
- 参考資料
1. KarpathyがXで発見した「53トークン現象」とは
AI研究の第一線で活躍するAndrej Karpathy氏が、X上で「絵文字(emoji)をLLMに入力したところ、1文字で53トークンを消費した」と報告しました。
通常の絵文字なら多くても2〜3トークン程度が一般的ですが、大量の“見えない文字”が含まれることでトークン数が爆発的に増加する可能性があるのです。
- 背景にある仕組み:Unicodeの不可視文字
- LLMがトークンを分割する際、それらを“別の文字”として数えてしまう
さらに、この技術はただの興味深い現象にとどまらず、悪意ある攻撃にも利用可能です。例えば、
- LLMを意図的にクラッシュさせる「トークン爆弾」
- Prompt Injection攻撃による不正指令の埋め込み
- 情報を隠すためのステルスデータ埋め込み
こうした背景から、このUnicodeの仕組みは今後のLLMセキュリティ上の重要な課題となる可能性があります。
2. Unicode変体選択子とは:不可視文字の仕組み
2-1. 変体選択子(Variation Selectors)の概要
Unicodeでは、文字のスタイルやバリエーションを指定するためのコードポイントが用意されています。それらが「変体選択子(VS)」です。
しかし、現状多くの文字に対応するバリエーションが定義されていないため、変体選択子を付与しても表示上は変わらない場合がほとんどです。
- U+FE00 ~ U+FE0F (16種類)
- U+E0100 ~ U+E01EF (240種類)
合計256種類あるため、理論上は1バイト(0〜255)をそのまま表現できます。
2-2. なぜ不可視なのに消されない?
Unicodeのポリシーとして「将来的に使われるかもしれない変体選択子は消去しない」方針があるため、エディタやアプリケーションも変体選択子をコピー&ペーストで保持しがちです。
これによって、一見何もない文字列に大量の不可視文字が含まれる状態が生じ、攻撃者にとっては格好の悪用ポイントとなります。
3. LLMを無力化し得るトークン爆発のメカニズム
LLMは入力された文字列をトークンという単位に分割して解析します。そのため、以下のような問題が起こり得ます。
-
トークン数が激増
変体選択子を何十、何百と仕込まれた絵文字1つが、LLM内部では多数の文字列としてカウントされる。 -
メモリ負荷・API利用料の上昇
- トークン数が増えるほど計算コストやAPI料金が跳ね上がる
- 上限トークン数を超えると処理不能や応答カットが発生
-
Prompt Injectionなどの悪用余地
- 見た目ではわからない場所に命令コードなどを混入させる
- 人的レビュー(目視チェック)もスルーしやすい
また、Gemini 2 Flash や Claude などのLLMは、適切なデコーダーと組み合わせることで、このような隠された情報を見破ることが可能であると報告されています。
4. 簡単な実装例:1つの絵文字にデータを埋め込む
以下の例はあくまで概念実証であり、悪用は厳禁です。
fn byte_to_variation_selector(byte: u8) -> char {
if byte < 16 {
// U+FE00 ~ U+FE0F
char::from_u32(0xFE00 + byte as u32).unwrap()
} else {
// U+E0100 ~ U+E01EF
char::from_u32(0xE0100 + (byte - 16) as u32).unwrap()
}
}
fn encode(base: char, bytes: &[u8]) -> String {
let mut result = String::new();
// ベース文字(例: 😀)
result.push(base);
for &b in bytes {
result.push(byte_to_variation_selector(b));
}
result
}
fn decode(input: &str) -> Vec<u8> {
let mut result = Vec::new();
let mut started = false;
for ch in input.chars() {
let code = ch as u32;
if (0xFE00..=0xFE0F).contains(&code) {
result.push((code - 0xFE00) as u8);
started = true;
} else if (0xE0100..=0xE01EF).contains(&code) {
result.push((code - 0xE0100 + 16) as u8);
started = true;
} else if started {
// 変体選択子の列が終了したタイミングで抜ける
break;
}
}
result
}
5. 悪用シナリオとセキュリティ対策
5-1. トークン爆弾攻撃
意図的に大量の変体選択子を仕込むことで、LLMが受け取ったトークン数を急増させ、リソース枯渇やサービスダウンを狙う攻撃です。
5-2. Prompt Injection
不可視文字列を使って指示を埋め込み、LLMに悪意あるコマンドを実行させる可能性があります。表面上は普通のテキストに見えるため、人間のレビューを突破しやすいという特徴があります。
5-3. 情報隠蔽・機密漏洩
SNSやチャットなどに不可視文字を混入させ、機密情報を秘匿したまま投稿する手法が考えられます。暗号化に比べ気付きにくい点が危険です。
セキュリティ対策のポイント
- 不可視文字のフィルタリング:特定のコードポイントを検出・警告する仕組みを導入
- トークン数の監視:異常にトークン数が多い入力を遮断
- モデルやAPIの更新:変体選択子を検出・無視する機能を持つ仕組みの採用
6. まとめ
たった1つの絵文字が53トークンにもなり得る背景には、Unicode変体選択子を活用した「不可視文字」テクニックが隠れています。無邪気に見える仕組みでも、悪用されるとLLMへの攻撃手段や機密情報の隠蔽に使われる可能性があるため、注意が必要です。
今後はUnicodeやLLMの仕様理解を深め、フィルタリングやアクセス制限などのセキュリティ対策を適切に行うことで、これらのリスクを最小限に抑えることが求められます。
7. 参考資料
- Smuggling arbitrary data through an emoji - Paul Butler
- Andrej Karpathy (Xアカウント)
- ChatGPT Prompt Injection 実験ログ
※本記事は一部ChatGPTのサポートを受けて作成し、加筆・修正を行いました。内容に誤りや至らない点がございましたら、コメントやDMでご指摘いただけますと幸いです。