体育知识图谱最容易被做成一张漂亮的关系网络图:球员是圆点,球队是大圆点,教练是另一种颜色,中间拉满了线。这种图适合放在展示屏上,却很少回答任何具体问题。真正有价值的部分通常根本不需要被画出来,而是藏在多跳查询里,比如这句:“某教练执教期间在队里踢过球,后来又效力另一支球队的球员有哪些?”本文以这个问题为线索,讲清半岛体育知识图谱为什么要把球员、球队、教练和比赛“连起来”,以及连起来以后究竟解决了什么。

先看关系表能不能答:可以,但代价在别处

严格说,这个问题用普通关系型数据库也能回答,所以“图数据库比表格强”并不是一个成立的说法。假设我们有三张表:

  • coach_tenure:coach_id、team_id、start_date、end_date,记录教练在哪支球队任职、任职起止;
  • player_contract:player_id、team_id、start_date、end_date,记录球员在哪支球队效力、效力起止;
  • player、team、coach:各自的实体主表,含 id 与名称。

答案就是把 coach_tenure 与 player_contract 按 team_id 连接,并要求两段时间区间有重叠,再回到 player_contract 找出这批球员后续在其他球队的记录。写成SQL大概需要两次自连接加一个区间重叠判断,可读性已经开始下降。问题再加一跳,比如“这些球员后来效力的球队里,又有哪些聘请过同一位教练的助理”,表的连接层数就翻倍,SQL 会变成没人愿意维护的长句。

所以更准确的说法是:关系越多跳,表格写法越吃力;图的表达方式更接近人描述问题的方式。图的价值并不在于存了什么表格存不下的东西,而在于把“沿着关系走几步”变成一等公民,查询规划、路径约束和结果解释都围绕路径展开。

图里存什么:节点、边,以及边上的时间

半岛体育知识图谱的基本单位是三元组,也就是“实体—关系—实体”。以下是示例数据中会出现的几类节点和边(全部为虚构示例,不对应任何真实球员或球队):

节点类型典型字段说明
Player player_id、规范名、出生年份、惯用脚、主要位置 主键必须是稳定ID,名字只是别名
Team team_id、名称、所属联赛级别 球队更名、合并时保留同一个 team_id
Coach coach_id、规范名、执教资质说明主教练与助理教练用类型区分
Match match_id、比赛日期、赛季、阶段、轮次赛季与阶段建成显式字段,便于时间过滤

边的重点在于类型与时间。PLAYED_FOR 连接球员与球队,带 start_date、end_date;COACHED 连接教练与球队,同样带任职区间;APPEARED_IN 连接球员与比赛,带出场分钟与是否首发。如果只画 Player→Team 一条无时间的线,图就会告诉你“某球员和某球队有关”,却说不清“什么时候有关”。体育数据里几乎每一种关系都随时间变化,忽略有效时间是知识图谱在体育场景里最常见的第一个坑。这一点在球员转会以后旧数据怎么办一文里有更具体的展开。

公开研究方面,MDPI Data 上有一篇讨论篮球技术统计的本体驱动知识图谱的工作,其本体显式建模了赛季、阶段、轮次和日期范围,再交给大模型做查询。这类工作的共同点是:先把体育数据的语义骨架搭起来,再谈问答。半岛体育在设计自己的图谱模式时,思路与之相近,但具体实现以自己的数据接入方式为准。

三种关系不能混在一层:长期关系、阶段关系与事件

把所有连线都当作同一种边,是第二个常见的设计错误。体育里的关系至少分三层,它们的更新频率、存储量和查询方式都不一样。

长期关系

“球员出生于某城市”“球队隶属某联赛”,几乎不变,边上不需要时间区间,或者区间是开放的。

阶段关系

“球员效力于某队”“教练执教某队”“球员与某队签约至某日”,有明确起止,需要有效时间区间。绝大多数教练、转会类问题落在这一层。

事件关系

“球员甲在某场比赛的某分钟助攻球员乙”,是一次性的,量级最大。一个赛季里,一支球队的事件边可能比阶段边多出几个数量级。要不要把每次助攻都建成节点,取决于要不要回答“谁与谁之间的助攻链最稳定”这类问题。如果只需要“谁助攻最多”,一张聚合后的统计表就够了,这个取舍需要单独权衡:事件节点越细,存储和维护成本越高,收益只在少数问题上才体现。

