
ChatGPT, 클로드, 제미나이. 매일 도구로 쓰면서도 "이 안에서 정확히 뭐가 일어나는가"를 설명하라고 하면 막힌다. 토큰, 임베딩, 어텐션 같은 단어는 분명히 들어봤는데, 직접 코드로 돌려서 눈으로 확인해본 적은 없었다.
그래서 정리하는 김에 6가지를 차례로 확인했다. 토크나이저에 단어를 직접 넣어보고, 임베딩 API를 호출해서 단어 거리를 측정하고, 어텐션 시각화 도구로 모델이 어디를 보는지 들여다보고, Temperature 값을 극단으로 바꿔서 결과를 비교했다. 마지막에는 한계를 일부러 부딪혀보고, API의 시스템 프롬프트가 모델 동작을 어떻게 통째로 바꾸는지까지 확인했다.
이 글은 그 과정을 그대로 옮긴 정리다. 코드가 중간중간 나오는데 코드 자체를 따라할 필요는 없다. 실행 결과에서 뭐가 보이는지만 따라오면 된다.
토크나이저 - LLM은 단어가 아니라 토큰으로 본다
제일 먼저 깬 오해가 이거였다. 우리는 글을 "단어" 단위로 인식한다고 생각하지만, LLM은 그렇게 보지 않는다. 토큰(token)이라는, 학습 데이터에서 통계적으로 자주 등장하는 글자 묶음 단위로 자른다.
이 토큰이 단어 하나일 수도, 단어의 일부일 수도, 글자 두 개 조합일 수도 있다. 그리고 이게 단순히 "내부 구현 디테일"이 아니라 비용, 처리 속도, 컨텍스트 한계에 다 직결된다.

OpenAI 토크나이저로 직접 확인
platform.openai.com/tokenizer에 가면 입력한 문장이 토큰으로 어떻게 잘리는지 색깔별로 보여준다. 직접 몇 가지 넣어봤다.
| 입력 문장 | 언어 | 글자 수 | 토큰 수 |
|---|---|---|---|
| 안녕 | 한국어 | 2 | 2 |
| 안녕하세요, 어떻게 지내세요? | 한국어 | 15 | 8 |
| How are you? | 영어 | 12 | 6 |
| ChatGPT | 영어(고유명사) | 7 | 2 |
"안녕"이라는 한 단어가 토큰 두 개로 쪼개진다. 직관에 반하는 부분이다. 우리한테는 한 덩어리지만 모델은 그렇게 안 본다. 반대로 "ChatGPT"는 영어 고유명사라도 'Chat'과 'GPT'로 분리된다. 학습 데이터에서 두 조각이 따로 빈번하게 등장했다는 뜻이다.
한국어가 영어보다 토큰 효율이 떨어진다
표를 보면 같은 의미를 표현해도 한국어가 영어보다 토큰을 더 많이 쓴다. 글자 수 대비 토큰 비율로 보면 한국어가 영어의 2~3배 수준이다.
이게 왜 중요하냐. 컨텍스트 윈도우가 같은 GPT-4o라도, 한국어 사용자가 채울 수 있는 실질 정보량이 영어 사용자보다 적다. API 호출 비용도 토큰 단위로 계산되니까 같은 내용을 한국어로 처리하면 영어보다 비싸진다.
임베딩 - 단어의 의미를 좌표로 바꾸기
두 번째로 확인한 게 임베딩이다. 컴퓨터는 텍스트를 직접 처리하지 못한다. 모든 게 숫자가 되어야 한다. 단어를 일정 크기의 숫자 벡터로 바꾸는 게 임베딩(embedding)이다.
이걸 어떻게 비유해야 할까. GPS 좌표가 지구의 한 위치를 숫자로 나타내는 것과 비슷하다. 다만 임베딩은 "의미의 위치"를 숫자로 표현한다. 의미가 비슷한 단어들은 이 숫자 공간에서 가까이 모인다.

