用户输入“王磊最近10场得分”,数据库里有三个王磊:一个是示例队甲的后卫,一个是示例队乙的前锋,还有一个已经退役、只留下历史记录。如果系统用 WHERE name = '王磊' 直接查,结果不是报错,而是把三个人的数据混在一起求了个平均,得到一个谁也不是的数字。这种错误最隐蔽的地方在于,SQL 没有任何问题,计算也没有任何问题,错的是“查谁”。半岛体育AI把这一步单独拿出来,放在所有查询之前,叫做实体识别,或者更准确地说,实体链接:把用户话里的一个名字,对应到数据库里唯一的一个球员编号。

先看名字为什么不能当主键

把名字当主键是最常见的起点,也是最常见的坑。至少有四种情形会让它失效,下面每一种都用虚构的例子说明。

情形 用户可能怎么写 问题所在
同姓同名 “王磊” 库里有多名球员叫这个名字,字符串完全相同
缩写与简称 “小王”“磊哥”“W. Lei” 不是数据库里的标准名,字符串匹配直接落空
中文译名不统一 同一名外籍球员的三种译法 不同媒体、不同年份译法不同,库里可能只存了一种
转会与更名 “示例队丙的王磊” 球员已转会,用户记忆里的球队与数据库当前记录不一致

这四种情形里,前两种是“找不准”,第三种是“找不到”,第四种则是“找到的和用户想的不是同一个人”。它们需要不同的处理方式,所以实体识别不是一个字符串函数,而是一套分步骤的流程。

别名表:把“他们叫过什么”单独存起来

第一步是把球员的标准信息与各种称呼分开存放。球员主表只保存稳定的东西,别名表负责保存所有已知的写法。一个简化的结构可以是这样:

CREATE TABLE player (
  player_id     VARCHAR(16) PRIMARY KEY,
  canonical_name VARCHAR(64),
  birth_date    DATE,
  position      VARCHAR(8),
  is_active     SMALLINT
);

CREATE TABLE player_alias (
  alias_id   INTEGER PRIMARY KEY,
  player_id  VARCHAR(16),
  alias_text VARCHAR(64),
  alias_type VARCHAR(16),  -- full_name, short_name, translation, romanized, nickname
  source     VARCHAR(32),
  valid_from DATE,
  valid_to   DATE
);

几个设计细节。alias_type 区分别名类型,是因为不同类型的可信度不同:全名与标准译名可信度高,昵称可信度低,而且昵称常常是多人共用的。source 记录别名的来源,方便后续人工核对。valid_from 与 valid_to 是为了处理更名、译名变更,同一个别名在不同时期可能指向不同的人,这一点后面还会提到。

有了别名表,“小王”不再靠猜,而是通过一次查询得到候选集合:

SELECT p.player_id, p.canonical_name, a.alias_type
FROM player_alias a
JOIN player p ON p.player_id = a.player_id
WHERE a.alias_text = '磊哥';

返回的可能是零个、一个或多个候选。零个说明别名缺失;一个看起来是理想情况,但仍需确认;多个则进入下一步:给候选打分。

候选打分:让上下文来投票

当候选不止一个时,系统需要根据问题里的其他信息给每个候选打分。这里的“分”不是玄学,而是把几个可检查的线索加权合并。下面是一个示意性的清单,权重需要根据实际数据校准,文章里不给具体数值:

  • 名称匹配类型:与标准全名完全一致,高于标准译名,高于简称,高于昵称;
  • 球队线索:问题里出现了球队,且候选在问题所指时间点属于该球队,加分;不属于则明显扣分;
  • 位置线索:问题里提到“后卫”“前锋”“门将”,与候选的位置一致,加分;
  • 时间线索:问的是“最近”,则现役球员优先;问的是“2019年”,则那一年有记录的球员优先;
  • 活跃度先验:在没有其他线索时,近期比赛更多、出场时间更长的球员,被搜索到的概率通常更高,可作为弱先验,但不能压过明确的线索;
  • 对话上下文:用户在上一轮已经确认过的对象,本轮沿用。

以开头那个例子为例,用户输入“示例队乙的王磊最近10场得分”。三个候选里,示例队乙的前锋在球队线索上得分最高,退役的那位因为时间线索被排除,后卫因球队不匹配被扣分。最终系统选出前锋,编号 P0044(虚构),并在答案里写出“已按示例队乙前锋王磊查询”,让用户可以看到这一步的判断结果。

分数不够时,是猜还是问

打分之后还有一个决策:最高分够不够高,与第二名差距够不够大。这里最忌讳的是“只要有排名第一就取第一”。合理的做法是设置两条门槛:绝对分数要达到一个下限,与第二名的差距要达到一个间隔。两条都过,才自动选择;有一条没过,就让用户确认。

打分结果 系统行为 界面上的表现(示例)
唯一候选,分数高直接查询,并写明识别结果 “已识别为示例队乙前锋王磊”
多个候选,第一名明显领先 采用第一名,同时提供“不是他?”的切换 提示可切换到其他同名球员
多个候选,分数接近 先反问,不查询 “找到三名王磊,请选择所属球队或位置”
无候选提示未找到,给出相近名字建议 “未找到该名字,是否想查询……”

第三行是最重要的一行:分数接近就问,比猜要好。一次反问的成本是用户多点一下,一次错认的成本是给出一个完全无关的数字,而且用户可能没有察觉。这类“需要补充条件”的交互思路,我们在半岛体育App为什么有时会要求你补充条件里有更多描述。

转会之后,同一个人为什么会变成“另一个人”

实体识别在时间维度上还有一层麻烦:球员会转会,所以“他属于哪支球队”这个线索本身是随时间变化的。用户说“示例队丙的王磊”,可能指现在效力于示例队丙的人,也可能指两年前效力于该队的人。如果球员主表里只保存当前所属球队,那么历史问题里的球队线索就会失灵。

解决办法是把球员与球队的关系单独建成一张带起止日期的表,例如 player_team_stint,包含 player_id、team_id、start_date、end_date。打分时,球队线索的判定不是“候选当前是否在该队”,而是“候选在问题所指时间点是否在该队”。这也是为什么实体链接和时间理解无法完全拆开:谁是“示例队丙的王磊”,取决于你问的是哪一天。更完整的时间关系存储思路见球员转会以后旧数据怎么办。

同理,别名表里的 valid_from 与 valid_to 也是为时间准备的。比如某名球员在某一年改用了新的中文译名,旧译名在旧文章里仍然会出现,用户在旧资料里学到的写法,不应该因为数据库更新而查不到。

走查一次:三个候选是怎样被筛掉的

为了让打分不显得抽象,下面用虚构数据把开头的例子完整走一遍。用户问的是“示例队乙的王磊,去年这个时候的场均得分”。系统先得到三名候选,再逐条核对线索,这里只用“符合、不符合、无法判断”三种记号,不涉及具体权重。

线索候选1:后卫(示例队甲) 候选2:前锋(示例队乙)候选3:已退役
名称类型 标准全名,符合标准全名,符合标准全名,符合
球队线索(去年此时在示例队乙)不符合 符合无法判断,去年已无记录
时间线索(去年有出场记录) 符合 符合 不符合
位置线索 问句未提及 问句未提及 问句未提及
结论 排除 选中排除

这个例子里,三个候选在名字上没有任何区别,靠的是球队与时间两条线索。可以看到,如果系统没有球员与球队的起止日期表,“去年这个时候在示例队乙”这一条根本无法核对,只能退回到猜。实体识别的准确程度,很大程度上取决于数据里是否保存了足够多能用来区分的属性,而不是名字匹配算法本身有多聪明。

另外,这个例子还提示了一件事:问句里没有出现的线索,系统可以主动补充展示。在半岛体育AI给出的答案里,识别结果会以“示例队乙前锋王磊,编号 P0044”的形式出现,用户一眼就能确认是不是自己想问的那个人。这与我们在搜索侧对复杂问句的拆解方式相互呼应,可以参考复杂体育搜索的拆解流程。

几个容易被忽略的失败模式

在实际设计中,还有几种失败模式值得提前想好对策:

  1. 别名污染:自动从文本里挖别名,容易把“队友的名字”或“对手的名字”也收进来,需要对新增别名做审核或置信度过滤;
  2. 过度归并:把两个不同的人合并成同一个编号,比拆成两个更难发现,一旦合并错误,历史数据就被混在一起,因此合并操作应保留记录并可回滚;
  3. 拼音与谐音:语音输入或拼音输入时,同音字很多,匹配应容忍拼音层面的相似,但要降低其得分;
  4. 球队与球员重名:某些队名里含有人名,或者球员名与城市名相同,需要用实体类型先做一次分类,避免把球队当球员;
  5. 新球员未入库:库里没有的球员,应当明确说“数据库中暂无该球员”,而不是匹配到一个发音接近的人。

这些问题没有一劳永逸的解,只有持续的核对和回归测试。实体链接做得好不好,不能只看能识别多少个名字,更要看识别错误时是否被发现、被记录、被修正。

先认对人,后面的数字才有意义

回到那个三个王磊的问题。体育数据系统里,“认对人”常常被当成前置的琐事,一旦出错,后面所有精确的计算都只是把错误算得更精确。半岛体育的做法是把实体链接当成一个可以被审视的独立步骤:候选是谁、为什么选这个、还有谁没选,都应该能在答案的证据里看到,这和我们在自然语言体育查询里强调的“先解析、再查询、后解释”是一致的。

如果只能给使用者一句建议,那就是:提问时尽量带上球队、位置或时间中的一项。这不是对用户的苛求,而是因为名字本身携带的信息,确实不够指向一个人。系统能做的,是在信息不足时诚实地问,而不是替用户悄悄做出选择。