“示例队甲的前锋林某(虚构示例),最近5场主场比赛场均射门几次?顺便画个图。”这句话只有三十来个字,却同时包含一个球员、一支球队、一个统计指标、一个时间窗口、一个主客场条件和一个可视化要求。人读起来毫不费力,机器要把它变成一条可以执行、可以检查的查询,中间隔着至少九道工序。半岛体育官网把这九道工序逐一公开,不是为了展示流程图好看,而是因为每一步都可能出错,而出错的位置不同,补救的办法也不同。本文以这一句问题为线索,从头走一遍,每一步都给出示例输入和示例输出。文中所有球员、球队和数字均为模拟场景,不对应任何真实比赛。

先看整条链路:九步各自负责什么

在展开之前,先把全程放进一张表里。表中“谁来做”一栏最值得留意:并不是每一步都由大模型完成。语言理解类的步骤交给模型,计算类的步骤交给数据库和统计引擎,校验类的步骤交给规则代码。三类角色混在一起,是体育问答系统最常见的设计缺陷。

步骤 名称 输入 输出 谁来做
1Intent Parsing原始中文问题 意图:统计查询+可视化语言模型
2 Entity Linking “林某”“示例队甲” player_id、team_id 模型+别名表+打分规则
3 Time Range “最近5场主场” 窗口定义:排序键、条数、过滤条件规则+模型
4 Query Planning 前三步的结构化结果 选用SQL还是图查询,以及查询草稿模型生成,规则审核
5执行查询查询草稿结果行 数据库
6 Data Validation 结果行+计划 通过、警告或打回 规则代码
7 Chart Selection 结果行的形状 图表类型与坐标设置规则+模型
8 Natural Language Answer 校验后的结果与口径 一段带口径说明的回答语言模型
9 Trace 留存 全部中间产物 可回看的查询记录系统日志

第九步在很多流程图里被省略,但它决定了用户之后能不能追问“这个数是怎么来的”。关于答案溯源的字段设计,可以参考站内的体育答案证据链一文,这里只强调一点:证据链不是最后补上的,而是前八步的每一步各自留下一小块。

第一步:意图解析,先分清用户到底要什么

意图解析(Intent Parsing)的任务不是回答问题,而是给问题贴标签。同一句话里可能叠着几个意图:主意图是“查统计值”,附带意图是“要一张图”。如果系统只识别出主意图,图就会丢;如果把“画个图”误判成主意图,系统可能只返回一张没有数字标注的图,用户连均值是多少都看不到。

示例输入输出如下:

输入:示例队甲的前锋林某最近5场主场比赛场均射门几次?顺便画个图。
输出(结构化意图,示例):
  primary_intent   = stat_lookup
  secondary_intent = visualize
  metric_phrase    = 场均射门
  aggregation_hint = average
  raw_entities     = 林某 / 示例队甲
  raw_time_phrase  = 最近5场主场

注意这一步的输出里还没有任何id,也没有任何日期。“场均射门”只是一个待落地的短语,它对应 shots 还是 shots_on_target,要等指标口径表才能确定。意图解析阶段把“不知道”原样保留,比硬猜一个字段名安全得多。半岛体育AI还会在这里标出歧义信号:口径表里同时有“总射门”和“射正”时,歧义被记录下来,留给规划阶段决定采用默认口径并说明,还是反问用户。

第二步:实体链接,“林某”是谁

实体链接(Entity Linking)是把名字变成主键的过程。文本里的“林某”只是一串字符,数据库里认的是 player_id。链接不能只靠字符串完全匹配:名字可能有别名,可能有缩写,也可能同名。更细的做法可以看同名球员的实体识别一文,这里只演示这个示例里的处理。

假设别名表里有两个候选:

候选player_id当前所属 位置 与问句的匹配情况
林某(前锋) P_0421 示例队甲 前锋球队、位置均命中
林某(后卫) P_0877 示例队丙后卫 球队、位置均不符

问句里出现了“示例队甲”和“前锋”两个限定词,第一个候选得分明显更高,于是系统选定 P_0421。这里有一条重要的设计原则:打分结果必须写进查询记录,而不是悄悄使用。如果两个候选得分接近,比如问句里只有“林某”两个字,系统就不应该替用户选,而应该给出两个选项让用户点。选错人之后查询语句写得再漂亮,答案也是错的,而且错得很难被发现,因为返回的数字看起来完全正常。

