翻译求助(번역 도움 요청)의 내부 구조: LectuLibre에서 문맥 인식 번역 어시스턴트 구축하기
요약
LectuLibre가 Python, FastAPI, LLM을 활용하여 구축한 문맥 인식 번역 수정 시스템의 아키텍처를 소개합니다. 사용자가 특정 구절을 선택해 문맥을 유지하며 실시간으로 번역을 개선할 수 있는 기술적 구현 방법을 다룹니다.
핵심 포인트
- FastAPI와 비동기 I/O를 활용한 고성능 백엔드 구축
- Claude 3 Haiku와 DeepSeek-V2를 결합한 모델 선택 전략
- PostgreSQL과 Redis를 이용한 문맥 유지 및 캐싱 최적화
- 전체 재번역 없이 특정 구절만 수정하는 선택적 정제 기능
우리가 Python, FastAPI, 그리고 LLM을 결합하여 사용자가 실시간으로 도서 번역을 수정할 수 있도록 만든 방법.
서론 (Introduction)
LectuLibre에서는 AI를 사용하여 사용자가 책 전체를 번역할 수 있도록 돕고 있습니다. Claude나 DeepSeek와 같은 LLM(대규모 언어 모델)은 인상적인 성능을 보여주지만, 관용구, 문화적 참조 또는 복잡한 문장이 가득한 문학 텍스트의 경우 때때로 미묘한 차이를 놓치기도 합니다. 초기 사용자들은 전체 장(chapter)을 다시 생성하는 대신 여기저기서 몇 단락만 수정할 수 있기를 바란다고 말했습니다. 그것이 우리의 “翻译求助(번역 도움 요청)” 기능의 탄생 배경입니다. 이 기능은 어떤 구절이든 선택하여 왜 변경했는지에 대한 짧은 설명과 함께 대안 번역을 얻을 수 있는 간단한 방법입니다.
이 글에서는 기술적 여정—우리가 직면했던 도전 과제, 내린 아키텍처(architectural) 결정, 그리고 문맥 인식 번역 수정(context-aware translation refinement)을 대규모로 작동하게 만들기 위해 작성한 코드—을 공유하고자 합니다.
문제점: 역동적인 세상에서의 정적 번역 (The Problem: Static Translations in a Dynamic World)
초기에 우리의 번역 파이프라인(pipeline)은 간단했습니다: 책 업로드 → 장(chapter) 단위로 분할(chunk) → 각 청크를 LLM에 전송 → 결과물 결합. 이 방식은 작동했지만, 일회성 프로세스였습니다. 만약 사용자가 특정 문장이 마음에 들지 않는다면, 다른 곳에서 수행했을지도 모를 수동 편집 사항을 모두 잃으면서 전체 장을 다시 번역하는 것 외에는 방법이 없었습니다.
우리는 사용자가 전체적인 흐름을 깨뜨리지 않으면서 번역된 텍스트와 상호작용하고 개선을 요청할 수 있는 방법이 필요했습니다. 핵심 요구 사항은 다음과 같았습니다:
- 선택적 수정 (Selective refinement): 사용자가 어떤 구절이든 강조 표시하고 더 나은 번역을 요청할 수 있도록 함.
- 문맥 인식 (Context awareness): 일관성을 유지하기 위해 LLM이 주변 텍스트를 고려해야 함.
- 투명성 (Transparency): 무엇이 왜 바뀌었는지에 대한 짧은 설명을 제공하여 사용자가 이를 통해 배울 수 있도록 함.
- 속도 (Speed): 거의 즉각적으로 느껴져야 함; 아무도 문장 하나를 위해 10초를 기다리지 않음.
우리의 접근 방식: 설계 결정 및 도구 (Our Approach: Design Decisions and Tooling)
우리는 구절 인덱스(번역된 문서 내 시작 및 종료 위치)를 수락하고, 데이터베이스에서 주변 문맥(context)을 가져오며, 맞춤형 LLM 프롬프트(prompt)를 구성한 뒤, 개선된 번역과 설명을 반환하는 전용 REST 엔드포인트(endpoint)를 구축하기로 결정했습니다.
스택 (Stack)
- 백엔드 (Backend): 비동기 I/O 성능을 위해 Python 및 FastAPI 사용.
- LLMs: 속도를 위해 Anthropic의 Claude 3 Haiku를, 품질을 위해 DeepSeek-V2를 사용합니다. 이 기능을 통해 사용자 설정에 따라 모델을 선택할 수 있습니다.
- 데이터베이스 (Database): PostgreSQL에 원본 도서 텍스트, 번역된 텍스트 및 사용자 편집 사항을 저장합니다. 청크 인덱스(chunk indices)를 사용하여 주변 세그먼트(segments)를 쿼리합니다.
- 캐싱 (Caching): 비용 절감을 위해 동일한 문맥/문구에 대한 단기 LLM 응답을 저장하는 데 Redis를 사용합니다.
왜 채팅 인터페이스가 아닌가? (Why Not a Chat Interface?)
일부 플랫폼은 번역 개선을 위해 대화형 접근 방식을 사용하지만, 우리는 채팅을 통해 LLM에게 "이 번역을 개선해줘"라고 요청하는 것이 가변적인 결과를 초래한다는 것을 발견했습니다. 명시적인 지침이 포함된 구조화된 프롬프트(structured prompt)를 사용하는 것이 출력 형식에 대해 더 많은 제어권을 제공했습니다.
구현 세부 사항 (Implementation Details)
1. API 엔드포인트 (The API Endpoint)
우리는 POST /api/translation-help 엔드포인트를 생성했습니다. 요청 본문(request body)에는 book_id, segment_start, segment_end가 포함되며, 선택적으로 model 선호도가 포함됩니다. 응답은 refined_translation과 explanation을 포함하는 JSON 객체입니다.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
...
2. 문맥 검색: 적절한 이웃 제공하기 (Context Retrieval: Feeding the Right Neighbors)
문맥 없는 번역 개선은 종종 단절된 결과를 생성합니다. 예를 들어, LLM에게 "그는 웃었다"로 시작하는 문장을 재번역하도록 요청했는데, 앞선 문단이 슬픈 어조로 끝났다면 출력 결과의 어조가 틀릴 수 있습니다. 따라서 우리는 대상 세그먼트의 앞뒤로 N개의 문단 범위를 가져옵니다. 우리의 도서 텍스트는 문단 단위의 인덱스와 함께 저장됩니다.
async def fetch_context(book_id: str, start: int, end: int, window=3) -> dict:
query = """
SELECT paragraph_index, original_text, translated_text
...```
윈도우 크기(window size)는 설정 가능합니다. 우리는 양옆으로 3개의 문단을 가져오는 것이 충분한 문맥(context)을 제공하면서도 토큰 제한(token limits) 내에 머무는 적절한 균형점임을 확인했습니다.
### 3. LLM 프롬프트 제작 (Crafting the LLM Prompt)
프롬프트 엔지니어링 (Prompt engineering)이 결정적인 차이를 만들었습니다. 우리는 응답에 개선된 번역과 간결한 설명이라는 두 가지 명확한 부분이 포함되기를 원했습니다. 우리는 역할과 출력 형식을 설정하는 시스템 프롬프트 (system prompt)와 텍스트를 주입하는 사용자 프롬프트 (user prompt)를 설계했습니다.
SYSTEM_PROMPT = """당신은 전문적인 문학 번역 보조원입니다. 책의 구절과 현재 번역본이 주어지면, 당신의 임무는 뉘앙스, 흐름, 문화적 정확성에 주의를 기울여 번역을 개선하는 것입니다. 당신은 반드시 "refined_translation"과 "explanation"이라는 정확히 두 개의 필드를 가진 JSON 객체를 출력해야 합니다. 오직 JSON만 출력하고 다른 텍스트는 출력하지 마세요."""
USER_PROMPT_TEMPLATE = """원본 구절: {target_original}
...```
우리는 신뢰성 있게 파싱(parse)하기 쉽도록 JSON 출력을 강제합니다. LLM 응답을 받은 후에는 json.loads()를 사용하며, 모델이 잘못된 형식의 JSON을 반환할 경우를 대비한 에러 핸들링 (error handling)을 포함합니다.
4. 비동기 LLM 호출 및 폴백 (Async LLM Calls and Fallback)
우리는 여러 모델을 지원하기 때문에 LLM 클라이언트 (client)를 추상화합니다. Claude의 API와 DeepSeek의 API에 대한 비동기 HTTP 호출을 위해 httpx를 사용합니다. 엔드포인트 (endpoint)의 응답성을 유지하기 위해 타임아웃 (timeout)을 8초로 설정했습니다. 만약 한 모델이 실패하면 다른 모델을 시도합니다.
import httpx
import json
...
우리는 평균 지연 시간 (latency)을 측정했습니다: Claude Haiku는 약 3.2초, DeepSeek는 약 4.7초였으며, 둘 다 우리의 목표 범위 내에 있었습니다.
5. 캐싱 전략 (Caching Strategy)
사용자들은 종종 동일한 구절을 여러 번 다듬습니다. 불필요한 LLM 호출을 피하기 위해, 우리는 (book_id, segment_start, segment_end, model)을 키로 사용하여 결과를 캐싱(Caching)합니다. 이는 1시간의 TTL(Time To Live)과 함께 Redis에 저장됩니다. 만약 사용자의 다른 편집으로 인해 주변 문맥이 변경되면, 중첩되는 범위에 대한 캐시를 무효화(Invalidate)합니다. 우리는 간단한 버전 관리 체계를 사용했습니다. 각 도서에는 편집 시마다 증가하는 content_version이 있으며, 캐시 키에는 이 버전이 포함됩니다. 이는 보수적인 방식이지만 정확합니다.
도전 과제 및 트레이드오프 (Challenges and Trade-offs)
- 토큰 제한 (Token limits): 도서의 단락은 길 수 있습니다. 우리는 간단한 휴리스틱(단락당 처음 1000자)을 사용하여 주변 문맥을 2000 토큰으로 자릅니다(Truncate). 때때로 이 방식은 중요한 문맥을 잘라내기도 합니다. 향후에는 동적 크기를 가진 슬라이딩 윈도우(Sliding window)를 사용할 계획입니다.
- 프롬프트 인젝션 (Prompt injection): 사용자가 악의적인 지침이 포함된 텍스트를 선택할 수 있습니다. 우리는 입력을 정화(Sanitize)하지만, LLM은 본질적으로 취약합니다. 우리는 엄격한 출력 파싱(Output parsing)과 모델의 온도(Temperature) 제한을 통해 이를 완화합니다.
- 비용 (Cost): 각 다듬기(Refinement) 호출에는 몇 센트의 비용이 듭니다. 캐싱이 도움이 되지만, 헤비 유저(Heavy users)는 비용을 크게 발생시킬 수 있습니다. 이에 따라 사용자당 속도 제한(Rate limits)을 추가했습니다.
- 문화적 뉘앙스 (Cultural nuance): 번역이 왜 바뀌었는지 설명할 때 때때로 문화적 배경이 필요하지만, LLM이 이를 항상 정확하게 파악하는 것은 아닙니다. 우리는 문학적 참조 지식 베이스를 활용한 RAG(Retrieval-Augmented Generation)를 실험하고 있습니다.
결과 및 사용자 피드백 (Results and User Feedback)
기능 출시 후, 다음과 같은 결과를 확인했습니다:
- 도서당 수동 재번역 요청이 23% 감소했습니다.
- 사용자들은 翻译求助를 사용한 후 편집에 소요되는 시간이 줄어들었다고 보고했습니다.
- 설명 부분은 우리를 놀라게 했습니다. 많은 비원어민 사용자들이 관용구(Idiomatic expressions)가 왜 특정 방식으로 번역되는지 배우기 위해 이 기능을 사용하고 있습니다.
현재 우리는 프롬프트를 더욱 미세 조정(Fine-tune)할 수 있도록 사용자가 설명에 대해 피드백을 제공하는 방법을 탐색하고 있습니다.
교훈 (Lessons Learned)
- 문맥이 핵심이다 (Context is king): 컨텍스트 윈도우(Window)를 확장하는 것만으로도 출력 품질이 크게 향상되었습니다. 초기에는 현재 문단만을 사용했는데, 이 경우 결과물의 문체(Stylistically)가 자주 맞지 않는 문제가 발생했습니다.
- JSON 출력 제약 (JSON output constraints): LLM이 특정 스키마(Schema)를 가진 JSON을 출력하도록 강제함으로써 파싱(Parsing) 오류를 줄이고 기능을 더욱 견고하게 만들었습니다.
- 캐싱은 간단하지 않다 (Caching is non-trivial): 단순한 캐싱은 사용자가 주변 텍스트를 편집하기 시작하면 한계에 부딪힙니다. 변경 사항을 전파(Propagate)할 수 있는 전략이 필요합니다.
- 비동기의 중요성 (Async matters): FastAPI의 비동기(Async) 기능 덕분에 부하가 걸리는 상황에서도 엔드포인트(Endpoint)의 응답성을 유지할 수 있었습니다.
다음 단계는? (What’s Next?)
우리는 사용자가 지문과 함께 자연어 지시사항(예: "더 격식 있게 만들어줘" 또는 "더 쉬운 단어를 사용해줘")을 전달할 수 있도록 하는 방안을 고려하고 있습니다. 이것이 구현된다면 翻译求助는 완전한 형태의 반복적 번역 어시스턴트(Iterative translation assistant)로 거듭날 것입니다.
커뮤니티의 의견을 듣고 싶습니다. LLM 기반의 편집 작업에서 문맥 주입(Context injection)을 어떻게 처리하시나요? 가변적인 텍스트(Mutable text)에 대해 더 효과적이었던 캐싱 패턴이 있을까요? 여러분의 생각을 댓글로 남겨주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기