콘텐츠로 이동

LLM 모델 운용 정책

OpenAI 한도 안에서 gpt-4o 를 쓰고, 부족해질 때만 gpt-4o-mini 로 내린다.

작성 2026-09-02 · 대상 sohri-graph-builder(prd) · 근거는 전부 코드·실측이며 추정치는 산출 방법을 적었다. 오너 지시: "우선 gpt-4o 를 사용하고 부족할 것으로 예상되는 시점에 gpt-4o-mini 로 변경하는 정책".


0. 결론 먼저

  1. 전환 신호는 OpenAI 응답 헤더의 잔량(x-ratelimit-remaining-requests/-tokens)이다. 429 를 기다리지 않고, 자체 카운터도 두지 않는다. 헤더는 계정 전역 값이라 8~12 파드가 Redis 없이도 같은 것을 본다. 잔량이 한도의 10% 아래로 내려가면 그 파드의 NLG 가 gpt-4o-mini 로 내려가고, 30% 위로 회복된 것을 확인하면 돌아온다(히스테리시스 3배 · 최소 쿨다운 10초 · 재발 시 쿨다운 2배).
  2. 429 는 안전망이다. 그래도 429 를 받으면 그 호출을 즉시 gpt-4o-mini 로 한 번 다시 보내고 강등 상태로 들어간다. 지금은 429 가 "죄송합니다. 응답을 생성하는 중 문제가 발생했습니다" 라는 문구로 사용자에게 나간다(§2-3).
  3. 강등보다 먼저 줄일 수요가 있다. NLG 중복 회피 재생성(2·3번째 호출)은 강등 상태에서는 호출하지 않고 이미 있는 비반복 고정 문구로 끝낸다. 이것이 증폭기의 스위치다. 모델을 내리는 것보다 싸고 품질 대가가 작다.
  4. 어느 모델이 응답을 만들었는지 턴마다 남긴다(로그 + /v1/message 응답 필드). 노인 대상 대화·분석 맥락에서 강등은 무해하지 않으므로 사후에 가려낼 수 있어야 한다.
  5. 금요일(09-04) 전에는 위 본 정책을 넣지 않는다. 최소안은 ①429 안전망(1회 재시도) + ②잔량 20% 미만 경고 로그 + ③수동 전환 절차 세 가지뿐이다(§6). 본 정책(헤더 기반 자동 강등·계측·기록)은 시험 후 넣는다.
  6. ✅ 한도 확인됨(2026-09-02) — 500 세션은 두 축 모두 여유롭게 들어간다. gpt-4o 10,000 RPM / 30M TPM, gpt-4o-mini 30,000 RPM / 150M TPM. 정상 경로 사용률은 gpt-4o RPM 38% · TPM 43% 다. 수용 한계는 min(10,000, 30M/3.6k) × 8/60 ≈ 1,111 세션 으로 목표의 2.2배이고, 전체 턴의 65% 가 fallback 이어도 한도 안이다(토큰 추정 오차 +30% 를 최악으로 잡아도 39%). ⇒ 금요일 시험을 위한 모델 변경은 필요 없다. 이 정책은 그래도 값한다 — 안전망은 싸고, 429 가 나지 않으면 동작이 바뀌지 않는다.

1. 코드가 실제로 하는 일 (설계의 전제 — 통설과 다른 곳을 먼저 바로잡는다)

