news 2026/9/6 16:05:58

大模型当足球经理:构建AI决策仿真联赛

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型当足球经理:构建AI决策仿真联赛

1. 项目背景:这场比赛的管理者,不再是人类主教练

如果你平时关注 AI 应用,或者玩过《足球经理》这类模拟经营游戏,可能会发现一个很有意思的趋势:过去我们玩足球经理,是“人在操作、电脑模拟”;而最近开始流行一种新玩法——人类只负责搭建规则和模拟环境,真正做决策的是大语言模型。

本文要拆解的 Show HN 项目就是一个典型代表:一个机器人足球联赛,不同的前沿 AI 模型分别担任不同俱乐部的经理,AI 负责排兵布阵、制定战术、调整阵容,而比赛则由机器人球队在仿真环境中完成。

这类项目看起来像“娱乐向 Demo”,但它背后的技术栈其实非常值得学习。它把大模型的能力从“回答问题”延伸到了“在动态环境里持续做决策”,涉及模拟引擎、结构化提示词、多智能体循环、状态反馈、数据记录等多个环节。无论你是想做 AI Agent 应用,还是想设计一个类似的仿真联赛项目,都能从这套流程里找到可复用的思路。

1.1 AI 当足球经理,到底管什么

在传统足球游戏中,主教练要做的事情大概可以分成三类:

  • 赛前准备:选择首发阵容、设置战术倾向、安排盯人策略。
  • 赛中调整:根据场上形势换人、变阵。
  • 赛后复盘:观察数据,决定是否需要购买球员、调整训练计划、处理球员状态。

这个机器人足球联赛把这三类工作交给了 AI 模型。系统每一轮会给 AI 经理发送一份“俱乐部状态报告”,里面包含:

  • 球队积分和联赛排名;
  • 球员能力数据与体能状态;
  • 对手近期战绩和风格;
  • 俱乐部财务状况;
  • 上一场比赛的详细统计。

AI 经理根据这些信息返回决策,比如:

  • 本场攻击性设置多少(0-100);
  • 是否更换首发球员;
  • 是否在转会市场上买人或卖人;
  • 下一轮比赛采用保守还是激进策略。

系统拿到决策后,用比赛模拟引擎算出一场完整比赛,再把结果写回状态,进入下一轮循环。如此反复,直到一个赛季结束。

1.2 为什么选前沿 AI 模型做俱乐部经理

“前沿 AI 模型”通常指当前能力最强的商用或开源大模型。它们具备几个共同特征:

  • 指令理解能力强:能正确理解一份结构化的“比赛报告”,提取关键信息。
  • 多步推理能力不错:可以结合实际数据做“为什么输球”“下轮怎么踢”这类综合判断。
  • 输出结构化内容:通过 JSON 模式或符号约束,可以稳定输出可被程序解析的决策。
  • 成本可控:对一次决策几百字级别的输入输出来说,大部分模型的成本都落在可接受范围。

这不是说模型真的懂足球战术,而是它在文本模式上表现出了足够的“策略生成能力”。换句话说,这个项目本质上是把大模型放进了一个可验证的决策闭环里,模拟引擎给出的比赛结果,就是对它决策能力的客观评分。

1.3 技术地图

从工程实现角度看,这个项目可以拆成四个核心模块:

模块职责技术要点
数据模型描述球员、俱乐部、比赛结果Python dataclass / Pydantic
比赛引擎根据球队实力和战术计算比分泊松分布、随机模拟
AI 经理读取状态并生成管理决策LLM API、Prompt 模板、JSON 输出
联赛循环安排赛程、更新积分、输出赛季报告主循环、持久化记录

下面我们就从零开始,手写一个简化但完整的版本。


2. 整体架构与核心流程

在写代码之前,先理解整体运行流程。这个项目的核心是一个固定频率的主循环:AI 决策 → 比赛模拟 → 数据更新 → 下一轮。

2.1 核心角色

项目里只有三个角色:

  1. 球员(Player):拥有速度、射门、传球、防守等属性,还有一个身价和工资。
  2. 俱乐部(Club):拥有球员列表、预算、联赛积分和战术状态。
  3. AI 经理(AI Manager):一个外部大模型客户端,负责根据俱乐部状态输出战术和转会建议。

比赛模拟引擎不属于角色,它像裁判一样,根据双方球员能力和战术参数算出比分。

2.2 单个比赛周期流程

