“最近10场”是体育数据里最常被用到、也最少被定义的词。用户以为它是一个明确的数字,系统却要先回答一串问题:这10场是球队的,还是球员自己的?球员缺阵的那几场算不算?替补上场三分钟的比赛要不要计入平均?中途取消的比赛怎么办?只要其中一条没有定好,同一句话就会在不同系统里得到不同的数字,而且每一个数字都“有道理”。这篇文章不讲宏大的架构,只把“最近10场”拆成一张可以落地的规则表,逐条说明半岛体育AI是怎样处理的。

先分清两个“最近10场”

第一种叫球队窗口:取球队最近10场比赛,再看球员在这10场里的表现,缺阵的场次按“没有数据”处理。第二种叫球员窗口:取球员自己最近10次有出场记录的比赛,不管球队间隔了多少场。

假设示例队甲最近12场里,中场球员陈某(虚构)因伤缺席了中间4场。按球队窗口,他只有6场数据;按球员窗口,可以往前多找4场,凑满10场,但这10场跨越的时间更长,包含了他伤前的状态。两种算法没有对错,只是回答的问题不同:前者问“球队近期这段时间里他的贡献”,后者问“他近期每次上场的表现”。

半岛体育AI的默认做法,是在用户没有说明时采用球员窗口,并在答案里写明“按该球员最近10次上场记录计算,覆盖日期从某月某日到某月某日”。如果用户的问法里出现“球队最近10场”“这段时间”这类措辞,则切换成球队窗口。这个默认值本身不是技术问题,而是产品约定,重要的是它必须被明说。

一张窗口定义规则表

下面这张表是我们处理边界情况时使用的规则骨架,所有数据均为示例口径,实际取值以数据源的字段为准。

情形 默认处理 原因 用户可调整
球员缺阵(未进入比赛名单)不计入,也不占用10场名额 没有上场就没有表现可言 可改为球队窗口
进入名单但未出场 不计入minutes = 0,会拉低所有平均值 可选“含未出场”
替补出场时间很短 计入,但在答案里提示出场时间偏低 样本被拉低的是场均,而不是场次 可设最低出场时间
比赛加时 计入,并标注含加时 加时会增加出场时间与技术统计 可选“仅常规时间”
季后赛或杯赛 默认仅含常规联赛,并明示 不同阶段对手强度与节奏不同 可选“含全部赛事”
比赛中途取消或延期 取消的不计;延期已补赛的按补赛日期排序 排序依据应为实际比赛日期 无
赛季初场次不足10按实际场次计算,并写明“仅N场” 不能用少于10场的数据冒充10场 可选跨赛季补足
数据源尚未更新到最新一场 回答中写明数据截止日期 避免用户误以为包含了昨天的比赛无

这张表最有意义的部分是最后一列:默认值可以有,但不能不可见、不可改。用户如果发现数字与别处对不上,第一步应该是打开口径,而不是怀疑系统算错了。

把规则写成SQL会长什么样

规则最终要落到查询上。假设有一张球员逐场记录表 player_game_stats,包含 player_id、game_id、game_date、minutes、points、is_playoff、is_cancelled 等字段,球员窗口的查询可以写成这样:

SELECT AVG(points)  AS avg_points,
       AVG(minutes) AS avg_minutes,
       COUNT(*)     AS games_counted,
       MIN(game_date) AS window_start,
       MAX(game_date) AS window_end
FROM (
  SELECT points, minutes, game_date
  FROM player_game_stats
  WHERE player_id = 'P0012'
    AND minutes > 0
    AND is_cancelled = 0
    AND is_playoff = 0
  ORDER BY game_date DESC
  LIMIT 10
) AS w;

几个细节值得说明。ORDER BY game_date DESC 依据的是比赛实际日期,而不是比赛编号或录入顺序,否则补赛会被排错位置。minutes > 0 是“出场”的判定,如果数据源用 appearances 或 played 之类的字段,要换成对应条件。返回的 window_start 与 window_end 不是装饰,它们是答案里“覆盖日期”的来源。

球队窗口的写法则要先取球队的比赛,再与球员记录左连接:

SELECT AVG(s.points) AS avg_points,
       COUNT(s.player_id) AS games_played,
       COUNT(g.game_id)   AS team_games
FROM (
  SELECT game_id
  FROM games
  WHERE team_id = 'T03'
    AND is_cancelled = 0
    AND is_playoff = 0
  ORDER BY game_date DESC
  LIMIT 10
) AS g
LEFT JOIN player_game_stats s
  ON s.game_id = g.game_id
 AND s.player_id = 'P0012'
 AND s.minutes > 0;

这里 games_played 会小于 team_games,两者之差就是球员缺席的场次,可以直接写进回答:“球队最近10场中,他出场6场”。这也是球队窗口更适合回答“这段时间他有多稳定”的原因。

加权平均还是简单平均

窗口取对了,平均值还可能算错。场均得分用简单平均没问题;但如果指标是命中率、传球成功率这类比率,把每场的百分比直接平均,就会给出场次不同、出手次数不同的比赛相同的权重。比如某场只投了2次、命中2次,命中率100%;另一场投了20次、命中8次,命中率40%,两场“平均”是70%,而按总数计算是22次出手命中10次,约45%。差距不小。

