
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 核心角色项目里只有三个角色球员Player拥有速度、射门、传球、防守等属性还有一个身价和工资。俱乐部Club拥有球员列表、预算、联赛积分和战术状态。AI 经理AI Manager一个外部大模型客户端负责根据俱乐部状态输出战术和转会建议。比赛模拟引擎不属于角色它像裁判一样根据双方球员能力和战术参数算出比分。2.2 单个比赛周期流程一场比赛从“AI 做赛前决策”到“比赛结束”的完整流程如下系统生成“对手报告”包含对手球队实力、核心球员、近期战绩。将报告发送给 AI 经理。AI 经理返回本场比赛的战术设置例如攻击性数值。系统把主队和客队的战术设置传入比赛引擎。比赛引擎基于球员平均能力、战术数值计算双方进球分布生成比分。系统更新俱乐部积分、进球数、球员状态。将赛后报告发送给 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_factorylist) 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 简化比赛模拟引擎比赛模拟是整个项目里最需要“可信感”的部分。这里使用一个在足球数据领域常用的模型泊松分布。核心思路是根据双方球队的攻防能力计算各自的期望进球数。期望进球数作为泊松分布的参数 λ随机采样得到本场进球数。战术值会动态修正期望进球数。公式可以简化成下面这样期望进球 基础攻击力 × 战术修正 × 对手防守修正代码实现如下# 文件路径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.0100 时约 1.40 时约 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, defense80), Player(R2, DF, defense70, passing60), Player(R3, DF, defense75, passing65), Player(R4, MF, passing80, shooting60), Player(R5, MF, passing75, shooting70), Player(R6, FW, shooting85, speed80), ] players_away [ Player(B1, GK, defense70), Player(B2, DF, defense65, passing55), Player(B3, DF, defense60, passing60), Player(B4, MF, passing70, shooting55), Player(B5, MF, passing65, shooting60), Player(B6, FW, shooting70, speed70), ] home Club(nameAlpha, playersplayers_home, tactic60) away Club(nameBeta, playersplayers_away, tactic40) 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_keyapi_key, base_urlbase_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( modelself.model, messages[ {role: system, content: system_prompt}, {role: user, content: user_prompt}, ], max_tokensmax_tokens, temperature0.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( clubself.club, opponentopponent, league_standingleague_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( clubself.club, match_reportmatch_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()提示词里的几个设计细节值得学习系统提示词只给规则和输出格式不给具体数据保证通用性。用户提示词按固定格式填充数据模型容易提取关键信息。每个输出都做了强约束战术决策只输出整数转会决策只输出 JSON。给了一个默认值兜底解析失败时返回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( namef{prefix}-{idx1}, positionpos, speedmax(30, min(99, base random.randint(-10, 10))), shootingmax(30, min(99, base random.randint(-10, 10))), passingmax(30, min(99, base random.randint(-10, 10))), defensemax(30, min(99, base random.randint(-10, 10))), value200 base * 20, salary20 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(namename, playersplayers, budget5000)) 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 1update_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( namef{club.name}-NEW, positiontarget_pos, speedrandom.randint(70, 85), shootingrandom.randint(70, 85), passingrandom.randint(70, 85), defenserandom.randint(70, 85), valuebudget_expect, salaryint(budget_expect * 0.1), ) # 找到评分最低的球员 weakest min(club.players, keylambda 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, keylambda 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, keylambda 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_keyconfig.LLM_API_KEY, base_urlconfig.LLM_BASE_URL, model_nameconfig.LLM_MODEL, ) clubs create_clubs() report run_season(clubs, llm) with open(data/season_result.json, w, encodingutf-8) as f: json.dump(report, f, ensure_asciiFalse, indent2)random.seed(42)很重要它让比赛模拟结果可复现。调 AI 时仍然会有随机性但至少比赛引擎部分是可验证的。5.5 运行与预期效果运行项目python main.py控制台会输出每一轮的战术决策和比分例如 第 1 轮Alpha vs Beta Alpha 战术65Beta 战术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 } ] }从这个结果里你可以做两件事对照现实最强的 Delta 队大概率夺冠说明引擎设计是合理的。观察 AI 决策查看某支球队的战术值是否根据对手强弱动态调整。如果 Alpha 面对弱队时打出了 80 的激进战术而面对 Delta 时降到了 30说明提示词和模型都发挥了作用如果所有比赛战术值都是 50那就是提示词需要优化。6. 常见问题与排查思路在实际运行这类 AI 模拟器的项目时会遇到一些典型问题我整理在下表里。问题现象常见原因解决思路AI 返回内容无法解析成整数模型输了额外文字或者走了 markdown 格式在提示词里强调“只输出一个整数”用正则提取再留兜底默认值转会决策 JSON 解析失败模型输出格式不稳定或者超出 max_tokens启用 JSON 模式如果 API 支持清理代码块标记解析失败时返回 keep每轮比赛结果都一样没有调用 random.seed或者模拟引擎输入完全相同检查是否固定了种子确认每次比赛的期望进球会随战术变化模型调用太频繁成本太高每场比赛调用 2 次甚至 4 次 API减少调用频次示例里只在奇数轮做一次转会决策或者把多个决策合并到一个 prompt球队实力差距过大出现极端比分期望进球没有做截断检查simulate_match里home_expect是否限制在 5.0 以内提示词上下文太长球队数量多、联赛场次多历史数据全塞进提示词只保留最近 3-5 轮战绩和当前排名不要告诉模型所有历史数据模型给出不合理战术例如 0 或 100提示词里没有解释极端值的风险在系统提示词中加入“极端战术会降低防守稳定性”这类约束6.1 排查清单遇到问题时按下面顺序排查先跑通无 AI 版本把get_match_tactic临时改成返回 50确认比赛引擎本身没有问题。看模型原始输出在chat()方法里打印content确认输出是纯文本还是带了多余内容。检查解析函数把模型原始输出粘贴到re.search(r(\d{1,3}), text)里测试。检查请求费用给LLMClient加一个简单计数看一个赛季调用了多少次 API评估成本是否可接受。控制随机性固定种子后只有 AI 的内容生成是随机的比赛引擎随机性可复现。7. 最佳实践与工程建议当你想把这个 Demo 扩展成更完整、更稳定的 AI Agent 项目时下面的建议是按优先级排列的。7.1 使用结构化输出代替正则解析目前的示例中战术决策靠正则提取整数转会决策靠 JSON 解析。实际生产环境里更推荐启用模型的JSON Mode / Structured Output能力。如果 API 支持response_format或response_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( modelself.model, messages[ {role: system, content: system_prompt}, {role: user, content: user_prompt}, ], response_format{type: json_object}, max_tokens500, ) return json.loads(response.choices[0].message.content)7.2 为每一次 AI 调用加日志AI 决策类项目最怕“黑盒”。模型在何时、基于什么数据、做了哪些决策都必须留痕。建议为每次调用记录输入 prompt 的摘要而不是完整的几千字模型返回的原始内容解析后的结构化结果当前轮次、球队名、耗时、token 数。推荐使用结构化日志例如逐行 JSONimport 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. 总结与扩展方向通过本文我们从零实现了一个由大模型管理俱乐部的机器人足球联赛。核心流程是定义球员与俱乐部的数据模型实现基于泊松分布的比赛模拟引擎封装大模型客户端让 AI 经理能输出战术决策和转会决策用主循环把赛程、决策、比赛模拟、积分更新串联起来输出赛季报告并围绕 API 解析、成本、稳定性给出了排查建议。这个项目的最大价值不是“做出一个足球游戏”而是验证了一个通用模式大模型作为决策者仿真环境作为评估器。你可以把“足球比赛”换成任何模拟场景——电商广告出价、城市交通调度、投资组合调整、游戏 NPC 行为策略。架构核心都是给模型提供结构化状态模型输出决策环境执行并返回结果循环往复。如果你想往更深的方向扩展有三个思路值得继续研究多智能体协作让每个俱乐部不只由一个 AI 经理负责而是分成战术教练、转会总监、数据分析师三个角色由不同模型承担。引入记忆系统让 AI 经理能回顾过去 5 轮的对手录像历史比赛报告而不是只看当前报告。用强化学习做对照实验用一个简单的规则化策略或者强化学习模型作为基准对比大模型的决策质量。如果你对上面的实现感兴趣可以先把代码跑起来观察前几轮 AI 的战术分布再逐渐修改提示词或引擎参数。你会发现问题往往不在代码本身而在“如何让模型做出真正可解释、可托底的决策”上——这也是所有 AI Agent 项目都需要长期打磨的地方。