一场比赛从“AI 做赛前决策”到“比赛结束”的完整流程如下:

  1. 系统生成“对手报告”,包含对手球队实力、核心球员、近期战绩。
  2. 将报告发送给 AI 经理。
  3. AI 经理返回本场比赛的战术设置,例如攻击性数值。
  4. 系统把主队和客队的战术设置传入比赛引擎。
  5. 比赛引擎基于球员平均能力、战术数值,计算双方进球分布,生成比分。
  6. 系统更新俱乐部积分、进球数、球员状态。
  7. 将赛后报告发送给 AI 经理,AI 决定是否调整阵容或进行转会。

这样一个回合完成后,联赛进入下一轮循环。

2.3 为什么要这样设计

把 AI 决策和比赛模拟完全解耦,是这个项目最重要的设计原则。

如果让 AI 直接决定比分,那整个项目就变成了“模型自问自答”,没有可信度。而使用独立的比赛引擎,AI 模型只负责输出战术和阵容决策,最终的“胜负”是引擎通过概率模型算出来的,这样比赛就有了客观随机性,也更容易观察不同模型的策略差异。


3. 环境准备与依赖说明

本文示例使用 Python 3.10+,核心依赖比较简单。版本方面需要根据你的项目实际情况调整,我这里以常见环境为例,重点演示配置思路。

3.1 环境依赖

Python 3.10+ openai requests

如果你的模型服务兼容 OpenAI 接口,可以直接使用openai客户端;如果不兼容,也可以用requests直接调用 HTTP 接口。为了示例通用,我在代码中封装了一个LLMClient类,你可以按需替换实现。

3.2 项目目录结构

先规划好项目结构:

ai-football-league/ ├── main.py # 联赛主循环 ├── models.py # 球员、俱乐部数据结构 ├── engine.py # 比赛模拟引擎 ├── manager.py # AI 经理实现 ├── prompts.py # 提示词模板 ├── config.py # 模型配置、联赛参数 └── data/ └── season_result.json # 赛季结果输出

下面按照模块逐个实现。


4. 核心代码实现:从球员到比赛引擎

4.1 球员与俱乐部数据结构

首先定义球员和俱乐部。使用dataclass可以简化代码,同时保证字段清晰。

# 文件路径:models.py from dataclasses import dataclass, field from typing import List @dataclass class Player: name: str position: str # 位置:GK / DF / MF / FW speed: int = 50 # 速度 0-100 shooting: int = 50 # 射门 0-100 passing: int = 50 # 传球 0-100 defense: int = 50 # 防守 0-100 value: int = 100 # 身价(万) salary: int = 10 # 工资(万) @property def rating(self) -> float: """根据位置计算加权能力值""" weights = { "GK": {"speed": 0.1, "shooting": 0.1, "passing": 0.3, "defense": 0.5}, "DF": {"speed": 0.2, "shooting": 0.1, "passing": 0.3, "defense": 0.4}, "MF": {"speed": 0.2, "shooting": 0.2, "passing": 0.4, "defense": 0.2}, "FW": {"speed": 0.3, "shooting": 0.4, "passing": 0.2, "defense": 0.1}, } w = weights.get(self.position, weights["MF"]) return ( self.speed * w["speed"] + self.shooting * w["shooting"] + self.passing * w["passing"] + self.defense * w["defense"] ) @dataclass class Club: name: str budget: int = 5000 # 转会预算(万) players: List[Player] = field(default_factory=list) points: int = 0 wins: int = 0 draws: int = 0 losses: int = 0 goals_for: int = 0 goals_against: int = 0 tactic: int = 50 # 当前战术倾向 0-100,越高越激进 @property def average_rating(self) -> float: if not self.players: return 0.0 return sum(p.rating for p in self.players) / len(self.players) @property def squad_cost(self) -> int: return sum(p.value for p in self.players)

这里需要特别解释Player.rating这个属性。不同位置的球员,各项能力的重要程度不同。例如前锋最重要的是射门和速度,后卫最重要的是防守和传球。因此我没有简单地对四项能力取平均值,而是根据位置做了加权。这样比赛引擎、AI 经理看到的球队实力评估会更有区分度。

Club.tactic是 AI 经理可以调整的核心变量,范围是 0 到 100。0 表示极致防守反击,100 表示疯狂进攻。

4.2 简化比赛模拟引擎

比赛模拟是整个项目里最需要“可信感”的部分。这里使用一个在足球数据领域常用的模型:泊松分布

核心思路是:

  1. 根据双方球队的攻防能力,计算各自的期望进球数。
  2. 期望进球数作为泊松分布的参数 λ,随机采样得到本场进球数。
  3. 战术值会动态修正期望进球数。

公式可以简化成下面这样:

期望进球 = 基础攻击力 × 战术修正 × 对手防守修正

代码实现如下:

