JSON 스트림 파싱이 중단되면 번역이 조용히 잘림 (오류 발생 안 함)

요약
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자)로 번역했을 때, 두 언어 모두 원본 텍스트의 이모지 쇼트코드(:collision:) 바로 앞에서 정확히 같은 지점에서 잘렸습니다. 같은 게시물의 다른 모든 로케일(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 트래커 디버깅에 도움이 된다면, 원본/번역 텍스트 전체 쌍을 비공개로 제공할 수 있습니다.

3개의 좋아요

이 문제에 대한 데이터를 하나 더 추가하겠습니다. 우리는 동일한 근본 메커니즘이 서로 다른 형태로 발현되는 것을 관찰하고 있습니다.

관측 환경: Discourse 코어 + discourse-ai, LLM: OpenAI를 통한 GPT-5.1(시드 모델이 아닌 커스텀 API 연결).

재현 증거

동일한 원본 게시물(러시아어, 약 2,000자, 헤딩/굵게/불릿 리스트/링크 포함)을 네이티브 AI 번역을 통해 여러 로케일로 번역했을 때:

  • 폴란드어(pl): 내장된 이미지 마크다운 구조가 손실되었습니다. ![alt text|690x460](upload://...)에서 ! 접두사와 | 구분자가 누락되고, 대체 텍스트와 치수가 일반 텍스트로 결합된 잘못된 링크로 변형되었습니다. 결과적으로 내장된 이미지가 아닌 클릭 가능한 링크로 렌더링되었습니다.
  • 우크라이나어(uk): 문단 구분 대신 게시물 전체에 걸쳐 리터럴 \n\n 시퀀스가 일반 텍스트로 나타났습니다. 이 번역에서는 이미지 마크다운이 온전했으므로, 손상이 하나의 고정된 증상에 국한되지 않고 실행마다 달라진다는 것을 의미합니다.

위에서 설명한 절단(truncation)과 달리, 우리의 경우 누락된 콘텐츠가 없습니다. 전체 텍스트는 존재하지만, 출력 단축 대신 이스케이프 시퀀스/마크다운 문법이 손상되었습니다. 다른 프로바이더(여기서는 OpenAI vs Google), 다른 증상(손상 vs 절단), 동일한 의심되는 근본 원인: 파싱 중반에 폴백이 트리거될 때 StructuredOutput#read_buffered_propertyBestEffortJsonParser#extract_key로 폴백하며, 이는 JSON 문자열 시퀀스(\n가 리터럴로 유지됨)를 언이스케이프하지 않고 마크다운 문법 인접 특수 문자도 잘못 처리하는 것으로 보입니다.

우측에서도 Sidekiq 또는 Rails 로그에 오류가 없습니다. 동일한 무음 실패(silent-failure) 동작을 보이고 있습니다.

1개의 좋아요

가능한 수정 사항을 작업하기 시작했습니다

3개의 좋아요

이 주제는 11일 후 자동으로 닫혔습니다. 새로운 답변은 더 이상 허용되지 않습니다.