서버 안정화와 평가 모델 라우팅 보강

This commit is contained in:
Yun Chan 2026-06-27 16:34:27 +09:00
parent 0cdbf8e4fe
commit 0eb7d925ed
10 changed files with 179 additions and 39 deletions

View file

@ -84,6 +84,14 @@ class Settings(BaseSettings):
default=0.0,
validation_alias="ADMIN_USAGE_BUDGET_USD",
)
evaluator_fast_model: str = Field(
default="",
validation_alias="EVALUATOR_FAST_MODEL",
)
evaluator_deep_model: str = Field(
default="",
validation_alias="EVALUATOR_DEEP_MODEL",
)
# ── 외부 LLM 키 (게이트웨이가 못 받을 때 직접 폴백, PII 마스킹 후만) ──
anthropic_api_key: str = Field(default="", validation_alias="ANTHROPIC_API_KEY")

View file

@ -13,7 +13,7 @@ MASTERPLAN §2.3 (평가 AI 2-tier 루프):
후보로 제시하고 *근거(rationale)* 요구한다. LLM 출력은 enum 으로 안전 파싱(미지값은 버림).
- intent_deviation('의도와 다른 부분') 1 시민 SupervisorComment(critique) 형식과 정합:
{dimension, expected, actual, severity}.
- engine_client.generate(tier='feedback', structured_schema=...) LLM 평가. 엔진 장애·파싱
- engine_client.generate(structured_schema=...) LLM 평가. 엔진 장애·파싱
실패는 *비치명적* orchestrator eval_hook None 으로 흡수(상담 루프를 막지 않음).
- 결과는 pydantic 모델로 반환. orchestrator 주입형으로 부르는 async 함수
evaluate_turn(...) / evaluate_session(...) export.
@ -31,6 +31,7 @@ from typing import TYPE_CHECKING, Any, Optional
from pydantic import BaseModel, Field
from ..config import settings
from ..engine_client import (
EngineClient,
EngineError,
@ -67,6 +68,11 @@ _APPROPRIATENESS = ("pos", "warn", "neutral")
_SEVERITY = ("minor", "moderate", "major")
def _configured_model(value: str | None) -> str | None:
model = (value or "").strip()
return model or None
def _parse_technique(raw: str) -> Optional[Technique]:
s = (raw or "").strip()
return _TECHNIQUE_BY_KO.get(s) or _TECHNIQUE_BY_CODE.get(s)
@ -641,6 +647,7 @@ async def evaluate_turn(
ai_role="evaluator",
messages=build_fast_messages(ctx, client_reply),
structured_schema=_fast_schema(),
model=_configured_model(settings.evaluator_fast_model),
max_tokens=900,
temperature=0.2, # 평가는 보수적·재현적으로
session_id=ctx.session_id,
@ -717,6 +724,7 @@ async def evaluate_session(
distribution=distribution,
),
structured_schema=_deep_schema(),
model=_configured_model(settings.evaluator_deep_model),
max_tokens=2048,
temperature=0.3,
session_id=session_id,

View file

@ -0,0 +1,117 @@
"""Regression tests for evaluator low-cost model routing."""
from __future__ import annotations
import unittest
from typing import Any
from .config import settings
from .engine_client import GenerateResponse
from .services import evaluator, orchestrator, persona, state_machine
def _initial_state() -> state_machine.SessionState:
return state_machine.init_state(
params=persona.P1.openness_params(),
)
def _turn_context() -> orchestrator.TurnContext:
return orchestrator.prepare_turn(
session_id="evaluator-model-session",
case_id="evaluator-model-case",
card=persona.P1,
state=_initial_state(),
learner_text="요즘 학교 가는 게 너무 부담돼요.",
theory_mode="humanistic",
)
class CaptureEvaluatorEngine:
def __init__(self) -> None:
self.requests: list[Any] = []
async def generate(self, req: Any) -> GenerateResponse:
self.requests.append(req)
loop = req.metadata.get("loop")
if loop == "fast":
structured = {
"techniques": [],
"client_state_read": [],
"appropriateness": "neutral",
"rapport_signal": 0.0,
"intent_deviations": [],
}
else:
structured = {
"strengths": [],
"improvements": [],
"alternative_utterances": [],
"intent_deviations": [],
}
return GenerateResponse(
text="",
model=req.model or "gateway-default",
provider="fake-provider",
structured=structured,
)
class EvaluatorModelRoutingTest(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self) -> None:
self._fast_model = settings.evaluator_fast_model
self._deep_model = settings.evaluator_deep_model
settings.evaluator_fast_model = ""
settings.evaluator_deep_model = ""
async def asyncTearDown(self) -> None:
settings.evaluator_fast_model = self._fast_model
settings.evaluator_deep_model = self._deep_model
async def test_fast_evaluator_uses_configured_model_override(self) -> None:
settings.evaluator_fast_model = "cheap-fast"
engine = CaptureEvaluatorEngine()
result = await evaluator.evaluate_turn(
_turn_context(),
"괜찮아요.",
engine=engine, # type: ignore[arg-type]
)
self.assertIsNone(result.error)
self.assertEqual(len(engine.requests), 1)
self.assertEqual(engine.requests[0].ai_role, "evaluator")
self.assertEqual(engine.requests[0].model, "cheap-fast")
async def test_deep_evaluator_uses_configured_model_override(self) -> None:
settings.evaluator_deep_model = "cheap-deep"
engine = CaptureEvaluatorEngine()
result = await evaluator.evaluate_session(
session_id="evaluator-model-session",
stage="라포",
masked_turns=[
{"speaker": "counselor", "text": "천천히 이야기해줘도 괜찮아요."},
{"speaker": "client", "text": "잘 모르겠어요."},
],
engine=engine, # type: ignore[arg-type]
)
self.assertIsNone(result.error)
self.assertEqual(len(engine.requests), 1)
self.assertEqual(engine.requests[0].ai_role, "evaluator")
self.assertEqual(engine.requests[0].model, "cheap-deep")
async def test_blank_model_settings_keep_gateway_default_routing(self) -> None:
settings.evaluator_fast_model = " "
settings.evaluator_deep_model = ""
engine = CaptureEvaluatorEngine()
await evaluator.evaluate_turn(
_turn_context(),
"괜찮아요.",
engine=engine, # type: ignore[arg-type]
)
self.assertEqual(len(engine.requests), 1)
self.assertIsNone(engine.requests[0].model)