# 文件路径:engine.py import random import math from typing import Tuple, List from models import Club, Player def get_attack_power(players: List[Player]) -> float: """计算球队攻击力:前锋射门 + 中场传球 + 整体速度""" forwards = [p for p in players if p.position == "FW"] midfielders = [p for p in players if p.position == "MF"] fw_shooting = sum(p.shooting for p in forwards) / len(forwards) if forwards else 40.0 mf_passing = sum(p.passing for p in midfielders) / len(midfielders) if midfielders else 40.0 speed_avg = sum(p.speed for p in players) / len(players) if players else 40.0 return fw_shooting * 0.5 + mf_passing * 0.3 + speed_avg * 0.2 def get_defense_power(players: List[Player]) -> float: """计算球队防守力:后卫防守 + 门将传球 + 阵容防守均值""" defenders = [p for p in players if p.position == "DF"] keepers = [p for p in players if p.position == "GK"] df_defense = sum(p.defense for p in defenders) / len(defenders) if defenders else 40.0 gk_defense = sum(p.defense for p in keepers) / len(keepers) if keepers else 40.0 team_defense = sum(p.defense for p in players) / len(players) if players else 40.0 return df_defense * 0.5 + gk_defense * 0.3 + team_defense * 0.2 def tactic_modifier(tactic: int) -> float: """战术值转成进攻倍率:50 时为 1.0,100 时约 1.4,0 时约 0.6""" return 1.0 + (tactic - 50) / 125 def simulate_match(home: Club, away: Club) -> Tuple[int, int]: """模拟一场比赛,返回 (主队进球, 客队进球)""" home_attack = get_attack_power(home.players) away_attack = get_attack_power(away.players) home_defense = get_defense_power(home.players) away_defense = get_defense_power(away.players) # 期望进球计算 home_expect = (home_attack / 100) * (100 / max(away_defense, 30)) away_expect = (away_attack / 100) * (100 / max(home_defense, 30)) home_expect *= tactic_modifier(home.tactic) away_expect *= tactic_modifier(away.tactic) # 防止 λ 过大导致极端比分,做一次截断 home_expect = min(home_expect, 5.0) away_expect = min(away_expect, 5.0) # 泊松采样 home_goals = random.poisson(home_expect) away_goals = random.poisson(away_expect) return home_goals, away_goals

这里有几个可以调整的地方:

  • tactic_modifier决定战术值对进攻的影响强度,你可以通过调大分母系数让战术更“钝”一些。
  • expect的最大值限制为 5.0,是为了避免球队实力差距过大时出现 10:0 这种没有参考意义的比分。
  • 随机种子可以在主循环外层控制,方便复现实验。

我们随后可以简单验证一下引擎效果:

# 快速测试 from models import Player, Club players_home = [ Player("R1", "GK", defense=80), Player("R2", "DF", defense=70, passing=60), Player("R3", "DF", defense=75, passing=65), Player("R4", "MF", passing=80, shooting=60), Player("R5", "MF", passing=75, shooting=70), Player("R6", "FW", shooting=85, speed=80), ] players_away = [ Player("B1", "GK", defense=70), Player("B2", "DF", defense=65, passing=55), Player("B3", "DF", defense=60, passing=60), Player("B4", "MF", passing=70, shooting=55), Player("B5", "MF", passing=65, shooting=60), Player("B6", "FW", shooting=70, speed=70), ] home = Club(name="Alpha", players=players_home, tactic=60) away = Club(name="Beta", players=players_away, tactic=40) print(simulate_match(home, away))

不同种子下结果不同,但从整体分布看,实力更强且战术更主动的 Alpha 队赢面更大。引擎的可信度就在这里。

4.3 AI 经理决策实现

接下来是项目的“灵魂”:AI 经理。

为了让不同模型都能接入,定义一个LLMClient抽象类,外部统一使用chat()方法发送消息并拿到返回内容。

