
논문을 번역하다 보면 한 가지 문제가 반복해서 발생한다. "attention mechanism"이라는 용어 하나가 어떤 페이지에선 "주의 메커니즘", 다른 페이지에선 "어텐션 메카니즘", 또 다른 곳에선 "주목 기법"으로 번역된다. 한 논문 안에서 이렇게 흔들리면 읽는 사람 입장에서 같은 개념인지 다른 개념인지 헷갈린다.
한두 편이면 수동으로 고쳐도 되겠지만 여러 논문을 번역하기 시작하면 프로젝트 전체에서 용어 통일이 필요해진다. 이전 글에서 BabelDOC에 커스텀 번역 모델을 붙이는 작업까지 마쳤는데, 번역 품질 자체보다 이 일관성 문제가 더 골치였다.
그래서 Google의 TranslateGemma 27B 모델이 glossary 주입을 네이티브로 지원한다는 걸 발견했을 때 바로 이거다 싶었다. 모델을 파인튜닝하거나 후처리로 치환하는 게 아니라, 번역 시점에 "이 용어는 이렇게 번역해"라고 알려주는 방식이다.
시스템은 크게 세 단계로 나뉜다. 용어 추출, 마스터 사전 관리, 번역 실행이다.
핵심 아이디어는 glossary.json이라는 마스터 용어 사전 파일 하나를 중심에 두는 것이다. 모든 논문에서 추출된 용어가 여기에 누적되고, 번역할 때는 해당 논문에 필요한 용어만 뽑아서 CSV로 만든 뒤 BabelDOC의 Glossary API에 주입한다.
BabelDOC에 TranslateGemma 27B를 커스텀 Translator로 붙였다. 기존에 만들었던 Rosetta나 GLM Translator와 같은 패턴인데 결정적 차이가 있다. TranslateGemma는 glossary 주입을 네이티브로 지원하는 첫 번째 모델이라는 점이다.
번역 스크립트가 시작되면 glossary.json을 자동으로 읽어서 BabelDOC의 Glossary 객체로 변환한다.
GLOSSARY_JSON = Path(__file__).parent / "glossary.json"
if GLOSSARY_JSON.exists():
master = json.loads(GLOSSARY_JSON.read_text(encoding="utf-8"))
if master:
entries = [GlossaryEntry(src, tgt) for src, tgt in master.items()]
glossaries.append(Glossary(name="master", entries=entries))
print(f"Loaded master glossary ({len(entries)} terms)")TranslateGemma는 번역 전용 모델이라 auto_extract_glossary=False로 설정해야 한다. 용어 자동 추출 기능(do_llm_translate)이 구현되어 있지 않기 때문이다. 이전에 Rosetta Translator에서 같은 실수를 했던 경험이 있어서 이번엔 바로 잡았다.
config = TranslationConfig(
translator=translator,
glossaries=glossaries if glossaries else None,
auto_extract_glossary=False, # 번역 전용 모델
use_alternating_pages_dual=True, # 원문-번역 교대 페이지
)용어 관리의 핵심 로직은 agent_total_glossary.py에 있다. 서브에이전트가 PDF에서 추출한 용어 목록을 받아서 마스터 사전과 병합하는 스크립트다.
규칙은 단순하다. 마스터에 이미 있는 용어는 마스터의 번역을 따르고, 새로운 용어만 추가한다.
# 신규 용어만 마스터에 추가, 기존 항목은 마스터의 번역 유지
for src, tgt in new_terms.items():
if src in master:
existing[src] = master[src] # 마스터의 기존 번역 사용
else:
master[src] = tgt
added[src] = tgt
# 논문별 CSV = 기존 + 신규 전부 포함
paper_terms = {**existing, **added}이렇게 하면 논문 A에서 "attention mechanism"을 "어텐션 메커니즘"으로 번역했다면, 나중에 논문 B를 번역할 때도 같은 용어가 같은 번역으로 나온다. 서브에이전트가 논문 B에서 다른 번역을 제안하더라도 마스터에 이미 등록된 번역이 우선한다.
논문별 CSV는 마스터의 부분집합이다. 전체 마스터를 통째로 넣지 않고 해당 논문에 등장하는 용어만 골라서 CSV로 만든다. 왜 이렇게 하는지는 삽질 이야기에서 다룬다.
용어를 수동으로 정리하면 한 논문당 30분은 걸린다. 그래서 AI 에이전트에게 맡겼는데, 한 단계로는 부족했다.
먼저 agent_extract_pdf_text.py로 PDF에서 텍스트를 추출한다. 그 다음 서브에이전트(Haiku)가 텍스트를 훑으며 용어 후보를 뽑는다. 빠르고 저렴하지만 정확도가 떨어진다. source와 target이 동일한 약어("GPU" -> "GPU" 같은)를 용어로 잡아내는 식이다. 있으나 마나한 항목이 꽤 섞여 들어온다.
그래서 메인 에이전트(Opus)가 검수하는 2단계 구조로 설계했다. Haiku가 추출한 용어 목록을 Opus가 검토해서 쓸모없는 항목을 걸러내고 번역 품질을 확인한다. 비용 대비 품질의 균형을 잡은 셈이다.
처음에는 마스터 glossary.json 전체를 번역할 때마다 통째로 주입했다. 용어가 30개일 때는 문제가 없었다. 그런데 50개, 100개로 늘어나면서 번역 품질이 눈에 띄게 떨어지기 시작했다.
원인은 간단했다. TranslateGemma가 glossary를 프롬프트에 직접 삽입하는 방식이라 용어가 많을수록 context window에서 본문이 차지할 수 있는 공간이 줄어든다. 번역해야 할 텍스트가 잘리면 당연히 결과도 나빠진다.
해결 방법이 논문별 CSV다. 마스터에 용어를 100개 쌓아두더라도 논문 A를 번역할 때는 논문 A에 등장하는 용어 20개만 골라서 넣는다. 이렇게 하니 context window 문제가 사라졌다.
또 한 가지. glossary.json의 key 정렬을 처음에 알파벳순으로 했다가 역시간순(최근 추가한 것이 위)으로 바꿨다. 실제로 사전을 열어서 용어를 확인할 일이 잦은데, 알파벳순이면 방금 작업한 논문의 용어를 찾으려고 한참 스크롤해야 했다. 작은 변경이지만 작업 흐름에서 체감 차이가 크다.
이번 작업에서 쓴 방법을 일반화하면 "컨텍스트 주입" 패턴이라고 부를 수 있겠다. LLM의 출력 일관성이 필요할 때 모델 자체를 바꾸는 게 아니라 매 요청마다 필요한 맥락을 주입하는 방식이다.
파인튜닝은 비싸고 모델이 바뀔 때마다 다시 해야 한다. 후처리 치환은 문맥을 무시해서 어색한 결과가 나올 수 있다. glossary injection은 그 중간 지점이다. 모델이 번역하는 시점에 "이 용어는 이렇게 써"라고 컨텍스트를 넣어주니 자연스러운 문장 안에서 용어만 고정된다.
번역뿐 아니라 코드 생성이나 문서 작성처럼 LLM 출력의 일관성이 중요한 영역이라면 어디든 적용할 수 있는 접근법이다. 용어 사전 대신 코딩 컨벤션이나 문서 스타일 가이드를 주입하면 된다.
결국 만든 건 세 가지다.
모델을 바꾸거나 파인튜닝할 필요 없이, JSON 파일 하나와 스크립트 하나로 번역 일관성 문제를 해결했다. 다음 글에서는 이 glossary 워크플로우를 에이전트가 자동으로 돌리도록 리팩토링한 과정을 다룬다.