Repository navigation
fix(document): 분석 실패 재분석 + 에러 문구 사람말 + LLM 타임아웃 조정 - #246
Merged
Merged
Conversation
운영 점검에서 사용자의 웹 이력서 1건(portfolio URL)이 2026-09-21 부터 사흘째
FAILED 로 남아 있는 걸 발견했다. 화면에는 "APITimeoutError: Request timed out."
이라는 AI 서버 예외 문자열이 한글 UI 에 그대로 노출됐고, 재분석 API 가 없어
자료를 지우고 다시 등록하는 것 말고는 방법이 없었다.
방금 고친 STT 실패와 같은 결함 유형이다 — 일시 장애가 영구 실패로 굳고,
되돌릴 경로가 없고, 사용자는 영문 예외를 본다.
- POST /api/documents/{id}/reanalyze (DocumentReanalysisService, 202)
실패 문서를 soft delete 하고 출처(resume/web/repository/coverLetter)에 맞는
AnalysisRequestService.request*Analysis 로 새 문서를 만들어 재발행. 실패 행을
남기면 목록에 실패 카드와 진행 카드가 나란히 보인다.
- 프론트는 errorCode 로 고른 한국어 문구만 표시(analysisError.ts). 원문은 서버
로그용으로만. 재시도해도 같은 실패(빈 PDF·차단 URL 등)엔 버튼을 내지 않는다.
- LLM_PRO_TIMEOUT_SEC 30 → 60. 운영 성공 호출이 synthesis p90 31.9초, 최대 56초
(=타임아웃 30초 + 내부 재시도 26초)로 찍힌다. 즉 Pro 는 이미 30초에 상습적으로
걸려 재시도로 겨우 성공하고 있었고(같은 호출을 두 번 물어 비용도 두 배),
문서 분석은 3회 모두 30초를 넘겨 영구 실패했다.
- DEEPGRAM_TIMEOUT_SEC 30 → 20. STT 성공은 p50 3.8초/p90 8.1초/최대 12.1초라
20초면 1.65배 여유. 멎은 호출을 붙잡는 시간이 곧 재시도 전 낭비 대기다.
- VOICE_RETRANSCRIBE_NOT_ALLOWED 가 쓰던 deprecated UNPROCESSABLE_ENTITY 를
UNPROCESSABLE_CONTENT 로 정리(직전 PR 에서 들어간 것).
This was referenced Sep 25, 2026
i3months
added a commit
to i3months/stackup
that referenced
this pull request
Oct 6, 2026
PR Team-StackUp#246 에서 LLM_PRO_TIMEOUT_SEC 를 30→60 으로 올렸는데 운영에 도달하지 못했다. docker-compose.yml 의 `${LLM_PRO_TIMEOUT_SEC:-30.0}` 과 운영 .env 의 30.0 이 코드 기본값을 덮고 있었다. 그래서 웹 이력서 재분석이 92초(=30초×3) 만에 또 타임아웃으로 죽었다 — 60초를 실제로 적용하니 37.4초에 성공했다. 30초로는 구조적으로 불가능한 작업이었고, 그걸 고친 PR 이 조용히 무효가 되어 있었다. 기본값이 세 군데(settings.py / compose / .env)에 흩어져 있고 코드가 제일 약한 구조가 원인이다. ai 서비스 32개 키 중 24개가 compose 에 기본값을 중복으로 갖고 있었다. - docker-compose.yml: ai 환경변수를 리스트 형태로 바꾸고, 튜닝 값은 이름만 적어 **전달만** 한다(`- LLM_PRO_TIMEOUT_SEC`). .env 에 있으면 그 값이 가고 없으면 아무것도 안 가서 코드 기본값이 산다. 미설정이 진짜 미설정으로 전달되는 것은 서버에서 `docker compose run` 으로 실측 확인했다(빈 문자열이 아님). 값을 유지한 것은 컨테이너 토폴로지·이름 변환·로컬 기동용 기본값뿐이다. - .env.example: 코드 기본값과 같은 17개 키를 주석 처리. deploy-app.yml 이 `KEY=값` 줄을 운영 .env 에 영구 복사하므로, 여기 적는 순간 코드 기본값이 영원히 가려진다. - .env.example 에 EMBEDDING_PROVIDER=gemini 추가. 코드 기본값이 mock 이라 신규 배포가 **가짜 임베딩으로 조용히 뜨는** 지뢰였다(현재 운영은 누군가 .env 에 수동으로 넣어둔 덕에 정상). - ai/tests/test_compose_defaults.py: 중복 기본값·빈 문자열 fallback·오타 키를 CI 에서 차단. 세 가지 회귀를 실제로 잡는지 확인했다. - docs/environment.md §0 에 우선순위와 예외 규칙 명시.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
작업 내용
POST /api/documents/{id}/reanalyze(DocumentReanalysisService, 202) — 실패 문서를 같은 원본으로 다시 분석errorCode→ 한국어 문구 매핑(analysisError.ts) + '다시 분석하기' 버튼LLM_PRO_TIMEOUT_SEC30 → 60DEEPGRAM_TIMEOUT_SEC30 → 20VOICE_RETRANSCRIBE_NOT_ALLOWED의 deprecatedUNPROCESSABLE_ENTITY→UNPROCESSABLE_CONTENT(직전 PR feat(session): STT 실패한 음성 답변 재전사 #245 에서 들어간 것)변경 이유 / 배경
운영 데이터 점검 중 사용자의 웹 이력서 1건이 사흘째 FAILED 로 방치된 걸 발견했습니다.
DocumentList.tsx가errorMessage를 그대로 렌더해서 한글 UI 에 영문 예외 문자열이 노출되고 있었고, 재분석 API 가 없어 자료를 지우고 다시 등록하는 것 말고는 방법이 없었습니다. #244/#245 에서 고친 STT 실패와 정확히 같은 결함 유형입니다 — 일시 장애 → 영구 실패, 복구 경로 없음.타임아웃을 올리는 근거 (운영
ai_request_logs)56초짜리 성공 호출이 존재한다는 게 핵심입니다.
llm_pro_timeout_sec=30인데 56초가 SUCCESS 로 찍혔다는 건 첫 시도가 30초에 걸려 죽고 내부 재시도가 26초에 성공했다는 뜻입니다. 즉 Pro 는 이미 상습적으로 타임아웃에 걸려 같은 호출을 두 번 물고 있었고(지연 2배, 비용 2배), 입력이 더 큰 문서 분석은 3회 모두 30초를 넘겨 영구 실패했습니다. 60초는 관측 p90(31.9초)의 1.9배입니다.반대로 STT 는 성공이 p50 3.8초 / p90 8.1초 / 최대 12.1초 라 30초가 과하게 느슨했습니다. 어제 실측에서 멎은 호출을 31초나 붙잡았는데, 그 시간이 곧 재시도 전 낭비 대기입니다. 20초면 최대치의 1.65배.
설계 결정
AnalysisRequestService)가 매번AnalyzedDocument를 새로 만들기 때문에 그 흐름을 그대로 탑니다. 대신 실패 행을 soft delete 해서 목록에 실패 카드와 진행 카드가 같이 뜨지 않게 했습니다.errorMessage는 API 에 그대로 둡니다 — 지원 문의·운영 디버깅에 필요합니다. 화면에서만 안 씁니다.테스트
vitest run46 files / 197 tests,tsc --noEmit,eslint --max-warnings=0cleanuv run pytest405 passed,black/flake8cleanbackend/openapi.json+generated.ts재생성 (추가만)영향 범위
LLM_PRO_TIMEOUT_SEC30→60,DEEPGRAM_TIMEOUT_SEC30→20). 운영.env에 두 키 모두 없어 코드 기본값이 적용됩니다.리뷰어 체크포인트
LLM_PRO_TIMEOUT_SEC60초가 피드백 예산과 충돌하지 않는지 봐주세요. 피드백은asyncio.gather병렬이고 대략 2분 예산인데, 최악의 경우 60초 × 내부 재시도로 그 예산을 넘길 수 있습니다. 재시도 횟수를 같이 줄이는 게 맞을지 판단이 필요합니다.🤖 Generated with Claude Code