# 文件路径:manager.py import json import re from typing import Dict, Any from models import Club, Player import prompts class LLMClient: """通用大模型客户端封装,兼容 OpenAI 风格接口""" def __init__(self, api_key: str, base_url: str, model_name: str): from openai import OpenAI self.client = OpenAI(api_key=api_key, base_url=base_url) self.model = model_name def chat(self, system_prompt: str, user_prompt: str, max_tokens: int = 500) -> str: resp = self.client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], max_tokens=max_tokens, temperature=0.7, ) return resp.choices[0].message.content class AIManager: """AI 经理:负责赛前战术决策和赛后阵容调整""" def __init__(self, club: Club, llm_client: LLMClient): self.club = club self.llm = llm_client def get_match_tactic(self, opponent: Club, league_standing: str) -> int: """根据对手和联赛排名,决定本场攻击性数值""" system_prompt = prompts.TACTIC_SYSTEM_PROMPT user_prompt = prompts.build_tactic_user_prompt( club=self.club, opponent=opponent, league_standing=league_standing, ) content = self.llm.chat(system_prompt, user_prompt) return self._parse_int(content) def get_transfer_decision(self, match_report: Dict[str, Any]) -> Dict[str, Any]: """赛后就是否调整阵容、购买球员给出决策""" system_prompt = prompts.TRANSFER_SYSTEM_PROMPT user_prompt = prompts.build_transfer_user_prompt( club=self.club, match_report=match_report, ) content = self.llm.chat(system_prompt, user_prompt) return self._parse_json(content) @staticmethod def _parse_int(text: str) -> int: """从模型输出中提取整数""" match = re.search(r"(\d{1,3})", text) if match: return max(0, min(100, int(match.group(1)))) return 50 @staticmethod def _parse_json(text: str) -> Dict[str, Any]: """从模型输出中提取 JSON 对象""" try: # 去掉可能的 markdown 代码块标记 cleaned = text.strip() if cleaned.startswith("```"): cleaned = re.sub(r"^```\w*\n?", "", cleaned) cleaned = re.sub(r"\n?```$", "", cleaned) return json.loads(cleaned) except json.JSONDecodeError: # 解析失败时返回默认决策 return {"action": "keep", "reason": "parse_failed"}

AIManager里有两个关键的输出解析方法:

  • _parse_int:因为战术决策只需要一个 0-100 的整数,所以用正则提取第一个数字,然后限制范围。
  • _parse_json:转会决策更适合结构化输出,我们要求模型返回 JSON,这里兼容了不带代码块标记和带代码块标记的两种情况。

4.4 提示词模板

提示词是这个项目最关键的部分。同样一个模型,提示词写得好不好,决策质量差异会非常明显。

# 文件路径:prompts.py from models import Club TACTIC_SYSTEM_PROMPT = """ 你是一名经验丰富的足球经理,管理着一支机器人足球队。 你需要根据球队实力、对手情况和联赛排名,决定本场比赛的战术倾向。 战术倾向用 0-100 的整数表示: - 0 表示极致防守反击 - 50 表示攻守平衡 - 100 表示全攻全守 你只能输出一个整数,不要输出其他任何文字。 """.strip() def build_tactic_user_prompt(club: Club, opponent: Club, league_standing: str) -> str: """构建赛前战术决策的用户提示词""" return f""" 请决定 {club.name} 下一场比赛的战术倾向。 【我的球队】 球队评分:{club.average_rating:.1f} 球队预算:{club.budget} 万 赛季目前战绩:{club.wins} 胜 {club.draws} 平 {club.losses} 负,进 {club.goals_for} 球,失 {club.goals_against} 球 当前积分:{club.points} 【对手球队】 球队名称:{opponent.name} 球队评分:{opponent.average_rating:.1f} 对手战绩:{opponent.wins} 胜 {opponent.draws} 平 {opponent.losses} 负,进 {opponent.goals_for} 球,失 {opponent.goals_against} 球 【联赛形势】 {league_standing} 请根据以上信息,输出本场的战术倾向整数。 """.strip() TRANSFER_SYSTEM_PROMPT = """ 你是一名足球经理。赛后会收到一份比赛报告,你需要决定: 1. 是否调整现有阵容(换下表现不佳的球员)。 2. 是否在转会市场上购买更强球员。 3. 如果购买,指出希望补强的位置。 请严格输出 JSON 格式,不要输出其他内容。 JSON 字段说明: - action: "keep" 表示不调整,"buy" 表示购买球员,"change" 表示调整阵容 - reason: 简短说明你的决策理由 - target_position: "GK" / "DF" / "MF" / "FW",表示期望补强的位置,不需要时填空字符串 - budget_expect: 期望花费的预算金额,不需要时为 0 """.strip() def build_transfer_user_prompt(club: Club, match_report: dict) -> str: """构建赛后转会决策的用户提示词""" opponent_name = match_report.get("opponent_name", "未知对手") goals_for = match_report.get("goals_for", 0) goals_against = match_report.get("goals_against", 0) team_rating = club.average_rating return f""" 请根据以下比赛报告,决定是否调整阵容或购买球员。 【比赛结果】 对手:{opponent_name} 比分:{goals_for} : {goals_against} 【我的球队当前阵容】 球队评分:{team_rating:.1f} 球员数量:{len(club.players)} 转会预算:{club.budget} 万 【阵型弱点分析】 请查看以上信息,判断当前最需要补强的位置。 请输出 JSON 决策。 """.strip()

