Python과 LLM을 활용한 도서 번역용 즉각적 번역 지원 도구 구축하기
요약
LectuLibre 플랫폼에서 사용자의 실시간 번역 피드백 요구를 충족하기 위해 구축한 즉각적 번역 지원 도구의 구현 과정을 다룹니다. SSE(Server-Sent Events)와 Claude 3 Haiku를 활용하여 문맥을 유지하면서도 1초 미만의 낮은 지연 시간을 달성하는 기술적 방법을 소개합니다.
핵심 포인트
- SSE를 활용한 실시간 토큰 스트리밍 구현
- 주변 문단을 포함한 문맥 기반 프롬프트 엔지니어링
- Claude 3 Haiku를 통한 저지연 고품질 번역 달성
- PostgreSQL 기반의 문단 인덱싱 및 컨텍스트 검색
우리가 어떻게 AI 기반 도서 번역 워크플로우에 실시간 구절 번역 피드백을 통합했는지, 그리고 지연 시간(latency), 문맥(context), 프롬프트 엔지니어링(prompt engineering)에 대해 무엇을 배웠는지에 대하여.
우리가 AI 기반 도서 번역 플랫폼인 LectuLibre를 출시했을 때, 사용자들은 전체 챕터 번역의 품질을 매우 좋아했습니다. 하지만 사용자들은 계속해서 다른 것을 요구했습니다. 부분적으로 번역된 책을 읽는 동안, 번역되지 않은 구절이나 어색한 자동 번역을 마주쳤을 때 페이지를 떠나지 않고 즉시 더 나은 버전을 얻고 싶어 했습니다. 그래서 우리는 即时翻译求助 (즉각적 번역 도움) 기능을 구축했습니다. 이 기능은 독자가 어떤 구절이든 하이라이트하면 몇 초 내에 문맥을 고려한 인간 수준의 번역과 까다로운 부분에 대한 짧은 설명을 제공합니다.
우리가 이를 어떻게 구축했는지, 직면했던 기술적 과제는 무엇인지, 그리고 LLM을 실시간 독서 경험에 결합하면서 배운 교훈은 무엇인지 소개합니다.
문제: 도서 내부에서의 실시간 문맥 인식 번역
대부분의 웹 앱은 API 호출을 통해 일반적인 번역을 제공합니다. 문장을 Google Translate로 보내고 결과를 받는 방식입니다. 하지만 이는 문학 텍스트에는 적합하지 않습니다. "She let the cat out of the bag"과 같은 구절은 관용적으로 번역되어야 하며, 적절한 표현 방식은 주변 문단에 크게 의존합니다 (어조가 격식 있는가? 비꼬는 말투인가? 은유의 연쇄 중 일부인가?). 우리의 기존 번역 파이프라인은 정교하게 설계된 프롬프트(prompts)를 사용하여 전체 챕터를 일괄 처리하지만, 즉각적인 도움을 위해서는 동일한 수준의 문맥 깊이를 유지하면서도 1초 미만의 지연 시간(sub-second latency)이 필요했습니다.
우리의 접근 방식: Server-Sent Events와 스마트 프롬프트 버퍼
우리는 WebSockets 대신 **Server-Sent Events (SSE)**를 선택했습니다. 통신이 단방향(서버가 번역 토큰을 푸시함)이고, SSE가 FastAPI로 구현하기 더 간단하기 때문입니다. 클라이언트(React 앱)는 다음을 포함한 POST 요청을 보냅니다:
- 번역할 구절
- 도서 ID 및 정확한 위치 (챕터/문단 인덱스)
- 대상 언어
우리의 백엔드(backend)는 PostgreSQL에서 주변 텍스트를 가져오고(원본 도서는 청크(chunk) 단위로 저장합니다), 정교하게 구성된 프롬프트(prompt)를 LLM(속도를 위해 Claude 3 Haiku 사용)에 전달하며, 응답을 토큰(token) 단위로 스트리밍(streaming)하여 다시 보냅니다.
구현 심층 분석 (Implementation Deep Dive)
1. 배경 문맥 검색 (Background Context Retrieval)
우리는 각 문단에 위치 정보를 인덱싱(indexing)합니다. 하이라이트된 구절이 주어지면, 해당 구절을 포함하는 문단과 그 앞뒤 문단을 각각 하나씩 가져옵니다. 이는 프롬프트 크기를 과도하게 키우지 않으면서도 충분한 서사적 문맥(narrative context)을 제공합니다.
async def get_context(book_id: str, para_index: int, db: AsyncSession):
# 주변 문단 가져오기
stmt = (
...
2. 즉각적인 도움을 위한 프롬프트 엔지니어링 (Prompt Engineering for Instant Help)
우리는 LLM에게 다음과 같이 지시하는 프롬프트가 필요했습니다:
- 주어진 구절을 주변 텍스트의 정확한 어조와 스타일로 번역할 것
- 구절에 관용구(idiom)나 문화적 참조(cultural reference)가 포함된 경우, 대상 언어로 자연스러운 대응 표현을 제공하고 짧은 설명을 덧붙일 것
- 결과를 깔끔한 마크다운(Markdown) 스니펫(snippet) 형식(번역 + 설명)으로 반환할 것
- 간결하게 유지할 것 (작은 팝오버(popover)에 표시하기 위함)
핵심 프롬프트 템플릿은 다음과 같습니다:
INSTANT_HELP_PROMPT = """
You are a literary translator. Below is the source text surrounding a highlighted phrase, the phrase itself, and the target language.
Translate the highlighted phrase into {target_lang} in a way that fits the style of the surrounding text.
...
우리는 Claude 3 Haiku가 거의 항상 이 형식을 준수하며, 필요하지 않은 경우 "참고(Note)" 부분이 생략된다는 것을 확인했습니다.
3. FastAPI와 SSE를 이용한 응답 스트리밍 (Streaming the Response with FastAPI and SSE)
우리는 SSE 청크(chunk)를 생성(yield)하는 비동기 엔드포인트(async endpoint)를 구축했습니다. 클라이언트는 토큰이 도착하는 대로 번역을 렌더링(rendering)하기 시작할 수 있으며, 이는 즉각적인 느낌을 줍니다.
from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse
import json
...
프론트엔드(frontend)에서는 이러한 이벤트(event)를 소비하기 위해 EventSource를 사용합니다. 일반적인 구절의 경우 클릭부터 첫 번째 토큰이 나타나기까지의 전체 왕복 시간(round-trip)은 약 400~600ms 정도 소요됩니다.
트레이드오프(Trade-offs)와 어려운 결정들
지연 시간(Latency) vs 품질(Quality)
Haiku는 빠르지만 항상 완벽하지는 않습니다. 저희는 DeepSeek-V2(더 느리지만 관용구 표현에 더 뛰어남)를 시도해 보았으나, 지연 시간(latency)이 2초를 넘어가면서 "즉각적인" 느낌을 해쳤습니다. 현재로서는 Haiku를 기본으로 사용하되, 요청 시에만 사용할 수 있는 더 상세한 번역 옵션(백그라운드에서 Claude 3 Opus 사용)을 제공하는 방식으로 결정했습니다.
비용 관리(Cost Management)
즉각적인 도움(instant help) 호출 한 번당 약 $0.002(입력 + 출력 토큰)의 비용이 발생합니다. 사용자가 수천 명에 달하면 이 비용은 상당해집니다. 저희는 Redis를 사용하여 (book_id, para_index, phrase, target_lang)를 키로 하는 **로컬 캐시(local cache)**를 구현했습니다. 동일한 구절에 대한 반복적인 요청(예: 여러 사용자가 동일한 책을 읽는 경우)은 캐시에서 즉시 제공되어, 베타 테스트 기간 동안 LLM 호출을 약 30% 감소시켰습니다.
프롬프트 버퍼 크기(Prompt Buffer Size)
실험 결과, 더 많은 컨텍스트(2개 단락)를 제공하는 것이 토큰을 과도하게 추가하지 않으면서도 품질을 크게 향상시켰습니다. 하지만 챕터 전체를 포함하면 응답 속도가 느려지고 가끔 주제에서 벗어난 해석이 발생했습니다. 저희는 컨텍스트를 평균 약 500토큰 정도로 유지하고 있습니다.
결과 및 학습한 내용
- 사용자 만족도: 독자들은 다른 도구로 복사하여 붙여넣어야 했을 때보다 현재 3배 더 많은 구절을 번역하고 있습니다. 인라인 설명(inline Explanation)은 종종 새로운 관용구를 가르쳐 주는데, 사용자들은 이를 매우 좋아합니다.
- 엔지니어링 측면의 교훈: LLM 스트리밍(streaming)에 있어 서버 전송 이벤트(Server-Sent Events, SSE)는 과소평가되어 있습니다. SSE는 HTTP/2 환경에서 완벽하게 작동하며, WebSocket에 비해 디버깅이 매우 간편합니다.
- 프롬프트 민감도(Prompt sensitivity):
Output format: **Translation:** ... **Note:** ...와 같이 정확한 문구를 사용하는 것만으로도 잘못된 형식의 응답을 90% 줄일 수 있었습니다. 작은 미세 조정이 중요합니다. - 캐싱의 중요성: Redis를 통해 추가적인 LLM 비용을 억제하고, 인기 있는 도서에 대한 체감 성능을 개선했습니다.
향후 계획
저희는 챕터 전체를 사용하되, 저렴한 모델 호출을 통해 이전 문단들을 공격적으로 요약하는 컨텍스트 윈도우 확장 (context window expansion) 방식을 탐색하고 있습니다. 또한, 저희의 번역 스타일에 맞춰 작은 오픈 소스 모델을 미세 조정 (fine-tuning)한다면 비용을 거의 제로에 가깝게 낮출 수 있을 것입니다. 만약 여러분도 이와 유사한 인라인 AI 기능을 구축해 보셨다면, 비용/지연 시간/품질의 삼각형(cost/latency/quality triangle) 문제를 어떻게 해결하셨나요? 댓글을 통해 여러분의 접근 방식을 공유해 주세요.
LectuLibre를 구축하며 저희가 배운 점은, AI 기반 도구는 사용자의 워크플로 (workflow)에 매끄럽게 녹아들 때 빛을 발한다는 것입니다. 즉각적인 번역 지원은 바로 그 연결 고리입니다. 독자의 흐름을 존중함으로써 마법처럼 느껴지는 작은 기능 말이죠.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기