코사인 유사도로 거리 측정하기
이걸 머리로만 받아들이면 뜬구름이라, OpenAI의 text-embedding-3-small 모델로 직접 단어 쌍의 거리를 측정했다. 코드를 다 이해할 필요는 없고, 결과 숫자만 보면 된다. 1에 가까울수록 비슷, 0에 가까울수록 다르다.
pairs = [("사과", "배"), ("사과", "자동차"),
("왕", "여왕"), ("한국", "서울")]
for w1, w2 in pairs:
sim = cosine_similarity(embed(w1), embed(w2))
print(f"{w1} ↔ {w2}: {sim:.4f}")
실행 결과:
| 단어 쌍 | 유사도 | 해석 |
|---|---|---|
| 사과 ↔ 배 | 0.34 | 같은 카테고리(과일) |
| 사과 ↔ 자동차 | 0.26 | 전혀 다른 카테고리 |
| 왕 ↔ 여왕 | 0.60 | 같은 의미 영역, 성별만 다름 |
| 한국 ↔ 서울 | 0.40 | 나라-수도 관계 |
예상대로 의미가 가까운 쌍일수록 숫자가 크다. "왕↔여왕"이 0.6으로 제일 가깝게 나오고, "사과↔자동차"가 가장 멀다. 이 정도면 임베딩이 의미를 어느 정도 잡아낸다는 게 확인된다.
그런데 좀 신경 쓰이는 부분이 있다
같은 단어 쌍이라도 small 모델과 large 모델 결과가 다르다. 같은 모델 안에서도 호출마다 미세하게 변한다. 임베딩이 확률적 산물이라 그렇다.
더 본질적으로 짚을 게 있다. "사과"는 한국어에서 적어도 두 가지 의미를 가진다.
- 먹는 사과 (과일)
- 미안하다고 하는 사과 (apology)
"배"도 마찬가지다. 먹는 배, 타고 다니는 배, 신체 부위 배. 그러면 "사과↔배"의 거리는 어떤 의미를 기준으로 보느냐에 따라 달라져야 정상이다. "사과"가 사과(apology)일 때 "배"와 가까울 이유는 없으니까.
그래서 등장하는 게 문맥 의존적 표현
단순히 "단어 → 고정된 벡터"로는 동음이의어 처리가 안 된다. 같은 단어라도 주변에 어떤 단어들이 있느냐에 따라 벡터가 동적으로 달라져야 한다. 이걸 문맥 의존적 표현(Contextual Representation)이라고 한다.
"나는 사과를 먹었다"의 "사과"와 "나는 사과를 했다"의 "사과"가 다른 벡터로 표현되는 현상. 이게 어떻게 가능한지는 다음 절의 어텐션에서 단서가 나온다.
어텐션 - 모델이 어디를 보는지 시각화
트랜스포머의 심장은 어텐션(attention)이다. "그 소설의 결말은 충격적이었다"라는 문장에서 "충격적"이 수식하는 게 "결말"이라는 걸 잡아내는 것 - 이게 어텐션의 역할이다.