提示词里的几个设计细节值得学习:

  1. 系统提示词只给规则和输出格式,不给具体数据,保证通用性。
  2. 用户提示词按固定格式填充数据,模型容易提取关键信息。
  3. 每个输出都做了强约束:战术决策只输出整数,转会决策只输出 JSON。
  4. 给了一个默认值兜底:解析失败时返回"action": "keep",不会让主循环崩溃。

5. 完整实战:跑通一个赛季

有了数据模型、比赛引擎和 AI 经理,现在把它们组装成一个完整的赛季循环。

为了控制复杂度和模型调用成本,赛季设计如下:

  • 联赛共 4 支球队,每两队交手两轮,共 12 场比赛。
  • 每轮比赛前,每支球队的经理调用一次 AI,决定战术。
  • 每轮比赛后,每支球队的经理调用一次 AI,决定是否转会。
  • 转会决策统一在周末处理,假设购买的球员会提升球队评分,具体金额直接从预算扣除。

5.1 配置与初始化

# 文件路径:config.py LLM_API_KEY = "your-api-key" LLM_BASE_URL = "https://api.example.com/v1" LLM_MODEL = "your-model-name" TEAMS_PER_SEASON = 4 ROUNDS_PER_MATCHUP = 2

这些配置需要根据你实际使用的模型服务调整。比如你用 OpenAI 兼容服务,就填对应的 key、base_url 和模型名;如果你通过国内云厂商接入,换成对应的 endpoint 即可。

5.2 初始化球队

为了减少代码量,用一个函数批量生成球员。这里刻意让球队实力有一定差异,才能观察 AI 经理的决策是否合理。

# 文件路径:main.py import random from models import Player, Club from engine import simulate_match from manager import AIManager, LLMClient import config import json def generate_squad(prefix: str, avg_level: int) -> list: """生成一支由 6 名球员组成的简化球队""" players = [] # 位置顺序:门将、后卫、后卫、中场、中场、前锋 positions = ["GK", "DF", "DF", "MF", "MF", "FW"] for idx, pos in enumerate(positions): # 在 avg_level 附近上下浮动 10 点 base = avg_level + random.randint(-10, 10) player = Player( name=f"{prefix}-{idx+1}", position=pos, speed=max(30, min(99, base + random.randint(-10, 10))), shooting=max(30, min(99, base + random.randint(-10, 10))), passing=max(30, min(99, base + random.randint(-10, 10))), defense=max(30, min(99, base + random.randint(-10, 10))), value=200 + base * 20, salary=20 + base, ) players.append(player) return players def create_clubs(): teams_config = [ ("Alpha", 68), ("Beta", 62), ("Gamma", 55), ("Delta", 72), ] clubs = [] for name, level in teams_config: players = generate_squad(name, level) clubs.append(Club(name=name, players=players, budget=5000)) return clubs

这里的avg_level决定了球队整体实力。Delta 队最强,Gamma 队最弱。为了让联赛更有竞争性,可以在真实版本中加入“转会升级”机制:购买球员后,用更强的新球员替换队里评分最低的球员。

5.3 主循环

主循环是整个项目的控制中心。

# 文件路径:main.py def run_season(clubs, llm_client): """运行一个完整赛季,返回赛季报告""" managers = {club.name: AIManager(club, llm_client) for club in clubs} # 生成赛程:单循环两次 schedule = [] for club_a in clubs: for club_b in clubs: if club_a.name == club_b.name: continue schedule.append((club_a, club_b)) # 主客场各一次,直接复制一遍赛程并交换主客队 schedule = schedule + [(away, home) for home, away in schedule] round_num = 0 # 每 2 场算一轮展示,方便观察 for idx, (home, away) in enumerate(schedule): round_num = idx // 2 + 1 print(f"\n===== 第 {round_num} 轮:{home.name} vs {away.name} =====") # 获取联赛排名文本 standing = get_league_standing(clubs) # AI 经理给出战术 home.tactic = managers[home.name].get_match_tactic(away, standing) away.tactic = managers[away.name].get_match_tactic(home, standing) print(f"{home.name} 战术:{home.tactic},{away.name} 战术:{away.tactic}") # 比赛模拟 home_goals, away_goals = simulate_match(home, away) print(f"比分:{home.name} {home_goals} : {away_goals} {away.name}") # 更新积分 update_stats(home, away, home_goals, away_goals) # 赛后 AI 决策是否转会(这里只展示一支球队做示例) if idx % 2 == 1: handle_transfer(home, managers[home.name], home_goals, away_goals, away.name) handle_transfer(away, managers[away.name], away_goals, home_goals, home.name) print("\n========== 赛季结束 ==========") return generate_season_report(clubs) def update_stats(home: Club, away: Club, hg: int, ag: int): home.goals_for += hg home.goals_against += ag away.goals_for += ag away.goals_against += hg if hg > ag: home.wins += 1 home.points += 3 away.losses += 1 elif hg == ag: home.draws += 1 away.draws += 1 home.points += 1 away.points += 1 else: away.wins += 1 away.points += 3 home.losses += 1

