요약
DiscourseAi::Translation::PostLocalizer(콘텐츠 로컬라이제이션)가 간헐적으로 에러가 어디에도 기록되지 않는(Rails 로그, Sidekiq 로그, Sidekiq dead/retry 큐 포함) 잘린 번역을 생성하는 경우가 있습니다. 잘림 현상은 문장 중간에서 조용히 발생하며, 부분적인 결과가 마치 완전하고 성공적인 번역인 것처럼 저장됩니다.
관측된 환경: Discourse core + discourse-ai 46bea6613 (2026-07-09), LLM: Google 제공자를 통한 Gemini 2.5 Flash.
재현 증거
우리 인스턴스에서 발생한 실제 사례 두 건:
- 약 2090자 길이의 독일어 게시물을 스페인어로 번역했을 때, 원본 텍스트의 인용된 단어(“aufwendiger”) 바로 앞에서 잘려나간 50자만 생성되었습니다.
- 약 1830자 길이의 독일어 게시물을 영어(83자)와 슬로바키아어(97자)로 번역했을 때, 두 언어 모두 원본 텍스트의 이모지 쇼트코드(
) 바로 앞에서 정확히 같은 지점에서 잘렸습니다. 같은 게시물의 다른 모든 로케일(fr, it, pt, ru, he)은 완전히 번역되었습니다(1600~1950자).
근본 원인(제가 추적할 수 있는 범위 내에서)
lib/completions/structured_output.rb에서 StructuredOutput#read_buffered_property는 @partial_json_tracker.broken?가 true일 때마다 DiscourseAi::Utils::BestEffortJsonParser.extract_key로 폴백합니다. 이 폴백은 JSON 스트림이 깨지기 전에 파싱된 내용을 반환하며, 예외나 경고, 또는 출력이 불완전하다는 어떤 표시도 제공하지 않습니다. lib/translation/base_translator.rb#get_translation에서 이 값은 최종 번역으로 그대로 사용되어 PostLocalization#save!를 통해 영구 저장됩니다.
우리 데이터의 두 잘림 지점은 모두 원본 텍스트의 “특수” 문자(직선 따옴표, 이모지 쇼트코드) 바로 앞에 위치하며, 이는 JSON 스트리밍 트래커가 모델의 스트리밍 출력 내 특정 문자/시퀀스에 혼란을 겪고 스트림이 깨졌다고(broken?) 너무 일찍 판단하는 것을 시사합니다.
영향
콘텐츠가 눈에 띄게 불완전하고 때로는 무의미한 번역과 함께 게시되며, 이를 포착하기 위한 운영 신호(로그, dead job, 관리자 알림)가 없습니다. 커뮤니티 구성원이 UI에서 문장 중간에 뜬 쓰레기 텍스트를 발견해서야 우리는 이를 알게 되었습니다.
제안된 수정 사항
StructuredOutput#broken?가 true이고 best-effort 폴백이 사용될 때마다 경고/에러(Rails.logger + Discourse-error-log)를 노출하여, 관리자가 부분적인 콘텐츠를 조용히 배포하는 대신 이를 감지할 수 있도록 해야 합니다.PostLocalization를 저장하기 전에 원본 길이에 대한 합리적 임계값과 번역된 길이를 비교하여 검증하는 것을 고려해 보십시오.
JSON 트래커 디버깅에 도움이 된다면, 원본/번역 텍스트 전체 쌍을 비공개로 제공할 수 있습니다.