왜 어휘 제어가 필요한가
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를 정확히 다루고, 공백·서브워드 변형을 관리하며, 강제력이 필요한 범위에서만 값을 조절하면 디코딩 단계에서 출력 어휘를 안정적으로 통제할 수 있다.