update_stats是联赛积分的核心逻辑,很简单但容易出错。注意主队和客队都要更新进球数、失球数、胜平负和积分。

5.4 转会处理与赛季报告

def handle_transfer(club, manager, goals_for, goals_against, opponent_name): """示例版转会处理,减少 API 调用次数""" match_report = { "opponent_name": opponent_name, "goals_for": goals_for, "goals_against": goals_against, } decision = manager.get_transfer_decision(match_report) if decision.get("action") == "buy": # 模拟购买一名新球员,替换队内评分最低的球员 target_pos = decision.get("target_position", "MF") budget_expect = int(decision.get("budget_expect", 0)) if 0 < budget_expect <= club.budget: new_player = Player( name=f"{club.name}-NEW", position=target_pos, speed=random.randint(70, 85), shooting=random.randint(70, 85), passing=random.randint(70, 85), defense=random.randint(70, 85), value=budget_expect, salary=int(budget_expect * 0.1), ) # 找到评分最低的球员 weakest = min(club.players, key=lambda p: p.rating) club.players.remove(weakest) club.players.append(new_player) club.budget -= budget_expect print(f"[转会] {club.name} 花费 {budget_expect} 万购买 {target_pos} 位置球员,替换 {weakest.name}") def get_league_standing(clubs) -> str: """生成联赛排名文本,用于 AI 提示词""" sorted_clubs = sorted(clubs, key=lambda c: (-c.points, -c.goals_for)) lines = [] for i, c in enumerate(sorted_clubs, 1): lines.append(f"{i}. {c.name}:{c.points} 分,进 {c.goals_for} 失 {c.goals_against}") return "\n".join(lines) def generate_season_report(clubs) -> dict: """输出赛季最终排名""" sorted_clubs = sorted(clubs, key=lambda c: (-c.points, -(c.goals_for - c.goals_against))) report = [] for i, c in enumerate(sorted_clubs, 1): report.append({ "rank": i, "club": c.name, "points": c.points, "wins": c.wins, "draws": c.draws, "losses": c.losses, "goals_for": c.goals_for, "goals_against": c.goals_against, "avg_rating": round(c.average_rating, 2), "budget": c.budget, }) return {"season_report": report} if __name__ == "__main__": random.seed(42) # 固定随机种子,方便复现 llm = LLMClient( api_key=config.LLM_API_KEY, base_url=config.LLM_BASE_URL, model_name=config.LLM_MODEL, ) clubs = create_clubs() report = run_season(clubs, llm) with open("data/season_result.json", "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2)

random.seed(42)很重要,它让比赛模拟结果可复现。调 AI 时仍然会有随机性,但至少比赛引擎部分是可验证的。

5.5 运行与预期效果

运行项目:

python main.py

控制台会输出每一轮的战术决策和比分,例如:

===== 第 1 轮:Alpha vs Beta ===== Alpha 战术:65,Beta 战术:42 比分:Alpha 2 : 1 Beta

最终生成data/season_result.json,内容大致如下:

{ "season_report": [ { "rank": 1, "club": "Delta", "points": 28, "wins": 9, "draws": 1, "losses": 2, "goals_for": 45, "goals_against": 12, "avg_rating": 73.2, "budget": 4600 } ] }

从这个结果里,你可以做两件事:

  1. 对照现实:最强的 Delta 队大概率夺冠,说明引擎设计是合理的。
  2. 观察 AI 决策:查看某支球队的战术值是否根据对手强弱动态调整。如果 Alpha 面对弱队时打出了 80+ 的激进战术,而面对 Delta 时降到了 30,说明提示词和模型都发挥了作用;如果所有比赛战术值都是 50,那就是提示词需要优化。

6. 常见问题与排查思路

在实际运行这类 AI + 模拟器的项目时,会遇到一些典型问题,我整理在下表里。

