왜 어휘 제어가 필요한가

LLM을 서비스에 붙이면 "특정 단어를 절대 쓰지 마라" 혹은 "정해진 라벨 중 하나만 출력하라" 같은 요구가 반드시 생긴다. 프롬프트로 "다음 중 하나만 답하라"고 지시해도 모델은 설명을 덧붙이거나, 금칙어를 회피 표현으로 우회하거나, 포맷을 깨뜨린다. 프롬프트는 확률 분포를 기울일 뿐 강제하지 못한다. 반면 로짓 바이어스는 토큰이 샘플링되기 직전의 로짓(logit) 값에 직접 상수를 더해, 특정 토큰의 등장 확률을 통계가 아니라 디코딩 단계에서 조정한다.

로짓 바이어스의 동작 원리

디코딩 시 각 토큰은 로짓 값을 가지며, softmax를 거쳐 확률이 된다. 로짓 바이어스는 지정한 토큰 ID의 로짓에 값을 더한다. 양수는 등장 확률을 높이고, 음수는 낮춘다. OpenAI 계열 API 기준 -100은 사실상 완전 금지, +100은 사실상 강제에 가깝다. 핵심은 토큰 ID 단위로 동작한다는 점이다. "apple"이라는 단어가 app+le로 쪼개지면, 두 서브워드를 모두 다뤄야 하고 앞뒤 공백 유무( apple vs apple)에 따라 ID가 달라진다.

기본 예시: 금칙어 억제

tiktoken으로 대상 토큰 ID를 얻어 바이어스를 건다. 공백 프리픽스 변형까지 함께 처리하는 것이 실무 포인트다.

import tiktoken
from openai import OpenAI

client = OpenAI()
enc = tiktoken.encoding_for_model("gpt-4o-mini")

def ban_words(words):
    bias = {}
    for w in words:
        for variant in (w, " " + w, w.capitalize(), " " + w.capitalize()):
            for tid in enc.encode(variant):
                bias[tid] = -100
    return bias

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "이 제품을 한 문장으로 소개해줘"}],
    logit_bias=ban_words(["무료", "최고", "혁신"]),
    max_tokens=60,
)
print(resp.choices[0].message.content)

강한 제약: 분류 라벨 강제

감성 분석처럼 출력을 닫힌 집합으로 묶어야 할 때는, 허용 토큰에만 큰 양수 바이어스를 주고 max_tokens=1로 제한한다. 단일 토큰으로 표현되는 라벨(예: 숫자)을 고르면 안정성이 높다.

# 0=부정, 1=중립, 2=긍정 — 각 라벨을 단일 토큰으로 설계
labels = {enc.encode("0")[0]: 100,
          enc.encode("1")[0]: 100,
          enc.encode("2")[0]: 100}

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "리뷰 감성을 0/1/2 중 하나로만 답하라."},
        {"role": "user", "content": "배송이 너무 느리고 실망했어요."},
    ],
    logit_bias=labels,
    max_tokens=1,
    temperature=0,
)
print(resp.choices[0].message.content)  # -> 0

대안 기법과 비교

로짓 바이어스가 항상 최선은 아니다. 상황별로 선택지가 다르다.

기법강제력비용/난이도적합한 경우
프롬프트 지시낮음매우 낮음느슨한 스타일 유도
로짓 바이어스중~높음낮음(토큰 ID 관리 필요)소수 토큰 금지/유도, 라벨 고정
구조화 출력·문법 제약(GBNF 등)매우 높음중간JSON/스키마 등 복잡한 형식 강제

어휘가 수십 개 이내면 로짓 바이어스가 가볍고 빠르다. 반면 중첩 JSON처럼 구조 자체를 보장해야 한다면 문법 기반 제약 디코딩이 맞다.

실무 주의점

첫째, 토크나이저 의존성이다. 모델마다 토큰 ID가 다르므로 모델 교체 시 바이어스 맵을 반드시 재생성해야 한다. 둘째, 서브워드 함정이다. "카드"를 막아도 다른 조합으로 우회될 수 있어, 완전 차단은 어렵고 억제로 이해하는 편이 안전하다. 셋째, 과도한 양수 바이어스는 문맥을 무시한 반복·붕괴를 유발한다. +100은 라벨 강제처럼 출력 길이가 짧을 때만 쓰고, 자유 생성에는 +2~+8 정도의 완만한 값을 쓴다. 넷째, API마다 허용 개수 제한(예: 300개)이 있으니 대량 금칙어는 후처리 필터와 병행해야 한다.

정리

로짓 바이어스는 프롬프트와 구조화 출력 사이의 실용적인 중간 지대다. 소수 토큰을 확실히 억제하거나 닫힌 라벨을 강제할 때 가장 효율적이다. 토큰 ID를 정확히 다루고, 공백·서브워드 변형을 관리하며, 강제력이 필요한 범위에서만 값을 조절하면 디코딩 단계에서 출력 어휘를 안정적으로 통제할 수 있다.