所以在半岛体育AI的查询规划里,比率类指标默认按总数相除,先求分子之和与分母之和,再相除;只有明确要求“逐场平均”的时候才做简单平均。这是数据口径的问题,与模型的语言能力无关,也是为什么统计要交给数据库计算,而不是让语言模型心算。关于这种分工,可以参考大模型会算错平均数怎么办一文。

缺数据和没上场,是两件不同的事

窗口规则里最容易被混淆的,是“球员没上场”与“数据源没记录”。前者是事实,后者是数据缺口,处理方式完全相反:没上场的比赛不该占用名额,缺记录的比赛却不该被悄悄跳过,否则窗口会不知不觉往更早的日期滑动。

为了区分这两种情况,比赛表和球员记录表之间需要一个显式的出场状态字段,例如 appearance_status,取值可以是 played、bench_unused、absent_injury、absent_other、data_missing。下面是一个虚构的球员最近若干场的状态示例:

game_date(示例) appearance_statusminutes points是否进入球员窗口
3月14日 played 3118 是
3月10日 bench_unused 0 0 否
3月7日 absent_injury 0 0 否
3月3日 data_missing 空 空否,且触发提示
2月28日played 6 2 是,提示出场时间偏低

第四行是关键:当窗口里出现 data_missing,系统应该在答案里补一句“3月3日一场缺少记录,未计入”,而不是让用户以为10场都是完整的。如果没有这个状态字段,只靠 minutes 是否为空来判断,就分不清“没上场”和“没数据”,窗口的边界会悄悄出错。

这类设计属于典型的数据工程细节,对最终用户不可见,却直接决定答案是否值得信任。它与答案证据链的思路一脉相承:凡是影响数字的前提条件,都应当被记录下来,并在需要的时候展示出来。

一次完整的例子:同一句话,两种窗口,两个答案

把前面的内容合起来,看一个完整的虚构例子。用户问:“边锋周某最近10场场均得分是多少?”

  1. 实体识别:系统把“周某”对应到示例队乙的边锋,编号 P0031(虚构)。如果库里存在两个候选,会先让用户确认。
  2. 窗口选择:问法中没有“球队”字样,采用球员窗口;赛事默认仅含常规联赛。
  3. 查询执行:返回 games_counted = 9,因为其中一场记录缺失。
  4. 结果检查:9不等于10,触发提示。
  5. 答案生成:“按周某最近可用的9次上场记录计算,场均得分为示例值;有1场比赛缺少数据,未计入。”

如果用户接着追问“那按球队最近10场呢”,系统会改用球队窗口重新查询,并展示他在这10场里的出场场次。两个答案都正确,只是回答了不同的问题,重要的是用户清楚这一点。

用户能做什么:几种更清楚的问法

系统再谨慎,最好的方式仍然是让问题本身更清晰。下面几种问法,能帮助半岛体育AI直接选中正确的窗口:

  • “球员A最近10次上场的场均得分”:明确是球员窗口;
  • “球员A在球队最近10场比赛中的表现”:明确是球队窗口,缺阵场次会显示;
  • “球员A最近10场常规赛,不含加时”:直接给出了赛事类型和时间范围;
  • “球员A最近30天的场均得分”:换成日期窗口,避免场次数量的歧义。

如果这些都没有说明,系统会采用默认口径并在答案顶部标出,用户可以一键改成别的口径的做法,是我们在产品设计中想要达到的目标。至于“时间”本身的理解,可以继续参考问“最近10场”和问“本赛季”为什么不是一个问题。

赛季交界处的窗口

还有一种边界情况,发生在新赛季开始的头几周。这时“最近10场”可能一半在新赛季、一半在上赛季,中间隔着几个月的休赛期,球员可能换了球队、换了位置,甚至换了教练体系。把这10场直接平均,得到的数字并不能说明“最近的状态”。

我们的处理思路分三层:默认不跨赛季,新赛季场次不足10场时按实际场次回答并明示;用户明确要求跨赛季时才补足,并在答案里标出每场所属的赛季与所属球队;如果跨赛季窗口里包含转会,则额外提示球队发生了变化。这样做的代价是答案会长一些,好处是用户不会在不知情时得到一个“混合口径”的数字。

这一节也提醒我们,窗口不是一个孤立的过滤条件,它和实体的时间变化、赛季划分是缠在一起的。球员的所属球队随时间变化的问题,我们放在知识图谱如何保存时间关系里讨论。

窗口只是起点,样本量才是提醒

把10场取对,并不意味着这个数字值得被信任。10场比赛里出现一两场极端的高分或低分,就足以改变平均值;缺阵、对手强弱、主客场分布的变化,也都会在小样本中被放大。一个负责任的系统,除了报出平均值,还应该报出样本数、最高最低值或者标准差,必要时提示“样本较小,不宜过度解读”。

所以,“最近10场”这个看似简单的词,其实是一个入口:它把身份识别、时间排序、口径约定、计算方式和不确定性表达,全部牵了出来。把它定义清楚,用户看到的就不再是一个孤零零的数字,而是一个知道自己从哪些比赛算出来的数字。这样的答案,才配得上被追问。