
tiktoken으로 센 토큰 수가 청구서와 다른 이유는 크게 두 가지입니다. 모델마다 실제로 사용하는 토크나이저(인코딩)가 다르고, API가 메시지를 처리할 때 시스템 프롬프트·역할 구분자·함수 스키마 같은 눈에 안 보이는 오버헤드가 추가로 붙기 때문입니다. 순수하게 본문 텍스트만 encode()로 세면 이 두 요소가 빠져서 실제 청구 토큰보다 항상 적게 나옵니다.
이 글에서는 어떤 모델이 어떤 인코딩을 쓰는지, 오버헤드가 정확히 몇 토큰인지, 그리고 tiktoken 계산값을 청구서에 최대한 가깝게 맞추는 실제 코드를 다룹니다.
tiktoken으로 센 토큰 수와 청구서 토큰 수, 왜 정확히 안 맞을까요?
가장 흔한 실수는 사용자 메시지 문자열 하나만 encoding.encode(text)에 넣고 그 길이를 “이번 요청의 토큰 수”라고 착각하는 것입니다. 하지만 실제로 모델에 들어가는 입력은 사용자 메시지 하나가 아니라, 시스템 프롬프트·이전 대화 기록·역할(role) 메타데이터·(있다면) 함수/도구 스키마까지 전부 합쳐진 형태입니다.
여기에 더해 채팅 모델은 메시지 하나마다 구분자 역할을 하는 특수 토큰을 추가로 소비합니다. 이 구분자는 텍스트로 보이지 않기 때문에 본문만 셀 경우 실제보다 적게 잡힙니다. 결과적으로 tiktoken 계산값은 항상 청구서보다 같거나 작게 나오고, 반대로 청구서가 더 작게 나오는 경우는 거의 없습니다.
모델마다 토크나이저가 다르다는 게 정확히 무슨 뜻인가요?
tiktoken은 모델별로 서로 다른 BPE(바이트 페어 인코딩) 사전을 씁니다. 같은 한글·영어 문장이라도 어떤 인코딩으로 세느냐에 따라 토큰 수 자체가 달라집니다. 정리하면 다음과 같습니다.

| 인코딩 이름 | 주로 쓰이는 모델 |
|---|---|
| o200k_base | gpt-4o, gpt-4o-mini, o1 계열 |
| cl100k_base | gpt-4, gpt-4-turbo, gpt-3.5-turbo, text-embedding-ada-002 |
| p50k_base | text-davinci-002/003, code-davinci-002 (구형 Codex) |
| r50k_base(gpt2) | GPT-3 구형 모델(davinci 등) |
즉 같은 문장을 cl100k_base로 세면 300토큰인데 o200k_base로 세면 280토큰이 나오는 일이 실제로 생깁니다. 어떤 인코딩을 쓸지 직접 고를 필요 없이, tiktoken.encoding_for_model("gpt-4o")처럼 모델 이름을 넘기면 라이브러리가 알아서 매핑해 줍니다. 이 매핑 테이블은 tiktoken 공식 저장소의 model.py에 정의돼 있고, 새 모델이 나올 때마다 업데이트되므로 오래된 tiktoken 버전을 쓰면 최신 모델 이름을 못 알아보고 KeyError가 날 수 있습니다.
pip install --upgrade tiktoken
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
text = "청구서보다 토큰이 적게 나온 이유를 알아보자"
print(enc.encode(text))
print(len(enc.encode(text)))
이 코드를 최신 tiktoken(0.8.x 이상)에서 실행하면 gpt-4o가 o200k_base 인코딩으로 매핑되어 토큰 리스트와 개수를 출력합니다. 반대로 tiktoken이 0.5.x처럼 오래됐다면 gpt-4o를 인식하지 못해 예외가 발생하므로, try/except로 cl100k_base에 폴백시키는 방어 코드가 필요합니다.
시스템 프롬프트와 메시지 포맷 오버헤드는 몇 토큰이나 될까요?
채팅 API는 [{"role": "system", ...}, {"role": "user", ...}] 형태의 메시지 배열을 내부적으로 ChatML 비슷한 포맷으로 직렬화한 뒤 토큰화합니다. 이 직렬화 과정에서 메시지마다 고정 토큰이 붙습니다. OpenAI 쿡북에 공개된 계산 함수를 보면 그 규칙이 이렇습니다.
import tiktoken
def num_tokens_from_messages(messages, model="gpt-4o-mini"):
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base")
tokens_per_message = 3
tokens_per_name = 1
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(encoding.encode(value))
if key == "name":
num_tokens += tokens_per_name
num_tokens += 3 # 응답 시작을 알리는 토큰
return num_tokens
messages = [
{"role": "system", "content": "당신은 친절한 한국어 어시스턴트입니다."},
{"role": "user", "content": "토큰 계산법 알려줘"},
]
print(num_tokens_from_messages(messages))
메시지당 3토큰, 그리고 응답을 시작하기 위한 3토큰이 본문 텍스트와 별도로 붙는 구조입니다. 시스템 프롬프트가 짧은 한두 문장이라도 이 고정 오버헤드 때문에 순수 본문 토큰 합계보다 6~10토큰 정도는 항상 더 나온다고 보면 됩니다. 대화가 길어져 메시지 수가 늘어날수록 이 오버헤드도 메시지 개수에 비례해 누적됩니다.