Q, K, V - 세 가지 역할
모델은 문장 안의 모든 단어가 서로 얼마나 관련 있는지를 세 가지 역할로 계산한다.
| 역할 | 이름 | 의미 |
|---|---|---|
| Q (Query) | 쿼리 | 지금 무엇을 찾고 있나 |
| K (Key) | 키 | 나는 어떤 정보를 가지고 있나 |
| V (Value) | 밸류 | 실제로 전달할 내용 |
도서관에서 책을 찾는 과정에 비유하면 직관이 좀 잡힌다. Q는 검색어, K는 책 색인, V는 책의 실제 내용. 쿼리와 키가 잘 맞는 쌍일수록 그 밸류가 최종 결과에 더 많이 반영된다.
Transformer Explainer로 실제 패턴 보기
Transformer Explainer라는 시각화 사이트가 있다. 단어마다 Q, K, V가 색깔로 보이고, 어텐션 가중치가 선의 굵기로 나타난다. 다만 GPT의 과거 버전을 쓰는 데모라서 영어 입력만 받는다.
SF 풍 영문 문장을 넣고 다음 단어를 12개까지 생성하면서 패턴을 봤다.
딱 한국어 문법으로 생각해도 자연스러운 결과다. 형용사가 자기가 수식하는 명사를 가장 강하게 본다. 모델이 의미와 문법을 따로 학습한 게 아니라, 어텐션을 학습시켰더니 알아서 이런 관계를 잡아내게 된 거다.
그런데 같은 시작 문장을 다시 넣고 돌리면 어텐션 패턴이 달라진다. 첫 번째와 두 번째 시도의 결과 문장이 완전히 다르고, 그 결과 문장에 대한 어텐션도 매번 다르게 그려진다. Temperature 값이 높게 설정돼 있어서 그렇다. 이건 다음 절에서 본격적으로 다룬다.
12 × 12 = 144개 어텐션이 매번 동시에 돈다
Transformer Explainer 화면 위에 "Transformer Block 1"이라는 표시와 좌우 화살표가 있다. 화살표를 누르면 같은 구조의 블록이 옆에 11개 더 있다. 그리고 각 블록 안에는 12개의 어텐션 헤드가 들어있다.
화면 하나에 보이는 어텐션은 빙산의 일각이고, 실제로는 한 번의 토큰 예측마다 144개의 어텐션 연산이 동시에 수행된다. ChatGPT가 따다닥 단어를 뱉어낼 때마다 매번이다.
이 구조 덕분에 모델은 같은 문장을 144가지 관점으로 동시에 분석하고, 그 결과를 합쳐서 다음 토큰의 확률 분포를 만든다. GPU/TPU 같은 병렬 연산 하드웨어가 왜 LLM의 운명적 요건인지, 이 그림 한 장으로 설명이 된다. 연산량이 어마어마하다.
Temperature와 Top P - 다음 토큰 고르는 규칙
LLM은 다음 단어를 어떻게 고를까. 모델은 매 스텝마다 다음에 올 수 있는 모든 토큰에 각각 확률을 부여하고, 그 중 하나를 뽑는다. 각 면에 다른 단어가 적힌 거대한 주사위를 매 토큰마다 굴리는 셈이다.
이 주사위를 어떻게 굴리느냐, 즉 샘플링 전략이 출력의 다양성과 일관성을 결정한다. 가장 확률 높은 토큰만 항상 고르는 건 아니다.
Temperature - 분포의 모양을 바꾼다
"오늘 점심으로"라는 문장 뒤에 올 수 있는 토큰은 수만 가지다. 각각에 확률값이 붙은 분포가 만들어진다. Temperature는 이 분포의 형태를 조절한다.
| Temperature | 분포 형태 | 결과 성격 |
|---|---|---|
| 낮음 (0에 가까움) | 날카로운 분포 | 결정적, 거의 같은 답 |
| 높음 (1.5~2.0) | 평평한 분포 | 창의적, 일관성 낮음 |
낮으면 확률 분포가 뾰족해져서 1등 토큰이 거의 항상 뽑힌다. 높이면 분포가 평평해져서 낮은 확률 토큰도 뽑힐 기회를 가진다. 0에 가까울수록 결정적, 높을수록 창의적인데 그만큼 일관성은 떨어진다.
Top P - 후보 자체를 잘라낸다
Top P는 다른 방식이다. 확률 순으로 토큰을 줄세운 뒤, 누적 확률이 P가 될 때까지만 남기고 나머지는 후보에서 빼버린다. Top P가 0.9면 상위 90% 확률에 해당하는 토큰만 남기는 식이다.
이러면 너무 엉뚱한 토큰이 뽑힐 가능성을 원천 차단할 수 있다. Temperature가 분포의 모양을 바꾸는 거라면, Top P는 후보 풀 자체를 잘라내는 거다. 다른 차원의 통제 수단이라 실무에서는 보통 둘을 같이 조정한다.
같은 입력, 다른 결과 - 직접 비교
"오늘 점심으로"라는 동일 입력을 Temperature 0.1과 1.8로 각각 세 번씩 돌렸다.
for temp in [0.1, 1.8]:
for i in range(3):
response = generate(prompt, temperature=temp)
print(f"T={temp}, 시도 {i+1}: {response}")
결과는 명확하게 갈렸다.
- Temperature 0.1: 세 번 다 거의 똑같은 답. 마지막 한 글자 정도만 다르다.
- Temperature 1.8: 매번 시작 단어부터 다른 방향. "오늘 점심으로 마른..." 같은 예상 못한 시작도 나온다.
같은 모델, 같은 입력인데 Temperature라는 값 하나로 행동 양식이 완전히 바뀐다. 이게 그냥 "옵션 하나"가 아니라 LLM 활용에서 가장 중요한 손잡이 중 하나라는 생각이 든다.
LLM의 세 가지 구조적 한계
여기까지가 LLM이 어떻게 작동하는지에 대한 정리였다면, 이 절은 반대로 LLM이 뭘 못하는지를 확인하는 부분이다. 우회하거나 보완할 수는 있지만, 모델 자체로는 해결되지 않는 한계들이다.
한계 1. 할루시네이션 (Hallucination)
일부러 존재하지 않는 논문에 대해 질문해봤다. Temperature는 높게.
이런 논문은 실재하지 않는다. 내가 방금 만들어낸 가짜 제목이다. 모델이 어떻게 답할까.
없는 논문을 그럴듯하게 요약해버린다. 이게 할루시네이션이다.
중요한 건 모델이 "거짓말을 하려고" 그러는 게 아니라는 점이다. 모델 입장에서는 "모르겠다"라고 답하는 것보다 그럴듯한 다음 토큰을 이어붙이는 게 확률 분포상 더 자연스럽다. 학습 과정에서 "이런 질문 다음엔 이런 형식의 답이 온다"는 패턴이 강하게 박혀있기 때문이다.
업무에서 LLM 답변을 그대로 신뢰하면 안 되는 본질적 이유가 여기 있다. 특히 인용, 수치, 사람 이름, 법령 같은 검증 가능한 정보가 들어있을수록 의심부터 해야 한다. 모의해킹 보고서나 컴플라이언스 문서를 LLM이 도와주는 시대지만, 출처와 수치는 사람이 다시 확인하는 게 맞다.
한계 2. 지식 단절 (Knowledge Cutoff)
"오늘 날짜와 현재 달러 환율을 알려주세요." 단순한 요청인데 답은 이렇다.
"제 지식은 2023년 10월까지로 한정되어 있습니다."
모델은 훈련 시점 이후의 세상을 모른다. 학습 데이터의 시간 경계 너머는 모델 자체로는 닿을 방법이 없다.
환율, 주가, 뉴스, 오늘 날짜 같은 실시간 정보는 모델 단독으로 해결되지 않는다. 이걸 해결하려면 외부 도구를 호출하는 메커니즘 - 흔히 Tool Use나 Function Calling이라 부르는 것 - 이 필요해진다. 이건 별도 글에서 다룰 주제다.
한계 3. 컨텍스트 윈도우 제약
같은 의미의 문단을 한국어와 영어로 각각 토크나이저에 넣어서 토큰 비율을 비교했다. 이미 첫 번째 절에서 본 그 비효율이 정량적으로 드러난다.
| 구분 | 글자 수 대비 토큰 비율(예시) | 의미 |
|---|---|---|
| 한국어 | 0.47 ~ 0.75 | 글자 100자에 토큰 약 50~75개 |
| 영어 | 0.13 ~ 0.26 | 글자 100자에 토큰 약 13~26개 |
같은 모델, 같은 컨텍스트 윈도우를 쓰더라도 한국어는 그 공간을 훨씬 빠르게 소모한다. 긴 문서를 요약하거나 멀티턴 대화를 길게 가져갈수록 곧바로 비용과 한계로 이어진다.
API 호출 - 시스템 프롬프트와 멀티턴 대화
지금까지가 "LLM이 어떻게 돌아가는가"였다면, 이 절은 그 이해를 바탕으로 LLM을 제어하는 방법이다. 같은 모델이라도 어떻게 호출하느냐에 따라 결과가 완전히 달라진다.

