Home [AI 기초] LLM 작동 원리 - Token, Next Token Prediction, Hallucination
Post
Cancel

[AI 기초] LLM 작동 원리 - Token, Next Token Prediction, Hallucination

1. Token (토큰)

1.1. 개념

  • LLM은 텍스트를 char[] 단위가 아닌 Token 단위로 처리함
  • 의미 있는 단위로 묶어서 처리함으로써 계산 비용 절감
1
2
3
4
5
6
7
8
// 문자 단위 처리 (비효율)
String text = "안녕하세요";
char[] chars = text.toCharArray();
// ['안', '녕', '하', '세', '요'] - 5개 문자

// Token 단위 처리 (효율)
List<String> tokens = tokenizer.encode(text);
// ["안녕", "하세요"] - 2개 토큰

1.2. 비유: JVM 바이트코드 vs 어셈블리어

  • char 단위: 바이트코드 수준 (Low-Level, 의미 파악 불가)
  • Token 단위: 어셈블리어 수준 (의미 있는 명령어 단위)
1
2
3
4
5
// char 단위 = 바이트코드
byte[] bytecode = {0x61, 0x62, 0x63}; // 의미 없음

// Token 단위 = 어셈블리어
String[] instructions = {"LOAD", "ADD", "STORE"}; // 의미 있음

1.3. Token 단위 처리의 이점

1.3.1. 계산 비용 절감

  • DB 인덱싱과 유사: Full Table Scan vs Index Scan
  • 자주 사용되는 단어는 사전에 등록되어 있어 조합 연산 불필요
처리 방식예시처리 횟수
문자 단위“데이터베이스” → [‘데’,’이’,’터’,’베’,’이’,’스’]6회
토큰 단위“데이터베이스” → [“데이터베이스”]1회
1
2
3
4
5
-- char 단위 = Full Table Scan
SELECT * FROM characters WHERE char IN ('J', 'a', 'v', 'a'); -- 4번 조회

-- Token 단위 = Index Scan
SELECT * FROM tokens WHERE token = 'Java'; -- 1번 조회

1.3.2. 의미 단위 처리

  • 객체 지향의 캡슐화 원리와 유사
  • 자주 함께 등장하는 문자들을 하나의 의미 단위로 묶음
1
2
3
4
5
// 문자 단위 - 의미 없음
char c1 = 'J', c2 = 'a', c3 = 'v', c4 = 'a';

// 토큰 단위 - 의미 있음
Token token = new Token("Java"); // 프로그래밍 언어 개념 캡슐화

1.3.3. 메모리 효율

  • 문자열 대신 정수 ID로 매핑 (Enum 패턴)
  • 메모리 절약 및 연산 속도 향상
1
2
3
4
5
6
7
8
// 실제 처리: 문자열 → 정수 ID
public enum Token {
    HELLO(1), WORLD(2), JAVA(3);
    private final int id;
}

// "Hello World" → [1, 2] (정수 배열)
int[] tokenIds = {1, 2};

1.4. 실제 토큰화 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String prompt = "ChatGPT는 정말 똑똑해!";

// OpenAI Tokenizer 결과
List<String> tokens = Arrays.asList(
    "Chat",      // 1개 토큰
    "GPT",       // 1개 토큰
    "는",        // 1개 토큰
    " 정말",     // 공백 포함 1개 토큰
    " 똑",       // 공백 포함 1개 토큰
    "똑",        // 1개 토큰
    "해",        // 1개 토큰
    "!"          // 1개 토큰
);
// 총 8개 토큰

특징:

  • 공백도 토큰의 일부 (예: " 정말")
  • 영어 단어는 대부분 1개 토큰
  • 한글은 음절별로 분리 가능
  • 특수 기호(!, ?)도 토큰으로 처리

1.5. 토큰 비용 최적화

  • API 요금이 토큰 단위로 청구됨
  • 입력/출력 토큰 수에 따라 비용 결정
1
2
3
4
5
6
7
// OpenAI API 요금 계산 (GPT-4 기준)
int inputTokens = tokenizer.count(prompt);      // 입력: 500 토큰
int outputTokens = tokenizer.count(response);   // 출력: 1000 토큰

double inputCost = inputTokens * 0.00003;   // $0.015
double outputCost = outputTokens * 0.00006;  // $0.06
double totalCost = inputCost + outputCost;   // $0.075

Note

  • 전체 코드베이스 통째 전송 시 토큰 폭증 → 필요한 메서드만 발췌
  • JSON 등 구조화 데이터는 토큰 과다 → 간결하게 요약
  • 이전 대화 누적 시 토큰 증가 → 중요한 것만 컨텍스트 유지

2. Next Token Prediction