tiktoken 계산값을 청구서에 최대한 가깝게 맞추려면 어떻게 해야 하나요?
우선 사용자 메시지 하나만이 아니라 실제로 API에 보내는 messages 배열 전체(시스템 프롬프트 포함)를 위 num_tokens_from_messages 함수에 넣어야 합니다. tools나 function calling을 쓰는 경우라면 함수 스키마(JSON)도 프롬프트에 직렬화되어 토큰으로 계산되므로, 스키마 문자열까지 합산 대상에 넣어야 오차가 줄어듭니다.
두 번째로, 모델 스냅샷 이름을 정확히 맞춰야 합니다. gpt-4o처럼 별칭(alias)으로 부르면 실제로는 gpt-4o-2024-08-06 같은 특정 스냅샷이 호출되는데, tiktoken의 인코딩 자체는 같아도 모델 매핑 로직이 tiktoken 버전에 따라 달라질 수 있어 항상 최신 버전을 유지하는 편이 안전합니다. 세 번째로, 실제 응답을 받은 뒤에는 API가 돌려주는 usage 필드(prompt_tokens, completion_tokens, total_tokens)와 사전 계산값을 비교해서 오차를 로그로 남겨두면, 나중에 예산을 설계할 때 보정 계수로 쓸 수 있습니다.
그래도 usage 필드랑 다르게 나올 때는 뭘 봐야 하나요?
위 방법을 다 적용해도 딱 맞아떨어지지 않는 경우가 있습니다. 대표적으로 o1이나 최신 추론 계열 모델은 답변을 만들기 전에 내부적으로 “추론 토큰(reasoning tokens)”을 생성하는데, 이 토큰은 사용자에게 보이는 텍스트에 포함되지 않으면서도 completion_tokens에 합산되어 과금됩니다. tiktoken은 실제로 눈에 보이는 문자열만 세는 도구이기 때문에, 애초에 보이지 않는 이 토큰을 미리 예측할 방법이 없습니다.
이 부분이 이 방식의 명확한 한계입니다. 사전 계산은 “입력 프롬프트 쪽 예상치”를 정교하게 만드는 데는 유효하지만, 출력 쪽 추론 토큰이나 모델이 내부적으로 재시도·검열 등을 처리하며 소비하는 토큰까지는 tiktoken만으로 재현할 수 없습니다. 예산을 엄격하게 관리해야 한다면 사전 계산값을 참고용 상한선 정도로만 쓰고, 최종 근거는 항상 응답의 usage 필드로 삼는 편이 안전합니다.
정확한 사전 예측이 꼭 필요한 상황이 아니라면, 매 요청마다 무거운 토큰 계산 로직을 돌리는 것 자체가 비효율일 수도 있습니다. 짧은 채팅 앱처럼 오차 몇십 토큰이 큰 의미가 없는 경우에는 대략적인 문자 수 기반 추정만으로도 충분한 경우가 많습니다.
이렇게 모델별 인코딩 차이와 메시지 오버헤드를 감안해서 tiktoken 계산을 다시 짜면, 순수 본문만 셌을 때보다 청구서와의 오차를 크게 줄일 수 있습니다. 다음 단계로는 본인이 쓰는 모델 이름을 tiktoken.encoding_for_model()에 넣어 실제 인코딩이 무엇으로 매핑되는지 먼저 확인해 보고, num_tokens_from_messages 함수를 프로젝트에 맞게 이식해서 요청 직전에 예상 토큰 수를 로그로 남겨보는 것을 권합니다.