# 항목 확인한 사실 근거
F1 모든 LLM 호출의 단일 관문 NLU·NLG·bot-first 인사·정책 응답 전부 OpenAIClient._call_chat() → get_model_name(llm_choice) 를 지난다. 전환 지점은 이 한 곳이면 된다 dm/core/llm/clients/openai_client.py:158-195
F2 NLG 3회 루프의 발동 조건 모든 턴이 아니다. generate_natural_response 는 context.fallback_exhausted 가 있을 때만 3회 루프에 들어간다. 정상 턴은 1회 호출로 끝나고 중복 검사조차 하지 않는다. 3회 루프가 매 턴 도는 곳은 FallbackPolicy 의 _generate_policy_response — 즉 무응답(silence)·이해불가(null)·STT 저신뢰(≤ −0.7)·NLU 3회 실패 턴뿐이다 dm/core/executors/mixins.py:206-238, dm/core/dialog/policy/global_policies.py:161-199, 355-401, dst_manager.py:442-456
F3 중복 판정 방식 직전 assistant 발화들과 정규화 문자열 완전 일치. 온도 0.1 이라 fallback 턴에서 같은 문구가 다시 나올 확률은 있으나, 프롬프트에 전체 히스토리가 실려 턴마다 달라진다 mixins.py:262-282, openai_client.py:290
F4 429 의 실제 경로 NLG: chat() 이 예외를 올리지만 호출자 전원이 except Exception 으로 삼킨다 → DEFAULT_MESSAGES["llm_error"] 문구 반환 → process_turn 정상 완료 → HTTP 200 · isValidResponse()=true. NLU: RateLimitError 는 재시도 대상 → 1s·2s 백오프 후 3회 실패 시 intent=unknown, act=null → FallbackPolicy 로 흘러 NLG 3회 루프를 켠다 mixins.py:239-248, global_policies.py:395-401, dst_manager.py:364-388, 543-558, dst_nlu.py:48-55, 120-152, socket DmMessageResponse.isValidResponse()
F5 관측 결과 그러므로 stage=dm 실패율(A13)도 429 를 못 본다 — 성공으로 집계되고 지연만 짧아진다. 부하시험 중단기준 문서 §0-5 의 "429 → 500 → socket 무한 재시도" 경로는 NLG 429 에는 성립하지 않는다(socket 무한 루프 결함 자체는 사실이며 503 에는 성립한다). 유일한 흔적은 graph-builder 로그 Chat completion failed: Error code: 429 … 와 LLM response generation failed sohri-gcp-infra/docs/LOAD-TEST-ABORT-CRITERIA.md:317-390, socket DialogManagerIntegrationService.java:258-340
F6 NLU 3회의 근거 재시도는 일시 오류 전용(연결·타임아웃·429·5xx·JSON 파싱). 정상 턴은 1회. "최악 3회"는 중복 회피가 아니라 오류 재시도 상한이다. 규칙 기반 NLU 가 짧은 긍정·부정·인사는 LLM 없이 처리한다 dst_nlu.py:48-55, dst_rule_nlu.py
F7 헤더 노출 방식 openai-python: client.chat.completions.with_raw_response.create(...) → .headers 로 읽고 .parse() 로 본래 객체를 얻는다. 429 예외는 RateLimitError(APIStatusError) 로 e.response.headers 가 그대로 있다 Context7 /openai/openai-python README "Accessing raw response data", _exceptions.py
F8 강등 대상 모델 선택 세션 생성 시 llm_choice/nlu_llm_choice 를 받아 Redis 세션 config 에 저장하고 매 턴 prepare_turn_context 가 context["llm_choice"] 로 싣는다. 런타임에 세션 설정을 바꾸는 API 는 없다(update 메서드는 있으나 라우트 없음) session_manager.py:71-138, 195-230, dst_state.py:52-54, routes/sessions.py
F9 락 TTL 결합 LLM 호출 수는 WORST_CASE_TURN_MS < SESSION_LOCK_TTL_MS(180s) 부등식에 들어간다(현재 149.0s). 호출을 1개라도 더하면 이 산술과 테스트를 같이 고쳐야 한다 config.py, dm/tests/test_llm_timeout_budget.py

F2·F4·F5 는 이 건의 초기 진단("이 시험이 최악 경로를 유발한다", "429 즉시 턴 실패")을 뒤집는다. 정상 발화가 실린 녹음이면 500 세션에서 NLG 는 3,750 RPM(한도의 38%)에 머문다. 11,250 은 "모든 턴이 fallback 턴이고 매번 완전 일치 중복" 일 때다. 다만 부하 생성기가 빈 입력·무응답 타이머(socket requery-delay-ms 5초) 를 유발하면 그 턴들은 전부 fallback 턴이라 3회 루프 + 전체 히스토리 프롬프트를 탄다. 녹음 길이와 8초 간격의 관계는 확인하지 못했다(§8).


2. 부하 산식 — RPM 과 TPM 두 축

2-1. 턴당 토큰 추정 (o200k_base 로 실측)

