把“过去两个月,面对排名前六的球队时,哪名前锋的射门转化效率最高?”这句话交给一个只会检索文章的系统,它多半会带回几篇赛季回顾、两三条前锋专访,再配上一段语气很笃定的总结。麻烦在于,这个问题的答案并不写在任何一篇文章里。它散落在几百条比赛记录、一张随时间变化的积分排名快照和一份球员位置表的交叉处,而文章里最多只有对这些数字的转述,还常常是过期的转述。半岛体育在做体育搜索时最先想清楚的一件事就是:能找到相关文章,和能回答问题,是两种不同的能力。

先把这句问话拆开,看它到底要什么

先别急着谈技术,把这句话逐个词拆一遍。“过去两个月”是一个相对当前日期的时间窗口,起点和终点都要换算成具体日期。“排名前六的球队”是一个会变的集合:球队的名次每轮都在变,所以要问的其实是“比赛当天,对手排在前六的那些场次”,而不是“现在排在前六的球队”。这两种理解会导出完全不同的场次集合。“前锋”是一个位置条件,而位置本身又有口径:登记位置、实际出场位置、首发阵型中的站位,三者未必一致。“射门转化效率”则是一个派生指标,可以是进球数除以射门数,也可以是进球数除以预期进球,还可以按每90分钟折算。

拆完之后会发现,这句话至少牵涉四类数据:比赛日期与对手(关系型的赛程数据)、比赛当天的排名快照(时间序列数据)、球员与位置的对应(实体属性数据)、每场的射门和进球(结构化技术统计)。没有一类是“一段可以被检索到的文字”。这就是为什么我们说,这类问题的入口可以是自然语言,但答案的来源必须是结构化的事实。

普通RAG会在三个地方掉链子

检索增强生成(RAG)的基本做法是:把文档切成段落,转成向量存起来;用户提问时,找出语义最接近的几个段落,塞进提示词,让大模型据此作答。对于“某项规则最近改了什么”“某位教练的执教理念是什么”这类文本型问题,它非常好用,半岛体育在规则解读和背景资料方面同样使用它。但对上面那个问题,它会在三个地方出问题。

第一处:召回的是“讨论过”,不是“算出来”

向量检索按语义相似度排序,所以它擅长找到“谈论前锋效率的文章”。可一篇写于三周前的评论,引用的数字截止在四周前,且只选了作者想强调的几名球员。模型读了之后能写出流畅的回答,但那是在转述一篇文章的局部视角,而不是对完整数据集做聚合。

第二处:条件叠加时,向量没有“交集”这个概念

“两个月内”“对手前六”“前锋”“射门转化”四个条件同时成立,需要的是严格的集合交运算。向量检索只会给出“整体上和问题接近”的段落,没有任何机制保证每个条件都被满足。一个段落可能满足三个条件,漏掉时间窗口,检索器不会报错,只会把它排在前面。

第三处:数字要被计算,而不是被复述

即便召回了几段带数字的文字,让语言模型去做加权平均、按分钟折算,本质上仍是让它心算。这一点我们在大模型会算错平均数怎么办里专门讨论过:语言模型可以理解“转化效率”是什么,但求和、相除和排序应该交给执行引擎,因为引擎会在漏场次、重复计数时给出可以被检查的结果。

GraphRAG:让检索沿着关系走,而不是沿着相似度走

GraphRAG 这个名字被用得很宽泛,在体育场景里,我们把它理解为三个环节的组合,而不是一个单一的产品。

  1. 用图组织实体与关系。球员、球队、教练、比赛、赛季作为节点,“效力于”“出场于”“对阵”“执教”作为带时间区间的边。这一层的价值在于,“对手是谁”“当天排名如何”这样的多跳问题,可以变成沿着边走两三步,而不是在文本里碰运气。
  2. 用图缩小候选范围。先在图里找出“过去两个月内、对手当天排名不超过第六的所有比赛”,再取出这些比赛里出场的前锋。这一步的结果是一个精确的实体集合,而不是一堆相似段落。
  3. 用结构化数据完成计算,用文本检索补充解释。对候选集合读取射门与进球,交给统计引擎;如果用户还想知道“为什么这名前锋最近效率高”,再检索赛后报道和战术分析作为背景说明,并标明它们只是解释材料,不是计算依据。

这里有一个容易被忽略的细节:GraphRAG 里的“图”并不是拿来给用户看的。它在后台做的事,是把一个含糊的问题变成一个边界清晰的候选集合。这个观点我们在知识图谱那篇文章里展开过:真正有价值的部分,往往不需要被画出来。

公开研究方面,篮球比赛技术统计的本体驱动知识图谱结合大模型查询的工作,就是把赛季、阶段、轮次和日期范围显式建模,让语言模型生成的查询能落在明确的时间语义上。半岛体育的思路与这类研究方向相近,但我们不宣称复现了其中任何结果,只是借鉴“先建模时间与关系,再让模型提问”这个顺序。

一次完整的示例查询:从问句到带条件的答案

下面用一个完全虚构的场景把流程走一遍。所有球员、球队和数字都是示例数据,只用来说明系统各步骤在做什么。

用户输入:“过去两个月,面对排名前六的球队时,哪名前锋射门转化率最高?”假设示例截止日期为 2026-03-15。

步骤系统在做什么 用到的数据或工具 可能出错的地方
1. 条件解析 识别时间窗口、位置、对手排名条件、指标语言模型 + 规则模板 把“排名前六”理解成当前排名而非比赛当天排名
2. 澄清口径 确定转化率 = 进球 ÷ 射门;最少射门数门槛 口径配置表 不设门槛时,只射门1次并进球的球员会排第一
3. 图上筛选比赛找出 2026-01-15 至 2026-03-15 内,对手当天排名不超过6的比赛 图数据库 + 排名快照 排名快照缺某一轮,导致比赛被漏掉
4. 筛选球员 取这些比赛中位置为前锋的出场记录 球员位置表 球员中途改踢边路,位置口径不一致
5. 聚合计算 按球员汇总进球、射门,计算转化率并排序SQL 与统计引擎 对百分比取平均而不是先求和再相除
6. 生成回答 用自然语言说明结果,并列出所用场次和口径 语言模型 把样本很小的结果说成“效率最高”

第3步在图里的查询思路,可以用简化的图查询风格写成下面这样(仅为示意):

MATCH (g:Game)-[:AGAINST]->(t:Team)
WHERE g.game_date >= date('2026-01-15')
  AND g.game_date <= date('2026-03-15')
  AND t.rank_at_game <= 6
MATCH (p:Player)-[:APPEARED_IN]->(g)
WHERE p.position_in_game = 'FW'
RETURN p.player_id, g.game_id

得到候选的球员与比赛对之后,第5步交给关系型数据库做聚合:

SELECT s.player_id,
       SUM(s.goals)  AS goals,
       SUM(s.shots)  AS shots,
       ROUND(SUM(s.goals) * 1.0 / NULLIF(SUM(s.shots), 0), 3) AS conversion
FROM player_game_stats s
JOIN candidate_games c
  ON c.game_id = s.game_id AND c.player_id = s.player_id
GROUP BY s.player_id
HAVING SUM(s.shots) >= 10
ORDER BY conversion DESC
LIMIT 5;

请注意 SUM 后再相除这个写法:它是先合计进球、再合计射门,最后一次相除,而不是把每场的转化率求平均。两者在射门次数差异很大时会给出明显不同的排序。HAVING SUM(s.shots) >= 10 是一个口径决定,示例里取10,真实系统里这个门槛应该写进口径说明,并让用户看到。

返回结果大致是这样(示例数据):前锋甲,进球4,射门13,转化率0.308;前锋乙,进球3,射门11,转化率0.273;前锋丙,进球2,射门10,转化率0.200。回答里不应该只写“前锋甲最高”,而应该同时说明:统计的是2026-01-15至2026-03-15,符合条件的示例比赛共若干场,射门不足10次的球员已排除,样本量都很小,差异不宜解读为稳定的能力差别。

建模细节:快照与角色,决定这条链路能不能走通

上面的示例看起来只是几步查询,真正花功夫的地方在数据建模上。这里挑两个最容易被低估的细节。

排名不是球队的属性,而是球队在某一天的状态

很多初版数据表会给球队表加一列 current_rank,方便前端展示。这对页面展示没问题,对分析却是陷阱:一旦把它当成球队的固有属性,“面对前六球队”就只能理解成“面对现在排名前六的球队”。正确的做法是单独维护一张快照表,字段至少包括 team_id、snapshot_date、rank_no、points、games_played,每轮比赛结束后追加一行而不是覆盖。查询时,用比赛日期去找“不晚于该日期的最近一次快照”。这样,问“当时的对手强不强”和问“现在的对手强不强”就成了两个可以分别回答的问题,用户也可以在提问时选择其中一种。

同一名球员在不同比赛里可能扮演不同角色

“前锋”既可能来自球员登记信息,也可能来自这场比赛的首发站位。一名登记为中场的球员,在某几场比赛里被安排在最前面,算不算前锋?不同答案会改变候选集合。半岛体育的设计是把“登记位置”和“本场位置”分成两个字段保存,并在回答里写明采用了哪一个。当用户没有说明时,系统默认采用本场位置,因为它更贴近“这场比赛里谁在打前锋”的直觉,同时在口径说明里提示可以切换。这个选择没有绝对的对错,重要的是让它显式出现,而不是悄悄决定。

这两个细节也说明了图和表分工的边界:图适合表达“比赛—对手—球队”这类关系,快照和角色这类随时间变化的属性值,则更适合放在带时间索引的结构化表里,由图上的节点通过 ID 关联过去。把所有东西都塞进图,或者把所有东西都放进向量库,都是在用一种工具做它不擅长的事。

什么问题留给RAG,什么问题交给图和表

很多团队第一次接触这类系统时,会把所有检索方式混成“搜索”一个词。我们更倾向于让每种方式承担它最擅长的那一件事。

问题类型 示例 更合适的做法
找文本 “越位规则最近有什么调整?”关键词与向量检索,返回原文段落
解释与背景 “这名前锋为什么被称为擅长无球跑动?” RAG,引用报道和分析文章,标明来源
单表统计 “球员A最近10场场均得分?” Text-to-SQL,读结构化数据库
多跳关系 “某教练执教期间效力过、后来又去了另一支球队的球员”图查询
关系加时间加统计 本文开头的问题图缩小范围,SQL 聚合,文本补充解释

这张表里最后一行是最难的,也是最能体现 GraphRAG 思路的一行:它不是让某一种检索“更强”,而是让几种检索按顺序接力。至于 SQL 与图查询之间的分工,我们在Text-to-SQL 与 Text-to-Graph 的比较里有更细的对照。

GraphRAG 自己也会出错,而且方式更隐蔽

把知识图谱接进来以后,系统在关系和时间类问题上的可靠性会提高,但绝不是“不再出错”。我们在设计时反复提醒自己以下几种情况。

图的质量决定检索的上限

如果“效力于”这条边缺了一段转会区间,那么“当天属于哪支球队”这个判断就会出错,后面所有沿着边走的查询都会得到看起来合理却不完整的结果。图里一条错误的边,比文本里一句错误的话更危险,因为它会被当作事实使用。

实体链接错了,后面全错

问句里的“林某”如果被链接到同名的另一位球员,图查询和SQL都会忠实地对错误的对象执行。所以实体识别的结果必须能在回答中被看到,让用户有机会发现“不是这个人”。

社区摘要是文本,不是数据

有些GraphRAG做法会对图中的社区(例如某个战术体系下相互关联的球员群)生成摘要,用于回答概括性问题。摘要是由模型生成的文字,可以用来帮助理解,却不能用来引用数字。凡是要落到具体数字的地方,必须回到结构化数据重新读一遍。

一个典型的失败案例

设想示例队甲在图里被登记为“2026年2月换帅”,但排名快照表只在整月首日更新。系统按“比赛当天排名”筛选时,2月中旬的比赛用的是2月1日的名次。如果那段时间对手名次发生了跨越第六名的变化,这场比赛会被错误地纳入或排除。这类问题没有任何语言模型能靠“更聪明”解决,只能靠数据表的更新频率、快照粒度和校验规则来解决。这也是为什么我们在答案证据链的设计里,要求回答同时给出数据截止时间和快照口径。

当系统不确定时,应该怎么说

面对上面这些风险,半岛体育更看重的是回答的表达方式。如果快照缺了一轮,正确的回答应当是“在已有数据中,符合条件的比赛共若干场,其中某一轮的排名数据缺失,未纳入统计”,而不是悄悄少算一场后给出一个干净的排名。如果实体链接有多个候选,回答开头应先写“这里按照球员A(示例队甲,前锋)理解,如果你指的是另一位,请告诉我”。这种略显啰嗦的写法,换来的是用户可以检查、可以纠正,比一个漂亮但无法核对的结论更有价值。

检索的单位应该是事实,而不是段落

回到开头那个问题。普通RAG把知识切成段落,因此它擅长回答“哪里讨论过这件事”;体育分析要回答的却是“这件事的数字是多少、依据哪些场次”,检索的单位必须是事实:某球员在某场比赛的某项统计,某球队在某日的名次,某教练在某段时间执教某队。GraphRAG 的意义,是让这些事实可以沿着关系和时间被有条件地取出来,再交给能够计算的引擎。

半岛体育在产品设计上遵循一个朴素的分工:语言模型负责理解问题、选择工具、解释结果;图和表负责保存事实;计算引擎负责算数;文本检索负责补充背景。任何一个环节都不宣称能保证零错误,所以每个回答都应该带着自己的条件和来源。如果你想了解半岛体育在这方面更具体的做法,可以从半岛体育AI大模型栏目开始读起。