分层的直接好处是:查询规划器拿到问题后,可以先判断需要哪一层的边,再决定是走图、走SQL,还是两者组合。

图的入口是别名:实体没链对,后面全白走

多跳查询的第一跳永远是“从哪个节点出发”。用户输入的是“顾教练”“小林”“示例甲队”,图里存的是 coach_id、player_id、team_id。中间这一层是别名表:一个实体可以有多个别名,同一个别名也可能指向多个实体。比如示例数据里有两位都被叫作“林某”的球员,一位是前锋,一位是后卫,出生年份不同,效力球队也不同。

半岛体育的处理思路是:先用别名表取出候选实体,再用问题里出现的其他线索给候选打分,比如问题里提到的球队、位置、时间段是否与候选的履历相容。只有一个候选明显胜出才继续查询;分数接近时,系统应当反问用户“你说的是前锋林某还是后卫林某”,而不是替用户选一个。这一步做得好不好,决定了后面所有路径查询的意义。关于同名和别名的细节,本站另有专文讨论。

另外,别名本身也有时间性。球队更名、球员改名、中文译名调整,都会在别名表里留下带日期的记录。如果查询的是十年前的比赛,就应该优先匹配当时使用的名称,否则“示例甲队”这个名字在早年可能指的是另一支球队。

一次多跳查询长什么样

回到开头的问题。我们把它拆成图上的路径:教练节点 → 任职球队 → 在任职期间效力该队的球员 → 该球员的其他球队记录,并要求“其他球队”的效力起点不早于任职结束前的重叠段。下面是一个简化的、类Cypher风格的写法,只是为了让读者看清路径,并非某个数据库的原样语句:

MATCH (c:Coach)-[t:COACHED]->(a:Team)
MATCH (p:Player)-[f1:PLAYED_FOR]->(a)
MATCH (p)-[f2:PLAYED_FOR]->(b:Team)
WHERE c.coach_id = 'coach_demo_07'
  AND b.team_id <> a.team_id
  AND f1.start_date <= t.end_date
  AND f1.end_date >= t.start_date
  AND f2.start_date >= f1.end_date
RETURN p.player_id, a.team_id, b.team_id, f2.start_date
ORDER BY f2.start_date

这里有三个值得注意的细节。第一,重叠判断用的是区间相交的标准写法,两个“不早于、不晚于”的比较。第二,“后来又效力”被翻译成 f2 的起点不早于 f1 的终点,这是一次把自然语言里的时间词落成日期比较的过程,最容易被大模型随手写错。第三,结果里保留了球队ID与日期,是为了后面把它交给大模型做解释时,答案里的每一条都可回查。

示例查询场景:从一句话到可回查的答案

下面用一个完整的示例走一遍。所有名字和数字都是模拟场景,不代表任何真实人物或球队。

步骤系统做什么 示例内容
用户提问 接收自然语言 “教练顾某(虚构)带示例队甲期间踢过球、后来去了别队的球员有哪些?”
实体识别 把“顾某”“示例队甲”链接到 coach_id 与 team_id coach_demo_07;team_demo_A;若同名则请用户确认
关系与时间条件确定需要 COACHED 与 PLAYED_FOR 两类阶段边,时间为“任职期间重叠”与“之后”任职期 2019-07-01 至 2022-06-30(示例)
查询执行 在图上走三跳,取路径返回球员A、球员B、球员C(虚构代号)及其后续球队
统计补充 若用户还想知道他们在任期内的出场数,转交结构化数据库聚合 球员A 出场 31 场(示例)
答案生成 大模型把路径与统计整理成一段话,并附上使用的数据范围 “共3人符合,见下表,任期区间按合同记录计算”

可能出错的位置也在这张表里。第一步的实体识别如果链接到同名的另一位教练,后面的每一跳都是对的,答案却整体是错的。第三步如果“期间”被理解成“任职当天所在赛季”而不是精确日期区间,结果集会偏大。第五步的出场数如果由图里的事件边直接数出来,而事件边并不完整,统计就会漏场次。图查询给了我们路径,并没有替我们保证每一条边都完整。

图查询会在哪里出错:三个典型案例

案例一:边缺失被当成“没有关系”

