有人在搜索框里敲下一句话:“球员A最近10场到底打得怎么样?”这句话看上去只有十几个字,任何一个球迷都能读懂,但对一个要给出可靠答案的系统来说,它几乎没有一个条件是完整的。谁是“球员A”?“最近”从哪天算起?“10场”包不包括没上场的比赛?“打得怎么样”是看得分、效率还是胜负?大模型最擅长的恰恰是把这种含糊的句子流畅地接下去,于是最危险的事情就发生了:它会用一段听起来非常专业的话,配上一个看起来很精确的数字,把这些未定义的条件全部悄悄吞掉。半岛体育AI在设计之初就把这件事当作头号问题:体育数据大模型最重要的能力不是会聊天,而是知道什么时候不该凭记忆说话,而该先去查数据。
一句话里藏着的五个未定义量
把“球员A最近10场表现”拆开看,至少有五个必须被明确的量,缺任何一个,后面的计算都没有意义。
- 对象:“球员A”是数据库里哪一条记录。同名、缩写、译名不同都会让字符串匹配出错,所以系统要先把名字落到一个唯一的 player_id 上。这一步在半岛体育的流程里叫实体链接,细节可以参考同名球员怎么办这篇专门的文章。
- 时间基准:“最近”是相对于哪一天。如果数据库更新到昨天,就是昨天之前;如果用户其实在问赛季初的某个阶段,“最近”就要换一个参照点。
- 窗口单位:“10场”是10场比赛、10次出场,还是10个自然日内的比赛。球员因伤缺席时,这三者会给出完全不同的结果。
- 指标:“打得怎么样”不是一个字段。它可能是场均得分,可能是命中率,可能是每分钟产出,也可能是综合评价。系统必须选定一个或几个,并且告诉用户选了什么。
- 口径:是否包含季后赛或杯赛,是否包含加时,不足一定时间的出场算不算有效样本。
这五个量里,前两个是“识别问题”,中间两个是“定义问题”,最后一个是“数据口径问题”。人类球迷在聊天时会用默契补全,而系统没有默契,它只有两种选择:要么把补全的过程显式地摆出来,要么让语言模型偷偷补全。后一种做法,恰恰是体育问答里最常见的隐形错误来源。
让大模型直接回答,会错在哪里
不妨设想一个纯语言模型的回答方式。用户问上面那句话,模型给出:“球员A最近10场场均约21分,命中率稳定,状态火热。”这段话至少可能在四个地方出错,而且每一处都很难被读者发现。
错误一:数字来自训练语料,而不是当前数据
语言模型的参数里保存的是训练时见过的文本的统计规律,而不是一张随比赛更新的技术统计表。它可以复述训练截止之前某篇报道里的说法,却无法知道上一周发生了什么。更麻烦的是,它并不会说“我不知道”,而是倾向于生成一个在语言上最合理的数字。
错误二:把“最近”当成一个模糊的形容词
在语言里,“最近”是个软概念,可以指最近几场、最近几周,甚至最近几个赛季。模型的回答会在这几种含义之间随意漂移,同一个问题问两次,窗口可能都不一样。而对统计来说,窗口必须固定,否则两次得到的数字根本没有可比性。
错误三:平均数被算错
即使模型拿到了10场的原始数据,让它心算平均值也不可靠。它可能漏掉一场,可能把百分比直接相加取平均,也可能在需要按出场时间加权的地方做了简单平均。这类错误的可怕之处在于结果“看起来合理”。关于这一点,半岛体育在大模型会算错平均数怎么办里有更完整的讨论。
错误四:口吻比证据更自信
语言模型的表达天然流畅,“状态火热”“表现稳定”这类判断性词汇在文本里出现得很多,模型很容易顺手加上。可是这些判断并没有对应的计算,没有阈值,也没有比较对象。读者看到的是一个结论,而看不到结论从哪里来。
所以,问题不是模型“不够聪明”,而是这类任务本来就不是靠语言记忆能完成的。精确的数字来自数据库,比较与排序来自查询引擎,语言模型该做的是前后两头:理解问题,以及解释结果。
把一句话变成一条可执行的查询
要让系统去查而不是去猜,首先得有一份可查的结构化数据。下面是一份为说明用途而简化的示例表结构,字段名和数值都是示例,不对应任何真实球员或真实比赛。
| 表名 | 关键字段 | 作用 |
|---|---|---|
| players | player_id, display_name, birth_year, position | 球员唯一身份与基础信息 |
| player_aliases | alias, player_id, alias_type | 别名、缩写、译名,用于实体链接 |
| games | game_id, game_date, season, game_type, status | 比赛日期、赛季、常规赛或季后赛、是否取消 |
| player_game_stats | player_id, game_id, minutes, points, field_goals_made, field_goals_attempted, played_flag | 每名球员每场比赛的技术统计,是聚合的原料 |
有了这张“每人每场一行”的明细表,“最近10场场均得分”就不再是一个语言问题,而是一个排序加过滤加聚合的问题。系统在解析出 player_id 和数据截止日期之后,可以生成类似下面的查询。
SELECT AVG(points) AS avg_points,
COUNT(*) AS games_counted
FROM (
SELECT s.points
FROM player_game_stats s
JOIN games g ON g.game_id = s.game_id
WHERE s.player_id = 'P_000123'
AND s.played_flag = 1
AND g.status = 'finished'
AND g.game_date <= '2026-03-01'
ORDER BY g.game_date DESC
LIMIT 10
) AS recent;
这段SQL做了几件语言模型无法保证的事:先按日期倒序,再截取10条,然后才求平均;用 played_flag 排除没上场的记录;用 status 排除被取消的比赛;用 COUNT(*) 把实际参与计算的场次一并返回,方便系统检查“是不是真的凑够了10场”。如果用户想知道命中率,那么正确的写法是先把 field_goals_made 与 field_goals_attempted 分别求和再相除,而不是把每场的命中率直接取平均,这一点在数据表里预留了两个原始字段,就是为了避免错误的聚合方式。
需要强调的是,这份SQL并不是让大模型“写完就用”。在半岛体育AI的设计里,生成出来的查询要经过检查:表名和字段名是否存在、过滤条件是否覆盖了已解析的实体与时间、聚合方式是否与指标定义一致,然后才交给数据库执行。公开研究里有类似方向,例如SPORTSQL系统演示,就是把自然语言问题转成可执行的SQL并在带时间索引的数据库上运行,输出表格与可视化。这里只是说明这个思路已被公开的系统演示所验证过其可行性,具体到不同数据库与不同项目,仍然要各自设计并验证。
一次完整查询的分解与检查
下面用一个示例场景,把上面的部分串起来。这里的“前锋林某”是虚构人物,数字均为演示用的示例数据。
用户提问:“前锋林某(虚构示例)最近10场到底打得怎么样?”
| 步骤 | 系统做的事 | 示例输出 | 可能出错的地方 |
|---|---|---|---|
| 1. 意图识别 | 判断这是“近期表现统计”,不是新闻检索 | intent = recent_form | 把它当成资讯搜索,返回一堆文章链接 |
| 2. 实体链接 | 把“林某”映射到 player_id | P_000123,置信度高 | 同名球员,或译名不一致导致选错人 |
| 3. 时间窗口 | “最近”解析为截至数据更新日的最近10场有效出场 | window = last_10_played | 把“最近10场”理解成“最近10天” |
| 4. 指标选择 | “打得怎么样”没有指定指标,选默认指标组,并保留追问的可能 | points, minutes, fg_pct | 指标组选得太窄,或没有告诉用户选了什么 |
| 5. 生成并检查查询 | 生成SQL,做字段与条件核对 | 通过 | 字段名幻觉,条件漏掉过滤 |
| 6. 执行统计 | 数据库执行聚合 | 场均得分18.6,场均上场31分钟,实际计入10场(示例) | 数据库本身缺场次 |
| 7. 结果解释 | 大模型把结果写成人话,并注明口径和截止日期 | 见下文 | 解释里夸大,或加入了数据里没有的判断 |
最后一步的回答,应当写成类似这样:“按示例数据,前锋林某截至更新日的最近10场有效出场中,场均得分18.6,场均上场约31分钟,实际计入场次为10场。以上不含被取消的比赛。”注意,这句话里没有“状态火热”这类无法被数据支撑的判断,只有数字、口径与范围。如果用户想要更主观的评价,系统可以继续追问“想看得分、效率还是综合表现”,把选择权交回给用户,而不是替用户悄悄决定。
整个流程里有一个容易被忽略的检查点:第6步返回的场次数量。如果数据库中只有7场符合条件,系统不应该假装凑足了10场,而应该把“只找到7场”这个事实告诉用户。这正是让COUNT(*)一并返回的意义:数字本身之外,样本量也属于答案的一部分。
“最近”一词的更多陷阱
在这条流程里,时间窗口是最容易被低估的部分。“最近10场”看似一个固定说法,实际却至少有三种合理读法:按“比赛场次”取最近10条比赛记录,按“有效出场”取最近10次上场记录,按“日期”取最近某段时间内的全部比赛。如果球员在过去一个月里因伤缺了四场,这三种读法会得出三个不同的平均值,而它们在语言上都可以叫“最近10场”。
半岛体育的做法是把窗口类型当成显式参数,并给出默认值:在没有其他说明时,采用“最近10次有效出场”,并且在答案里写明。至于比赛被取消、加时、季后赛是否计入等更细的规则,可以查看“最近10场”到底是哪10场一文中的窗口定义规则表。时间条件本身的更多类型,例如整赛季、某个月、换帅之后,则放在体育查询中的时间那篇里展开。这里只强调一点:时间条件不是修饰语,而是查询的主干。
还有一种情况是数据本身的滞后。用户问“最近10场”时,很可能刚刚打完一场,而数据库还没有同步。这时系统给出的“最近”其实截止于上一次数据更新。所以答案里带上“数据截至某日”是一个必须的动作,它既保护了用户,也保护了系统的可信度。
大模型在这条链路里到底做什么
把统计交给数据库,并不意味着大模型没有价值,恰恰相反,整条链路的入口和出口都离不开它。
- 入口:把自由的中文变成结构化的条件。用户可能说“林某最近咋样”“林某近期状态”“他这十来场”,这些说法需要被统一映射成同一种查询意图和参数。这是语言理解的强项,传统的关键词规则很难覆盖。
- 中间:辅助生成查询草稿,并在需要时向用户澄清。模型可以起草SQL,也可以判断哪些条件缺失、值得追问一次。但草稿必须经过检查才能执行。
- 出口:把表格和数字翻译成读者能看懂的话。数据库返回的是一行两列的结果,读者要的是一段讲清楚范围、口径和含义的文字。这一步同样是语言模型擅长的,但输入必须是已经验证过的结果,输出中的数字也应当与结果一一对应。
换句话说,半岛体育的设计把两类能力放在各自最合适的位置:语言模型负责“理解”和“表达”,数据库与计算引擎负责“事实”和“计算”。这两者的边界越清晰,整个系统的可解释性就越好,出了问题也更容易定位到具体一层,而不是面对一个无法拆解的黑箱回答。
这种分工也带来一个直接的工程收益:当数据更新时,不需要重新训练语言模型,只需要数据库里的数据变化即可。语言模型看到的永远是“这次查询返回了什么”,而不是“它在训练时记住了什么”。
这条路线的限制,需要老实说
把问题交给结构化数据,并不等于问题都被解决了。下面几类限制,是这条路线上真实存在的。
- 数据本身可能有缺口。数据库里缺一场、某个字段为空,聚合结果就会偏离事实。系统能做的是把样本量和缺失情况暴露出来,而不是替数据补洞。
- 问题本身可能无法用现有字段回答。“他的防守贡献大不大”这类问题,需要事先定义防守贡献由哪些字段构成。没有定义,就应当追问或明确说明无法回答,而不是编一个口径。
- 自然语言到查询的转换会出错。公开研究方向里,CricBench一类的评测基准就是专门用来测试大模型在体育分析场景中把问题转成SQL的能力,这本身说明这个环节并非天然可靠,需要评测与检查机制。
- 口径差异不可避免。不同数据源对“助攻”“有效出场”的定义可能不同,同一个问题在不同数据源上得到不同数字是正常现象,系统必须让口径可见。
因此,半岛体育在产品设计里始终把“可追溯”放在和“可回答”同等重要的位置:答案要带着它的条件一起出现。这部分的具体设计,可以参考给答案加证据链的那篇文章。
知道什么时候去查,比会聊天更难
回到开头那句“某球员最近10场到底打得怎么样”。一个只会聊天的系统会立刻回答,一个可靠的系统会先问自己:这是不是一个需要读数据的问题?如果是,对象、时间、指标、口径分别是什么?数据库里有没有足够的记录?算出来之后,我能否把范围和口径一起交代清楚?这一串“先问自己”,才是体育数据大模型真正需要的能力。
让语言模型显得博学并不难,难的是让它在该沉默的地方沉默、该查表的地方查表。半岛体育AI的思路,是把体育问题里“靠语言能回答的部分”和“必须靠数据回答的部分”分开对待:前者交给大模型,后者交给结构化数据库和计算引擎。用户看到的是一句自然的中文回答,背后是一条可以被检查的查询链路。这样做不会让系统变得万无一失,却能让每一个错误都有迹可循,让每一个数字都能追到它的来源。