feat(eval): 평가 AI fast-loop에 골든셋 few-shot 주입 — 합성 데이터 실활용

- evaluator._load_fewshot_examples/_fewshot_block: data/golden 골든셋에서
  기법 다양성 커버하는 상담자 발화 6개를 few-shot 예시로 로드(GOLDEN_DIR env,
  없으면 graceful 빈 블록).
- build_fast_messages 프롬프트에 주입 → 평가 라벨 일관성 보정.
- 윤찬 6결정 '재귀학습 few-shot부터' 실제 구현, P4-P7 골든셋 실활용.
- 검증: 9기법 커버(empathy/reflection/validation/holding/exploration/
  facilitative_question/interpretation/normalization/principle_explanation),
  app.main import OK.
This commit is contained in:
Yun Chan 2026-06-26 01:17:44 +09:00
parent 4187936848
commit c953aa5bea

View file

@ -25,6 +25,7 @@ MASTERPLAN §2.3 (평가 AI 2-tier 루프):
from __future__ import annotations from __future__ import annotations
import json import json
import os
from typing import TYPE_CHECKING, Any, Optional from typing import TYPE_CHECKING, Any, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -300,6 +301,62 @@ def _client_state_candidates_block() -> str:
return f"[내담자 상태 후보 — code:라벨]\n- {items}" return f"[내담자 상태 후보 — code:라벨]\n- {items}"
# ─ few-shot 골든셋 예시(data/golden) — 발화→기법 라벨 일관성 보정 ────────────
# 윤찬 6결정 '재귀학습 few-shot부터' + taxonomy '0615 합성변형' 설계의 실제 활용.
# 컨테이너/배포에선 GOLDEN_DIR env 로 마운트 경로 지정. 없으면 graceful(빈 블록).
_GOLDEN_DIR = os.environ.get("GOLDEN_DIR") or os.path.join(
os.path.dirname(__file__), "..", "..", "..", "..", "data", "golden"
)
def _load_fewshot_examples(max_n: int = 6) -> list[dict[str, Any]]:
"""골든셋에서 기법 다양성을 커버하는 상담자 발화 few-shot 예시(없으면 빈 리스트)."""
out: list[dict[str, Any]] = []
seen: set[str] = set()
try:
files = sorted(f for f in os.listdir(_GOLDEN_DIR) if f.endswith(".jsonl"))
except OSError:
return out
for fn in files:
try:
with open(os.path.join(_GOLDEN_DIR, fn), encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
d = json.loads(line)
if d.get("speaker") != "counselor" or not d.get("techniques"):
continue
if set(d["techniques"]) - seen: # 새 기법 커버 예시 우선
out.append(d)
seen.update(d["techniques"])
if len(out) >= max_n:
return out
except (OSError, json.JSONDecodeError):
continue
return out
def _fewshot_block() -> str:
"""발화→기법 few-shot 예시 블록(평가 라벨 일관성 보정). 골든셋 없으면 빈 문자열."""
ex = _load_fewshot_examples()
if not ex:
return ""
lines = ["[few-shot 예시 — 발화 → 기법 code (골든셋 참고, 라벨 일관성 보정용)]"]
for e in ex:
techs = ",".join(e.get("techniques", []))
text = (e.get("text") or "").replace("\n", " ")[:70]
rat = next(
(c.get("text", "") for c in e.get("comments", []) if c.get("kind") == "rationale"),
"",
)
line = f'- "{text}"{techs}'
if rat:
line += f" (근거: {rat[:48]}…)"
lines.append(line)
return "\n".join(lines)
def _theory_mode(ctx: "TurnContext") -> Optional[str]: def _theory_mode(ctx: "TurnContext") -> Optional[str]:
"""페르소나 theory_target 에서 이론 모드 힌트(이론부합 평가용). 없으면 None.""" """페르소나 theory_target 에서 이론 모드 힌트(이론부합 평가용). 없으면 None."""
tt = getattr(ctx.persona, "theory_target", None) tt = getattr(ctx.persona, "theory_target", None)
@ -325,10 +382,11 @@ def build_fast_messages(ctx: "TurnContext", client_reply: str) -> list[EngineMes
) )
system = "\n\n".join( system = "\n\n".join(
[ p for p in [
_EVAL_ROLE, _EVAL_ROLE,
_technique_candidates_block(), _technique_candidates_block(),
_client_state_candidates_block(), _client_state_candidates_block(),
_fewshot_block(), # 골든셋 few-shot(없으면 빈 문자열 → 필터됨)
( (
"[평가 4차원]\n" "[평가 4차원]\n"
"① technique: 이번 *상담자(학습자)* 발화에 부착되는 기법(복수 가능, 후보 code 만).\n" "① technique: 이번 *상담자(학습자)* 발화에 부착되는 기법(복수 가능, 후보 code 만).\n"
@ -338,7 +396,7 @@ def build_fast_messages(ctx: "TurnContext", client_reply: str) -> list[EngineMes
"없으면 null. 이 항목은 가장 중요하다 — 무리한 생성 금지, 진짜 이탈만.\n" "없으면 null. 이 항목은 가장 중요하다 — 무리한 생성 금지, 진짜 이탈만.\n"
"추가로 rapport_signal(1~+1): 이 발화가 라포에 끼친 방향(공감·반영=+, 조언점프·평가=)." "추가로 rapport_signal(1~+1): 이 발화가 라포에 끼친 방향(공감·반영=+, 조언점프·평가=)."
), ),
] ] if p
) )
user = ( user = (