第三步:时间范围,“最近5场主场”不是一个日期区间

很多人以为时间解析就是把“最近”翻译成“过去30天”。体育查询里的“最近5场”是按场次计数的窗口,与日历时间无关。要把它落地,至少要回答四个问题:

  1. 按什么排序?通常是 game_date 降序,同一天有多场时再按 game_id 或开赛时间兜底。
  2. 先过滤还是先截取?“最近5场主场”的意思是先筛出主场比赛,再取最近5场,而不是先取最近5场再看其中几场是主场。两种顺序的结果可能完全不同。
  3. 缺席的比赛算不算?如果林某有一场因伤没有出场,那一场应该计入“比赛场次”还是跳过?
  4. 取消或延期的比赛怎么处理?

这四点的默认处理规则,半岛体育的做法是写进一张窗口定义表,而不是写进提示词里。表里的规则可被检查、可被版本化,模型只负责把问句映射到规则编号。关于窗口口径的详细讨论见“最近10场到底是哪10场”一文。本例中采用的窗口定义是:只计林某实际出场的主场比赛,取最近5场,取消或延期的比赛不计入。输出如下:

window_spec(示例)
  order_by      = game_date DESC, game_id DESC
  filter_first  = venue = 'home'
  take          = 5
  appearance    = played_only   (minutes > 0)
  excluded      = cancelled, postponed
  timestamp_asof = 数据库最近一次更新时间

最后一行常被忽略:数据截止时间。如果昨晚的比赛还没有入库,“最近5场”里就少一场,用户需要知道答案是基于哪一天的数据算出来的。

第四步:查询规划,决定走SQL还是走图

前三步得到的都是结构化的条件,第四步才真正决定“用什么工具去查”。这一问句的核心是筛选、排序、取前N、求平均,典型的关系型统计,走SQL最合适。如果换一句“示例教练乙执教期间,先后效力过示例队甲和示例队丁的球员有哪些”,那就是多跳关系,应改走图查询,具体的区别见Text-to-SQL与Text-to-Graph一文。

规划器(Query Planner)输出的SQL草稿大致是这样的:

SELECT g.game_id, g.game_date, s.shots
FROM player_game_stats s
JOIN games g ON g.game_id = s.game_id
WHERE s.player_id = 'P_0421'
  AND g.home_team_id = 'T_0007'
  AND s.team_id = 'T_0007'
  AND s.minutes > 0
  AND g.status = 'finished'
ORDER BY g.game_date DESC, g.game_id DESC
LIMIT 5;

草稿只取明细行,没有直接在SQL里写 AVG。原因是需要保留每一场的数字,第七步画图要用,第六步的校验也要用。平均值可以由统计模块对这五行再算一次,两者交叉比对,比只信一个数字稳妥。

规划阶段还有一道容易被忽略的审核:SQL 草稿生成之后,系统要用规则检查它是否真的实现了第三步的窗口定义。比如检查 WHERE 子句里有没有 minutes 条件、ORDER BY 是否是降序、LIMIT 是否等于5。模型可能写出语法完全正确、语义却悄悄偏离的查询。比如漏掉主场条件,或者把 game_date 升序排列,此时数据库照样返回五行数字,不会报任何错。这类“静默错误”是自然语言查数据最危险的一类。

第五、六步:执行之后先别急着回答,先验数

假设数据库返回下面五行(示例数据):

game_date game_id shotsminutes
2025-11-09G_1183 590
2025-10-26G_11612 73
2025-10-12 G_1140 4 90
2025-09-28 G_1122 3 81
2025-09-14 G_1105 3 90

合计17次,均值3.4次。数据校验(Data Validation)要做的检查不神秘,但每一项都有价值:

  • 行数检查:应当是5行。如果只有3行,说明窗口内符合条件的比赛不足,回答里要写明“仅有3场可用”,而不是照样输出“最近5场场均”。
  • 日期检查:五个日期应当严格降序,且没有重复的 game_id。
  • 新鲜度检查:最近一场的日期与数据库最新入库日期比较,如果之间隔了一场已经踢完但没有入库的比赛,必须提示。
  • 交叉计算:统计模块独立算一遍均值,与任何在SQL里用 AVG 得到的值比较,两者必须一致。