API가 왜 채팅창보다 강력한가
웹사이트(chat.openai.com)에 들어가서 채팅창에 입력하는 대신, 내 코드가 직접 모델을 호출하게 만드는 통로가 API다. 별 차이 없어 보이지만 실제로는 활용 폭이 완전히 다르다.
자동화, 대량 처리, 응답 형식 강제, 시스템 프롬프트 세팅, 페르소나 분리 같은 게 가능해진다. 챗봇 서비스, 문서 자동 분류, 보고서 초안 자동 생성, MCP 서버 같은 도구가 다 API 위에 올라간다.
메시지의 세 가지 역할
API 호출의 기본 구조는 messages라는 목록이다. 이 안의 각 메시지는 세 가지 역할(role) 중 하나를 가진다.
| 역할(role) | 설명 |
|---|---|
| system | 모델의 행동 방식, 말투, 페르소나를 설정. "이 배우는 이런 인물이다"라고 캐릭터를 부여하는 자리. |
| user | 사용자의 실제 입력. |
| assistant | 모델의 이전 응답. 대화의 맥락을 이어갈 때 다시 넣어줘야 한다. |
시스템 프롬프트 한 줄로 모델이 완전히 바뀐다
같은 질문 - "블랙홀이 무엇인가요?" - 에 시스템 프롬프트만 세 가지로 바꿔서 호출했다.
"당신은 중학생을 가르치는 친절한 과학 선생님입니다.",
"당신은 물리학 박사입니다. 전문 용어와 수식을 사용해 정확하고 간결하게 답하세요.",
"당신은 소크라테스식 교육자입니다. 답을 알려주지 말고 질문을 던져 상대방이 스스로 답을 찾도록 유도하세요."
]
question = "블랙홀이 무엇인가요?"
for prompt in system_prompts:
response = chat(system=prompt, user=question)
print(response)
결과는 그야말로 다른 사람이 답한 것 같다.
- 과학 선생님 페르소나: "친구와 함께 물통에 들어가 있다고 가정해 볼까요?"로 시작하는 친근한 비유 위주
- 물리학 박사 페르소나: "슈바르츠실트 블랙홀..."로 시작하는 전문 용어 폭격
- 소크라테스 페르소나: "블랙홀의 어떤 특성이 일반적인 별이나 행성과 구별 짓는다고 생각하나요?"라고 되묻는다. 답을 안 준다.
같은 질문, 같은 모델, 같은 파라미터인데 시스템 프롬프트 한 줄이 모델 동작 전체를 정의한다. 챗봇 서비스를 만들 때 시스템 프롬프트가 왜 "보호할 자산"으로 취급되는지 직관적으로 이해된다. 페르소나, 응답 정책, 금지 사항이 다 거기 들어가 있으니까.
멀티턴 대화의 함정 - 모델은 기억하지 않는다
여기서 짚어야 할 게 있다. 모델은 대화를 기억하지 않는다. 우리가 보는 ChatGPT 같은 서비스가 "기억하는 척" 보이는 건, 서비스 레이어에서 매번 이전 대화를 통째로 다시 모델에 보내주기 때문이다.
컨텍스트 윈도우라는 책장이 점점 차오르는 진짜 이유가 이거다. 새로운 사람한테 "지금까지 이런 대화를 나눴는데 이번엔 이걸 물어보고 싶어요"라고 전체 맥락을 매번 다시 알려주는 것과 똑같다.
# 1번째 턴
messages.append({"role": "user", "content": "블랙홀이 무엇인가요?"})
response1 = chat(messages)
messages.append({"role": "assistant", "content": response1})
# 2번째 턴 - 1번째 대화 전체를 함께 전송
messages.append({"role": "user", "content": "실제 예시를 들어주세요."})
response2 = chat(messages)
messages.append({"role": "assistant", "content": response2})
# 3번째 턴 - 1, 2번째 대화 전체를 함께 전송
messages.append({"role": "user", "content": "그렇다면 우리 은하에도 블랙홀이 있나요?"})
response3 = chat(messages)
대화가 길어질수록 매 호출에서 보내는 토큰이 누적된다. 5번째 턴이면 이전 4턴의 모든 질문과 답변을 다시 보낸다. 10번째 턴이면 9턴 전부를. 비용은 토큰 단위로 계산되니까, 긴 대화일수록 후반의 한 마디가 초반의 한 마디보다 훨씬 비싸진다.
그래서 챗봇 백엔드를 설계할 때 항상 따라붙는 고민이 히스토리 관리다. 어디까지 그대로 보낼지, 어디서부터 요약해서 압축할지, 어디는 아예 잘라낼지. 한국어로 챗봇을 만들면 토큰 비효율까지 합쳐져서 이 고민이 영어 챗봇보다 훨씬 빨리 닥쳐온다.
정리하며
6가지를 한 표로 압축한다.
| 주제 | 핵심 |
|---|---|
| 토크나이저 | LLM은 단어가 아닌 토큰 단위로 본다. 한국어는 토큰 효율이 영어의 1/2 ~ 1/3 수준. |
| 임베딩 | 의미가 비슷한 단어는 벡터 공간에서 가깝다. 단, 동음이의어 처리는 문맥이 필요하다. |
| 어텐션 | 각 토큰이 다른 토큰을 얼마나 볼지 Q/K/V로 계산. 한 번의 예측에 144개 헤드가 동시에 돈다. |
| Temperature/Top P | 확률 분포에서 토큰을 어떻게 뽑느냐가 출력의 성격을 결정한다. |
| 한계 세 가지 | 할루시네이션, 지식 단절, 컨텍스트 윈도우 제약. 모델 자체로는 해결 불가. |
| API | 시스템 프롬프트 한 줄이 모델 동작 전체를 정의. 대화 맥락은 매번 다시 보내야 유지된다. |
그래서 결국
이 한계들 - 특히 할루시네이션, 지식 단절, 컨텍스트 부족 - 을 실무 시스템에서 어떻게 우회하느냐가 LLM 활용의 다음 단계다. 모델 자체는 해결책이 없으니, 모델 바깥에 무언가를 붙여야 한다.
대표적인 게 세 가지다.
- RAG (Retrieval-Augmented Generation) - 외부 지식 베이스에서 관련 문서를 먼저 검색해서 컨텍스트에 끼워넣는 방식. 할루시네이션 완화의 핵심.
- Tool Use / Function Calling - 환율, 날씨, 검색, 계산기 같은 외부 API를 모델이 호출하게 하는 메커니즘. 실시간 정보 문제를 해결한다.
- Agent - 모델이 여러 단계의 작업을 스스로 계획하고 실행하는 방식. 위 두 가지를 묶어서 자율적으로 굴린다.
이 셋은 별도 글에서 차례로 다룰 예정이다. 특히 요즘 MCP(Model Context Protocol)라는 표준이 빠르게 자리잡고 있어서, Agent 쪽은 따로 깊게 정리할 가치가 있다.
여기까지 따라온 분이라면 ChatGPT를 그냥 "쓰는" 사람에서 한 단계 더 들어간 거다. 토큰이 어떻게 잘리고, 어텐션이 어디를 보고, Temperature가 뭘 바꾸는지를 알면 - 같은 프롬프트도 다르게 짜게 되고, 같은 모델도 다르게 쓰게 된다. 적어도 본인은 이 정리를 하고 나서 그렇게 바뀌었다.
'금융 경제 월드 > AI 블로그 부업' 카테고리의 다른 글
| AI 의존 시대 (0) | 2026.04.21 |
|---|---|
| 스마트스토어 정책 대변화 지금 당장 알아야 할 것들 (1) | 2026.04.17 |
| 소상공인 사업자 카드 사용 가이드 (0) | 2026.04.09 |
| 사업자 카드 사용 가이드 (0) | 2026.03.31 |
| 직장인 N잡러 세금 가이드 (0) | 2026.03.21 |
광고