
논문 번역기에 glossary 시스템을 붙인 직후였다. 대용량 논문을 빨리 처리하겠다고 페이지 범위를 나눠 서브에이전트 여러 개를 동시에 돌렸는데, 결과가 엉망이었다. 두 에이전트가 동시에 같은 glossary.json을 수정하면서 한쪽 데이터가 통째로 날아간 거다.
문제의 원인은 명확했다. 하나의 에이전트가 용어 추출부터 파일 저장, 품질 검수, 번역 실행까지 다 하고 있었다. 프롬프트가 복잡해지니 역할 혼동이 생기고, 병렬 실행하면 파일 I/O가 충돌했다. 전통적인 소프트웨어 설계에서 이미 답을 알고 있는 문제였는데 에이전트라고 다를 게 없었다.
핵심 원칙은 단순하다. 서브에이전트는 데이터만 반환하고, 파일 I/O는 메인 에이전트가 독점한다.
서브에이전트(Claude Haiku)는 PDF 텍스트에서 용어를 추출해 JSON dict로 반환만 한다. 파일을 만들거나 기존 파일을 수정하는 건 절대 안 된다. 메인 에이전트(Claude Opus)가 그 결과를 받아서 검수하고, 불필요한 항목을 걸러낸 뒤 스크립트를 통해 파일로 저장한다.
이전 글에서 만든 마스터 용어 사전(glossary.json)과 논문별 CSV 시스템은 그대로 유지하되 "누가 쓰는가"만 명확히 분리한 셈이다.
번역 요청이 들어오면 가장 먼저 glossary_output/<논문>.glossary.csv 파일이 있는지 확인한다. 이미 있으면 용어 추출 과정 전체를 스킵하고 바로 번역으로 넘어간다.
처음에는 번역 직전에 체크했었는데 비효율적이었다. 페이지 수 파악하고 서브에이전트 몇 개 띄울지 결정하는 것만 해도 시간이 꽤 걸리거든. glossary가 이미 있으면 그 모든 과정이 필요 없으니 "가장 먼저" 확인하는 게 맞다.
glossary가 없으면 전체 경로를 탄다. 대용량 논문은 페이지 범위를 나눠 서브에이전트 여러 개가 병렬로 용어를 추출한다. 각 에이전트는 자기 범위의 텍스트를 읽고 JSON dict만 돌려준다. 파일은 안 건드린다.
메인 에이전트가 결과를 모아 검수한 뒤 agent_total_glossary.py --merge를 호출한다. 이 스크립트가 분할된 CSV를 하나로 합치고 원본 분할 파일은 삭제해준다.
def merge_csvs(input_patterns, output_path):
merged = OrderedDict()
for csv_path in input_files:
with csv_path.open("r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
src = row.get("source", "").strip()
if src and src not in merged:
merged[src] = row.get("target", "").strip()
# 원본 분할 파일 삭제
for csv_path in input_files:
if str(csv_path.resolve()) != out_real:
csv_path.unlink()OrderedDict를 쓰는 이유가 있다. Python 3.7+ dict도 삽입 순서를 보장하지만 JSON 직렬화할 때 sort_keys=True를 쓰면 순서가 날아간다. 신규 용어를 맨 위에 두고 싶으면 sort_keys=False + OrderedDict 조합이 필요하다.
glossary.json에서 신규 용어를 맨 위에 넣는 건 소소하지만 실용적인 변경이다. 논문을 번역하고 나서 용어 사전을 열어볼 때 방금 작업한 논문의 용어가 맨 위에 있으면 확인이 빠르다.
# 신규 용어를 맨 위에, 기존 용어를 아래에
updated_master = OrderedDict()
for src, tgt in added.items():
updated_master[src] = tgt
for src, tgt in master.items():
if src not in updated_master:
updated_master[src] = tgt처음에는 알파벳순 정렬이었는데 수백 개 용어가 쌓이니 최근 추가한 항목을 찾기가 어려웠다. 역시간순으로 바꾸고 나서 작업 흐름이 훨씬 매끄러워졌다.
리팩토링하면서 I/O 디렉토리도 정리했다. pdf_input/과 pdf_output/과 glossary_output/ 세 디렉토리로 입출력을 분리하고 출력 파일명에서 모델 태그도 제거했다. paper.translategemma-27b.mono.pdf 같은 긴 이름 대신 paper.mono.pdf로 단순화했다. 어차피 어떤 모델로 번역했는지는 디렉토리 구조나 로그에서 추적 가능하니까.
이번 리팩토링에서 가장 크게 배운 건 "AI 에이전트도 결국 소프트웨어"라는 점이다.
서브에이전트가 파일을 직접 저장하게 하니까 동시성 문제가 터졌다. 멀티스레드 프로그래밍에서 공유 자원 접근을 통제하는 것과 똑같은 문제다. 해법도 같았다. 파일 쓰기 권한을 한 곳(메인 에이전트)으로 집중시키면 된다.
Haiku로 돌리는 서브에이전트가 "있으나 마나한" 용어(source와 target이 같은 약어)를 걸러내지 못하는 것도 재밌는 포인트다. 추출은 저비용 모델로 빠르게, 검수는 고비용 모델로 꼼꼼하게. 사람 조직에서 주니어가 초안을 쓰고 시니어가 리뷰하는 것과 크게 다르지 않다.
관심사 분리, 동시성 제어, Short-Circuit 최적화. 멀티 에이전트 시스템을 설계할 때도 전통적인 설계 원칙이 그대로 먹힌다. LLM이라고 마법이 아니다.