2.1. 개념

  • LLM의 핵심: 다음 토큰을 예측하는 함수
  • while 루프로 한 토큰씩 순차 생성
1
2
3
4
5
6
-- LLM의 본질 (SQL 비유)
SELECT next_token
FROM vocabulary
WHERE context = '이전 모든 토큰'
ORDER BY probability DESC
LIMIT 1;

2.2. Java 코드로 표현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class SimpleLLM {
    private Vocabulary vocabulary; // 모든 가능한 토큰 사전
    
    /**
     * 다음 토큰을 확률 기반으로 예측
     */
    public Token predictNextToken(List<Token> context) {
        // 1. 이전 모든 토큰을 보고 각 토큰의 확률 계산
        Map<Token, Double> probabilities = new HashMap<>();
        for (Token candidate : vocabulary.getAllTokens()) {
            double prob = transformer.calculateProbability(context, candidate);
            probabilities.put(candidate, prob);
        }
        
        // 2. 확률 분포에서 샘플링
        // 예: {"는": 0.35, "을": 0.25, "가": 0.20, ...}
        return sampleFromDistribution(probabilities);
    }
    
    /**
     * 전체 문장 생성 (while 루프)
     */
    public String generateText(String prompt) {
        List<Token> tokens = tokenizer.encode(prompt);
        
        // 종료 토큰이 나오거나 최대 길이까지 반복
        while (!isEndToken(tokens.getLast()) && tokens.size() < maxLength) {
            Token next = predictNextToken(tokens); // 다음 토큰 예측
            tokens.add(next);                       // 토큰 추가
        }
        
        return tokenizer.decode(tokens);
    }
}

2.3. 작동 과정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
입력: "Java는"

Step 1: ["Java", "는"] → 확률 분포 계산
  - " 객체": 0.40 (선택)
  - " 프로그래밍": 0.25
  - " 언어": 0.20
현재: "Java는 객체"

Step 2: ["Java", "는", " 객체"] → 확률 분포 계산
  - "지향": 0.60 (선택)
  - "의": 0.25
현재: "Java는 객체지향"

Step 3: ["Java", "는", " 객체", "지향"] → 확률 분포 계산
  - " 프로그래밍": 0.70 (선택)
  - " 언어": 0.20
현재: "Java는 객체지향 프로그래밍"

...반복...

특징:

  • DB Cursor와 유사: 앞으로만 진행, 이전 토큰 수정 불가
  • 한 번에 하나씩 순차 생성 (병렬 생성 불가)
1
2
3
4
5
6
// DB Cursor와 유사
Cursor cursor = database.query("SELECT * FROM tokens");
while (cursor.hasNext()) {
    Token next = cursor.next(); // 앞으로만 진행
    // 이전 토큰은 수정 불가
}

2.4. Temperature 파라미터

  • 확률 분포에서 샘플링 방식 제어
  • Temperature 값에 따라 결정적/확률적 선택
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Token sampleFromDistribution(
    Map<Token, Double> probs, 
    double temperature
) {
    if (temperature == 0.0) {
        // 항상 가장 확률 높은 것만 선택 (결정적)
        return probs.entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .get().getKey();
    } else {
        // 확률 분포에 따라 무작위 선택 (확률적)
        return weightedRandomSample(probs, temperature);
    }
}
Temperature동작예시
0.0항상 가장 확률 높은 토큰 선택“Java는 객체지향 프로그래밍 언어입니다.” (매번 동일)
0.7 (기본)확률 분포에 따라 선택“Java는 객체지향 언어예요.” (다양함)
1.5낮은 확률 토큰도 자주 선택“Java는 커피 이름이기도 해요!” (창의적이지만 이상함)

2.5. 핵심 포인트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// LLM = while 루프 + 확률 분포 샘플링
String response = "";
List<Token> context = tokenize(prompt);

while (!isDone(context)) {
    // 1. 다음 토큰 확률 계산 (이전 모든 토큰 참고)
    Map<Token, Double> probs = calculateProbabilities(context);
    
    // 2. 확률 분포에서 샘플링
    Token next = sample(probs, temperature);
    
    // 3. 컨텍스트에 추가
    context.add(next);
    response += next.getText();
}

return response;

3가지 핵심:

  1. 한 번에 하나씩 순차 생성 (병렬 생성 불가)
  2. 이전 모든 토큰을 보고 다음 토큰 결정
  3. 확률이 가장 높은 것을 선택하지만, 100% 정확하지 않음

3. Hallucination (환각)

3.1. 개념

  • LLM이 사실이 아닌 정보를 그럴듯하게 지어내는 현상
  • 확률 기반 추론의 부작용
1
2
3
4
5
6
사용자: "2024년 출시된 Java 25의 신기능은?"

