用户输入“王磊最近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”的形式出现,用户一眼就能确认是不是自己想问的那个人。这与我们在搜索侧对复杂问句的拆解方式相互呼应,可以参考复杂体育搜索的拆解流程。
几个容易被忽略的失败模式
在实际设计中,还有几种失败模式值得提前想好对策:
- 别名污染:自动从文本里挖别名,容易把“队友的名字”或“对手的名字”也收进来,需要对新增别名做审核或置信度过滤;
- 过度归并:把两个不同的人合并成同一个编号,比拆成两个更难发现,一旦合并错误,历史数据就被混在一起,因此合并操作应保留记录并可回滚;
- 拼音与谐音:语音输入或拼音输入时,同音字很多,匹配应容忍拼音层面的相似,但要降低其得分;
- 球队与球员重名:某些队名里含有人名,或者球员名与城市名相同,需要用实体类型先做一次分类,避免把球队当球员;
- 新球员未入库:库里没有的球员,应当明确说“数据库中暂无该球员”,而不是匹配到一个发音接近的人。
这些问题没有一劳永逸的解,只有持续的核对和回归测试。实体链接做得好不好,不能只看能识别多少个名字,更要看识别错误时是否被发现、被记录、被修正。
先认对人,后面的数字才有意义
回到那个三个王磊的问题。体育数据系统里,“认对人”常常被当成前置的琐事,一旦出错,后面所有精确的计算都只是把错误算得更精确。半岛体育的做法是把实体链接当成一个可以被审视的独立步骤:候选是谁、为什么选这个、还有谁没选,都应该能在答案的证据里看到,这和我们在自然语言体育查询里强调的“先解析、再查询、后解释”是一致的。
如果只能给使用者一句建议,那就是:提问时尽量带上球队、位置或时间中的一项。这不是对用户的苛求,而是因为名字本身携带的信息,确实不够指向一个人。系统能做的,是在信息不足时诚实地问,而不是替用户悄悄做出选择。