LLM 모델 운용 정책¶
OpenAI 한도 안에서 gpt-4o 를 쓰고, 부족해질 때만 gpt-4o-mini 로 내린다.
작성 2026-09-02 · 대상
sohri-graph-builder(prd) · 근거는 전부 코드·실측이며 추정치는 산출 방법을 적었다. 오너 지시: "우선 gpt-4o 를 사용하고 부족할 것으로 예상되는 시점에 gpt-4o-mini 로 변경하는 정책".
0. 결론 먼저¶
- 전환 신호는 OpenAI 응답 헤더의 잔량(
x-ratelimit-remaining-requests/-tokens)이다. 429 를 기다리지 않고, 자체 카운터도 두지 않는다. 헤더는 계정 전역 값이라 8~12 파드가 Redis 없이도 같은 것을 본다. 잔량이 한도의 10% 아래로 내려가면 그 파드의 NLG 가gpt-4o-mini로 내려가고, 30% 위로 회복된 것을 확인하면 돌아온다(히스테리시스 3배 · 최소 쿨다운 10초 · 재발 시 쿨다운 2배). - 429 는 안전망이다. 그래도 429 를 받으면 그 호출을 즉시
gpt-4o-mini로 한 번 다시 보내고 강등 상태로 들어간다. 지금은 429 가 "죄송합니다. 응답을 생성하는 중 문제가 발생했습니다" 라는 문구로 사용자에게 나간다(§2-3). - 강등보다 먼저 줄일 수요가 있다. NLG 중복 회피 재생성(2·3번째 호출)은 강등 상태에서는 호출하지 않고 이미 있는 비반복 고정 문구로 끝낸다. 이것이 증폭기의 스위치다. 모델을 내리는 것보다 싸고 품질 대가가 작다.
- 어느 모델이 응답을 만들었는지 턴마다 남긴다(로그 +
/v1/message응답 필드). 노인 대상 대화·분석 맥락에서 강등은 무해하지 않으므로 사후에 가려낼 수 있어야 한다. - 금요일(09-04) 전에는 위 본 정책을 넣지 않는다. 최소안은 ①429 안전망(1회 재시도) + ②잔량 20% 미만 경고 로그 + ③수동 전환 절차 세 가지뿐이다(§6). 본 정책(헤더 기반 자동 강등·계측·기록)은 시험 후 넣는다.
- ✅ 한도 확인됨(2026-09-02) — 500 세션은 두 축 모두 여유롭게 들어간다.
gpt-4o10,000 RPM / 30M TPM,gpt-4o-mini30,000 RPM / 150M TPM. 정상 경로 사용률은gpt-4oRPM 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-ms5초) 를 유발하면 그 턴들은 전부 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.25분,rate(dm_llm_requests_total{outcome="429"}[5m]) > 0,dm_llm_degraded == 110분 지속. - 부하시험 중단기준 문서 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 는 경험적 판단이다.