From fb80514d671057200f71a7e6144b01aafefaff70 Mon Sep 17 00:00:00 2001 From: jmj Date: Mon, 24 Aug 2026 12:07:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=EB=8B=B5=EB=B3=80=EB=B3=84=20?= =?UTF-8?q?=EB=B3=B5=EA=B8=B0=20=EB=B6=80=ED=95=98=20=EC=95=88=EC=A0=84?= =?UTF-8?q?=EB=B0=B8=EB=B8=8C=EB=A5=BC=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feedback 의 최대 비용 지점은 답변별 복기(코칭)다 — 답변 수만큼 LLM 을 부른다. 이를 막는 두 안전밸브(상한 30, 동시성 5)가 FeedbackConsumer 생성자 기본값으로만 존재하고 runner 가 전달하지도, settings 에 있지도 않았다. 게이트웨이가 429 를 내기 시작해도 코드를 고쳐 재배포하지 않으면 조일 수가 없었다. 같은 성격의 다른 값들(embedding_batch_size·llm_*_timeout_sec·*_rag_timeout_sec· feedback_rag_top_k)은 전부 settings 경유다 — 이 둘만 규약(ai/CLAUDE.md §6.2, "코드에 하드코딩 금지")에서 벗어나 있었다. 기본값은 그대로 두어 동작 변화는 없다. .env.example·docs/environment.md 동기화. --- ai/.env.example | 5 +++++ ai/src/ai_server/config/settings.py | 6 ++++++ ai/src/ai_server/messaging/runner.py | 2 ++ ai/tests/test_feedback_consumer.py | 22 ++++++++++++++++++++++ docs/environment.md | 2 ++ 5 files changed, 37 insertions(+) diff --git a/ai/.env.example b/ai/.env.example index 5bee7d7..2254501 100644 --- a/ai/.env.example +++ b/ai/.env.example @@ -49,6 +49,11 @@ EMBEDDING_DIM=1536 EMBEDDING_CHUNK_SIZE=1000 EMBEDDING_CHUNK_OVERLAP=200 EMBEDDING_BATCH_SIZE=32 + +# 답변별 복기(코칭) 부하 안전밸브 — 피드백의 최대 비용 지점(답변 수만큼 LLM 호출). +# 게이트웨이가 429 를 내기 시작하면 재배포 없이 이 둘을 낮춘다. +FEEDBACK_COACHING_MAX_ANSWERS=30 +FEEDBACK_COACHING_CONCURRENCY=5 EMBEDDING_MAX_RETRIES=5 # 429(RESOURCE_EXHAUSTED) 지수 백오프 재시도 횟수 EMBEDDING_RETRY_BASE_DELAY_SEC=2.0 # 재시도 기본 지연(초). delay=base*2^attempt (최대 30s) diff --git a/ai/src/ai_server/config/settings.py b/ai/src/ai_server/config/settings.py index 6f13435..f85441f 100644 --- a/ai/src/ai_server/config/settings.py +++ b/ai/src/ai_server/config/settings.py @@ -39,6 +39,12 @@ class Settings(BaseSettings): ai_realtime_exchange: str = "stackup.realtime" ai_realtime_routing_user: str = "realtime.user.notify" feedback_rag_top_k: int = 5 + # 답변별 복기(코칭)는 답변 수만큼 LLM 을 부르는 피드백 최대 비용 지점이다. 아래 둘이 + # 게이트웨이 429/과부하를 막는 안전밸브인데 상수로 박혀 있어 조일 수가 없었다 — + # 다른 부하 관련 값(embedding_batch_size·llm_*_timeout_sec·*_rag_timeout_sec)과 같이 + # 환경변수로 뺀다. 게이트웨이가 조이기 시작하면 재배포 없이 이 둘을 낮춘다. + feedback_coaching_max_answers: int = 30 + feedback_coaching_concurrency: int = 5 # RAG 컨텍스트 구성(임베딩+검색) 전체 상한. 초과 시 (none) 으로 폴백해 첫 토큰을 막지 않는다. followup_rag_timeout_sec: float = 1.5 # 질문 풀 생성 시 다문서 RAG 검색 상한. followup 과 대칭 — 초과 시 base_context 로 폴백. diff --git a/ai/src/ai_server/messaging/runner.py b/ai/src/ai_server/messaging/runner.py index 36d2f9e..eb4c51d 100644 --- a/ai/src/ai_server/messaging/runner.py +++ b/ai/src/ai_server/messaging/runner.py @@ -259,6 +259,8 @@ def __init__(self, settings: Settings) -> None: core_client=core_client, embedder=embedder, rag_top_k=settings.feedback_rag_top_k, + coaching_max_answers=settings.feedback_coaching_max_answers, + coaching_concurrency=settings.feedback_coaching_concurrency, # 자기소개 첫인상 평가(Flash, 경량). 종합 점수엔 미포함, 패널 '첫인상' 항목으로만 표시. self_intro_evaluator=LlmSelfIntroEvaluator( build_self_intro_evaluation_chain(settings, core_client=core_client) diff --git a/ai/tests/test_feedback_consumer.py b/ai/tests/test_feedback_consumer.py index f50ef76..f2997e1 100644 --- a/ai/tests/test_feedback_consumer.py +++ b/ai/tests/test_feedback_consumer.py @@ -1264,3 +1264,25 @@ async def test_no_storage_keeps_report_key_none(): payload: FeedbackCallbackPayload = publisher.publish.await_args.kwargs["payload"] assert payload.report_s3_key is None + + +# 코칭 상한·동시성은 게이트웨이 429 를 막는 안전밸브다. 상수로 박혀 있으면 게이트웨이가 +# 조이기 시작해도 재배포 없이는 낮출 수 없다 — runner 가 settings 값을 실제로 전달하는지 고정. +def test_runner_wires_coaching_limits_from_settings() -> None: + import inspect + + from ai_server.messaging import runner as runner_module + + # 공백·줄바꿈에 깨지지 않게 정규화한 뒤 본다. 런타임 조립에 필요한 의존성이 많아 + # 인스턴스를 만들어 검증하기보다 배선 자체를 고정하는 쪽이 싸다. + source = "".join(inspect.getsource(runner_module).split()) + assert "coaching_max_answers=settings.feedback_coaching_max_answers" in source + assert "coaching_concurrency=settings.feedback_coaching_concurrency" in source + + +def test_coaching_limits_are_settings_backed() -> None: + from ai_server.config.settings import Settings + + fields = Settings.model_fields + assert "feedback_coaching_max_answers" in fields + assert "feedback_coaching_concurrency" in fields diff --git a/docs/environment.md b/docs/environment.md index 47f14d9..6d53705 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -148,6 +148,8 @@ EMBEDDING_DIM=1536 # DB 컬럼 차원과 일치 필수 EMBEDDING_CHUNK_SIZE=1000 EMBEDDING_CHUNK_OVERLAP=200 EMBEDDING_BATCH_SIZE=32 # 한 요청당 청크 수 (작을수록 429 회피, 호출 수↑) +FEEDBACK_COACHING_MAX_ANSWERS=30 # 답변별 복기 대상 상한 (초과분은 잘라내고 로그 남김) +FEEDBACK_COACHING_CONCURRENCY=5 # 복기 동시 호출 수 (429 회피용 — 낮출수록 느려지고 안전) EMBEDDING_MAX_RETRIES=5 # 429(RESOURCE_EXHAUSTED) 지수 백오프 재시도 횟수 EMBEDDING_RETRY_BASE_DELAY_SEC=2.0 # delay = base*2^attempt + jitter (상한 30s)