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 — 프롬프트·스코어 범위 분기
|
league: str = "wc" # wc(축구) | kbo | mlb | mls — 프롬프트·스코어 범위 분기
|
||||||
|
|
||||||
|
|
||||||
# 모델별 분석 관점(페르소나) — 동일 경기라도 서로 다른 시각으로 보게 해
|
# 공통 페르소나 — 세 모델 모두 동일한 입력(동일 프롬프트+동일 데이터)을 받는다.
|
||||||
# 예측이 자연스럽게 갈리도록 한다(강제 분산이 아니라 진짜 판단의 다양화).
|
# 예측 차이는 모델 자체의 판단 차이에서만 나온다.
|
||||||
PERSONA = {
|
ANALYST = (
|
||||||
"GPT": (
|
"You are an expert soccer analyst. Use ALL of the factual match data provided "
|
||||||
"You are a DATA-DRIVEN analyst. Base your call on recent form, head-to-head, "
|
"below — recent form, standings, head-to-head and matchup context — and weigh "
|
||||||
"FIFA ranking gaps and goals-scored/conceded trends. Be objective and "
|
"it over your prior knowledge to make the most accurate prediction possible."
|
||||||
"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."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
BASEBALL_ANALYST = (
|
||||||
# 야구용 페르소나 — 축구 페르소나와 같은 3분화(데이터/수비·투수/공격) 구도.
|
"You are an expert baseball analyst. Use ALL of the factual match data provided "
|
||||||
BASEBALL_PERSONA = {
|
"below — recent form, standings with games-behind, team batting/ERA, "
|
||||||
"GPT": (
|
"head-to-head record, starting pitchers, confirmed lineups and yesterday's "
|
||||||
"You are a DATA-DRIVEN baseball analyst. Base your call on recent form, "
|
"results — and weigh it over your prior knowledge to make the most accurate "
|
||||||
"season standings, head-to-head record and run-scored/allowed trends. "
|
"prediction possible."
|
||||||
"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."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
_LEAGUE_LABEL = {
|
_LEAGUE_LABEL = {
|
||||||
"kbo": "2026 KBO League (Korean professional baseball) regular-season game",
|
"kbo": "2026 KBO League (Korean professional baseball) regular-season game",
|
||||||
@ -82,7 +57,7 @@ _LEAGUE_LABEL = {
|
|||||||
|
|
||||||
|
|
||||||
def _prompt_baseball(ctx: MatchContext, model: str) -> str:
|
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 ""
|
data = f"\n{ctx.data_block}\n" if ctx.data_block else ""
|
||||||
draw_note = (
|
draw_note = (
|
||||||
"KBO regular-season games can end in a DRAW after 12 innings, but draws "
|
"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 (
|
return (
|
||||||
f"{persona}\n"
|
f"{persona}\n"
|
||||||
f"Predict the result of this {_LEAGUE_LABEL[ctx.league]} using YOUR "
|
f"Predict the result of this {_LEAGUE_LABEL[ctx.league]}.\n"
|
||||||
f"perspective above. Judge independently — it is fine to differ from the "
|
|
||||||
f"obvious consensus pick when your perspective warrants it.\n"
|
|
||||||
f"Team A (away): {ctx.team_a}\nTeam B (home): {ctx.team_b}\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"Ballpark: {ctx.venue}\nFirst pitch: {ctx.kickoff}\n"
|
||||||
f"{data}"
|
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 ""
|
data = f"\n{ctx.data_block}\n" if ctx.data_block else ""
|
||||||
return (
|
return (
|
||||||
f"{persona}\n"
|
f"{persona}\n"
|
||||||
f"Predict the result of this 2026 FIFA World Cup match using YOUR perspective "
|
f"Predict the result of this 2026 FIFA World Cup match.\n"
|
||||||
f"above. Judge independently — it is fine to differ from the obvious consensus "
|
|
||||||
f"pick when your perspective warrants it.\n"
|
|
||||||
f"Team A: {ctx.team_a}\nTeam B: {ctx.team_b}\n"
|
f"Team A: {ctx.team_a}\nTeam B: {ctx.team_b}\n"
|
||||||
f"Venue: {ctx.venue}\nKickoff: {ctx.kickoff}\n"
|
f"Venue: {ctx.venue}\nKickoff: {ctx.kickoff}\n"
|
||||||
f"{data}"
|
f"{data}"
|
||||||
@ -142,8 +113,7 @@ def _prompt_mls(ctx: MatchContext, persona: str) -> str:
|
|||||||
return (
|
return (
|
||||||
f"{persona}\n"
|
f"{persona}\n"
|
||||||
f"Predict the result of this 2026 MLS (Major League Soccer) regular-season "
|
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"match.\n"
|
||||||
f"differ from the obvious consensus pick when your perspective warrants it.\n"
|
|
||||||
f"Team A (away): {ctx.team_a}\nTeam B (home): {ctx.team_b}\n"
|
f"Team A (away): {ctx.team_a}\nTeam B (home): {ctx.team_b}\n"
|
||||||
f"Venue: {ctx.venue}\nKickoff: {ctx.kickoff}\n"
|
f"Venue: {ctx.venue}\nKickoff: {ctx.kickoff}\n"
|
||||||
f"{data}"
|
f"{data}"
|
||||||
@ -166,8 +136,8 @@ def _build_prompt(ctx: MatchContext, model: str) -> str:
|
|||||||
if ctx.league in ("kbo", "mlb"):
|
if ctx.league in ("kbo", "mlb"):
|
||||||
return _prompt_baseball(ctx, model)
|
return _prompt_baseball(ctx, model)
|
||||||
if ctx.league == "mls":
|
if ctx.league == "mls":
|
||||||
return _prompt_mls(ctx, PERSONA[model])
|
return _prompt_mls(ctx, ANALYST)
|
||||||
return _prompt(ctx, PERSONA[model])
|
return _prompt(ctx, ANALYST)
|
||||||
|
|
||||||
|
|
||||||
# JSON Schema (구조화 출력용 — Anthropic/OpenAI 공통)
|
# 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)"
|
f" ({s['w']}W-{s['l']}L)"
|
||||||
if s.get("w") is not None and s.get("l") is not None else ""
|
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}"
|
return f"[{side} starting pitcher] {s['name']}{era}{rec}{vs}"
|
||||||
|
|
||||||
|
|
||||||
def _standing_line(name: str, st: dict | None) -> str | None:
|
def _standing_line(name: str, st: dict | None) -> str | None:
|
||||||
if not st:
|
if not st:
|
||||||
return None
|
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 ""
|
extra = f", last5 {st['last5']}" if st.get("last5") else ""
|
||||||
return (
|
return (
|
||||||
f"[{name} standings] rank {st.get('rank')}, {st.get('w')}W-"
|
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
|
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:
|
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)
|
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)
|
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(
|
h2h = await _h2h_line(
|
||||||
db, match.league, match.team_a_code, match.team_b_code, match.kickoff_at
|
db, match.league, match.team_a_code, match.team_b_code, match.kickoff_at
|
||||||
)
|
)
|
||||||
|
context = await _context_extras(db, match)
|
||||||
return "\n".join([
|
return "\n".join([
|
||||||
"=== MATCH DATA (factual; weigh heavily over priors) ===",
|
"=== MATCH DATA (factual; weigh heavily over priors) ===",
|
||||||
"Away " + _team_lines(match.team_a_name, ga),
|
"Away " + _team_lines(match.team_a_name, ga),
|
||||||
"Home " + _team_lines(match.team_b_name, gb),
|
"Home " + _team_lines(match.team_b_name, gb),
|
||||||
f"[Head-to-head in our data] {h2h}",
|
f"[Head-to-head in our data] {h2h}",
|
||||||
*extras,
|
*extras,
|
||||||
|
*context,
|
||||||
"===",
|
"===",
|
||||||
])
|
])
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user