校验有三种结局:通过、带警告通过、打回。带警告通过的例子是某场出场时间很短,仍然计入,但回答里说明一句。打回的例子是行数为0或日期乱序,此时查询规划要重来,或者转为向用户提问,而不是编一个答案。

第七步:选图,不是每个数字都需要图

用户说“画个图”,系统仍然要判断画什么图。五场比赛的射门次数,是一个很小的序列,适合的表达是柱状图或带标记点的折线图,横轴是比赛日期,纵轴是射门次数,并叠加一条均值参考线。有几件事不该做:纵轴不应从非零值截断,否则2次和5次的差距会被视觉放大;五个点不应做曲线平滑,平滑会制造并不存在的趋势;也不应把出场时间和射门次数套成双轴画在一起。

图表选择的原则在折线图什么时候会误导用户里有更细的讨论。规划器的输出大致如下:

chart_spec(示例)
  type        = bar
  x           = game_date(按时间正序显示)
  y           = shots,起点固定为0
  overlay     = 均值参考线 3.4
  annotation  = 数据点旁标注具体数值
  note        = 样本量为5,图中不做趋势拟合

注意“按时间正序显示”这一句:查询是按日期降序取的,但图上应当从左到右按时间由早到晚,否则读者会把趋势读反。这个反转发生在展示层,不应该回头去改查询。

第八步:自然语言回答,口径跟着数字一起走

只有走到这里,大模型才第二次登场。它拿到的输入不是“请回答用户的问题”,而是一份已经算好、已经校验的结果包:五场的明细、均值3.4、窗口定义、数据截止日期、校验警告。它的任务是把这些内容组织成一段人话,而不是重新计算或补充记忆里的信息。示例回答如下:

示例队甲前锋林某(虚构示例)最近5场主场出场比赛,场均射门3.4次。
共17次射门,其中一场出场73分钟。
统计口径:仅计实际出场的主场比赛,不含取消或延期场次;
数据更新至2025-11-10。

三行回答里,第一行是结论,第二行是补充说明,第三行是口径。口径行不是装饰,它让用户可以判断这个答案是否回答了自己真正想问的问题。如果用户想问的其实是“连同客场的最近5场”,看到口径行会立刻明白需要换问法。半岛体育官网在产品设计中,把这类口径行作为默认输出的一部分,而不是可选项。

生成回答之后还有一道“数字回查”:程序自动检查回答文本里出现的所有数字,是否都能在结果包里找到。如果模型在回答里写了“3.5次”或“18次射门”,回查会把它拦住。这一步很粗糙,但能防住模型改写句子时“顺手”改掉数字。

流程中最容易出错的三个位置

把九步走完,回头看故障的分布,错误并不平均,风险集中在三个位置。

实体选错,但结果“看起来正常”

查询语句完全正确,数据也真实存在,只是查的不是用户想问的那个人。置信度不够时不自动选择,回答里写出所选实体的全名、球队和位置,让用户有机会纠正。

窗口顺序写反

“先筛主场再取5场”和“先取5场再筛主场”结果可能天差地别,数据库不会报错,只能靠规划之后的规则审核逐项对照。

口径不一致

“射门”是否含被封堵的射门,不同数据源的定义不同。系统内部只用一套口径并写进回答。相关差异可以进一步阅读App数据与其他网站不一样怎么办。

三类错误的共同点是不产生异常,也不产生明显不合理的数字,所以校验层要盯的是“查询是否忠实实现了前面步骤的结论”,而不只是“有没有报错”。

可以被打回的流程,才值得信任

回到开头那句问题。用户看到的只是一句话进去,一段带图的回答出来,中间的九步都发生在看不见的地方。半岛体育官网想强调的判断是:一个问答系统是否可靠,不看它平时回答得多流畅,而看它在哪些地方会停下来。实体不确定时停下来问用户,行数不足时停下来改口径说明,校验不通过时停下来重新规划,而不是硬着头皮给一个漂亮的数字。

这也是为什么这套流程里,大模型只承担理解和表达两端,中间的查询、计算、校验交给可以被检查的组件。语言模型很擅长把话说得像那么回事,正因为如此,它最不该被放在“最后一道关口”的位置上。让每一步都留下可回看的中间产物,答案出错时才知道是哪一步错了,也才有办法修。半岛体育AI的设计目标,就是让“为什么是这个数”成为一个用户随时可以问、系统随时可以答的问题。