레포의 프롬프트 빌더(build_nlg_user_prompt·build_nlu_user_prompt)를 그대로 호출해 senimate.json 의 meal_check 노드, 시나리오 prompts.bot(1,031자)·prompts.nlu(380자), 78세 사용자 정보, 한국어 발화 55자/응답 68자짜리 히스토리를 넣고 tiktoken o200k_base 로 세었다(스크립트 scratchpad/tok_estimate.py).

호출 입력 토큰 출력 토큰(전형/상한) 호출당 합
NLG 정상 턴, 히스토리 0 / 8 / 20건 2,446 / 2,885 / 3,545 ~50 / 256 ≈ 3.6k
NLG fallback 턴(전체 히스토리 20 / 40 / 60건) 3,651 / 4,751 / 5,851 ~50 / 256 3.7k ~ 5.9k
NLG 중복 회피 재생성(히스토리 40건) 5,040 ~50 / 256 ≈ 5.1k
NLU(gpt-4o-mini), 히스토리 8건 4,169 (시스템 1,668) ~60 / 512 ≈ 4.2k
  • 히스토리 1쌍(user+assistant) ≈ 111 토큰. 정상 턴은 20건 상한이라 3.5k 에서 포화하지만 fallback 턴은 상한이 없어 세션이 길수록 커진다(30턴 세션 = 60건 = 5.9k).
  • NLU 프롬프트가 NLG 보다 크다. _NLU_CONTEXT_ALLOWED_KEYS 에 system_prompt(봇 프롬프트 전문)가 들어가 NLU 컨텍스트 JSON 에 통째로 실리고, 노드 설정도 컨텍스트와 별도 필드에 두 번 실린다(dst_nlu.py:56-66, prompts_nlu.py:194, 279). 이것만 걷어도 NLU 는 호출당 ~800 토큰(≈20%) 준다 — §4 수요 감축 D5.
  • 편차: 시나리오·사용자 정보·슬롯 수에 따라 ±30%. 일본어는 토큰 밀도가 달라 별도.

2-2. 500 세션 · 턴 간격 8초 = 3,750 턴/분 에서의 소비

모델 경로 RPM TPM (추정) RPM 한도 TPM 한도
gpt-4o (NLG) 정상(턴당 1회) 3,750 ≈ 13.1M 10,000 (38%) 30M (43%)
gpt-4o (NLG) 전 턴 fallback·3회 11,250 ≈ 39M 초과 초과
gpt-4o-mini (NLU) 정상(턴당 1회) 3,750 ≈ 15.6M 30,000 (13%) 150M (11%)

TPM 이 먼저 걸리는 축이지만 여유가 크다. gpt-4o 30M 에 대해 정상 경로 소비가 13.1M 이라 43% 다. 참고로 계정의 gpt-5 는 40M TPM, gpt-4o-mini·gpt-4.1-mini·gpt-4.1-nano 는 150M TPM 이다.

한도를 확인하기 전에는 이 값이 시작 형상을 좌우했다 — TPM 이 2M 이면 ~75 세션, 10M 이면 ~380 세션이라 500 세션을 gpt-4o 로 시작하는 것 자체가 결정되지 않은 상태였다. 30M 으로 확인되어 해소됐다.

세션 수 상한 공식(정상 경로): 세션 = min(RPM, TPM / 3.6k) × 8 / 60. gpt-4o 는 10,000 RPM · 30M TPM 이므로 36M 미만 → TPM 이 먼저 걸려 약 1,111 세션이 상한이다(목표 500 의 2.2배). fallback 비율 f 로 풀면 RPM 은 f ≤ 83%, TPM 은 f ≤ 65% 까지 허용한다.

2-3. 지금 429 를 받으면 사용자가 듣는 것

"죄송합니다. 응답을 생성하는 중 문제가 발생했습니다. 다시 말씀해 주세요."(messages.py:llm_error). 턴은 성공으로 집계되고 히스토리에 이 문구가 남아 다음 NLU·NLG 프롬프트에 실린다. 지연은 짧아진다. 강등이 이보다 나쁘기는 어렵다 — 그래서 429 안전망(재시도 1회)은 품질 저하가 아니라 품질 회복이다.


3. 전환 정책 (본 정책)

3-1. 무엇을 보고 전환하나 — 헤더 잔량(예측) + 429(반응) 조합

