Skip to content

fix(ai): 타임아웃 설정이 실제 호출 상한이 아니었고 문서 분석 재시도가 연속이었다 - #274

Merged
i3months merged 2 commits into
devfrom
fix/ai-llm-retry-budget
Sep 29, 2026
Merged

i3months merged 2 commits into
devfrom
fix/ai-llm-retry-budget

Conversation

@i3months

Copy link
Copy Markdown
Member

운영 ai_request_logs 를 훑다가 analyze.document 가 5건 중 4건 실패인 것을 발견해 따라간 결과입니다.

1. 타임아웃 설정이 호출 상한이 아니었다

id | latency_ms | error
538|      92247 | Request timed out.
539|      92130 | Request timed out.
559|      92577 | Request timed out.
560|      92160 | Request timed out.
561|      37438 | (SUCCESS)

llm_pro_timeout_sec 는 60초인데 실패가 전부 92.1~92.6초로 좁게 뭉쳐 있습니다. 운영 컨테이너에서 직접 확인했습니다:

$ docker exec stackup-ai ... python -c "ChatOpenAI(model='x', timeout=60, ...)"
request_timeout: 60.0
client max_retries: 2      # ← 지정한 적 없음

OpenAI SDK 클라이언트의 max_retries 기본값이 2 라 LangChain 호출 1회가 HTTP 3회입니다. 코드가 이 값을 지정한 적이 없어(grep max_retries → 임베딩 경로만) 곱셈이 보이지 않았습니다. 실제 상한은 설정값의 (1+n)배입니다.

ai_request_logs 는 LangChain 호출 1건에 한 행이므로 latency_ms 는 숨은 시도의 합입니다. settings.py 주석이 이미 "타임아웃 30초 + 재시도 26초 = 56초" 로 같은 현상을 관측했지만 원인이 특정되지 않은 채 남아 있었습니다.

→ 12개 ChatOpenAI 전부에 max_retries=settings.llm_max_retries 를 명시합니다. 기본값 2 로 현 동작을 유지했습니다 — 게이트웨이 429 를 흡수하는 값이라(임베딩 경로에서 429 폭풍이 문서화되어 있음) 0 으로 내리면 재시도 없는 경로(질문 풀·꼬리질문·패널)가 더 약해집니다. 보이게·튜닝 가능하게 만드는 것이 이 변경의 목적입니다.

문서 분석만 max_retries=0 입니다 — 바깥에 with_retry 가 있어 두 층이 곱해지면 한 메시지가 HTTP 를 6번 치고 로그 한 행이 여러 시도의 합이 됩니다.

2. 문서 분석 재시도가 사실상 연속이었다

with_retry 의 대기 기본값은 wait_exponential_jitter(initial=1) — 1초(+지터 1초) 입니다. 게이트웨이가 구간적으로 멎으면 두 시도가 같은 구간에 들어가 함께 죽습니다. 운영에서 4/4 가 그랬습니다.

같은 이력서(resume_id=18)의 실제 경로:

문서 시각 결과
34 09-21 06:01, 06:03 2시도 각 92초 타임아웃 → FAILED, 4일간 방치
35 09-25 11:41, 11:42 재분석 — 또 2시도 실패
36 09-25 11:45 재분석 — 37초에 성공

입력 문제가 아니라 상류 간헐 정지입니다(Deepgram 배치 호출과 같은 성질 — ai/CLAUDE.md §8). 그런 정지에는 간격을 둔 재시도가 약입니다.

→ document_analysis_max_attempts(3) + document_analysis_retry_initial_sec(15)로 간격을 15초→30초로 벌립니다. 소비자 prefetch 가 10 이라 한 문서가 오래 붙잡혀도 다른 분석을 막지 않습니다(확인: ai_queue_prefetch: int = 10).

테스트

ai/tests/test_llm_retry_budget.py (14건):

  • 지정 없는 ChatOpenAI 의 클라이언트 max_retries 가 2 임을 고정 — 이 테스트가 지키는 것은 "기본값에 맡기지 않는다" 입니다.
  • 빌더 11개 전수로 llm_max_retries 반영 확인. 한 곳만 빠지면 그 경로만 조용히 3배로 돌아갑니다.
  • 문서 분석: max_retries == 0, 시도 3회, 간격 ≥10초.

uv run pytest 463건 통과 · flake8 · black --check 통과.

환경변수

docker-compose.yml 에는 이름만 추가했습니다(값 없음) — test_compose_defaults.py 가 지키는 규칙이고, PR #246 이 compose 기본값에 가려 무효가 된 사고의 재발 방지입니다. .env.example · docs/environment.md 동기화.

기본값이 현 동작과 같으므로 운영 .env 변경 없이 배포 가능합니다.

🤖 Generated with Claude Code

OpenAI SDK 클라이언트의 max_retries 기본값이 2 라 LangChain 호출 1회가
HTTP 3회다. 코드가 그 값을 지정한 적이 없어 이 곱셈이 보이지 않았고,
llm_*_timeout_sec 는 호출 상한이 아니라 실제 상한이 그 (1+n)배였다.

ai_request_logs 는 LangChain 호출 1건에 한 행이라 latency_ms 가 숨은
시도의 합이다 — 운영 analyze.document 실패 4건이 타임아웃 60초 설정에서
전부 92.1~92.6초로 찍힌 이유다. settings.py 주석이 "타임아웃 30초 +
재시도 26초 = 56초" 로 관측한 것도 같은 현상이었는데 원인이 특정되지
않은 채 남아 있었다.

12개 ChatOpenAI 전부에 max_retries 를 명시한다(llm_max_retries, 기본 2 —
현 동작 유지). 게이트웨이 429 를 흡수하는 값이라 0 으로 내리지 않았지만
보이지 않는 채로 두지 않는다. 문서 분석만 0 이다 — 바깥 with_retry 와
곱해지면 한 메시지가 HTTP 를 6번 치고 로그 한 행이 여러 시도의 합이 된다.

그 with_retry 의 기본 대기는 1초(+지터 1초)라 사실상 연속 시도였다.
게이트웨이가 구간적으로 멎으면 두 시도가 같은 구간에 들어가 함께 죽는다:
resume_id=18 이 연속 2시도(각 92초)로 실패해 4일간 FAILED 로 남았고,
재분석도 또 2시도 실패, 3분 뒤 세 번째가 37초에 성공했다. 입력 문제가
아니라 상류 간헐 정지다(Deepgram 배치 호출과 같은 성질).

시도를 3회로 늘리고 간격을 15초→30초로 벌린다. 소비자 prefetch 가 10
이라 한 문서가 오래 붙잡혀도 다른 분석을 막지 않는다.
llm_api_key 를 주지 않아 빈 문자열이 되면 ChatOpenAI 생성 자체가
OpenAIError("Missing credentials") 로 터진다. 로컬에는 ai/.env 가 있어
pydantic-settings 가 키를 채워 주므로 통과하고, .env 가 없는 CI 에서만
13건이 깨졌다. 픽스처가 키를 직접 준다.

검증은 .env 를 치우고 돌렸다 — 로컬에서 CI 와 같은 조건으로 463건 통과.
@i3months
i3months merged commit c8ebe4c into dev Sep 29, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant