AI 예측 입력 통일·데이터 전면 보강
- 3모델 페르소나 분화 제거 — 동일 프롬프트+동일 데이터 (차이는 모델 판단만) - 순위 라인에 게임차·팀 타율·팀 ERA 추가 - 전날 결과+활약(홈런·맹타), 확정 라인업+타자 시즌 타율 주입 (응원가 파이프라인 헬퍼 재사용, 라인업 미발표 시 생략) - 선발 상대 ERA 0.00(기록 없음) 오표기 생략 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9e8993c67a
commit
f8c24d1f3c
@ -34,46 +34,21 @@ class MatchContext:
|
||||
league: str = "wc" # wc(축구) | kbo | mlb | mls — 프롬프트·스코어 범위 분기
|
||||
|
||||
|
||||
# 모델별 분석 관점(페르소나) — 동일 경기라도 서로 다른 시각으로 보게 해
|
||||
# 예측이 자연스럽게 갈리도록 한다(강제 분산이 아니라 진짜 판단의 다양화).
|
||||
PERSONA = {
|
||||
"GPT": (
|
||||
"You are a DATA-DRIVEN analyst. Base your call on recent form, head-to-head, "
|
||||
"FIFA ranking gaps and goals-scored/conceded trends. Be objective and "
|
||||
"evidence-led; pick the scoreline the numbers most support."
|
||||
),
|
||||
"Claude": (
|
||||
"You are a TACTICAL analyst. Focus on matchups, defensive organization, "
|
||||
"midfield control and game-state. Take low-scoring games, tight margins and "
|
||||
"genuine upset potential seriously — do not just rubber-stamp the favorite."
|
||||
),
|
||||
"Gemini": (
|
||||
"You are an ATTACKING-MINDED analyst. Weigh momentum, attacking quality, "
|
||||
"star players and scoring potential. Lean toward open, higher-scoring "
|
||||
"scenarios when the talent and tempo justify it."
|
||||
),
|
||||
}
|
||||
# 공통 페르소나 — 세 모델 모두 동일한 입력(동일 프롬프트+동일 데이터)을 받는다.
|
||||
# 예측 차이는 모델 자체의 판단 차이에서만 나온다.
|
||||
ANALYST = (
|
||||
"You are an expert soccer analyst. Use ALL of the factual match data provided "
|
||||
"below — recent form, standings, head-to-head and matchup context — and weigh "
|
||||
"it over your prior knowledge to make the most accurate prediction possible."
|
||||
)
|
||||
|
||||
|
||||
# 야구용 페르소나 — 축구 페르소나와 같은 3분화(데이터/수비·투수/공격) 구도.
|
||||
BASEBALL_PERSONA = {
|
||||
"GPT": (
|
||||
"You are a DATA-DRIVEN baseball analyst. Base your call on recent form, "
|
||||
"season standings, head-to-head record and run-scored/allowed trends. "
|
||||
"Be objective and evidence-led; pick the scoreline the numbers most support."
|
||||
),
|
||||
"Claude": (
|
||||
"You are a PITCHING-AND-DEFENSE analyst. Weigh starting rotation strength, "
|
||||
"bullpen fatigue, and defensive quality. Take low-scoring games, tight "
|
||||
"margins and genuine upset potential seriously — do not just rubber-stamp "
|
||||
"the favorite."
|
||||
),
|
||||
"Gemini": (
|
||||
"You are an OFFENSE-MINDED analyst. Weigh lineup depth, power hitting, "
|
||||
"momentum and ballpark factors. Lean toward open, higher-scoring scenarios "
|
||||
"when the bats and conditions justify it."
|
||||
),
|
||||
}
|
||||
BASEBALL_ANALYST = (
|
||||
"You are an expert baseball analyst. Use ALL of the factual match data provided "
|
||||
"below — recent form, standings with games-behind, team batting/ERA, "
|
||||
"head-to-head record, starting pitchers, confirmed lineups and yesterday's "
|
||||
"results — and weigh it over your prior knowledge to make the most accurate "
|
||||
"prediction possible."
|
||||
)
|
||||
|
||||
_LEAGUE_LABEL = {
|
||||
"kbo": "2026 KBO League (Korean professional baseball) regular-season game",
|
||||
@ -82,7 +57,7 @@ _LEAGUE_LABEL = {
|
||||
|
||||
|
||||
def _prompt_baseball(ctx: MatchContext, model: str) -> str:
|
||||
persona = BASEBALL_PERSONA[model]
|
||||
persona = BASEBALL_ANALYST # 모델 공통 — model 파라미터는 호출부 호환용
|
||||
data = f"\n{ctx.data_block}\n" if ctx.data_block else ""
|
||||
draw_note = (
|
||||
"KBO regular-season games can end in a DRAW after 12 innings, but draws "
|
||||
@ -92,9 +67,7 @@ def _prompt_baseball(ctx: MatchContext, model: str) -> str:
|
||||
)
|
||||
return (
|
||||
f"{persona}\n"
|
||||
f"Predict the result of this {_LEAGUE_LABEL[ctx.league]} using YOUR "
|
||||
f"perspective above. Judge independently — it is fine to differ from the "
|
||||
f"obvious consensus pick when your perspective warrants it.\n"
|
||||
f"Predict the result of this {_LEAGUE_LABEL[ctx.league]}.\n"
|
||||
f"Team A (away): {ctx.team_a}\nTeam B (home): {ctx.team_b}\n"
|
||||
f"Ballpark: {ctx.venue}\nFirst pitch: {ctx.kickoff}\n"
|
||||
f"{data}"
|
||||
@ -115,9 +88,7 @@ def _prompt(ctx: MatchContext, persona: str) -> str:
|
||||
data = f"\n{ctx.data_block}\n" if ctx.data_block else ""
|
||||
return (
|
||||
f"{persona}\n"
|
||||
f"Predict the result of this 2026 FIFA World Cup match using YOUR perspective "
|
||||
f"above. Judge independently — it is fine to differ from the obvious consensus "
|
||||
f"pick when your perspective warrants it.\n"
|
||||
f"Predict the result of this 2026 FIFA World Cup match.\n"
|
||||
f"Team A: {ctx.team_a}\nTeam B: {ctx.team_b}\n"
|
||||
f"Venue: {ctx.venue}\nKickoff: {ctx.kickoff}\n"
|
||||
f"{data}"
|
||||
@ -142,8 +113,7 @@ def _prompt_mls(ctx: MatchContext, persona: str) -> str:
|
||||
return (
|
||||
f"{persona}\n"
|
||||
f"Predict the result of this 2026 MLS (Major League Soccer) regular-season "
|
||||
f"match using YOUR perspective above. Judge independently — it is fine to "
|
||||
f"differ from the obvious consensus pick when your perspective warrants it.\n"
|
||||
f"match.\n"
|
||||
f"Team A (away): {ctx.team_a}\nTeam B (home): {ctx.team_b}\n"
|
||||
f"Venue: {ctx.venue}\nKickoff: {ctx.kickoff}\n"
|
||||
f"{data}"
|
||||
@ -166,8 +136,8 @@ def _build_prompt(ctx: MatchContext, model: str) -> str:
|
||||
if ctx.league in ("kbo", "mlb"):
|
||||
return _prompt_baseball(ctx, model)
|
||||
if ctx.league == "mls":
|
||||
return _prompt_mls(ctx, PERSONA[model])
|
||||
return _prompt(ctx, PERSONA[model])
|
||||
return _prompt_mls(ctx, ANALYST)
|
||||
return _prompt(ctx, ANALYST)
|
||||
|
||||
|
||||
# JSON Schema (구조화 출력용 — Anthropic/OpenAI 공통)
|
||||
|
||||
@ -89,17 +89,26 @@ def _starter_line(side: str, s: dict | None) -> str | None:
|
||||
f" ({s['w']}W-{s['l']}L)"
|
||||
if s.get("w") is not None and s.get("l") is not None else ""
|
||||
)
|
||||
vs = f", ERA vs this opponent {s['vsEra']}" if s.get("vsEra") else ""
|
||||
# 상대 ERA 0.00 은 대부분 '대전 기록 없음' — 무실점으로 오해하지 않게 생략
|
||||
vs_val = s.get("vsEra")
|
||||
vs = (
|
||||
f", ERA vs this opponent {vs_val}"
|
||||
if vs_val not in (None, "", 0, "0", "0.00", "-")
|
||||
else ""
|
||||
)
|
||||
return f"[{side} starting pitcher] {s['name']}{era}{rec}{vs}"
|
||||
|
||||
|
||||
def _standing_line(name: str, st: dict | None) -> str | None:
|
||||
if not st:
|
||||
return None
|
||||
gb = f", GB {st['gb']}" if st.get("gb") not in (None, "", 0, "0.0") else ""
|
||||
ba = f", team AVG {st['avg']}" if st.get("avg") else ""
|
||||
era = f", team ERA {st['era']}" if st.get("era") else ""
|
||||
extra = f", last5 {st['last5']}" if st.get("last5") else ""
|
||||
return (
|
||||
f"[{name} standings] rank {st.get('rank')}, {st.get('w')}W-"
|
||||
f"{st.get('l')}L (pct {st.get('wra')}){extra}"
|
||||
f"{st.get('l')}L (pct {st.get('wra')}){gb}{ba}{era}{extra}"
|
||||
)
|
||||
|
||||
|
||||
@ -132,6 +141,40 @@ async def _cached_extras(db, match: Match) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
async def _context_extras(db, match: Match) -> list[str]:
|
||||
"""전날 결과·활약 + 확정 라인업(타자 시즌 타율) — 응원가 파이프라인 헬퍼 재사용.
|
||||
|
||||
라인업은 발표 전이면 생략(예측이 킥오프 22h 전부터 생성되므로 보통 미포함).
|
||||
"""
|
||||
from .songs import _lineup_line, _yesterday_info, fetch_lineups
|
||||
|
||||
lines: list[str] = []
|
||||
avg_maps: dict[str, dict] = {}
|
||||
for side, code, name in (
|
||||
("a", match.team_a_code, match.team_a_short),
|
||||
("b", match.team_b_code, match.team_b_short),
|
||||
):
|
||||
label = "Away" if side == "a" else "Home"
|
||||
try:
|
||||
y, avg_map = await _yesterday_info(db, match, code, name)
|
||||
except Exception: # noqa: BLE001 — 부가 데이터 실패는 생략
|
||||
y, avg_map = None, {}
|
||||
avg_maps[side] = avg_map
|
||||
if y:
|
||||
lines.append(f"[{label} yesterday] {y}")
|
||||
try:
|
||||
lu = await fetch_lineups(match)
|
||||
except Exception: # noqa: BLE001
|
||||
lu = None
|
||||
if lu and lu.get("announced"):
|
||||
for side in ("a", "b"):
|
||||
label = "Away" if side == "a" else "Home"
|
||||
line = _lineup_line(lu, side, avg_maps.get(side))
|
||||
if line:
|
||||
lines.append(f"[{label}] {line}")
|
||||
return lines
|
||||
|
||||
|
||||
async def build_baseball_data_block(db, match: Match) -> str | None:
|
||||
ga = await _team_games(db, match.league, match.team_a_code, match.kickoff_at)
|
||||
gb = await _team_games(db, match.league, match.team_b_code, match.kickoff_at)
|
||||
@ -141,11 +184,13 @@ async def build_baseball_data_block(db, match: Match) -> str | None:
|
||||
h2h = await _h2h_line(
|
||||
db, match.league, match.team_a_code, match.team_b_code, match.kickoff_at
|
||||
)
|
||||
context = await _context_extras(db, match)
|
||||
return "\n".join([
|
||||
"=== MATCH DATA (factual; weigh heavily over priors) ===",
|
||||
"Away " + _team_lines(match.team_a_name, ga),
|
||||
"Home " + _team_lines(match.team_b_name, gb),
|
||||
f"[Head-to-head in our data] {h2h}",
|
||||
*extras,
|
||||
*context,
|
||||
"===",
|
||||
])
|
||||
|
||||
Loading…
Reference in New Issue
Block a user