서버 안정화와 평가 모델 라우팅 보강
This commit is contained in:
parent
0cdbf8e4fe
commit
0eb7d925ed
10 changed files with 179 additions and 39 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
117
apps/api/app/test_evaluator_model_routing.py
Normal file
117
apps/api/app/test_evaluator_model_routing.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue