“球员A最近10场场均得分22.4(示例数据)。”这句话读起来完整、干净,也几乎没法被检查。你不知道这10场是哪10场,不知道数据截止到哪一天,不知道加时赛的分钟数有没有折算,也不知道他缺席的那两场是被跳过了还是被记成了零。问题不在于22.4对不对,而在于用户没有任何办法判断它对不对。一个只给结论、不给依据的体育AI,在用户眼里和一个自信的路人没有区别。半岛体育开始给每个涉及数字的回答附上一条“证据链”,出发点就是这个:答案必须能被追问,也必须能被推翻。
先看一个人会怎么追问
假设你是一位认真的球迷或分析爱好者,读到上面那句话,你脑子里大概会冒出六个问题,而且顺序几乎固定。
- 这10场是哪10场?
- 数据是哪天截止的?昨晚那场算进去了吗?
- “得分”指的是什么,包不包括罚球?
- 季后赛、杯赛、友谊赛算不算?
- 他缺阵的场次怎么处理?
- 这些数字是谁提供的,有没有可能和别处的对不上?
这六个问题不是吹毛求疵,每一个都足以让答案变一个数。我们的设计目标是:用户在读到答案的同一屏里,就能找到这六个问题的回答,而不必再问一遍。下面逐个拆开,说明每一个背后系统需要记录什么。
“哪10场”:窗口必须落实到具体场次编号
“最近10场”在SQL里其实是一个排序加截断的动作:按 game_date 倒序,取前10条。但真正要保存的不是这个规则,而是它执行后的结果——10个具体的 game_id。因为规则会随数据更新而产生不同结果,只有保存结果,用户明天再看这个回答时才能知道“当时说的是哪10场”。窗口如何定义(缺阵算不算一场、取消的比赛怎么办),我们在“最近10场”到底是哪10场里有专门的规则表,这里只强调一点:回答中应至少可以展开看到这10场的日期和对手。
“数据截止时间”:新数据晚到是常态
体育数据的入库往往比比赛结束晚一些,有的字段还会在赛后被校正。所以回答里必须有两个不同的时间:一是数据快照的截止时间(数据库当时包含到哪里),二是回答生成的时间。两者相差很久的话,用户应该看到提示。如果最近一场比赛尚未入库,系统应当说“最新一场(日期)的数据尚未更新,本次统计不含该场”,而不是默默把上一场当作最近一场。
“统计口径”:同一个词,在不同表里含义不同
得分是否含罚球、助攻是否要求直接传球、出场时间是否含伤停补时,这些定义决定数字。口径需要作为字段存在,不能只存在于开发者的脑子里。我们把每个指标都对应到一个口径条目,包含名称、定义一句话、计算公式、适用范围。用户看到“场均得分”时,可以点开看到“总得分(含罚球)÷ 计入场次”。
“赛事范围”:一个被反复忽视的过滤条件
常规赛、季后赛、杯赛、友谊赛的场次混在同一张比赛表里,靠 competition_type 之类的字段区分。如果用户问的是“最近10场”而没有说范围,系统必须选一个默认值并说出来。默认值可以有不同选择(只算常规赛,或全部正式比赛),重要的是显式声明,并允许用户改。
“缺席处理”:分母怎么算
球员因伤缺阵,那场比赛在“最近10场球队比赛”里,还是在“最近10场球员出场”里?前者会得到一个覆盖时间更长、但出场场次不足10的结果;后者会得到刚好10场出场、但时间跨度更长的结果。两种都合理,回答必须说明是哪一种。我们的默认设计是后者,即“该球员最近10次有出场记录的比赛”,同时在证据链里写出跨越的日期范围和期间的缺席场次。
“数据来源”:谁说的,什么时候取的
每一批数据都有来源标识和取得时间。当两个来源对同一个数字有分歧时(这在体育数据里非常常见,半岛体育App数据和其他网站不一样一文里详细谈过原因),证据链应该显示当前采用的是哪个来源,而不是把差异隐藏掉。
证据链字段清单:一张可以直接对照检查的表
把上面六个问题整理成字段,就是一份证据链的最小集合。实际系统里字段还会更多,但少了下面这些,追溯就会出现断点。
| 分组 | 字段 | 示例值 | 回答了什么问题 |
|---|---|---|---|
| 问题解析 | original_question | 球员A最近10场场均得分 | 系统听到了什么 |
| 问题解析 | resolved_entity | player_id = P0007(球员A,示例队甲) | 指的是哪个人 |
| 问题解析 | metric | points_per_game | 算的是什么指标 |
| 数据范围 | game_ids | 共10个场次编号,可展开 | 哪10场 |
| 数据范围 | date_span | 2026-01-04 至 2026-02-08 | 时间跨度 |
| 数据范围 | competition_scope | 仅常规赛 | 哪些赛事被计入 |
| 数据范围 | data_as_of | 2026-02-09 06:00(示例) | 数据截止时间 |
| 处理规则 | missed_games | 期间缺阵2场,未计入 | 缺席怎么算 |
| 处理规则 | metric_definition | 总得分(含罚球)÷ 出场场次 | 统计口径 |
| 结果 | result_value | 22.4 | 数字本身 |
| 来源 | source_id | 示例数据源1,取得于同上 | 数据从哪来 |
| 执行 | query_text | 可折叠显示的SQL | 怎么算出来的 |
| 提示 | caveats | 样本仅10场,波动较大 | 使用限制 |
这份清单里的字段分成两类:一类是用户直接关心的(哪10场、什么口径),应该默认可见;一类是审计用的(query_text、source_id),可以折叠。我们希望做到的是,普通用户不必看SQL也能理解回答,而爱好者或数据分析者想复核时,一步就能展开。
把它们拼在一起:一次示例回答长什么样
下面是在半岛体育的设计中,一次回答的“证据卡”示意,全部为示例数据。为了避免代码块里出现大括号,这里用逐行的“字段:值”格式表示。
问题:球员A最近10场场均得分是多少?
解析:球员=球员A(示例队甲,前锋);指标=场均得分;窗口=最近10次出场
数据范围:2026-01-04 至 2026-02-08,共10场,仅常规赛
数据截止:2026-02-09 06:00(示例)
处理规则:期间缺阵2场,不计入;得分含罚球
结果:22.4
来源:示例数据源1
使用限制:样本仅10场,可能受个别高分场次影响
用户读到的自然语言版本应该是:“按球员A(示例队甲)最近10次出场计算,场均得分为22.4,覆盖1月4日至2月8日,仅含常规赛,期间缺阵的2场未计入,数据截至2月9日早晨。”一句话里带上了范围、规则和截止时间,语气依然自然。证据链不该是一张吓人的表格,而应当是嵌在回答里的那几个短语。
为了让“哪10场”可以复现,系统在生成结果的同时,会保留下面这样一个查询,把场次明细一并取出:
SELECT g.game_id, g.game_date, g.opponent_id, s.points, s.minutes
FROM player_game_stats s
JOIN games g ON g.game_id = s.game_id
WHERE s.player_id = 'P0007'
AND g.competition_type = 'regular'
AND s.minutes > 0
AND g.game_date <= DATE '2026-02-09'
ORDER BY g.game_date DESC
LIMIT 10;
这段查询做了三件用户看不见、却决定结果的事:只取常规赛,只取上场时间大于0的记录(即真正出场),并且用数据截止日期限制上界。场均得分则基于这10行再做一次汇总。把明细与汇总分开保存,是因为用户往往想看的是哪几场拉高了平均数,明细里能一眼看出。
没有证据链时,三类错误会长期潜伏
为什么一定要费力做这些字段?因为下面这几类错误,在没有证据链的系统里几乎不会被发现。以下三个场景都是虚构的示例,用来说明问题的形态。
场景一:更新了一半的数据
示例队乙昨晚结束了一场加时赛,数据源先入库了比分,第二天才补上球员的分钟数。中间这段时间里,用户问“前锋丁最近5场场均得分”,系统读到了这场比赛的得分,却读不到分钟数,于是把它当成“上场0分钟”排除,或者干脆按默认时长计算。无论哪种,结果都和事实有偏差,而回答里没有任何痕迹。有了 data_as_of 和“最新一场数据不完整”的提示,用户至少知道这个数字要谨慎使用。
场景二:口径悄悄变了
某个指标的定义在赛季中期被数据提供方调整,例如把某类特殊回合从助攻中剔除。同一名球员“本赛季助攻数”在调整前后不可直接比较。如果系统没有记录口径版本,一个月前的回答和今天的回答会给出不同数字,用户会以为是系统出错,开发者也很难解释。给口径条目加上版本号和生效日期,问题就变得可追踪。
场景三:实体被换成了另一个人
用户问“林某最近状态怎么样”,系统把它链接到了示例队丙的同名球员,而不是用户想的那位。如果回答里没有写出 resolved_entity 的所属球队与位置,用户会以为系统在胡说,其实是对着另一个人在认真地回答。这类问题的详细讨论可以参考同名球员怎么办,这里只想指出:证据链里的第一项就应该是“我把你说的那个人理解成了谁”。
这三类错误有一个共同点:错的不是计算,而是计算之前和之后的那些“隐含选择”。所以证据链的重点不在于展示SQL有多复杂,而在于把这些选择摆到台面上。
不同类型的问题,证据链的重心不同
前面的例子偏向统计类问题。实际系统里,不同问题需要的证据并不相同,把同一套字段套到所有问题上,会让回答又长又空。
| 问题类型 | 示例 | 证据链的重心 |
|---|---|---|
| 窗口统计 | 球员A最近10场场均得分 | 场次列表、赛事范围、缺席处理、数据截止时间 |
| 赛季对比 | 球员A与球员B本赛季效率对比 | 双方的比较口径、出场时间门槛、是否按每90分钟或每36分钟折算 |
| 关系查询 | 某教练执教期间效力过、后来转队的球员 | 每条关系边的有效起止时间、关系来源 |
| 文本类问答 | 某项规则怎么规定 | 引用的原文段落、发布日期、文本版本 |
| 混合问题 | 换帅后谁的出场时间变化最大 | 任职日期来源、比较的前后窗口、样本场次数 |
关系查询的证据尤其容易被忽略。图里的一条边“球员C效力于示例队丁,2024至2025”,如果不同时给出这条边的来源和有效区间,用户无法判断这是官方登记的、由文本抽取的,还是推断出来的。三种来源的可靠程度显然不同,应该在证据链里区分展示。文本类问答则相反,它的证据就是引用本身:哪份文本、哪一版、何时发布。此时展示SQL毫无意义,展示原文出处才是对的。
证据链在页面上应该怎样出现
字段设计好之后,还有一个不小的问题:怎么展示才不打扰阅读。我们目前的设计思路有三条。
- 正文里带条件,折叠区里放明细。回答的第一句就包含范围、口径和截止时间,折叠区放场次列表和查询文本。
- 把“可以改的条件”做成可点击的词。比如“最近10次出场”“仅常规赛”,点一下就可以改成“最近5场”“含季后赛”,然后重新查询,而不是让用户重新打字提问。
- 异常情况用一句话直说。数据缺失、来源冲突、样本过小,都要以显眼的短句提示,不使用颜色加图标来暗示。用户看到“本次结果不含最新一场”比看到一个黄色感叹号更清楚。
需要说明的是,半岛体育目前是一个静态官网,这里描述的是产品设计与使用思路,不代表已经有一个完整开放的查询系统在线运行。我们把这些思路写出来,是希望它们本身就可以接受检验:如果一个体育AI产品声称能回答数据问题,你完全可以拿这张字段表去对照它,看它能回答其中的几项。
溯源做不到的事
给答案加证据链,很容易被夸大成“有了它,答案就可信了”。我们不想这么说,因为它至少有四个明确的限制。
- 证据链只能证明答案与数据一致,不能证明数据本身正确。如果源数据里某场比赛的得分录错了,证据链会忠实地把错误的数字连同来源一起展示。它的价值在于让错误可以被发现和定位,而不是自动消灭错误。
- 用户不一定会展开看。所以最关键的几项(范围、口径、截止时间)要写进回答正文,而不是全部藏在折叠区里。
- 字段多不等于清楚。把十几个字段全部摊开,反而会让人无从看起。什么该默认展示,需要按问题类型来定:统计类问题重点在范围和口径,关系类问题重点在关系的有效时间。
- 语言模型的解释仍可能出错。数字来自数据库,但“这说明他状态出色”这类判断由模型给出。我们的做法是把“数据结果”和“解释性评论”在版式上分开,评论部分使用更保守的措辞,并且不引入证据链之外的数字。
这也解释了为什么我们在自然语言体育查询的流程里坚持先查、后答:证据链不是事后补的说明,而是查询过程本身的副产品。查询时记录了什么,回答时才有什么可展示。如果系统是先凭印象写出答案,再倒过来找依据,那种“证据”就不能算证据。
好答案是允许被反驳的答案
回顾这六个追问和那张字段表,你会发现一个共同点:它们都在把答案里原本隐含的选择显式化。哪10场、什么口径、含不含季后赛,任何一个默认值都可能与用户的想法不一致。半岛体育的做法不是假装这些选择不存在,而是把它们摆在用户面前,让用户可以说“不对,我要的是含季后赛的”,系统再据此重新查询。
我们认为,体育AI的可信度不来自措辞的笃定,而来自答案的“可反驳性”:一个可以被逐条检查、逐项质疑并且可以重算的答案,比一个不容置疑的答案更值得信任。如果你想看这类设计怎样贯穿在具体的数据问答里,可以到半岛体育数据AI栏目继续了解。