ChatGPT: "Java 25에는 Pattern Matching for Arrays와 
          Virtual Threads 2.0이 추가되었습니다!"

→ 하지만 Java 25는 아직 출시 안 됨

3.2. 개발자의 버그 vs LLM의 환각

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 개발자의 버그 (결정적 Deterministic)
public int divide(int a, int b) {
    return a / b; 
    // b가 0이면 항상 ArithmeticException 발생
    // 재현 가능, 디버깅 가능
}

// 2. LLM의 환각 (확률적 Probabilistic)
public Token predictNext(List<Token> context) {
    Map<Token, Double> probs = calculateProbabilities(context);
    return sampleFromDistribution(probs);
    // "가장 확률 높은" 토큰이 "정확한" 토큰은 아님
    // 매번 다른 결과 가능
}
구분개발자의 버그LLM의 환각
발생 원인로직 오류, 코딩 실수확률 기반 추론, 학습 데이터 부족
재현성항상 재현 가능매번 다를 수 있음
예측 가능성디버깅으로 찾을 수 있음확률적으로만 예측 가능
해결법코드 수정프롬프트 개선, 외부 검증
책임 소재개발자 실수모델의 본질적 한계

3.3. 환각 발생 원인

3.3.1. 학습 데이터 부족

  • Cache Miss와 유사: 학습 데이터에 없는 정보는 비슷한 패턴으로 추론
1
2
3
4
5
6
7
8
9
10
11
// LLM = 거대한 Cache
Cache<String, String> trainingData = new HashMap<>();
// 2023년 10월까지의 인터넷 데이터만 학습

String query = "2024년 Java 신기능은?";

if (!trainingData.containsKey(query)) {
    // Cache Miss!
    // → 비슷한 패턴으로 "추론"해서 답변 생성
    // → 존재하지 않는 기능을 "그럴듯하게" 지어냄
}

DB vs LLM:

  • DB (Retrieval): 데이터 없으면 null 반환 (솔직함)
  • LLM (Generation): 데이터 없어도 생성 (추측)
1
2
3
4
5
6
7
8
9
// DB: Retrieval - 없으면 없다고 말함
Optional<String> result = database.findById(id);
if (result.isEmpty()) {
    return "데이터를 찾을 수 없습니다.";
}

// LLM: Generation - 없어도 만들어냄
String result = llm.generate(prompt);
// "아마도 이럴 것 같은데..." (추측)

3.3.2. 확률 기반 선택

  • Best Effort Delivery: 가장 확률 높은 토큰도 30% 수준
  • 70% 확률로 다른 토큰이 선택됨
1
2
3
4
5
6
7
8
9
10
Map<Token, Double> probabilities = Map.of(
    "정확한_토큰", 0.30,
    "비슷한_토큰", 0.28,    // ← 선택될 수도
    "그럴듯한_토큰", 0.22,  // ← 선택될 수도
    "관련없는_토큰", 0.10,
    "이상한_토큰", 0.10
);

Token selected = sampleFromDistribution(probabilities);
// 30%만 정확한 토큰이 선택됨

누적 확률:

1
2
3
4
5
6
Step 1: 정확한 토큰 선택 (30%)
Step 2: 정확한 토큰 선택 (30%)
Step 3: 정확한 토큰 선택 (30%)

→ 3단계 모두 정확할 확률 = 0.3³ = 2.7%
→ 10개 토큰: 0.3¹⁰ = 0.0006%

Note
확률적으로는 맞을 수도 있지만, 실제로는 틀릴 수 있음

3.3.3. 컨텍스트 길이 제한

  • JVM Heap 메모리와 유사: 용량 초과 시 오래된 데이터 삭제
1
2
3
4
5
6
7
8
9
10
11
12
13
// LLM의 메모리 제한
public class LLMContext {
    private static final int MAX_TOKENS = 8192; // GPT-4 기준
    private List<Token> context = new ArrayList<>();
    
    public void addToken(Token token) {
        context.add(token);
        if (context.size() > MAX_TOKENS) {
            // 오래된 토큰은 "잊어버림" (FIFO Queue)
            context.remove(0);
        }
    }
}

예시:

1
2
3
4
5
6
7
8
사용자: "내 이름은 김개발이야."
ChatGPT: "안녕하세요, 개발님!"

...(8000 토큰 분량의 대화)...

사용자: "내 이름이 뭐였지?"
ChatGPT: "죄송하지만 대화 기록에서 찾을 수 없네요."
← 초반 대화가 메모리에서 삭제됨

3.4. 환각 vs Not Found 에러

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 데이터베이스 - 솔직하게 에러 발생
try {
    User user = database.findById(999);
} catch (NotFoundException e) {
    throw new ResponseStatusException(
        HttpStatus.NOT_FOUND, 
        "User not found"
    ); // 명확한 에러
}

