知识图谱这几年被聊得很多,但大部分教程都停在概念层面:什么是实体、什么是关系、RDF和属性图有什么区别,看完之后还是不知道一个能跑起来的系统长什么样。我花了几周时间做了完整的《水浒传》人物关系知识图谱问答可视化系统,从原著文本一路做到图数据库、自然语言问答、关系图展示,全链路打通。这篇文章把选型、架构、实现细节和踩过的坑都拆开讲一遍。
这个系统能干的事,一句话说清楚:输入“武松和鲁智深是什么关系”,它告诉你两人是结拜兄弟,还把关系路径在图上高亮出来;输入“宋江的绰号是什么”,它返回“及时雨”;输入“梁山排名前十的好汉有哪些”,它按座次排序给出一串名单。源码可跑,数据层、问答层、可视化层都留了扩展接口,换一本书、换一套自定义数据集也能用同一套框架搭起来。
适合谁来参考:想正经入门知识图谱、想看明白图数据库在真实业务场景里怎么落地的开发者,以及正在找毕设方向或技术演示素材的同学。下文按我做这个项目的实际推进顺序来写:先讲为什么选《水浒传》和系统怎么分层,再讲图谱构建、问答实现、可视化,然后是二次扩展设计,最后是一堆排查问题的实录。
1. 项目整体设计思路:为什么是水浒传,系统怎么拆
1.1 数据源选型:人物规模、关系密度与认知度的平衡
做知识图谱的第一个问题永远是:拿什么数据做?公开数据集、百科爬虫、自己标注,都试过一圈之后我发现,都不如找一本结构清晰的小说来得踏实。《水浒传》是我对比过好几部名著后确定的选择,理由有三个层面。
第一是人物规模适中。全书有名有姓的人物三百多个,梁山一百单八将本身就是一份现成的高质量实体清单。这个规模比《三国演义》全书上千的人物更可控,比《西游记》核心几十人的体量更有图谱的感觉。节点太少看不出关系网络的层次,节点太多又会把精力耗在数据清洗上,而不是系统本身的建设上。
第二是关系类型足够多样。结拜兄弟、师徒传承、主仆情义、同乡旧识、上下级统属、仇敌对抗,基本覆盖了知识图谱里常见的几大类关系:鲁智深和林冲是结拜兄弟,卢俊义和燕青既是主仆又有师徒之实,武松和西门庆是敌对,宋江和众好汉是统领关系。关系类型丰富,问答系统和可视化展示才有足够的素材。
第三是认知度高、校验成本低。水浒人物的绰号、排名、出身在中文互联网上有大量现成资料,后面我核查某条关系有没有抽错的时候,不需要翻原著逐字比对。这一点看着无关紧要,实际做起来影响巨大——你总不想在一个关系真假的判断上耗掉半天。
这里面还有一个很多人忽略的务实考量:古籍文本属于公共领域,获取成本为零,版权上没有任何顾虑。真到了动手阶段,你会发现一个免费、量大、结构清晰的数据源有多珍贵。
1.2 系统分层:数据、服务、展示各管一摊
整个系统拆成三层,每层职责单一,层与层之间用明确接口隔开。这个决定让我在后续迭代里少走了很多弯路。
数据层负责把原始文本变成图谱里的节点和边,流程是:清洗文本、按章回分句、实体识别(人名和绰号)、关系抽取(规则模板为主)、写入图数据库。这层全程用Python处理,因为中文NLP的现成工具基本都在Python生态里。
服务层是整个系统的中枢,提供HTTP接口,内部就做一件事:接收自然语言问句,解析出意图和实体,生成图查询语句,到图数据库执行,再把原始结果格式化成可读的答案。我选了一个轻量Python后端框架来搭,原因很简单:自带数据校验和交互式接口文档,前后端联调时能省掉大量口头沟通成本。
展示层是用户直接看到的部分:一个关系图总览页面、一个问答输入框,点击节点可以展开邻居,问答命中时高亮对应的节点和边。可视化用关系图组件,前端框架用Vue3。
为什么坚持三层分离?因为我在动手前就预判到这个项目会频繁调整。比如关系抽取一开始用纯规则,后来想叠加统计策略;存储一开始打算全内存,后来才切到图数据库。如果代码耦合成一坨,改一处就崩三处。分层之后,数据层的改动只影响图谱内容,服务层和展示层完全无感。
设计取舍:问答模块很多人一上来就想接大模型做开放域问答。我的建议是第一步别这么干。用规则模板加图查询,你能清清楚楚看到知识图谱本身的价值——答案是从图谱里查出来的,不是模型现场编的。等图谱链路完全跑通,再考虑用大模型增强问句理解,那是锦上添花的事。
2. 图谱构建的核心环节:从文本到可查询的图数据
2.1 Schema设计:先定数据字典,再写任何代码
动手写代码前,必须先把图谱的Schema定下来。Schema就是图谱的数据字典:有哪些节点类型、节点有哪些属性、有哪些关系类型、关系能不能带属性。这个设计定得越清楚,后面写数据导入脚本越省事。
《水浒传》这个数据集我用了极简设计。节点类型只保留三类:人物、地点、组织。人物是最核心的节点,属性包括姓名、绰号、座次排名、兵器、家乡、阵营(天罡还是地煞);地点属性的核心就是名称和类型,比如梁山是山寨、东京是城池;组织主要是梁山泊、朝廷这类集团。
关系类型列了六类,这六类基本覆盖了全书的核心人物关系:
- 结拜:鲁智深与林冲、武松与宋江
- 师徒:卢俊义点拨过燕青的武艺
- 主仆:燕青之于卢俊义
- 同乡:郓城出身的宋江、朱仝、雷横
- 上下级:宋江统领梁山众好汉
- 敌对:武松与西门庆、林冲与高俅
这里有个很多新手会踩的坑:关系类型不是越多越好。每多一种关系,问答模板、可视化边样式、数据校验规则都要跟着加,工作量是成倍涨的。宁可先用六类覆盖九成核心关系,跑通整条链路之后再扩充。以后想细分“师兄弟”“翁侄”之类的亲属关系,在关系类型表里加一行就行,代码层几乎不需要动。
属性索引也要在一开始就设计好。人物姓名必须建唯一约束和索引,因为问答系统每一次查询几乎都从名字出发;绰号也经常出现在问句里,比如“及时雨的排名是多少”,所以绰号也要建索引。兵器、家乡这类低频属性,数据量小的时候不建索引完全没影响。
2.2 实体识别与关系抽取:半自动加人工校验的组合
从原始文本到结构化关系,是整条链路里最脏最累的步骤。我一开始天真地以为全自动抽取加现成的中文分词就能搞定,做了一版发现准确率根本没法直接用,最后退成“规则抽取为主、人工校验兜底”的组合,效率和准确率反而都上来了。
实体识别反而比预想中简单,因为一百单八将的名单是现成的。我把所有人的姓名、字、绰号整理成一个自定义词典,喂给中文分词工具。为什么必须这样做?因为默认词典根本不认识“鲁智深”这种专名,会把“鲁”和“智深”切开。自定义词典里的词会获得高优先级,分词时优先作为整体识别:
import jieba jieba.load_userdict('custom_dict.txt') # custom_dict.txt 内容示例: # 宋江 100 nr # 宋公明 100 nz # 及时雨 100 nz # 鲁智深 100 nr # 花和尚 100 nz词典里每一项都带词性和权重,nr表示人名、nz表示专名。这样处理之后,“鲁智深”才能作为一个完整实体被识别出来。
关系抽取我用的策略是“触发词加句法模式”。先人工整理一批高频关系触发表达,比如“与……结拜”“拜……为师”“是……的部下”“称呼哥哥”,句子同时匹配到人物实体和触发词,就候选生成一条关系。古籍文本的表达高度套路化,“结拜”“拜为兄弟”这类词反复出现,规则的实际覆盖效果很不错。
但纯规则必然带来两类问题:误抽和漏抽。误抽的典型场景是原文里一句“宋江道:贤弟,你且随我来”,这里的“贤弟”只是称呼,不构成结拜关系,规则却会误报;漏抽则是因为很多关系是叙事性表达的,比如“史进自此常在鲁提辖处往来”,没有明显的触发词,规则捕捉不到。
处理方式是:把规则抽出的所有候选关系输出成一张表,然后做一遍集中人工校验,只保留能在原文上下文印证、或能在百科资料里对上的关系。这一遍校验花不了几个小时,但对整个项目的质量提升是决定性的。做演示或交作业的时候,尤其值得做——你永远不希望被问“这个关系是不是搞错了”时只能挠头。
提示:别迷信全自动抽取。业界有更强的大模型和工具,但放到具体古籍语料上依然需要人工把关。你的目标不是发论文,而是做一个能演示、能问答、能扩展的系统,一次人工校验换来的下游置信度非常划算。
2.3 存储选型:为什么把节点和边放进图数据库
图谱数据存在哪,主流有三种选择:内存里的Python对象、关系型数据库、图数据库。我最终选了图数据库,但三种方案都实际试过,说下真实感受。
内存方案最快,加载一份人物边列表,用现成的图分析库就能做原型验证。但它有个致命短板:问答系统要支持动态查询,比如“找出宋江到武松的所有路径”,内存方案你得自己写图的遍历和最短路径算法,深度一超过两层,代码瞬间复杂起来。图数据库把这些能力做成内置算子,这是它最大的价值,不在存数据,而在查数据。
关系型数据库也能用节点表加边表建模,但多跳查询要反复自连接,SQL写得又长又绕,性能还差。查“宋江和武松之间有没有三层以内的路径”,在SQL里表达路径遍历极其痛苦。图数据库把关系作为一等公民,查询语法天然就是“沿着关系走多远、经过哪些节点”的描述。
我用的开源图数据库支持Cypher查询语言,两个例子就能看出差距。属性查询“宋江的绰号是什么”:
MATCH (p:人物 {name: '宋江'}) RETURN p.name, p.nickname关系路径查询“宋江和武松怎么认识的”:
MATCH p = shortestPath((a:人物 {name: '宋江'})-[:关系*1..5]-(b:人物 {name: '武松'})) RETURN p路径查询如果用Python手写BFS不是不能做,但图数据库一行搞定,还内置了最短路径算法。这就是选它的理由。
数据库初始化阶段有一步不能省:建约束和索引,尤其是人物姓名和绰号两条:
CREATE CONSTRAINT person_name IF NOT EXISTS FOR (p:人物) REQUIRE p.name IS UNIQUE; CREATE INDEX person_nickname IF NOT EXISTS FOR (p:人物) ON (p.nickname);建唯一约束能防止重复导入时同一个人物被拆成两个节点,这是图数据里特别常见的脏数据问题。建索引则是为了查询性能——几万条数据时感觉不到差异,但问答系统的每条查询都从名字定位节点开始,没有索引,后面会有明显延迟。
3. 问答系统实现:把自然语言翻译成图查询
3.1 问句分类:先想清楚用户可能问什么
问答模块是整个系统的灵魂,我设计它时做的第一件事,不是写代码,而是列一份“用户会问什么”的清单,再把清单归纳成类型。归类越清晰,后面的匹配模板越好写。最终归纳成五类:
- 属性查询:宋江的绰号是什么?武松的兵器是啥?鲁智深排第几?
- 关系查询:林冲和鲁智深是什么关系?卢俊义和燕青是什么关系?
- 路径查询:宋江和武松是怎么认识的?他们之间隔了几个人?
- 筛选统计:梁山三十六天罡里排名前五的是谁?郓城籍的好汉有哪些?
- 比较查询:武松和鲁智深谁的排名更高?宋江和吴用谁的座次靠前?
这里的核心设计思路是:没有一开始就上通用自然语言理解,而是先定义清楚这五类问题,再对每一类写匹配规则。知识图谱问答的句式其实是高度模板化的,你去看真实用户的提问,翻来覆去就是“X是什么”“X和Y是什么关系”“有哪些X”。规则模板能覆盖绝大多数问法,而且每一条规则都看得见、可调试。遇到覆盖不了的问法,再针对性加模板,这是工程上最务实的路径。
3.2 意图识别与槽位抽取:两步缺一不可
一句问话进来,系统要做两件事。第一步判断问句类型,这叫做意图识别;第二步抽出问句里的实体和属性名,叫做槽位抽取。两步做完,才有拼图查询语句的基础。
意图识别我用关键词加正则的组合。出现“什么关系”“怎么认识”判定为关系或路径查询;出现“绰号”“排名”“兵器”“家乡”,判定为属性查询,同时记下要查的属性名;出现“哪几个”“有哪些”“排前”,判定为筛选统计;出现“谁更”“比较高”,判定为比较查询。
槽位抽取用人物词典做匹配,这里有个细节特别关键:词典里不能只放姓名,要把绰号、字、江湖外号全部映射到同一个标准实体ID上。“及时雨”“宋公明”“呼保义”都必须指向“宋江”。我维护了一张别名映射表:
alias_map = { '宋江': ['宋江', '宋公明', '及时雨', '呼保义'], '武松': ['武松', '行者', '武二郎'], '鲁智深': ['鲁智深', '鲁达', '花和尚', '提辖'], }抽取时先做最长匹配,避免“宋江”被嵌在“宋江哥哥”里只匹配出“宋”字。然后是并列关系的切分,比如“武松和西门庆是什么关系”,句子中出现两个实体,我用“和”“与”“同”这类并列连接词作为分隔符,把两边的实体分别放进槽位,再去图数据库里查询两者之间的关系。
这里有个常被忽视的坑:停用词表。常规中文停用词表会把“和”“同”“与”全部过滤掉,但在关系问句里,“和”恰恰是定位两个实体的关键分隔符。所以关系问句的预处理必须单独做,裁剪掉这部分的停用词,否则“武松和西门庆是什么关系”会被处理成“武松西门庆是什么关系”,槽位抽取直接崩掉。
3.3 模板映射与答案生成:把Cypher变成人话
意图和槽位抽取完成,进入模板映射环节。我的做法是维护一张“问句模板到Cypher模板”的映射表,每个模板带参数占位符,运行时把实体填进去。这张表是整个问答模块最核心的结构:
| 问句类型 | 问句模板示例 | 生成的Cypher模板 |
|---|---|---|
| 属性查询 | {实体}的{属性}是什么 | MATCH (p:人物 {name:'{实体}'}) RETURN p.{属性} |
| 关系查询 | {实体A}和{实体B}是什么关系 | MATCH (a:人物 {name:'{实体A}'})-[r]-(b:人物 {name:'{实体B}'}) RETURN type(r) |
| 路径查询 | {实体A}和{实体B}怎么认识 | MATCH p=shortestPath((a:人物 {name:'{实体A}'})-[:关系*1..5]-(b:人物 {name:'{实体B}'})) RETURN p |
| 筛选统计 | {属性}排前{N}的{实体类型}有哪些 | MATCH (p:人物) WHERE p.{属性} IS NOT NULL RETURN p ORDER BY p.{属性} LIMIT {N} |
| 比较查询 | {实体A}和{实体B}谁{属性}更高 | MATCH (a:人物 {name:'{实体A}'}),(b:人物 {name:'{实体B}'}) RETURN a.{属性} > b.{属性} |
整个问答主流程封装在一个函数里,结构是这样的:
def answer_question(question: str) -> dict: intent = detect_intent(question) # 第一步:意图识别 slots = extract_slots(question) # 第二步:槽位抽取,含别名归一化 cypher = build_cypher(intent, slots) # 第三步:按模板生成Cypher result = graph_db.execute(cypher) # 第四步:执行查询 answer = format_answer(intent, result) # 第五步:结果格式化 return {'answer': answer, 'nodes': ..., 'edges': ...}生成Cypher后执行查询,拿到原始结果还要做一遍“答案格式化”。关系查询可能查出多条关系,需要合并去重,再用自然语言拼接,比如“林冲和鲁智深是结拜兄弟;此外两人还有同乡之谊”。查不出结果时,不要返回尴尬的空字符串,我加了一个兜底逻辑:返回与该实体直接相连的邻居列表,提示“没找到两者的直接关系,但你可以看看A认识的人里谁跟B相关联”。这个兜底看着简单,实际体验的提升非常明显。
还有一个安全细节。Cypher是用模板拼接生成的,模板参数必须校验,否则用户问句里携带恶意符号可能造成查询注入。我的做法是对实体名先做人名词典校验,词典里没有的词一律不放行,相当于白名单机制。这一步千万别省。
4. 可视化模块:让关系网络真正看得见
4.1 框架选型:先跑通,再求深
图谱可视化可选的方案不少,大致分三类:通用图表库的关系图、专门的关系图可视化库、手写Canvas。我列个对比再讲结论:
- 通用图表库关系图:上手最快,中文文档全,力导向布局、缩放拖拽、节点事件都是现成的,几百节点的演示场景完全够用。缺点是深度定制受限,上万人规模的数据性能撑不住。
- 专门关系图可视化库:交互能力更强,节点拖动、自定义边样式、大节点量性能优化做得更扎实,适合复杂图编辑场景。代价是学习成本高一截,API风格和通用图表库差别很大。
- 手写Canvas:最灵活但最不划算,连基础的缩放平移都要自己实现,没必要。
我最终选了通用图表库先跑通,原因很简单:这个项目的核心价值在问答和图谱本身,可视化是辅助展示层,“让结果可感知”就够了。先把效果做出来,别让第一步卡在复杂框架的学习上。如果以后真要支撑上千节点加频繁交互编辑,再迁移到专门的关系图可视化库,展示层内部接口已经做了隔离。
4.2 视觉编码:让一屏信息不靠解释也能看懂
可视化不只是把图画出来,更重要的是把信息编码进视觉元素里。我的设计是这样的。
节点大小编码重要性,用度数中心性计算,也就是该人物在图里和多少人直接相连。关系越多的节点越大,一眼能看出核心人物是宋江、卢俊义、鲁智深这批人,图谱的重心一目了然。
节点颜色编码阵营,第一版用天罡三十六和地煞七十二区分,再给非梁山人物单独一档灰色。对《水浒传》读者来说,天罡地煞几乎是常识框架,用这个维度做颜色分区,开页就能看出星团结构。
边的颜色和线型编码关系类型:结拜用暖色实线,上下级用冷色虚线,敌对用醒目的红色粗线。所有关系标签默认不显示,否则一团文字会糊住整张图,改成鼠标悬浮在边上时才弹出具体关系类型。
交互方面做了三件事:悬停节点弹出属性卡片,显示绰号、排名、兵器、家乡;点击节点展开它的邻居节点,新节点平滑进入布局;双击节点把视图聚焦到以它为中心的子图。问答返回结果时,前端定位到命中的节点和边做高亮闪烁,同时在侧边栏显示自然语言答案。做完这几步,整个系统的演示效果才算真正“像样”。
4.3 性能优化:全量渲染是条死路,按需加载才是正解
第一次把三百多个实体、六百多条关系一次性塞进关系图,页面肉眼可见地卡顿,拖拽掉帧严重。浏览器一次性渲染太多图形元素,再好的库也扛不住。我用三个策略解决。
第一是默认只渲染核心子图。页面初始加载只展示天罡三十六人以及他们之间的一跳关系,启动快、观感干净,用户想看全貌再点“显示全部”,异步加载全量数据。
第二是按需扩展。点击节点时,后端只返回该节点一跳以内的邻居和边,前端把增量数据加进图里,而不是重新渲染全图。这个接口设计成了一次最多返回三四十个节点,防止一次扩展把图瞬间塞爆。
第三是标签遮挡处理。节点多起来之后,中文姓名标签互相重叠,视觉非常混乱。我实现了基于缩放级别的标签显示策略:视野放大到一定程度才显示标签,缩小到总览级别时只显示节点圆点。这个细节对观感的影响远超预期。
后端接口最终定了三个:
GET /graph/core:返回核心子图数据,用于初始加载GET /graph/expand/{node_id}:返回指定节点的一跳邻居,用于按需扩展POST /qa:接收问句,返回答案和命中的子图路径
接口返回格式统一成{nodes: [], edges: []},前端不管数据来源,全部走同一条渲染管线。这个约定带来一个额外好处:图谱数据哪怕是换到三国演义的,前端一行代码都不用改。
5. 二次扩展设计:换一本书也一样跑
5.1 数据源配置化:让“换数据”变成“填表”
“可二次扩展”是项目标题里写清楚的承诺,也是我花心思最多的地方。核心思路一句话:把数据源变成配置,而不是写死的代码。
具体实现是定义了一个标准的数据导入接口。不管是《水浒传》《三国演义》还是你自己整理的某个行业数据集,只要最终输出成统一的中间格式——一张节点表加一张关系表,就能被系统直接加载。节点表字段固定为:实体ID、实体名称、实体类型、属性字典;关系表固定为:起点ID、终点ID、关系类型、关系属性。
中间格式固定之后,换数据源就变成了填表。《水浒传》的数据解析脚本负责把原著文本转成这个格式,换别的书时,只需要新写一个解析脚本输出同样的表,问答和可视化层完全不用动。这个中间格式是我在项目中期才意识到的关键设计,早做早省事。
5.2 代码分层的扩展点:清楚知道改哪几个文件
源码的扩展点其实只有四个,搞清楚之后二次开发会非常顺手。
第一是实体和关系类型注册。新增一个实体类型或关系类型,在schema配置文件里加一行类型定义,再在可视化样式配置里给它配一种颜色,就结束了。
第二是人物别名词典。换数据集时替换成语料对应的别名映射文件。问答识别的准确率第一道关口就在这里,同名实体、别称映射都得在这个文件里兜住。
第三是问句模板配置。我把“关键词到意图”“意图到Cypher模板”的映射全部抽成JSON配置。以后用户问出新的说法,比如“XX的师傅是谁”,只需要在配置里新增一个模板条目,指向已有的师徒关系查询,完全不用写代码。配置大概是这个样子:
{ "entity_types": ["人物", "地点", "组织"], "relation_types": ["结拜", "师徒", "主仆", "同乡", "上下级", "敌对"], "question_templates": [ { "intent": "relation", "keywords": ["什么关系", "怎么认识"], "cypher_template": "MATCH (a:人物 {name:'{实体A}'})-[r]-(b:人物 {name:'{实体B}'}) RETURN type(r)" } ] }第四是存储层适配器。图数据库的所有访问被封在一个适配器接口后面,增删改查都走统一方法。如果想换成另一个图数据库,甚至退回内存存储做轻量部署,只需要实现这个适配器的几个方法,服务层和展示层完全无感。这个设计一开始看着像过度设计,后来我调试时真的换过一种存储方案,才发现省了大把时间。
提示:扩展性设计要适度。我的原则是,接口隔离到“你能轻松改动的程度”就够了,不要为了所谓的扩展性抽象出三层工厂。尤其是个人项目和教学演示,过度设计比没有设计更糟糕。
6. 实操踩坑记录与排查经验
6.1 中文分词的三个经典坑
中文分词是这项目里第一个“看着简单做起来全是坑”的环节。第一个坑是专名不识。默认词典会把“鲁智深”拆成“鲁”和“智深”,“西门庆”可能被拆成“西门”和“庆”。解决办法是喂自定义词典,并且词典优先级要设置正确,否则还是会被常用词覆盖。
第二个坑是别名爆炸。“宋江”在全书里的叫法有“宋江”“宋公明”“及时雨”“呼保义”“押司”好几种,如果只按姓名字典匹配,问答系统永远答不对“及时雨的排名是多少”这种问题。最终靠维护别名映射表和给绰号建索引两个手段解决,让问句里的任何称呼都能落回同一个唯一实体。
第三个坑是“和”字被误伤。前面说过,通用停用词表会过滤掉“和”,直接导致关系问句的处理崩坏。最后的处理是把关系问句单独拉一条处理链路,停用词表对并列连接词放行,才把这个坑填平。
6.2 查询和图谱渲染的性能问题
性能问题集中出现在两个地方。第一是图数据库查询慢,排查下来发现是缺索引。某个查询要按绰号定位节点,全库扫描一次要好几秒,建完绰号索引后毫秒级返回。所以建索引要在数据导入阶段就做掉,不要等卡了再回头补。
第二是前端渲染卡顿。全量渲染三百加节点时掉帧,用浏览器开发者工具的性能面板录了一段拖拽操作,能清楚看到耗时瓶颈在渲染元素数量上。之后改用核心子图初始加载加按需扩展的策略,卡顿问题基本消失。如果元素数量不大依然卡顿,那就要检查标签文本渲染,一般用缩放级别控制标签显示就能解决。
6.3 问答系统调试的几个细节
最后说几个问答调试心得。第一是模板优先级。一句话可能同时命中两个意图模板,比如“宋江的绰号和排名是什么”既是属性查询又带统计意味。处理方案是给模板定义优先级,属性类优先于关系类;槽位里出现多个属性名时,拆成多条查询合并结果,而不是只返回第一个。
第二是空结果处理。用户问“宋江和方腊是什么关系”,图谱里如果确实没收录直接关系,直接返回空很扫兴。兜底策略之前提过,把宋江的直接好友列表带出来,再提示“两人暂未收录直接关系”。这是问答体验的分水岭,对比生硬报错完全是两个感觉。
第三是实体匹配失败的引导。问句里的实体识别不出来时,比起冷冰冰的“未识别”,更好的做法是返回一个推荐列表,把图谱里最接近的几个实体名展示出来让用户点选。这个交互在演示场合尤其加分,观众输入一个有偏差的名字后,系统给出友好引导,观感立刻不同。
最后说点大实话。这个项目花时间最多的是数据清洗和人工校验,这些环节一度让我觉得最没技术含量。但复盘之后我才意识到,正是这些看起来“不够酷”的功夫决定了系统能不能用。图谱数据质量不行,后面的问答和可视化全是空中楼阁。如果你也想做类似的东西,我的建议是别急着炫技,先把数据准确率的底子打好——图谱准了,用户随便问几句话你都能接得住;图谱不准,技术再花哨也只是在精致的废墟上跳舞。