From 1d406f6a4e38962d1b9e9b4c46c391632197ca90 Mon Sep 17 00:00:00 2001 From: hbyang Date: Fri, 12 Jun 2026 17:33:59 +0900 Subject: [PATCH] Diversify AI predictions via per-model analyst personas GPT (data-driven), Claude (tactical/upset-aware), Gemini (attacking-minded) each get a distinct perspective so the 3 picks genuinely diverge instead of producing identical scorelines. Honest: still each model's own judgment. Finished matches are untouched (generation filters result_outcome IS NULL). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/services/ai.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/backend/app/services/ai.py b/backend/app/services/ai.py index 4257404..4f41c3a 100644 --- a/backend/app/services/ai.py +++ b/backend/app/services/ai.py @@ -32,10 +32,33 @@ class MatchContext: kickoff: str # ISO -def _prompt(ctx: MatchContext) -> str: +# 모델별 분석 관점(페르소나) — 동일 경기라도 서로 다른 시각으로 보게 해 +# 예측이 자연스럽게 갈리도록 한다(강제 분산이 아니라 진짜 판단의 다양화). +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." + ), +} + + +def _prompt(ctx: MatchContext, persona: str) -> str: return ( - f"You are a football match analyst. Predict the result of this " - f"2026 FIFA World Cup match.\n" + 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"Team A: {ctx.team_a}\nTeam B: {ctx.team_b}\n" f"Venue: {ctx.venue}\nKickoff: {ctx.kickoff}\n" f"Important: World Cup group-stage matches are played at NEUTRAL venues. " @@ -96,7 +119,7 @@ async def predict_gpt(ctx: MatchContext) -> dict: model=settings.openai_model, messages=[ {"role": "system", "content": "You output only valid JSON."}, - {"role": "user", "content": _prompt(ctx)}, + {"role": "user", "content": _prompt(ctx, PERSONA["GPT"])}, ], response_format={"type": "json_object"}, ) @@ -116,7 +139,7 @@ async def predict_claude(ctx: MatchContext) -> dict: return await client.messages.create( model=settings.anthropic_model, max_tokens=1024, - messages=[{"role": "user", "content": _prompt(ctx)}], + messages=[{"role": "user", "content": _prompt(ctx, PERSONA["Claude"])}], **extra, ) @@ -143,7 +166,7 @@ async def predict_gemini(ctx: MatchContext) -> dict: client = genai.Client(api_key=settings.google_api_key) resp = await client.aio.models.generate_content( model=settings.google_model, - contents=_prompt(ctx), + contents=_prompt(ctx, PERSONA["Gemini"]), config=types.GenerateContentConfig(response_mime_type="application/json"), ) return _normalize(json.loads(resp.text or "{}"))