// 2. LLM - 억지로라도 답변 생성
String prompt = "존재하지 않는 Java 25 기능 알려줘";
String response = llm.generate(prompt);
// "Java 25에는 Pattern Matching for Arrays가 있습니다!"
// ← 존재하지 않는 기능을 지어냄 (에러 아님)
상황데이터베이스LLM
데이터 없음404 Not Found 에러그럴듯한 답변 생성 (환각)
모호한 쿼리400 Bad Request 에러추측해서 답변
잘못된 입력예외 발생최선의 답변 시도

설계 이유:

  • 대화형 서비스 특성상 “모르겠다”보다 “아마 이럴 거야”가 사용자 경험상 유리
  • 하지만 정확성을 해치는 주요 원인

3.5. 환각 완화 전략

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Service
public class AIAssistantService {
    @Autowired
    private OpenAIClient openAI;
    @Autowired
    private DocumentRepository documentRepo;
    
    public String answerQuestion(String question) {
        // 1. RAG: 내부 문서에서 검색
        List<String> relevantDocs = documentRepo
            .searchByEmbedding(question, limit = 3);
        
        // 2. 문서와 함께 프롬프트 작성
        String prompt = String.format("""
            다음 문서를 참고해서 질문에 답변해주세요.
            정확한 정보만 답변하고, 모르면 "모르겠습니다"라고 답변하세요.
            
            [참고 문서]
            %s
            
            [질문]
            %s
            """, String.join("\n\n", relevantDocs), question);
        
        // 3. Temperature 낮춰서 결정적으로
        String response = openAI.chat(prompt, temperature = 0.3);
        
        // 4. 응답 검증
        if (containsUncertainPhrases(response)) {
            logger.warn("LLM response contains uncertainty: {}", response);
        }
        
        return response;
    }
}

핵심 전략:

  1. RAG (Retrieval-Augmented Generation): DB에서 실제 사실 먼저 검색
  2. Temperature 낮추기: 확률적 선택을 줄여서 결정적으로
  3. 명확한 지시: “모르면 모르겠다고 말하세요” 명시
  4. 외부 검증: 응답을 받은 후 실제 DB나 API로 검증

4. 정리

4.1. LLM의 본질

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * LLM의 본질을 한 줄로 표현
 */
public class LLM {
    public String generate(String prompt) {
        List<Token> tokens = tokenize(prompt);
        
        while (!isDone(tokens)) {
            // 1. 확률 계산
            Map<Token, Double> probs = transformer.predict(tokens);
            
            // 2. 확률 기반 샘플링 (100% 정확하지 않음)
            Token next = sample(probs);
            
            // 3. 토큰 추가
            tokens.add(next);
        }
        
        return detokenize(tokens);
    }
}

4.2. 핵심 개념 정리

개념개발자 비유핵심 포인트
TokenDB Index, 어셈블리어의미 단위로 처리, 계산 비용 절감
Next Token Predictionwhile 루프 + SQL ORDER BY probability한 번에 하나씩 순차 생성
HallucinationCache Miss + Best Effort없는 정보를 확률적으로 지어냄

4.3. 실전 팁

4.3.1. 토큰 비용 최적화

1
2
3
4
5
// Bad: 전체 코드를 다 넣음
String prompt = "이 코드를 리팩토링해줘:\n" + entireCodebase;

// Good: 필요한 부분만 발췌
String prompt = "이 메서드를 리팩토링해줘:\n" + targetMethod;

4.3.2. 환각 방지

1
2
3
4
5
6
7
String prompt = """
    다음 질문에 답변해주세요.
    확실하지 않으면 "모르겠습니다"라고 답변하세요.
    추측하지 말고, 사실만 말씀해주세요.
    
    질문: %s
    """.formatted(userQuestion);

4.3.3. 확률적 특성 이해

1
2
3
4
5
6
7
8
// 같은 질문도 매번 다른 답변 (temperature > 0)
String answer1 = llm.generate(prompt, temperature = 0.7);
String answer2 = llm.generate(prompt, temperature = 0.7);
// answer1 != answer2 (확률적)

// 결정적 답변 필요 시
String answer = llm.generate(prompt, temperature = 0.0);
// 항상 동일한 답변

참고 자료

  • OpenAI Tokenizer: https://platform.openai.com/tokenizer
  • Attention Is All You Need (Transformer 논문): https://arxiv.org/abs/1706.03762
  • GPT-3 Paper: https://arxiv.org/abs/2005.14165
Contents

[REACT] 폰트, 이미지, 레이아웃 설정

[AI 도구] Cursor IDE - 설치 및 Codebase 인덱싱 설정