AI에 긴 문서를 넣기 전에 “이게 몇 토큰이지?”를 글자 수로 어림하는 경우가 많습니다. 흔히 듣는 기준이 “4글자에 1토큰”인데, 한글 글에서는 크게 빗나갑니다. 이 블로그에 실제로 발행한 한글 원고로 토큰 수 계산을 tiktoken으로 해 보니, 4글자=1토큰으로 어림한 값은 실제의 절반 정도(-42~-53%)였습니다. 영어 문서에서는 +16%로 꽤 맞았는데도요. 이 글에서는 tiktoken 설치와 처음 만난 SSL 오류, 글 종류별 실측표, 한글에 맞춘 어림식, 그리고 토큰 예산에 맞춰 자를 때 한글이 깨지는 문제까지 직접 돌린 결과로 정리합니다. 환경은 윈도우 11, Python 3.12.10, tiktoken 0.14.0입니다.
먼저 범위를 밝혀 둡니다. tiktoken은 OpenAI가 공개한 토크나이저라, 여기서 센 숫자는 OpenAI 모델 기준입니다. Claude처럼 다른 회사 모델은 토크나이저가 달라 숫자가 다르고, 정확한 값은 그 회사 API의 토큰 세기 기능으로 확인해야 합니다. 이 글에서는 그쪽은 재지 않았습니다. 다만 “한글은 글자 수 어림이 잘 안 맞는다”, “어림식은 실제 글로 맞춰 봐야 한다”는 방법 자체는 어느 모델에나 똑같이 쓸 수 있습니다.
tiktoken 설치하고 첫 줄 실행하기
설치는 pip install tiktoken 한 줄인데, 제 노트북에서는 여기서 바로 pip: Permission denied가 났습니다. 파이썬 설치 폴더의 pip.exe를 직접 실행하는 게 막힌 경우라, 파이썬을 통해 부르는 방식으로 바꾸니 바로 설치됐습니다.
python -m pip install tiktoken
사용법은 인코딩을 고르고 encode()로 토큰 목록을 받아 길이를 세는 게 전부입니다. o200k_base는 GPT-4o 계열, cl100k_base는 GPT-4·GPT-3.5 계열이 쓰는 인코딩입니다. 모델 이름을 알면 tiktoken.encoding_for_model()로 고를 수도 있습니다.
import tiktoken
enc = tiktoken.get_encoding("o200k_base")
ids = enc.encode("안녕하세요")
print(len(ids), ids) # 2 [14307, 171731]
처음 만난 오류: CERTIFICATE_VERIFY_FAILED
그런데 첫 get_encoding()에서 오류가 났습니다. tiktoken은 인코딩 표 파일을 처음 한 번 인터넷에서 내려받는데, 그 다운로드가 인증서 검증에서 막혔습니다.
requests.exceptions.SSLError: HTTPSConnectionPool(host='openaipublic.blob.core.windows.net', port=443):
Max retries exceeded with url: /encodings/cl100k_base.tiktoken
(Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate (_ssl.c:1010)')))
원인을 보려고 파이썬 기본 ssl 모듈로 같은 주소에 접속해 보니 이쪽은 성공했고, 받은 인증서의 발급자가 Norton Web/Mail Shield Root였습니다. 백신이 HTTPS 통신을 검사하려고 인증서를 자기 것으로 다시 서명하고 있었던 겁니다. 윈도우 인증서 저장소에는 이 백신 인증서가 등록돼 있지만, tiktoken이 쓰는 requests는 자체 인증서 묶음(certifi)만 믿기 때문에 거부한 것입니다.
verify=False처럼 검증을 끄는 대신, requests도 윈도우 인증서 저장소를 쓰게 하는 truststore로 해결했습니다. python -m pip install truststore 후 tiktoken보다 먼저 두 줄을 넣으면 됩니다.
import truststore
truststore.inject_into_ssl() # tiktoken보다 먼저
import tiktoken
다운로드가 한 번 끝나면 파일이 캐시(%TEMP%\data-gym-cache)에 남아서 다음부터는 인터넷 없이 됩니다. 실제로 인코딩 로드 시간이 크게 달랐습니다.
# 첫 실행 (인코딩 파일 다운로드)
cl100k_base: 로드 3.876초, '안녕하세요' -> 5토큰 [31495, 230, 75265, 243, 92245]
o200k_base: 로드 5.121초, '안녕하세요' -> 2토큰 [14307, 171731]
# 두 번째 실행 (캐시 사용)
cl100k_base: 로드 0.119초, '안녕하세요' -> 5토큰 [31495, 230, 75265, 243, 92245]
o200k_base: 로드 0.253초, '안녕하세요' -> 2토큰 [14307, 171731]
여기서 벌써 차이가 보입니다. 같은 “안녕하세요”가 cl100k_base에서는 5토큰, o200k_base에서는 2토큰입니다. 옛 인코딩은 한글 한 글자를 여러 조각으로 나누는 경우가 많습니다.
글 종류별 실측: 4글자=1토큰은 영어에서만 맞았다
시험 글은 지어낸 문장 대신 실제 파일을 썼습니다. 이 블로그에 발행한 cProfile 글 본문, 그중 한글 비율이 높은 설명 문장만 골라낸 것, 파이썬 설치 폴더의 영어 LICENSE.txt, 표준 라이브러리 csv.py, 그리고 이 발행 자동화가 남긴 실행 로그입니다. 오차는 o200k_base 실제값 기준입니다.
$ python count_tokens.py
샘플 글자 한글% cl100k o200k 글자/4 오차 글자/토큰
한글 설명문 4,951 50% 3,712 2,653 1,238 -53% 1.87
한글 글 전체(코드 포함) 10,941 23% 5,803 4,711 2,735 -42% 2.32
영어 문서(LICENSE) 36,235 0% 7,810 7,824 9,059 +16% 4.63
파이썬 코드(csv.py) 16,386 0% 3,743 3,760 4,096 +9% 4.36
실행 로그(한·영 혼합) 1,745 32% 1,187 931 436 -53% 1.87
속도 cl100k_base: 533,360자 -> 259,460토큰, 0.077초
속도 o200k_base: 533,360자 -> 213,100토큰, 0.070초
| 글 종류 | 글자/토큰(o200k) | “4글자=1토큰” 오차 | cl100k / o200k |
|---|---|---|---|
| 한글 설명문 | 1.87 | -53% | 3,712 / 2,653 |
| 한글 글 전체(코드 섞임) | 2.32 | -42% | 5,803 / 4,711 |
| 영어 문서 | 4.63 | +16% | 7,810 / 7,824 |
| 파이썬 코드 | 4.36 | +9% | 3,743 / 3,760 |
| 실행 로그(한·영 혼합) | 1.87 | -53% | 1,187 / 931 |
정리하면 세 가지입니다. 첫째, 영어와 코드는 4글자=1토큰이 그럭저럭 맞습니다. 둘째, 한글 설명문은 약 1.9글자에 1토큰이라, 4글자 기준으로 어림하면 실제의 절반으로 잡혀 예산을 넘기기 쉽습니다. 반대로 “한글은 1글자=1토큰”으로 잡으면 87% 크게 나옵니다. 셋째, 같은 한글 글이 cl100k_base에서는 o200k_base보다 약 1.4배 많고, 영어·코드는 두 인코딩 차이가 0.5% 이내입니다. 어느 모델에 보내는지에 따라 한글 비용이 달라진다는 뜻입니다.
속도는 걱정할 필요가 없었습니다. 한글 원고 두 편을 20번 이어 붙인 53만 자를 0.07초에 셌습니다.
한글에 맞춘 어림식 만들어 확인하기
매번 tiktoken을 돌리기 어려운 곳(예: 엑셀 수식, 다른 언어로 된 도구)에서 쓸 어림식이 있으면 편합니다. 한글 글자와 나머지 글자(영문·숫자·공백·기호)를 따로 세어 최소제곱으로 계수를 맞췄습니다. 식을 만들 때는 cProfile 원고, LICENSE, csv.py 세 개만 쓰고, 식을 만들 때 쓰지 않은 글로 맞는지 확인했습니다.
$ python estimate.py
o200k_base 어림식: 토큰 ≈ 한글 × 1.13 + 나머지 글자 × 0.218 (= 나머지 4.6글자당 1토큰)
확인용 글 글자 실제 어림식 오차 글자/4 오차
다른 한글 글(10/21 원고) 15,727 5,944 5,747 -3.3% 3,932 -34%
실행 로그 10/22 1,745 931 887 -4.7% 436 -53%
실행 로그 10/23 247 118 67 -43.2% 62 -47%
SKILL.md 9,035 4,719 5,513 +16.8% 2,259 -52%
def estimate_tokens(text):
"""o200k_base 기준 어림. 한도에 가까우면 실제로 세세요."""
hangul = sum("\uac00" <= ch <= "\ud7a3" for ch in text)
other = len(text) - hangul
return round(hangul * 1.13 + other * 0.218)
다른 날 쓴 한글 원고는 -3.3%, 긴 실행 로그는 -4.7%로 꽤 맞았습니다. 하지만 247자짜리 짧은 로그는 -43%로 크게 빗나갔습니다. 열어 보니 날짜·시각·윈도우 경로·버전 번호가 대부분인 세 줄이었습니다. 숫자와 경로는 잘게 쪼개지는 편이라 글자 수로는 잘 안 잡힙니다. 마크다운 표와 명령어가 많은 SKILL.md는 반대로 +16.8%였습니다. 그래서 어림식은 “여유가 충분한지” 판단하는 데만 쓰고, 한도에 가까우면 실제로 세는 게 안전합니다.
토큰 예산에 맞춰 자를 때 한글이 깨지는 문제
긴 글을 “앞에서 N토큰까지만” 넣으려면 토큰 목록을 잘라 다시 글자로 되돌리면 됩니다. 그런데 cl100k_base로 해 보니 끝에 깨진 글자 �가 붙었습니다.
cl100k_base: 24토큰
앞 3토큰 decode -> '자동화'
앞 4토큰 decode -> '자동화 �'
앞 5토큰 decode -> '자동화 스�'
o200k_base: 16토큰
앞 3토큰 decode -> '자동화 스'
앞 4토큰 decode -> '자동화 스크'
앞 5토큰 decode -> '자동화 스크립'
cl100k_base는 한글 한 글자를 여러 토큰(바이트 조각)으로 나누기 때문에, 중간에서 자르면 반쪽짜리 글자가 남습니다. o200k_base는 이 문장에서는 깨지지 않았지만, 인코딩에 따라 생길 수 있는 문제라 처리해 두는 게 좋습니다.
처음에는 “글자로 되돌리다 오류가 나면 토큰을 하나씩 덜어내는” 방식으로 짰는데, 이러면 멀쩡한 글자까지 버렸습니다. 5토큰 예산이면 자동화 스까지 담을 수 있는데 자동화만 남았습니다. 바이트로 되돌린 뒤 끝의 반쪽 글자 바이트만 버리도록 고친 최종본입니다.
def cut_to_tokens(text, limit, enc):
"""limit 토큰 이하가 되도록 자르되, 끝에 걸친 반쪽 글자만 버립니다."""
ids = enc.encode(text)
if len(ids) <= limit:
return text
return enc.decode_bytes(ids[:limit]).decode("utf-8", errors="ignore")
limit 3: v1 '자동화' (3토큰) / v2 '자동화' (3토큰)
limit 4: v1 '자동화' (3토큰) / v2 '자동화 ' (4토큰)
limit 5: v1 '자동화' (3토큰) / v2 '자동화 스' (5토큰)
limit 8: v1 '자동화 스크' (7토큰) / v2 '자동화 스크' (7토큰)
limit 1~2000 시험: 예산 초과 0건
v1이 처음 버전, v2가 고친 버전입니다. 고친 함수를 15,727자 원고에 예산 1~2,000토큰으로 2,000번 돌려, 잘라낸 글을 다시 셌을 때 예산을 넘은 경우는 0건이었습니다.
파일 토큰 수를 세고 예산을 검사하는 스크립트
위 내용을 모아 바로 쓸 수 있는 명령줄 도구로 만들었습니다. 파일 여러 개를 주면 각각과 합계를 세고, 합계가 예산을 넘으면 얼마나 줄여야 하는지 출력한 뒤 종료 코드 1로 끝납니다. 다른 스크립트에서 AI를 부르기 전에 이걸 먼저 돌려 종료 코드로 걸러낼 수 있습니다.
"""파일의 토큰 수를 세고 예산을 넘는지 알려 줍니다.
python tokcount.py 파일1 [파일2 ...] [--budget 4000] [--encoding o200k_base]
"""
import argparse
import sys
try:
import truststore
truststore.inject_into_ssl() # 백신이 SSL을 검사하는 PC에서 첫 다운로드 실패 방지
except ImportError:
pass
import tiktoken
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="파일 토큰 수 세기")
parser.add_argument("files", nargs="+")
parser.add_argument("--budget", type=int, default=4000, help="파일 전체 합계 상한(토큰)")
parser.add_argument("--encoding", default="o200k_base")
args = parser.parse_args()
enc = tiktoken.get_encoding(args.encoding)
total = 0
for path in args.files:
with open(path, encoding="utf-8") as f:
text = f.read()
n = len(enc.encode(text))
total += n
print(f"{n:>8,}토큰 {len(text):>8,}자 {path}")
print(f"{total:>8,}토큰 합계 ({args.encoding}, 예산 {args.budget:,})")
if total > args.budget:
print(f"예산 초과: {total - args.budget:,}토큰을 줄여야 합니다.")
sys.exit(1)
$ python tokcount.py ko_post.txt ko_post2.txt --budget 8000
4,711토큰 10,941자 ko_post.txt
5,944토큰 15,727자 ko_post2.txt
10,655토큰 합계 (o200k_base, 예산 8,000)
예산 초과: 2,655토큰을 줄여야 합니다.
[exit 1, 0.54초]
원고 두 편을 합치면 10,655토큰이라 8,000 예산을 2,655토큰 넘는다고 알려 줍니다. 같은 원고 한 편을 --encoding cl100k_base로 세면 4,711이 아니라 5,803토큰이 나왔습니다. 보낼 모델에 맞는 인코딩을 고르는 게 중요합니다.
주의할 점
- tiktoken 숫자는 OpenAI 모델 기준입니다. 다른 회사 모델은 토크나이저가 달라 숫자가 다릅니다. 정확한 값이 필요하면 그 모델 API의 토큰 세기 기능을 쓰세요.
- 첫 실행에는 인터넷이 필요합니다. 인코딩 파일을 내려받기 때문입니다(이번에 3.9~5.1초). 회사망·백신 SSL 검사 환경에서
CERTIFICATE_VERIFY_FAILED가 나면 검증을 끄지 말고truststore를 쓰세요. - 한글은 4글자=1토큰이 아닙니다. 이번 실측에서 한글 설명문은 약 1.9글자에 1토큰(o200k_base)이었습니다.
- 숫자·경로·표가 많은 글은 어림이 크게 빗나갑니다. 짧은 로그에서 -43%, 마크다운 문서에서 +17%가 나왔습니다.
- 토큰 목록을 잘라 decode하면 한글이 깨질 수 있습니다.
decode_bytes()후errors="ignore"로 끝의 반쪽 글자만 버리세요. - 윈도우에서
pip가 Permission denied로 막히면python -m pip로 실행해 보세요.
마무리
한글 프롬프트는 글자 수를 4로 나누지 말고, 실제로 세거나 한글을 따로 센 어림식을 쓰세요.
가장 작은 첫걸음은 이렇습니다.
python -m pip install tiktoken으로 설치하고, 평소 AI에 붙여 넣는 글 하나를len(enc.encode(text))로 세어 봅니다.- 같은 글의 글자 수와 나눠 “내 글은 몇 글자에 1토큰인지” 확인합니다. 한글 위주라면 2 안팎이 나올 겁니다.
- AI를 부르는 스크립트 앞에 위 tokcount.py 같은 예산 검사를 넣어, 한도를 넘는 입력은 보내기 전에 걸러냅니다.
다음 글에서는 JSONDecodeError가 났을 때 깨진 JSON 파일에서 틀린 곳을 정확히 찾아내는 방법을 실측해 보겠습니다.