후보 장점 단점 판정
429 를 받고 반응 신호가 곧 사실. 구현 최소 429 가 시작되면 이미 그 분(minute)의 버킷이 비어 있어 수십~수백 턴이 사과 문구로 나간 뒤다. 강등 중 원모델 상태를 알 길이 없다 안전망으로만
응답 헤더 잔량 매 응답에 공짜로 온다. 계정 전역 값이라 파드 수와 무관. limit 도 헤더로 와서 TPM 미확인 문제를 런타임에 스스로 푼다 파드가 마지막으로 본 값이라 최대 수백 ms 스테일. 강등 중 원모델 호출이 0 이면 갱신이 멈춘다(→ 프로브 필요) 주 신호
자체 RPM 카운터(파드 로컬) 외부 의존 없음 8~12 파드가 각자 1/N 만 본다. 한도는 전역이다 기각
Redis 공유 카운터 전역 매 LLM 호출마다 Redis 왕복 추가(3,750×2/분), 단일 파드·무영속 Redis 가 이미 SPOF 인데 제어 경로까지 얹는다. 헤더가 같은 정보를 공짜로 준다 기각

"예측"이라 부르지만 추측이 아니다 — OpenAI 가 계산한 잔량을 읽는 것이고 오차는 스테일함뿐이다.

3-2. 임계값과 근거

파드별 인메모리 상태 RateLimitState{model: {limit_req, remaining_req, limit_tok, remaining_tok, reset_req_s, observed_at}}. 매 응답(성공·429 모두)의 헤더로 갱신한다.

파라미터 값 근거
강등 진입 remaining_req < 0.10 × limit_req 또는 remaining_tok < 0.10 × limit_tok 500 세션은 세션당 동시 1턴이라 동시 재중(in-flight) NLG 호출 ≤ 500. 파드 하나가 헤더를 보는 간격은 평균 ≈ 8/62.5 ≈ 130ms, 최악 2초로 잡아도 스테일 소비 125건. 500 + 125 ≈ 625 → 여유 1.6배로 1,000 = 10k 의 10%. 토큰은 같은 비율(TPM 30M 가정 시 3M ≈ 830 호출분)로 대응된다
복귀 조건 프로브 응답의 remaining_req > 0.30 × limit_req 이고 remaining_tok > 0.30 × limit_tok, 그리고 쿨다운 경과 진입 10% ↔ 복귀 30% 의 3배 히스테리시스. 버킷은 연속 충전이라 10% 근처에서 진동하기 쉽다 — 복귀 문턱을 진입보다 훨씬 위에 두어 한 번 복귀하면 최소 20% 분량(2,000건 ≈ 32초치 부하)을 소비해야 다시 내려간다
쿨다운 최소 10초, 재발(직전 복귀 후 60초 내 재강등) 시 2배, 상한 120초 10초 = 1,000건 문턱을 3,750 RPM 이 재충전하는 데 걸리는 시간(≈ 16초)보다 약간 짧게, 프로브가 이르게 확인하도록. 플래핑은 배증으로 억제
프로브 강등 중 NLG 호출의 1/20 은 그대로 gpt-4o 로 보낸다 강등 중 gpt-4o 호출이 0 이면 헤더가 안 와서 복귀 판단이 불가하다. 5% = 3,750 RPM 기준 분당 ~190건, 한도의 2% — 잔량 관측에 충분하고 부담은 없다. 프로브가 429 를 받으면 §3-3 안전망이 처리한다
429 수신 그 호출을 폴백 모델로 즉시 1회 재전송 + 강등 진입 + 쿨다운 시작 429 응답은 수백 ms 라 지연 대가가 작다. §2-3 의 사과 문구보다 낫다
적용 대상 NLG 만(purpose=nlg). NLU 는 1단계에서 폴백 없음 NLU 는 30k RPM 에 13% 이고, NLU 폴백 후보(gpt-5-mini)는 미검증(§5-3). NLU 는 기존 3회 재시도가 429 를 이미 다룬다

3-3. 전역인가 파드별인가 — 파드별 판단, 전역 신호

헤더가 전역 값이라 파드마다 판단해도 결론이 같다(차이는 스테일함뿐). 파드 간 전환 시점이 수백 ms 어긋나는 것은 문제가 아니다 — 문턱 자체가 그 어긋남을 흡수하도록 잡았다. Redis 는 제어 경로에 쓰지 않는다. 강등 상태를 Redis 에 기록(관측용, SET dm:llm:degraded:{pod} EX 60)하는 것은 선택 사항이며 §7 계측이 들어오면 필요 없다.

