feat: EXAONE 분류기 — direct/route/clarify 라우팅 + 대화 기억
- EXAONE: 분류기+프롬프트엔지니어+직접응답 (JSON 출력) - 간단한 질문은 EXAONE이 직접 답변 (파이프라인 스킵) - 복잡한 질문은 AI 최적화 프롬프트로 Gemma에 전달 - 모호한 질문은 사용자에게 추가 질문 (clarify) - user별 최근 대화 기억 (최대 10개, 1시간 TTL) - ModelAdapter: messages 직접 전달 옵션 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,40 +10,45 @@ from services.model_adapter import ModelAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REWRITER_PROMPT = (
|
||||
"너는 질문 재구성 전문가다. "
|
||||
"사용자의 질문을 분석하여 의도를 명확히 하고, 더 명확한 질문으로 재작성하라. "
|
||||
"재구성된 질문만 출력하라. 부연 설명이나 답변은 절대 하지 마라. "
|
||||
"인사, 잡담, 간단한 질문, 1~2문장 질문은 원문 그대로 출력하라. "
|
||||
"복잡하거나 모호한 질문만 재구성하라."
|
||||
)
|
||||
CLASSIFIER_PROMPT = """\
|
||||
너는 AI 라우터다. 사용자 메시지를 분석하여 JSON으로 응답하라.
|
||||
반드시 아래 3가지 action 중 하나를 선택하라:
|
||||
|
||||
1. "direct" — 인사, 잡담, 간단한 질문, 자기소개 요청 등. 네가 직접 답변한다.
|
||||
2. "route" — 복잡한 질문, 분석, 설명, 코딩 등. 추론 모델에게 넘긴다. 이때 prompt 필드에 추론 모델이 이해하기 좋게 정리된 프롬프트를 작성하라.
|
||||
3. "clarify" — 질문이 모호하거나 정보가 부족할 때. 사용자에게 추가 질문한다.
|
||||
|
||||
반드시 아래 JSON 형식으로만 응답하라. JSON 외 텍스트는 절대 출력하지 마라:
|
||||
{"action": "direct|route|clarify", "response": "direct/clarify일 때 사용자에게 보낼 텍스트", "prompt": "route일 때 추론 모델에게 보낼 프롬프트"}
|
||||
|
||||
너의 이름은 '이드'이고, 상냥하고 친근하게 대화한다.
|
||||
너는 GPU 서버의 EXAONE 모델과 맥미니의 Gemma4 모델로 구성된 NanoClaude 파이프라인에서 돌아간다.
|
||||
대화 이력이 있으면 맥락을 고려하라.\
|
||||
"""
|
||||
|
||||
REASONER_PROMPT = (
|
||||
"너는 '이드'라는 이름의 상냥하고 친근한 AI 어시스턴트야. "
|
||||
"너는 GPU 서버의 EXAONE 모델(질문 정리)과 맥미니의 Gemma4 모델(답변 생성)로 구성된 "
|
||||
"NanoClaude 파이프라인 위에서 돌아가고 있어. "
|
||||
"간결하고 자연스럽게 대화해. 인사에는 짧게 인사로 답하고, "
|
||||
"간결하고 자연스럽게 대화해. "
|
||||
"질문에는 핵심만 명확하게 답해. "
|
||||
"불필요한 구조화(번호 매기기, 헤더, 마크다운)는 피하고, 대화하듯 편하게 답변해. "
|
||||
"너 자신에 대한 질문에는 솔직하게 답해."
|
||||
"불필요한 구조화(번호 매기기, 헤더, 마크다운)는 피하고, 대화하듯 편하게 답변해."
|
||||
)
|
||||
|
||||
|
||||
class BackendRegistry:
|
||||
def __init__(self) -> None:
|
||||
self.rewriter: ModelAdapter | None = None
|
||||
self.reasoner: ModelAdapter | None = None
|
||||
self._health: dict[str, bool] = {"rewriter": False, "reasoner": False}
|
||||
self._latency: dict[str, float] = {"rewriter": 0.0, "reasoner": 0.0}
|
||||
self.classifier: ModelAdapter | None = None # EXAONE: 분류 + 직접응답
|
||||
self.reasoner: ModelAdapter | None = None # Gemma4: 추론
|
||||
self._health: dict[str, bool] = {"classifier": False, "reasoner": False}
|
||||
self._latency: dict[str, float] = {"classifier": 0.0, "reasoner": 0.0}
|
||||
self._health_task: asyncio.Task | None = None
|
||||
|
||||
def init_from_settings(self, settings) -> None:
|
||||
self.rewriter = ModelAdapter(
|
||||
self.classifier = ModelAdapter(
|
||||
name="EXAONE",
|
||||
base_url=settings.exaone_base_url,
|
||||
model=settings.exaone_model,
|
||||
system_prompt=REWRITER_PROMPT,
|
||||
temperature=settings.exaone_temperature,
|
||||
system_prompt=CLASSIFIER_PROMPT,
|
||||
temperature=0.3, # 분류는 낮은 temperature
|
||||
timeout=settings.exaone_timeout,
|
||||
)
|
||||
self.reasoner = ModelAdapter(
|
||||
@@ -68,7 +73,7 @@ class BackendRegistry:
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
async def _check_all(self) -> None:
|
||||
for role, adapter in [("rewriter", self.rewriter), ("reasoner", self.reasoner)]:
|
||||
for role, adapter in [("classifier", self.classifier), ("reasoner", self.reasoner)]:
|
||||
if not adapter:
|
||||
continue
|
||||
start = time.monotonic()
|
||||
@@ -86,7 +91,7 @@ class BackendRegistry:
|
||||
|
||||
def health_summary(self) -> dict:
|
||||
result = {}
|
||||
for role, adapter in [("rewriter", self.rewriter), ("reasoner", self.reasoner)]:
|
||||
for role, adapter in [("classifier", self.classifier), ("reasoner", self.reasoner)]:
|
||||
if adapter:
|
||||
result[role] = {
|
||||
"name": adapter.name,
|
||||
|
||||
Reference in New Issue
Block a user