问题现象常见原因解决思路
AI 返回内容无法解析成整数模型输了额外文字,或者走了 markdown 格式在提示词里强调“只输出一个整数”,用正则提取;再留兜底默认值
转会决策 JSON 解析失败模型输出格式不稳定,或者超出 max_tokens启用 JSON 模式(如果 API 支持);清理代码块标记;解析失败时返回 keep
每轮比赛结果都一样没有调用 random.seed,或者模拟引擎输入完全相同检查是否固定了种子;确认每次比赛的期望进球会随战术变化
模型调用太频繁,成本太高每场比赛调用 2 次甚至 4 次 API减少调用频次:示例里只在奇数轮做一次转会决策;或者把多个决策合并到一个 prompt
球队实力差距过大,出现极端比分期望进球没有做截断检查simulate_matchhome_expect是否限制在 5.0 以内
提示词上下文太长球队数量多、联赛场次多,历史数据全塞进提示词只保留最近 3-5 轮战绩和当前排名,不要告诉模型所有历史数据
模型给出不合理战术(例如 0 或 100)提示词里没有解释极端值的风险在系统提示词中加入“极端战术会降低防守稳定性”这类约束

6.1 排查清单

遇到问题时按下面顺序排查:

  1. 先跑通无 AI 版本:把get_match_tactic临时改成返回 50,确认比赛引擎本身没有问题。
  2. 看模型原始输出:在chat()方法里打印content,确认输出是纯文本还是带了多余内容。
  3. 检查解析函数:把模型原始输出粘贴到re.search(r"(\d{1,3})", text)里测试。
  4. 检查请求费用:给LLMClient加一个简单计数,看一个赛季调用了多少次 API,评估成本是否可接受。
  5. 控制随机性:固定种子后,只有 AI 的内容生成是随机的,比赛引擎随机性可复现。

7. 最佳实践与工程建议

当你想把这个 Demo 扩展成更完整、更稳定的 AI Agent 项目时,下面的建议是按优先级排列的。

7.1 使用结构化输出代替正则解析

目前的示例中,战术决策靠正则提取整数,转会决策靠 JSON 解析。实际生产环境里,更推荐启用模型的JSON Mode / Structured Output能力。

如果 API 支持response_formatresponse_model参数,尽量在你封装的LLMClient里暴露出来。结构化输出能大幅降低解析失败率,也让数据字段更可控。代码示意:

def chat_json(self, system_prompt: str, user_prompt: str, json_schema: dict) -> dict: from openai import OpenAI response = self.client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], response_format={"type": "json_object"}, max_tokens=500, ) return json.loads(response.choices[0].message.content)

7.2 为每一次 AI 调用加日志

AI 决策类项目最怕“黑盒”。模型在何时、基于什么数据、做了哪些决策,都必须留痕。建议为每次调用记录:

  • 输入 prompt 的摘要(而不是完整的几千字);
  • 模型返回的原始内容;
  • 解析后的结构化结果;
  • 当前轮次、球队名、耗时、token 数。

推荐使用结构化日志,例如逐行 JSON:

import logging logger = logging.getLogger("ai_manager") logger.info({ "club": club.name, "round": round_num, "type": "tactic", "raw_output": content, "parsed_value": tactic, })

7.3 给模型设置“决策疲劳”边界

大模型在连续多轮决策后,容易出现策略漂移,例如从前几轮的理性判断突然变成“每场都全力进攻”。处理方法:

  • 在提示词里加入“你的决策会影响球队长期联赛排名,请兼顾短期赢球和长期预算”。
  • 每次只提供当前轮次的必要数据,不要给全赛季累计文本。
  • 设置默认策略:如果模型输出解析失败,返回上一轮策略,而不是返回默认 50。

7.4 引擎与 AI 解耦,方便 A/B 测试

这个项目最吸引人的地方是可以横向对比不同 AI 模型的管理能力。实现上建议把“俱乐部对应哪个模型”作为配置项,而不是把模型写死在管理器里。

# 示例:配置模型映射 MODEL_MAPPING = { "Alpha": {"model": "model-a", "base_url": "..."}, "Beta": {"model": "model-b", "base_url": "..."}, }

这样就能在同一个联赛里跑不同模型,看谁的最终积分更高。这也是 Show HN 项目最有话题性的玩法。

7.5 模拟引擎要保留随机种子与数据快照

无论比赛引擎多简单,都应该支持:

  • 传入随机种子,方便复现比赛。
  • 每轮结束后保存球队状态快照。
  • 赛季结束后保存完整比赛记录,而不仅是最终排名。

有了这些数据,你才能在赛季后复盘某个模型是不是在特定轮次做出了糟糕决策。

7.6 注意 API 成本和限流

一个完整赛季的 API 调用量可以这样估算:

  • 每场比赛 2 次赛前战术调用;
  • 每场比赛 2 次赛后转会调用;
  • 4 支球队双循环共 12 场,总计约 48 次调用。