3-4. 되돌아오는 판단 — 자동

프로브 + 히스테리시스 + 쿨다운으로 자동 복귀한다. 수동 개입은 CORE_NLG_FALLBACK_LLM=""(기능 끄기) 또는 CORE_DEFAULT_LLM=gpt-4o-mini(전면 강등) 환경변수뿐이다.

3-5. 결정 흐름 (NLG 호출 1건)

model = llm_choice(=gpt-4o)
if fallback 설정 없음 → 그대로 호출
elif state[model].degraded and not cooldown_elapsed → model = fallback   (1/20 은 프로브로 원모델)
elif state[model].remaining < 10% limit → degraded=True, cooldown 시작, model = fallback
호출(with_raw_response) → 헤더로 state 갱신
  429 이고 model == 원모델 → degraded=True, fallback 으로 1회 재호출(타임아웃 10s)
  성공이고 degraded 이고 원모델 프로브였고 remaining > 30% 이고 cooldown 경과 → degraded=False
context["nlg_model_used"] = 실제 model ; 로그 1줄 ; 메트릭

3-6. 강등 상태에서 수요를 먼저 줄인다 (모델 전환과 함께 작동)

  • 중복 회피 재생성 차단: degraded 이면 _generate_policy_response·generate_natural_response 의 2·3번째 생성은 호출하지 않고 _non_repeating_fallback_message / _final_closing_message 로 바로 끝낸다. 두 함수는 이미 있고 이미 최종 폴백으로 쓰이고 있다. 강등 상태에서 fallback 턴의 호출은 3 → 1 이 된다.
  • 정상 상태에서도 재생성은 온도를 0.1 → 0.7 로 올려 보낸다(regenerate_due_to_duplicate 플래그가 이미 intent 에 실린다 → generate_response 에서 분기 한 줄). 온도 0.1 로 같은 프롬프트에 "다르게 말하라"고 하는 것이 3번째 호출을 부르는 원인이다.

4. 수요 감축안 — 채택·기각

