tiktoken: OpenAI의 빠른 BPE 토크나이저

tiktoken이 Rust로 구현한 고속 BPE 엔진으로 cl100k_base, o200k_base 인코딩을 지원하며 실제 LLM 앱에서 토큰 수 계산, 컨텍스트 관리, 배치 인코딩에 어떻게 활용되는지 완전히 설명한다.

· 6 min read · PALDYN Team

지난 글에서 SentencePiece가 언어 독립적으로 원시 텍스트를 처리하는 방법을 살펴봤다. 이번에는 OpenAI가 개발하고 오픈소스로 공개한 tiktoken을 다룬다. tiktoken은 GPT-4, GPT-4o, ChatGPT 등 OpenAI 모델이 내부적으로 사용하는 BPE 토크나이저의 Python 바인딩이다. 핵심 연산이 Rust로 구현되어 순수 Python BPE 대비 5~10배 빠른 속도를 자랑하며, LLM 앱을 개발할 때 토큰 수 추정, 컨텍스트 관리, 비용 최적화에 필수적으로 쓰인다.

왜 tiktoken인가

LLM API를 사용하는 모든 앱은 두 가지 문제를 반드시 해결해야 한다.

토큰 수 계산: 대부분의 API는 입력+출력 토큰 수에 비례해 과금한다. 요청을 보내기 전에 토큰 수를 예측하지 않으면 비용 추정이 불가능하다.

컨텍스트 윈도우 관리: 모델마다 최대 컨텍스트(4K~200K 토큰)가 있다. 긴 문서를 처리할 때 이 한계를 넘지 않도록 텍스트를 자르거나 요약해야 한다.

tiktoken은 이 두 가지를 정확하고 빠르게 수행한다. HuggingFace tokenizers도 빠르지만, OpenAI API와 완전히 동일한 인코딩을 재현하려면 tiktoken이 필수다.

설치와 기본 사용

pip install tiktoken
import tiktoken

# 방법 1: 모델 이름으로 인코딩 자동 선택
enc = tiktoken.encoding_for_model("gpt-4o")

# 방법 2: 인코딩 이름 직접 지정
enc = tiktoken.get_encoding("o200k_base")

text = "tiktoken은 매우 빠릅니다."
ids = enc.encode(text)
print(ids)         # [83, 1609, 2963, 374, ...]
print(len(ids))    # 토큰 수

decoded = enc.decode(ids)
print(decoded)     # "tiktoken은 매우 빠릅니다."

인코딩 종류와 모델 매핑

tiktoken 인코딩별 특성 비교

핵심은 같은 텍스트라도 인코딩이 다르면 토큰 ID가 다르다는 점이다. cl100k_base의 ID 1234와 o200k_base의 ID 1234는 전혀 다른 토큰을 가리킨다. API를 바꿀 때 반드시 인코딩을 함께 교체해야 한다.

실전 활용 패턴

tiktoken 실전 활용 코드

가장 많이 쓰이는 세 가지 패턴이다:

패턴 1: 토큰 수 계산

import tiktoken

def num_tokens_from_messages(messages, model="gpt-4o"):
    enc = tiktoken.encoding_for_model(model)
    # 채팅 API 오버헤드: 메시지당 3 + 응답 프라이머 3
    total = 3
    for msg in messages:
        total += 3  # role + content + separator
        for key, value in msg.items():
            total += len(enc.encode(value))
            if key == "name":
                total += 1  # name이 있으면 role 생략 → -1 + 1
    return total

패턴 2: 안전한 텍스트 자르기

단순히 text[:n]으로 자르면 멀티바이트 유니코드 문자(한국어 등)가 깨진다. tiktoken으로 토큰 단위로 자르면 안전하다.

def safe_truncate(text: str, max_tokens: int, model="gpt-4o") -> str:
    enc = tiktoken.encoding_for_model(model)
    ids = enc.encode(text)
    if len(ids) <= max_tokens:
        return text
    return enc.decode(ids[:max_tokens])

패턴 3: 특수 토큰 처리

채팅 완성 API의 내부 포맷(<|im_start|>, <|im_end|>)을 파싱해야 할 때:

enc = tiktoken.get_encoding("o200k_base")
# 특수 토큰을 허용해서 인코딩
ids = enc.encode(
    "<|im_start|>user\nHello<|im_end|>",
    allowed_special={"<|im_start|>", "<|im_end|>"}
)

기본적으로 특수 토큰은 허용 목록에 명시하지 않으면 오류가 발생한다. 이는 프롬프트 인젝션을 방지하기 위한 안전장치다.

cl100k_base vs o200k_base

GPT-4와 GPT-4o의 인코딩 차이는 단순히 어휘 크기뿐이 아니다. 정규식 패턴도 다르다. cl100k_base는 소문자 단어와 대문자 단어를 별도 토큰으로 처리하는 반면, o200k_base는 수치 데이터, 코드, 다국어 문자에 최적화된 패턴을 사용한다.

cl100k = tiktoken.get_encoding("cl100k_base")
o200k = tiktoken.get_encoding("o200k_base")

text = "서울特별시 Tokyo 2024年"
print(f"cl100k: {len(cl100k.encode(text))}토큰")  # 예: 14토큰
print(f"o200k:  {len(o200k.encode(text))}토큰")   # 예: 9토큰 (더 효율적)

o200k는 한자, 한글, 일본어를 더 큰 단위로 묶어 아시아 언어 효율이 약 30~50% 향상됐다.

속도 비교

import time, tiktoken
from transformers import AutoTokenizer

enc = tiktoken.get_encoding("cl100k_base")
hf_tok = AutoTokenizer.from_pretrained("gpt2")
text = "Hello world " * 10000  # ~20K 단어

t0 = time.time(); enc.encode_batch([text]*100); print(f"tiktoken: {time.time()-t0:.2f}s")
t0 = time.time(); hf_tok([text]*100); print(f"HF fast:  {time.time()-t0:.2f}s")
# tiktoken이 약 2~5배 빠름 (Rust 멀티스레드)

LLM 앱에서 수백만 토큰을 처리해야 하는 전처리 파이프라인이라면 tiktoken의 속도 이점이 유의미하다. 다음 글부터는 토크나이저를 통해 처리된 토큰들이 모델에서 어떻게 고차원 벡터로 표현되는지, 임베딩의 세계로 들어간다.


지난 글: SentencePiece: 언어에 구애받지 않는 토크나이저

다음 글: 임베딩 기초: 단어를 벡터 공간에 배치하다


읽어주셔서 감사합니다. 😊