如果某个球员的一段租借记录没有被入库,图里就不存在那条 PLAYED_FOR 边。查询“后来又效力另一支球队的球员”时,这个人会被静默漏掉,而答案依旧读起来很完整。应对办法是在结果里注明数据覆盖的联赛与时间范围,并对已知有缺口的区间打上标记,而不是让语言模型用“没有找到”这种口吻暗示“不存在”。

案例二:方向写反

“球员甲助攻球员乙”和“球员乙助攻球员甲”在图上是两条方向相反的边。自然语言里的被动语态和“由谁完成”这类说法,会让大模型在生成图查询时把方向写反,查询依然能执行,只是返回的是另一批人。半岛体育的做法是让查询规划步骤先输出一份结构化的关系描述,再由程序校验方向、类型是否与图模式一致,而不是让模型直接吐出最终语句。

案例三:把图当作统计引擎

图数据库擅长路径与邻接关系,不擅长在大范围上做加权平均或滚动窗口。“这些球员最近10场平均得分”这种问题,应当让图先回答“是哪几个人”,再把 player_id 列表交给结构化数据库去算。大模型会算错平均数怎么办一文讲的是同一个分工原则:找关系的交给图,算数字的交给数据库。

案例四:时间区间边界的“一天之差”

示例数据里,教练在 6 月 30 日离任,球员在 7 月 1 日与新队签约。如果比较写成“小于”而不是“小于等于”,或者把结束日期当作开区间处理,就会多出或漏掉一批人。这类错误不会让查询报错,只会让答案在边界上悄悄偏一点。因此半岛体育的设计要求每类阶段边明确规定区间是闭区间还是半开区间,并在查询规划里统一使用同一种写法,同时在结果说明里写明“按合同起止日期,含首尾日”这样的口径。

关系质量比节点数量更值得盯

知识图谱有一个常见的展示套路:节点数上百万、边数上千万。但对回答问题来说,这些数字几乎没有信息量。一张图里有一百万个球员节点,如果同名合并错了两千个,或者转会边的日期有系统性偏移,多跳查询的误差会被一跳一跳地放大。相比规模,更值得盯的是几项质量指标:

  • 实体合并的准确度,也就是同一个人是否被拆成两个节点、两个人是否被并成一个;
  • 阶段边的日期是否闭合,例如是否存在起点晚于终点、两段任职区间重叠但业务上不该重叠;
  • 关系类型是否严格对应本体定义,避免“效力”和“租借”混用一个类型;
  • 每条边是否记录了来源,方便回溯。

这也是半岛体育更关心校验规则而不是节点总量的原因。对应到日常运维,就是给每类边写一组一致性检查,比如同一球员同一时间只应有一个正式效力球队,违反规则的边进入人工复核队列,而不是直接进入可查询的图。

图、数据库与大模型的分工

把上面的内容压缩成一张分工表,读者就能看到图在整个系统里的位置:

问题类型 首选组件原因
某球员最近10场平均得分 结构化数据库 + 统计计算 需要精确过滤、排序、聚合
教练执教期间效力过、后来转会的球员知识图谱多跳查询 沿着时间受限的关系走多步
某教练的战术风格评价文章文本检索与RAG 答案在文章里,不在表里
两种问题混合,例如上述球员的任期内场均数据 图给出名单,数据库算数字,大模型解释各司其职,分别校验

关于这种分工如何与文本检索结合,可以参考RAG能找到文章,为什么不一定能回答“最近20场谁效率最高”。在半岛体育的设计里,知识图谱是查询规划的一个资源,而不是整个系统。它提高的是关系类与时间类问题的可靠性,仍然需要结构化数据的校验,也不能把它理解成“接入图谱就不会出错”。

关系图不用画出来,路径要说得清

回到最初的判断:体育知识图谱的价值不在于让人看到一团线,而在于让机器能沿着有类型、有方向、有时间的关系一步一步走下去,并且每一步都可以解释。一个合格的图谱产品,展示给用户的更应该是“这个答案经过了哪几条边、每条边的有效期是什么、来源是什么”,而不是一张缩放以后什么也看不清的网络图。

所以衡量半岛体育知识图谱是否做对了,不必看画面,只需要拿几个多跳问题来试:能不能识别对实体,能不能把“期间”“之后”翻译成日期比较,能不能在数据有缺口的地方老实说明,能不能把统计交给数据库。这几件事做对了,图即使一次都不画出来,也已经在工作了。