# 안 판정 근거
D1 MAX_NLG_REQUESTS_PER_TURN 을 상시 3 → 1 기각(상시) / 채택(강등 중에만, §3-6) 정상 턴은 이미 1회라 상시 변경은 fallback 턴의 중복 회피만 없앤다. 그 대가(무응답 노인에게 같은 되묻기 반복)는 작지만, 한도 여유가 있을 때까지 포기할 이유가 없다. 강등 상태에서만 끄면 대가를 낼 때만 낸다
D2 중복 회피를 LLM 재호출 아닌 방법으로 채택 (a) 비반복 고정 문구는 이미 존재(_non_repeating_fallback_message, _contextual_fallback_message, _final_closing_message) — 재생성 없이 곧장 쓸 수 있다. (b) 재생성 온도 상향은 한 줄이다. 둘 다 §3-6
D3 NLU 재시도 3회 축소 기각 3회는 중복 회피가 아니라 일시 오류 재시도이고 정상 턴엔 안 쓰인다(F6). 줄이면 STT 잡음·JSON 파싱 실패 턴이 곧장 null → FallbackPolicy → NLG 3회 루프로 흘러 오히려 gpt-4o 호출이 는다. 단, 429 재시도 사이의 1s·2s 수면은 락 TTL 예산에 이미 반영돼 있어 그대로 둔다
D4 녹음을 여러 개로(이슈 #65 안 2) 효과 제한적 정상 턴은 중복 검사를 하지 않으므로(F2) 발화 다양성은 호출 수를 줄이지 않는다. 효과는 "녹음이 fallback 턴을 유발하는가"에 달렸다(§8)
D5 NLU 프롬프트 중복 제거(봇 system_prompt 제외 · 노드 설정 1회만) 채택(시험 후) 호출당 ~800 토큰, NLU TPM 20% 절감. 프롬프트 변경이라 NLU 회귀 테스트(dm/tests)를 거쳐야 하므로 금요일 전엔 안 넣는다
D6 fallback 턴 히스토리 상한(현재 무제한) 채택(시험 후, 상한 40건) 긴 세션에서 fallback 턴만 5.9k 로 자란다. "전체 흐름을 보게 한다"는 의도는 40건(20턴)으로 충분하다
D7 히스토리 요약·캐시(prompt caching) 보류 OpenAI 자동 프롬프트 캐시는 접두 1,024 토큰 이상 동일 시 적용되는데 현재 시스템 프롬프트(618 토큰)가 그 아래다. 시스템 프롬프트를 앞에 길게 고정하는 재배치가 필요하다. 별건

5. 품질 대가 — 정직하게

5-1. 이 서비스는 무엇인가

SeniMate — 노인 대상 안부·식사 확인 대화(senimate.json), socket 쪽에서 감정·치매·우울·biomarker 분석, 대화 요약을 마음온(Maumon)에 회신한다. NLG 는 사용자가 듣는 말이고, 봇 발화는 히스토리에 남아 다음 턴 NLU 와 요약의 입력이 된다. 도메인 규칙 텍스트(domain_rules_text.py, 18KB)는 마무리 노드 재개·슬롯 핸드오프·종료 인사 억제 같은 미세한 화법 제약이다.

5-2. gpt-4o-mini 로 내리면

  • 규칙 준수가 약해진다 — 특히 "작별 인사를 하지 마라"류의 부정 지시. 라우팅은 NLU 가 하므로 세션 흐름이 틀어지진 않지만 조기 작별 발화가 히스토리에 남아 다음 NLU 가 goodbye 로 읽을 가능성이 있다 (drop_contradicted_end_intent 안전망이 있다).
  • 어투가 얕아진다. 감정 분석은 음성에서 하므로 직접 영향은 없으나, 대화 요약(Maumon 회신)의 품질은 떨어진다.
  • 무해하지 않다. 그러나 사과 문구(§2-3)보다는 낫다. 강등은 "429 대신" 이지 "gpt-4o 대신" 이 아니다 — §3 의 문턱은 gpt-4o 가 실제로 부족할 때만 걸리도록 잡았다.

5-3. 강등 대상은 gpt-4o-mini. gpt-5-mini / gpt-5-nano 는 지금 쓰지 않는다

  • 한도는 gpt-5-mini 가 유리하다(30k RPM · 180M TPM, TPM 이 확실히 알려진 유일한 소형 모델).
  • 그러나 gpt-5 계열은 reasoning 모델이라 chat.completions 파라미터가 다르다. 클라이언트는 max_tokens(폐기, o-series 에서 거부됨 — Context7 /websites/developers_openai_api 확인)와 temperature=0.1 을 보낸다. gpt-5 계열이 이를 400 으로 거부하거나 reasoning 토큰이 256 출력 예산을 먹어 빈 응답을 낼 가능성이 있다 (gpt-5 에 대한 직접 문서 확인은 못 했다 — §8). 클라이언트 수정(max_completion_tokens, reasoning_effort)과 한국어 화법·지연 검증 없이는 폴백 모델로 쓸 수 없다. 폴백은 "검증된 더 작은 것"이어야지 "미검증의 새 것"이면 안 된다.
  • 결론: 1단계 폴백 = gpt-4o-mini. gpt-5-mini 는 시험 후 별도 검증 과제(NLU 폴백 후보로 먼저).

5-4. 기록 — 반드시 남긴다

  • graph-builder: 턴마다 context["nlg_model_used"](히스토리와 함께 Redis 에 남음) + 로그 1줄 LLM route: purpose=nlg model=gpt-4o-mini reason=degraded|ratelimit_429|probe session=….
  • /v1/message 응답에 llm_model(문자열, 추가 필드 · 기존 계약 불변) → socket 이 dialog_message 저장 시 메타에 싣는다 (socket 변경, /sync-contract 대상). 분석·요약 쪽이 "이 대화의 몇 %가 강등 모델인지" 를 사후에 볼 수 있어야 한다.
  • 강등 진입·복귀는 WARNING 로그 + 메트릭(§7).

6. 두 형상 — 금요일 부하시험 vs 운영

6-1. 금요일(09-04, 22:00~06:00, 500 세션) 최소안 — 코드 변경은 한 파일, 동작 변화는 429 가 났을 때만

원칙: 시험 대상 코드를 바꾸지 않는다. 429 가 안 나면 오늘과 완전히 같은 경로로 돈다.

# 변경 규모 왜 이것만
M1 OpenAIClient._call_chat: RateLimitError 이고 model == CORE_DEFAULT_LLM 이고 CORE_NLG_FALLBACK_LLM(신설 env, 기본 ""=꺼짐) 이 있으면 폴백 모델로 1회 재전송(타임아웃 10s). WARNING 로그에 e.response.headers 의 잔량·리셋 값을 싣는다 ~30줄 + 단위 테스트(가짜 클라이언트로 429 → 폴백 호출 1회 확인, 되돌리면 RED) 사과 문구 대신 응답을 준다. 429 가 없으면 코드 경로가 실행되지 않는다
M2 같은 함수에서 with_raw_response 로 바꾸고 remaining < 20% limit 이면 WARNING llm_ratelimit_low model=… remaining_requests=… remaining_tokens=… reset=… 1줄(분당 1회로 억제) ~15줄 관제 인원이 429 전에 접근을 본다. Prometheus 없이 되는 유일한 사전 신호. 분당 1회 억제는 Cloud Logging 비용 때문
M3 락 TTL 산술: M1 이 NLG 호출당 최대 1회(10s)를 더하므로 WORST_CASE_TURN_MS 에 MAX_NLG_REQUESTS_PER_TURN × 10s 항 추가 → 175.5s. SESSION_LOCK_TTL_MS 를 210,000 으로 같이 올린다(여유 16%). test_llm_timeout_budget.py 갱신 env 1개 + 상수 F9. 이걸 빼먹으면 락이 턴보다 먼저 만료된다 — 이 레포가 이미 한 번 겪은 실패다
M4 수동 전면 전환 절차(문서·런북, 코드 아님): 트리거 충족 시 CORE_DEFAULT_LLM=gpt-4o-mini 로 ConfigMap 변경 + 롤링 재시작(PDB minAvailable 6 이라 2대씩). 세션은 Redis 에 있어 살아남고, 재시작 중 턴은 socket 이 3회 재시도한다 0줄 오너 지시("부족할 것으로 예상되는 시점에 변경")의 문자 그대로의 구현. 자동화는 시험 후

M4 트리거(둘 중 하나, 5분 창): (a) llm_ratelimit_fallback WARNING 이 턴의 30% 이상(≈ 1,100건/분) — 이 시점엔 응답 절반 가까이가 이미 mini 이면서 429 왕복 지연까지 내고 있으므로 전면 전환이 낫다. (b) llm_ratelimit_low 가 remaining_tokens 쪽으로 2분 연속 — TPM 이 먼저 걸리는 신호이며 M1 로는 못 막는다.

🔴 결과 보고서 필수 기재: M1 이 발동한 건수·비율, M4 를 썼다면 시각·램프 단계. 시험 형상 ≠ 운영 형상이며 M4 이후의 DM 지연·품질 수치는 gpt-4o 기준선이 아니다.

🔴 M4 의 함정: graph-builder prd 가 ArgoCD selfHeal 대상이면 kubectl set env 는 되돌려진다(워크스페이스 gitops-drift-runbook.md). 시험 전에 확인해 git 경로(values-prd ConfigMap) 또는 sync 일시정지 중 하나를 정해 둔다(§8).

~~시험 전 반드시 결정할 것: gpt-4o TPM 확인~~ → 2026-09-02 확인 완료. 30M TPM 으로 500 세션은 43% 사용률이라 형상 변경이 필요 없다. gpt-4o 로 시작한다. 이 문서는 그 판단을 대신하지 않는다.

금요일에 넣지 않는 것: 헤더 기반 자동 강등(§3), 프로브·히스테리시스, 강등 중 재생성 차단(§3-6), 기록 필드(§5-4), Prometheus(§7), D5·D6. 이유는 한 가지 — 시험이 무엇을 측정한 것인지 흐려진다.

6-2. 운영 형상(본 정책) — 시험 후

§3 전체 + §3-6 + §5-4 + §7. 기본값은 CORE_NLG_FALLBACK_LLM=gpt-4o-mini, 문턱 10%/30%, 프로브 1/20. 운영에서는 강등이 드물어야 정상이다 — 자주 걸리면 한도 상향(오너)이나 D5·D6 가 답이지 정책 조정이 아니다.


7. 감지 수단 — 정책보다 먼저 있어야 하는 것 (시험 후 1순위)

graph-builder 에 Prometheus 계측이 없다. 헤더 기반 정책은 계측 없이도 동작하지만, 동작했는지를 사람이 볼 수 없다. 시험 후 첫 PR 은 이것이다.

  • prometheus_client + /metrics 마운트, 차트에 PodMonitoring(GMP) 추가 — socket 과 같은 경로.
  • 메트릭: dm_llm_requests_total{purpose,model,outcome=ok|429|timeout|error|fallback}, dm_llm_ratelimit_remaining{model,kind=requests|tokens}·dm_llm_ratelimit_limit{…}(게이지, 헤더 그대로), dm_llm_degraded{purpose}(0/1), dm_llm_prompt_tokens{purpose}(히스토그램 — §2-1 추정을 실측으로 바꾼다).
  • 알림: dm_llm_ratelimit_remaining/limit < 0.2 5분, rate(dm_llm_requests_total{outcome="429"}[5m]) > 0, dm_llm_degraded == 1 10분 지속.
  • 부하시험 중단기준 문서 A13 정정: NLG 429 는 stage=dm failure 로 안 나온다(F5). 금요일엔 로그 카운트 (llm_ratelimit_fallback, Error code: 429)를 판정 신호로 써야 한다.

8. 모르는 것 — 오너·PM 확인 항목

# 항목 왜 중요한가 누가
8-1 ✅ 해소(2026-09-02) — gpt-4o 10,000 RPM / 30M TPM · gpt-4o-mini 30,000 RPM / 150M TPM 500 세션 정상 경로 사용률 gpt-4o RPM 38% · TPM 43%. 형상 변경 불필요. RPD 는 여전히 미확인이나 8시간 소킹 총 호출이 약 180만 건이라 확인해 두면 좋다 완료
8-2 gpt-4o 한도 상향 요청 여부(#65 안 3) 상향되면 §6-1 이 거의 발동하지 않는다. 티어 승급은 결제 이력 조건이라 금요일 전에 될지 미지수 오너
8-3 부하 생성기의 녹음 길이·8초 간격·무응답 타이머(5초) 관계 fallback 턴 비율을 결정한다. 그 비율이 곧 3회 루프 발동률이고 §2-2 의 어느 행이 현실인지 정한다 시험 담당
8-4 graph-builder prd 가 ArgoCD selfHeal 대상인지 M4 수동 전환 경로가 갈린다 infra
8-5 gpt-5 계열의 temperature·max_tokens 수용 여부 gpt-5-mini 를 폴백 후보로 올릴지의 전제. 문서 직접 확인 못 함 시험 후 스파이크
8-6 socket 의 DM 무한 재시도 루프(processDialogManager break 누락) 수정 PR 존재 여부 429 와는 무관해졌지만(F5) 503 에는 여전히 자기증폭이다. 열린 PR 목록에 없다 PM

9. 확신하지 못하는 지점 (설계자 자기 평가)

  • 토큰 추정은 합성 히스토리다. 실제 노인 발화는 짧고 봇 응답은 길 수 있다. ±30% 를 보되, §7 의 히스토그램이 들어오기 전까지는 추정치다.
  • 문턱 10%/30% 는 in-flight ≤ 500 가정에 기대고 있다. socket 이 같은 세션의 턴을 겹쳐 보내지 않는다는 전제 (분산 락이 이를 강제한다)가 맞으면 성립한다. 파드 수가 12 를 넘거나 세션이 1,000 을 넘으면 다시 계산해야 한다.
  • 헤더 스테일 상한을 2초로 잡았다. 파드가 한동안 gpt-4o 를 안 부르는 경우(트래픽 불균형)는 더 길 수 있다. 프로브가 그 경우도 덮지만 실측은 없다.
  • 429 응답이 RPM 버킷을 소비하는지 모른다. 소비한다면 M1 의 재시도가 헛돈다기보다 429 자체가 상황을 악화시키므로 강등 진입을 더 빨리(문턱 15%) 잡아야 할 수 있다.
  • gpt-4o-mini 의 한국어 화법 저하 정도를 정량화하지 않았다. §5-2 는 경험적 판단이다.