1일차에서 LLM이 결국 "다음 단어를 예측하는 모델"이라는 걸 정리했습니다. 근데 이걸 실제로 API로 호출해서 쓰다 보면, 이 예측 과정이 구체적으로 토큰, 컨텍스트 윈도우, temperature라는 세 가지 단위로 쪼개진다는 걸 알게 됩니다. 처음에는 이 셋을 그냥 "API 호출할 때 신경 써야 하는 파라미터들" 정도로만 봤는데, 파보니까 서로 단단하게 연결된 하나의 흐름이었습니다. 이번 글에서 그 흐름을 따라가보겠습니다. 🤖
1. 토큰
1.1 텍스트와 토큰
모델은 사실 "글자"를 이해하는 게 아닙니다. 신경망 내부는 결국 숫자 연산으로 돌아가기 때문에, 입력된 텍스트는 모델에 들어가기 전에 반드시 숫자로 변환되는 과정을 거칩니다. 이 변환을 토큰화(tokenization)라고 부르고, 변환된 최소 단위가 토큰(token)입니다.
토큰을 이해하는 가장 쉬운 비유는 레고 블록입니다. "안녕하세요"라는 문장은 사람 눈에는 하나의 단어처럼 보이지만, 모델 입장에서는 여러 개의 작은 블록이 이어진 형태로 보입니다.
영어는 의미 단위(서브워드) 기준으로 블록이 나뉘는 경우가 많고, 한국어는 조사와 어미가 풍부한 교착어 특성 때문에 같은 의미라도 영어보다 훨씬 잘게, 더 많은 블록으로 쪼개지는 경향이 있습니다.
import tiktoken
encoding = tiktoken.get_encoding("cl100k_base")
text_en = "Hello, how are you today?"
text_ko = "안녕하세요, 오늘 기분 어떠세요?"
tokens_en = encoding.encode(text_en)
tokens_ko = encoding.encode(text_ko)
print(f"영어 토큰 수: {len(tokens_en)}")
print(f"한국어 토큰 수: {len(tokens_ko)}")
for t in tokens_ko:
print(t, "->", encoding.decode([t]))
이 코드를 돌려보면 비슷한 의미의 문장인데도 한국어 토큰 수가 영어보다 1.5~2배 가까이 많이 나옵니다. 토큰을 하나씩 디코딩해보면 "안녕하세요"가 "안녕", "하", "세요" 식으로 나뉘는 경우도 있고, 더 잘게 쪼개지는 경우도 있습니다. 처음 이걸 봤을 때 "토큰 = 단어"라고 생각했는데 아니었습니다. 토큰은 모델이 학습 과정에서 통계적으로 자주 등장하는 패턴을 묶어 만든 어휘 사전(vocabulary)의 한 항목일 뿐이고, 이 어휘 사전은 모델마다 다릅니다.
1.2 토큰과 비용, 토큰과 기억력
휴대폰 요금이 데이터 사용량에 따라 달라지는 것처럼, LLM도 토큰 수에 따라 비용이 달라집니다. 입력 토큰과 출력 토큰 각각에 단가가 매겨지고, 보통 출력 쪽이 더 비쌉니다. 그래서 시스템 프롬프트를 길게 짜놓으면 호출할 때마다 그 토큰을 매번 소비하게 되고, 이게 누적되면 생각보다 큰 비용 차이로 이어집니다.
그리고 AI가 대화의 맥락을 얼마나 길게 기억하는지도 결국 토큰 수에 달려 있습니다. 한국어가 토큰을 더 많이 소비한다는 건, 같은 분량을 입력해도 컨텍스트 한도에 더 빨리 다다른다는 뜻입니다. 한국어로 프롬프트를 쓸 때 이 설명이 진짜 필요한가를 한 번 더 생각해야 하는 이유가 여기 있습니다.
같은 문장이라도 GPT 계열과 Claude 계열의 토크나이저가 서로 다른 방식으로 쪼갤 수 있습니다. "이 문장은 토큰 몇 개"라는 게 절대적인 값이 아니라 모델마다 달라지는 값이라는 걸 염두에 둬야 합니다.
2. 컨텍스트 윈도우
2.1 모델의 작업 책상
토큰화로 텍스트가 숫자로 바뀌었다면, 이제 그 숫자들이 들어갈 공간이 필요합니다. 이 공간을 컨텍스트 윈도우(context window)라고 부릅니다. 여기서 헷갈렸던 부분이 있는데, "입력"이라는 말이 사용자가 타이핑하는 부분만을 의미하지 않는다는 점입니다. 실제로는 시스템 프롬프트 + 대화 기록 + 사용자 입력 + 모델이 만들어낼 출력까지 전부 이 윈도우 안에서 같이 계산됩니다.
책상으로 비유하면 이해가 쉬웠습니다. 책상 위에 참고서(시스템 프롬프트), 메모(대화 기록), 문제지(사용자 입력), 답안지(출력)를 같이 펼쳐놓아야 하는데, 책상 크기가 고정되어 있는 것입니다. 위 그림처럼 시스템 프롬프트와 대화 기록이 길어지면, 답을 적을 공간(모델 출력)이 그만큼 줄어듭니다. 컨텍스트 윈도우가 100K 토큰인 모델에 90K짜리 문서를 넣으면, 남은 10K 안에서 시스템 프롬프트와 답변이 다 처리돼야 합니다.
컨텍스트 윈도우는 "얼마나 긴 글을 넣을 수 있나"가 아니라, "모델이 한 번에 다룰 수 있는 정보 총량을 어떻게 나눠 쓰느냐"의 문제입니다.
2.2 컨텍스트 윈도우와 RAG
RAG 개념을 찾아보면서 컨텍스트 윈도우가 왜 중요한 변수인지 좀 더 이해됐습니다. 컨텍스트 윈도우가 제한되어 있어도, 생성 과정에서 외부 소스의 관련 데이터를 가져와 모델에 컨텍스트로 제공하면 상황에 맞는 정보에 접근할 수 있습니다. 이 방식은 답변이 내부 지식에 의존하는 경우 모델의 정확도를 크게 끌어올립니다.
다만 검색된 청크를 얼마나, 어떤 순서로 컨텍스트에 넣을지는 애플리케이션마다 다르고, 좋은 성능을 위해서는 신중한 평가와 실험이 필요합니다. 책상 비유로 돌아가면, 참고 자료를 너무 많이 펼쳐놓으면 오히려 지금 풀어야 할 문제지에 집중이 안 되는 셈입니다. 실제로 컨텍스트가 길어질수록 모델이 입력의 모든 부분을 똑같이 신경쓰지 못하고 앞/뒤쪽에 더 집중하는 경향도 있다고 합니다. 이 부분은 RAG 시리즈에서 청킹이랑 같이 더 파볼 예정입니다.
3. Temperature
3.1 확률 분포와 다음 토큰 선택
모델이 다음 토큰을 정할 때, 어휘 사전에 있는 모든 토큰에 대해 점수(logit)를 계산합니다. 이 점수는 그 자체로 확률이 아니라서 softmax를 거쳐 합이 1이 되는 확률 분포로 바뀝니다. temperature는 이 로짓 값을 직접 조절하는데, 로짓을 temperature로 나눈 뒤 softmax를 적용해서 최종 확률을 계산합니다.
"오늘 날씨가 정말" 다음에 "좋다"가 60%, "덥다"가 20%, "춥다"가 15%, 나머지가 5%라고 해보겠습니다.
로짓을 큰 값으로 나누면(temperature 높음) 점수 차이가 줄어들면서 위 그림 오른쪽처럼 분포가 평평해집니다. 작은 값으로 나누면(temperature 낮음) 원래 점수 차이가 더 벌어지면서 왼쪽 그림처럼 1위 토큰의 확률이 더 커집니다. temperature가 0이면 항상 확률 1위 토큰만 선택하는데, 이를 그리디 디코딩이라고 부릅니다.
SQL 생성, 데이터 추출, 분류
요약, 일반 대화, 코드 설명
브레인스토밍, 카피라이팅, 창작
3.2 top-p와 top-k
temperature만 알면 되는 줄 알았는데, 같이 따라오는 개념이 top-p랑 top-k였습니다. temperature가 분포 모양을 조절한다면, top-k는 그 분포의 꼬리를 잘라내는 역할입니다. 확률이 낮은 토큰들을 제거하고 상위 k개만 후보로 남깁니다. 앞의 예시에서 top-k가 3이면 "좋다, 덥다, 춥다"만 후보로 남고 나머지(5%)는 처음부터 선택지에서 빠집니다.
top-p는 기준이 다릅니다. 개수가 아니라 누적 확률로 자릅니다. top-p가 0.8이면 확률 높은 순서대로 더해가다가 80%를 넘는 시점까지의 토큰만 후보가 됩니다. 분포가 한쪽에 몰려있으면 후보가 적어지고, 평평하면 후보가 많아지는 식이라 상황에 따라 후보 개수가 달라진다는 게 top-k와 다른 점입니다.
temperature와 top-p를 같이 조정하면 결과가 예측하기 어려워지니 보통 둘 중 하나만 건드리는 걸 권장한다고 합니다. 일단은 temperature 하나만 만져보면서 결과가 어떻게 바뀌는지 보는 정도로 충분한 것 같고, top-p/top-k는 나중에 더 세밀하게 튜닝할 때 건드려볼 영역입니다.
4. 코드로 확인
4.1 temperature별 응답 비교
위 내용을 코드로 직접 확인해봤습니다. 같은 프롬프트를 temperature 0, 0.5, 1.0으로 각각 3번씩 호출해서 결과가 어떻게 달라지는지, 토큰 사용량은 어떻게 찍히는지 같이 봤습니다.
import anthropic
client = anthropic.Anthropic()
prompt = "다음 단어로 시작하는 짧은 문장을 하나만 만들어줘: '오늘'"
for temp in [0.0, 0.5, 1.0]:
print(f"\n--- temperature = {temp} ---")
for i in range(3):
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=50,
temperature=temp,
messages=[{"role": "user", "content": prompt}]
)
text = response.content[0].text
usage = response.usage
print(text)
print(f"input: {usage.input_tokens}, output: {usage.output_tokens}")
temperature 0에서는 세 번 다 거의 같은 문장이 나왔고, 1.0에서는 매번 표현이 달랐습니다. usage 값도 같이 찍어보니 입력 토큰은 거의 고정인데 출력 토큰은 응답 길이에 따라 조금씩 달라집니다. 이 usage 값이 비용 계산의 실제 단위라는 걸 직접 확인한 셈입니다.
4.2 분석 자동화 적용 고민
분석 자동화 파이프라인에 LLM을 끼워넣을 거라면, temperature 0으로 고정해서 같은 입력에 같은 출력이 나오게 하는 게 중요할 것 같습니다. 그리고 usage 값을 매 호출마다 로깅해두면, 나중에 "이 파이프라인 한 번 돌릴 때 비용이 얼마나 드는지"를 추정할 수 있습니다. 아직 직접 파이프라인을 짜본 건 아니라서, 이 부분은 나중에 자동 리포트 생성기 만들 때 실제로 적용해볼 생각입니다.
5. 정리
입력 텍스트 → 토큰화로 숫자(토큰 ID)로 변환 → 컨텍스트 윈도우라는 한정된 공간에 시스템 프롬프트/대화 기록/입력/출력이 같이 배치 → 모델이 다음 토큰의 확률 분포를 계산 → temperature(+top-p/top-k)가 그 분포에서 실제로 뭘 고를지 결정합니다.
비용은 토큰 수, 다룰 수 있는 정보량은 컨텍스트 윈도우, 응답의 일관성은 temperature. 이렇게 묶어놓으니까 셋을 따로 외울 때보다 훨씬 머리에 잘 남습니다. 모델 바꿀 때도 이 세 가지 수치(윈도우 크기, 토큰 단가, temperature 범위)를 가장 먼저 확인하면 될 것 같습니다.
softmax랑 로짓 스케일링까지 보고 나니 "temperature 0이면 왜 항상 같은 답이 나오는지"는 수식으로 이해가 됐습니다. 근데 top-p랑 top-k를 실제로 언제 같이 써야 효과적인지는 아직 감이 안 잡힙니다. 이건 프롬프트 엔지니어링 글 쓰면서 직접 실험해보고 정리할 계획입니다. 🤖
'AI > LLM' 카테고리의 다른 글
| [LLM] RAG, 모델이 스스로 검색하는 구조 (0) | 2026.06.23 |
|---|---|
| [LLM] 프롬프트 엔지니어링이란 (0) | 2026.06.19 |
| [LLM] Large Language Model이란 (1) | 2026.06.12 |
HELLO WORLD
안녕하세요. 데이터로 말하는 분석가 모모입니다.
데이터를 구조화하고 분석하는 과정과 실무에 활용되는 도구 중심의 내용을 기록합니다.