大模型解释数据很流畅,不代表它擅长算数据。给它一份十行的表格,它可以写出一段头头是道的分析;但让它对三十行的数据求一个加权平均,它有不小的概率给出一个“看起来合理”的错误数字,并且语气依旧笃定。体育数据尤其容易踩到这类坑,因为体育里的“平均”背后总藏着场次、分钟数、出手次数这些权重。这篇文章不讲抽象原则,而是把大模型和体育统计之间最常见的几种失误做成一本“病例本”,逐个说明它们怎么发生、怎么被发现,以及半岛体育为什么因此把语言理解和统计计算拆成两个环节。
先说结论:谁负责理解,谁负责计算
语言模型擅长的事情包括:读懂“最近状态怎么样”里的意图,识别问题里的球员和时间词,决定应该去哪张表取数据,最后把结果用人话讲出来。数据库和分析引擎擅长的事情是:按条件过滤,按日期排序,分组,聚合,输出可复现的数字。两边各做自己擅长的部分,出错时也更容易定位:是没理解对问题,还是数据本身有缺口,而不是面对一个说不清来路的数字。
半岛体育采用的分工可以写成一条链:自然语言问题,到查询规划,到可执行的SQL或图查询,到数据库执行,到经过校验的结果,最后才是自然语言解释。中间任何一个数字,都来自执行环节,而不是模型的“心算”。下面用五个病例说明,为什么这条链不能省略中间那几步。以下所有球员与数字均为虚构示例,仅用于演示。
病例一:把缺席的比赛算成了零分
问题:“球员C最近10场平均得分是多少?”
示例数据:球队最近10场比赛中,球员C有2场因伤缺席没有上场,另外8场的得分合计152分。
错误算法:152除以10,得到15.2。语言模型看到“最近10场”,很自然地把分母写成10。
正确算法:要看“最近10场”指的是球队的最近10场,还是球员本人最近10次出场。如果是后者,取最近10条出场记录,再计算平均;如果是前者,缺席的比赛是否计入,需要在口径里说明,并向用户展示。按“出场8场”计算是152除以8,等于19.0。两种口径差了将近4分,读者若不知道口径,根本无从判断哪个才是他想要的。
这个病例里,问题的关键并不是除法,而是“分母到底是什么”,这是需要在查询规划阶段就确定的。相关的口径讨论,可以进一步参考“某球员最近10场到底打得怎么样”一文。语言模型在没有明确规则时,往往选择最常见的字面读法,而不会主动提醒“这里存在两种理解”。
病例二:把每场的命中率直接取了平均
问题:“球员A最近3场的投篮命中率是多少?”
示例数据:
| 比赛 | 投中 fgm | 出手 fga | 单场命中率 |
|---|---|---|---|
| 第1场 | 3 | 4 | 75.0% |
| 第2场 | 5 | 10 | 50.0% |
| 第3场 | 2 | 10 | 20.0% |
错误算法:把三个百分比直接平均,(75 + 50 + 20) 除以 3,约等于48.3%。
正确算法:应当先合计投中数与出手数,再相除:(3 + 5 + 2) 除以 (4 + 10 + 10),等于10除以24,约为41.7%。差别在于第一场只出手4次,却在简单平均里获得了与另两场同样的权重。
这是最经典的“比率的平均不等于平均的比率”问题。语言模型看到一列百分比,很容易顺手求均值。而数据库里的写法非常朴素:
SELECT SUM(fgm) * 1.0 / SUM(fga) AS fg_pct
FROM (
SELECT fgm, fga
FROM player_game_stats
WHERE player_id = 'player_demo_A'
AND fga > 0
ORDER BY game_date DESC
LIMIT 3
) recent;
在示例SQL里,比率是在汇总之后才计算的,这个顺序由查询本身保证,不依赖任何人“记得”这条规则。同样的问题也出现在真实使用的效率类指标、传球成功率、抢断率等所有由“分子除以分母”构成的指标上。
病例三:简单平均与加权平均混用
问题:“球员B本赛季场均得分多少?”
示例数据:球员B本赛季先在示例队甲出场4场,共得96分,场均24分;后转会到示例队乙出场6场,共得72分,场均12分。
错误算法:把两支球队的场均求平均,(24 + 12) 除以 2,得到18。
正确算法:总得分除以总场次,(96 + 72) 除以 (4 + 6),等于16.8。
这个病例在数据库里的对应场景,是分段汇总后再合并。如果先按球队分组做了聚合,再让语言模型把两行结果“综合一下”,错误就出现了。稳妥的做法是,需要跨组的结果时,让数据库在原始行上做一次性汇总,或者把组内的求和与计数一并返回,让后续计算能还原出加权结果。类似的还有“每36分钟数据”“每90分钟数据”这类标准化指标,分母是分钟数总和,同样不能用各场的比率再平均。
病例四:过早四舍五入造成的并列与错排
问题:“本赛季场均得分排名,球员D和球员E谁更高?”
示例数据:球员D场均22.36分,球员E场均22.44分。
如果在中间步骤里就把两人都保留一位小数,就会得到同样的22.4分,形成并列,结果页里会出现顺序随机的排名。更隐蔽的错误发生在语言模型的复述环节:数据库返回了完整精度的数字,模型在回答里习惯性地写“约22.4分”,用户看到的就成了“两人一样”。较稳妥的处理方式是,排序与比较始终使用完整精度,展示层再统一格式化,并且在展示并列时明确标注“显示值相同,实际值不同”或直接多展示一位小数。
病例五:把不同时长的比赛当成等权重
问题:“球员F每36分钟能得多少分?”
示例数据:球员F本赛季三场:第一场上场12分钟得10分,第二场上场36分钟得18分,第三场上场24分钟得14分。
错误算法:先算每场的每36分钟得分:第一场30,第二场18,第三场21,再取平均,约为23.0。
正确算法:总得分42,总上场分钟72,42除以72再乘以36,等于21.0。第一场只上了12分钟,样本极小,却拉高了平均值。这个例子还顺带说明了另一件事:即使算法正确,也应提示“样本中有一场上场时间过短”,因为标准化指标在小样本上很容易失真。规划环节里可以设置一条规则,当窗口内单场分钟数过低或总分钟数过少时,在结果里附带提示,让模型解释时不能忽略它。
指标库:把“怎么算”写成数据,而不是写进提示词
前面几个病例有一个共同点:每个指标都有自己的“正确算法”。半岛体育把这些算法登记在指标库里,用类似下面的字段描述一个指标:
-
metric_id:指标的唯一编号,例如 fg_pct、points_per36; -
numerator与denominator:分子与分母对应的原始字段; -
aggregation:先汇总再相除,还是逐行计算,还是取最大值; -
min_sample:建议的最小样本量,低于它就附带提示; -
scope_note:口径说明,比如是否含季后赛、是否含加时。
规划环节从用户的话里识别出指标名称,就到指标库里取对应条目,再生成SQL。这样,新增一个指标只需要在库里新增一行,不必修改模型,也不会因为提示词过长而遗漏规则。用户问到库里没有的指标时,系统可以直接回答“暂不支持这个指标”,或者请用户选择一个已有指标,而不是让模型自行发明一个公式。
为什么这些错误很难靠提示词修好
有人会问:把规则写进提示词,例如“计算命中率时必须先汇总再相除”,是不是就够了?在数据量很小、口径固定的演示里,确实可以让模型做对。但在真实场景里,这种做法有三个问题。
- 规则列表会不断变长。每个新指标都有自己的权重与分母,而模型不一定在每次回答里都记得全部规则。
- 一旦数据量变大,把所有原始行都塞进上下文既昂贵又不可靠,模型可能只看到被截断后的一部分,却依旧给出完整的答案语气。
- 模型的错误不可复现。同一个问题问两遍,可能得到两个数字,对需要审计的分析产品来说,这是难以接受的。
因此,更可靠的做法是让计算发生在别处,模型只负责生成查询和解释结果。这也与体育大模型为什么需要工具调用中讨论的思路一致:让模型学会“什么时候调用什么工具”,比让它独自做完所有事情更靠谱。
拆开以后的流水线:每一步产出什么
把语言理解与统计计算拆开,并不是简单地在中间加一次数据库调用。每个环节都应有明确的输入、输出和校验点:
| 环节 | 输入 | 输出 | 校验点 |
|---|---|---|---|
| 问题理解 | “球员A最近3场投篮命中率” | 球员实体、时间窗口、指标名称 | 实体是否唯一,指标是否在指标库中 |
| 查询规划 | 上一步的结构化条件 | 选定表、字段、聚合方式 | 比率类指标必须先汇总再相除 |
| 查询生成 | 规划结果 | 可执行的SQL | 语法检查,只读权限,字段存在性 |
| 数据库执行 | SQL | 结果行、行数、场次范围 | 结果行数是否符合窗口预期 |
| 结果校验 | 结果行 | 通过或告警 | 数值范围检查,空结果与异常值处理 |
| 自然语言解释 | 校验后的结果与口径 | 用户看到的回答 | 回答里的数字必须与结果一致 |
其中“比率类指标必须先汇总再相除”这条规则,被写成了查询规划环节里的程序规则,而不是提示词里的一句提醒。指标库里每个指标都登记了分子、分母、聚合方式,规划环节据此生成SQL,模型不需要“记得”。这是半岛体育大模型与统计计算配合的基本做法:模型给出意图,指标库和查询引擎负责把意图变成确定的计算。
校验层:不信任任何一个数字
即使计算已经交给数据库,仍然有几类检查值得做,它们都很便宜,却能拦住大量错误。
行数检查
用户问“最近10场”,返回的记录却只有7行,系统就应该提示“只找到7场出场记录”,并在回答里如实说明,而不是把7场当成10场的样本悄悄算完。
范围检查
命中率不可能大于100%,场均得分为负数明显异常,出场分钟超过比赛时长也需要告警。这些边界写成规则以后,几乎没有成本。
回算检查
对于关键数字,系统可以用另一种独立写法再算一遍,例如用窗口函数与子查询各算一次,两者不一致就拒绝输出。这样做并不能保证绝对正确,但能拦住一部分由SQL生成造成的偏差。
展示一致性检查
最后一步是检查模型复述的数字是否与结果表一致。做法可以很简单:把回答里出现的数字抽出来,与结果集逐一比对,对不上就退回重写。语言模型在复述时把“19.0”写成“19”问题不大,把“16.8”写成“18”就不行。
一个完整的示例:从提问到可核对的答案
| 阶段 | 示例内容 |
|---|---|
| 用户提问 | “球员C最近10场平均得分?有两场没打,怎么算的?” |
| 解析 | 球员:player_demo_C;窗口:最近10次出场;指标:平均得分;附加要求:说明缺席处理 |
| 查询 | 按 game_date 倒序,过滤 minutes 大于0,取10行,计算总分与场数 |
| 返回(示例) | 出场10场,总分190,场均19.0,数据截止示例日期,窗口向前多取了2场以补足缺席 |
| 解释 | “按最近10次实际出场计算,场均19.0分;这10场跨越的日期范围见下表。” |
| 可能出错的地方 | 用户其实想看的是球队最近10场;系统应在回答里给出另一种口径的入口,避免被默认口径误导 |
这个示例里,回答并没有因为要“显得聪明”而省略口径。数字之所以可信,靠的是能被核对,而不是靠语言的流畅。至于哪类问题适合写成SQL、哪类适合走图查询,可以参考Text-to-SQL与Text-to-Graph的对比。
限制与诚实的边界
把统计交给数据库,并不意味着答案从此绝对准确。它只是把误差的来源集中到了几个可以检查的地方:数据本身是否完整,指标口径是否定义得当,问题是否被理解对了,SQL是否被写对了。这四处任何一处出问题,结果仍会出错。数据缺失的比赛、临时更正的技术统计、不同数据源对同一次助攻的判定差异,都不是靠换一种计算方式就能消除的。
另外,语言模型在“理解问题”环节依然会犯错,比如把“最近状态”默认为最近5场,或者把两位同名球员当成一个人。因此拆分之后,半岛体育的设计仍然要求在存在歧义时向用户反问,并把使用的口径展示出来。可靠的系统不是不出错,而是在出错时能被人发现。
还有一类限制来自“更新”。数据库里的技术统计可能在赛后被修订,某次助攻被改判、某次出场分钟被更正,都会让昨天的答案与今天的答案不一致。所以每次回答除了数字本身,还应带上数据截止日期与查询时间,方便用户判断这个数字是否已经过期。这些信息看起来琐碎,却是让统计答案能够被复查的基本条件。
该让模型做的事,和不该让它做的事
回到开头那句话:模型解释得流畅,说明它擅长语言,而不是擅长算术。把体育统计交给语言模型,就像让一个口才很好的解说员现场心算整个赛季的数据,多数时候他能说得很像,偶尔他会说错,而观众通常听不出来。半岛体育选择的做法是让解说员留在话筒前,让计算机房里的引擎去算数,然后把算好的结果交给他讲。这种分工不炫目,却让每个数字都有来处,也让错误有迹可循。