如果扩展到 20 支球队、38 轮联赛,调用量会上升到数千次。建议:

  • 开启本地缓存:同一轮同样的输入不重复调用;
  • 转会决策改为“每 5 轮做一次”;
  • 给每次请求加超时和重试机制。

8. 总结与扩展方向

通过本文,我们从零实现了一个由大模型管理俱乐部的机器人足球联赛。核心流程是:

  1. 定义球员与俱乐部的数据模型;
  2. 实现基于泊松分布的比赛模拟引擎;
  3. 封装大模型客户端,让 AI 经理能输出战术决策和转会决策;
  4. 用主循环把赛程、决策、比赛模拟、积分更新串联起来;
  5. 输出赛季报告,并围绕 API 解析、成本、稳定性给出了排查建议。

这个项目的最大价值不是“做出一个足球游戏”,而是验证了一个通用模式:大模型作为决策者,仿真环境作为评估器。你可以把“足球比赛”换成任何模拟场景——电商广告出价、城市交通调度、投资组合调整、游戏 NPC 行为策略。架构核心都是:给模型提供结构化状态,模型输出决策,环境执行并返回结果,循环往复。

如果你想往更深的方向扩展,有三个思路值得继续研究:

  1. 多智能体协作:让每个俱乐部不只由一个 AI 经理负责,而是分成战术教练、转会总监、数据分析师三个角色,由不同模型承担。
  2. 引入记忆系统:让 AI 经理能回顾过去 5 轮的对手录像(历史比赛报告),而不是只看当前报告。
  3. 用强化学习做对照实验:用一个简单的规则化策略或者强化学习模型作为基准,对比大模型的决策质量。

如果你对上面的实现感兴趣,可以先把代码跑起来,观察前几轮 AI 的战术分布,再逐渐修改提示词或引擎参数。你会发现问题往往不在代码本身,而在“如何让模型做出真正可解释、可托底的决策”上——这也是所有 AI Agent 项目都需要长期打磨的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 5:47:21

LLM训练数据识别:模型卡、记忆探测与行为验证指南

判断一个 LLM 到底在哪些数据上训练过&#xff0c;这个问题我在 HN 和技术社区都刷到过&#xff0c;也被同事问过很多次。它背后通常不是好奇心&#xff0c;而是现实需求&#xff1a;企业选模型要评估训练数据里有没有版权风险、合规风险&#xff0c;或者是否覆盖目标行业&…

作者头像 李华
网站建设 2026/8/31 19:39:00

基于深度学习的盲道语义分割实战:从U-Net模型到工程部署

简介&#xff1a;语义分割是计算机视觉的核心技术之一&#xff0c;旨在为图像中的每个像素分配类别标签&#xff0c;实现从‘看见’到‘理解’的跨越。其原理在于通过编码器-解码器架构&#xff0c;先提取图像的全局上下文语义信息&#xff0c;再恢复局部细节&#xff0c;从而精…

作者头像 李华
网站建设 2026/9/1 7:48:32

从机器人税到AI Agent:技术人如何评估自动化风险与岗位价值

早上打开朋友圈&#xff0c;很多人都在转一条消息&#xff1a;比尔盖茨再次发长文谈AI&#xff0c;并重提“机器人税”和“人类专属岗位”。如果你不是第一次看到这个词&#xff0c;可能会觉得这是2017年那场Reddit问答的翻版。但当我们把时间轴拉回到今天&#xff0c;在AI编程…

作者头像 李华
网站建设 2026/8/31 15:11:51

基于SpringBoot的健康食谱管理系统的设计与实现(源码+讲解视频+LW)

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/8/31 19:44:25

MATLAB结构化编程与函数封装:数学建模从脚本到工程的进阶指南

1. 从脚本到工程&#xff1a;为什么数学建模需要结构化思维如果你用过MATLAB&#xff0c;大概率是从一行行命令开始的。在命令窗口里敲下x 1:10; y sin(x); plot(x, y)&#xff0c;图形窗口立刻弹出正弦曲线&#xff0c;这种即时反馈的爽快感是MATLAB的魅力之一。很多同学&am…

作者头像 李华
网站建设 2026/9/1 7:00:41

C语言内存函数深度解析:从memcpy到memmove的原理与模拟实现

1. 项目概述&#xff1a;为什么内存函数是C语言进阶的必经之路刚接触C语言时&#xff0c;我们大多在跟变量、循环、条件判断这些基础语法打交道。一旦开始处理字符串、结构体数组&#xff0c;或者尝试自己管理一块数据缓冲区&#xff0c;很快就会撞上一堵墙&#xff1a;数据拷贝…

作者头像 李华