
简介一套基于知识图谱的《红楼梦》人物关系可视化与问答系统完整实现面向自然语言处理与知识图谱初学者及课程设计开发者。包内含8个Python核心模块覆盖LTP分词、词性标注、命名实体识别与关系抽取并集成neo4j图数据库构建、查询及KGQA问答交互。前端提供4个HTML页面实现欢迎、人物关系检索、全关系网络展示与问答功能配套CSS与JavaScript完成交互样式并基于Bootstrap等框架提供响应式布局。资源共297个文件以184张人物图片、Python脚本、HTML界面及样式脚本为主含49个zbak备份文件压缩包整体5.98MB。目录按raw_data、neo_db、spider等模块划分raw_data存放结构化三元组数据neo_db内含配置、建图与查询脚本spider已完成人物信息及图像数据采集数据流从原始采集、图谱构建到可视化问答形成完整闭环。已有92人浏览学习适合用于理解知识图谱构建全流程、复现人物关系抽取实验或作为毕业设计参考。1. 从「共现图」到「关系图」为什么红楼梦人物图谱必须走 LTP 这条线很多人第一次做《红楼梦》人物关系图时第一反应是统计两个人出现在同一段落里的次数把共现次数当关系强度画出来的图热闹但不可信贾宝玉和贾母同屏几十次可「祖孙」这条边是怎么来的图里完全说不清。真正能落地的做法是走中文信息抽取的标准路线——先分词、做词性标注再用 LTP 做命名实体识别把「贾宝玉」「薛宝钗」这样的实体捞出来接着做关系抽取得到「谁—对—谁—是什么关系」的三元组最后灌进知识图谱对外提供可视化和问答查询。这套方案适合手里有长篇中文文本、想快速搭出可演示知识图谱问答原型的人它不追求大模型式的无所不知但每一步都能白盒排查结果可解释、可修正。下面按我平时做这类项目的顺序把这套链路完整拆开。2. 先定骨架从 txt 到 Neo4j 的六步流水线与选型2.1 别上来就写代码先画一条能落地的处理管线拿到一部小说文本就急着装库、跑模型是最容易翻车的开局。正确做法是先定数据流明确每一步的输入和输出长什么样再动手写代码。我一般把全流程拆成六步文本预处理 → LTP 分词与词性标注 → 命名实体识别 → 关系抽取 → 别名合并与实体对齐 → 写入图数据库。第一步的文本预处理比想象中重要。公开渠道能拿到的《红楼梦》txt 经常有大量回目信息、批注残留、空行和章节错乱如果不先按回目切好、再把每个回目切成句子后面 LTP 处理长文本时要么结果漂移要么内存吃紧。我习惯先用正则把「第一回」「第二回」之类的回目标题提取出来再按句号、问号、感叹号分句以句子为单位进入 NLP 环节。第二步到第四步是 LTP 的主场。分词和词性标注解决「哪些词是人名」的候选问题命名实体识别进一步把人名、地名、机构名归类关系抽取则是把动词和语义角色拼成三元组。这里要先立一个预期老版小说文本和新闻语料差别很大LTP 的通用模型不会完美后面必须有后处理兜底。五六两步是把 NLP 的结果变成知识图谱涉及实体去重、关系类型归一化和图库导入是整个项目里最吃细节的部分。2.2 为什么选 LTP词典与正则的边界在哪市面上做中文命名的方案不少常见的就三种纯正则加词典、LTP 这类传统 NLP 工具、大模型抽取。纯正则和词典适合「已知名单」的场景——比如你手里有一张完整的红楼人物表可以把「贾宝玉」「林黛玉」穷举进词典。但小说里人物出场方式极不规律有全名「贾宝玉」有「宝二爷」「怡红公子」这种称位还有大量代词「他」「她」。正则词典面对这种开放集合召回率会掉得很难看。大模型抽取是另一个极端召回和零样本能力确实强但输出格式不稳定同一个句子跑两遍可能给出不同的三元组而且推理成本高、结果黑匣子出了问题不好定位是哪一层抽错了。对「要搭一个能演示、能讲解、还能逐步修正」的知识图谱问答系统来说LTP 是三者里性价比最稳的模型文件不大、完全本地运行、API 清晰还带语义角色标注SRL这是关系抽取最需要的燃料。一个小建议是别把 LTP 和大模型看成二选一。我现在的做法是 LTP 做主体抽取把置信度低的句子挑出来再用大模型对这批句子做二次校验两种工具互为后悔药。纯靠大模型清洗全量数据开销太大没必要。2.3 架构选型对比三条路线的适用边界方案优点缺点适用场景正则 人物词典极快、完全可控召回低别名和指代处理不了只有几十个核心人物的演示LTP 传统 NLP 管线本地运行、可解释、支持 SRL对新词和文言表述有误差长文本知识图谱、需逐层排查大模型抽取零样本能力强理解上下文输出不稳定、成本高、黑匣子小规模精标、复杂关系兜底选 LTP 还有一个实际原因它能产出词性、依存句法和语义角色多路特征同一个句子可以从不同角度验证结果。比如「林黛玉进贾府」这句话光靠分词看不出关系但 SRL 能标出「进」的施事是林黛玉这就为后续关系抽取埋好了伏笔。如果你打算做工业知识图谱那套「本体优先、数据清洗先行」的严格流程小说文本其实不适用——文学作品里模糊表达太多必须先跑通一个带噪音的快速原型再逐步往精确方向调。3. LTP 命名实体识别与关系抽取拿到的不是孤立词是上下文3.1 最小可用代码LTP 4.x 的分词、词性与 NER 一条龙用 LTP 做红楼梦 NER常见做法是直接用 LTP 的 Python SDK。网上大量教程还在讲 pyltpLTP 3.x 的绑定库和 LTP 4.x 的 API 完全不同抄代码前一定要先确认版本。这里以 LTP 4.x 的 Python SDK 为例先跑通最小流程from ltp import LTP # 首次运行会联网下载预训练模型离线环境请提前准备模型包 ltp LTP() text 贾宝玉是贾母的孙子林黛玉是贾宝玉的表妹。 seg, hidden ltp.seg([text]) pos ltp.postag(hidden) ner ltp.ner(hidden) print(分词:, seg[0]) print(词性:, pos[0]) print(NER:, ner[0])逻辑说明ltp.seg返回分词结果和 hidden 向量这个 hidden 是后续 postag、ner、srl 共用的中间表示不要在每次调用时都重新跑一遍 seg否则性能浪费严重。pos是词性序列ner返回命名实体跨度每个元素是实体类型加实体文本。LTP 的 NER 标签中Nh代表人名地名、机构名等标签不同模型版本略有差异拿到结果后先打印出来看一遍再做规则。参数说明ltp.seg接受的是句子列表不是一整段字符串所以调用前要把长文本按句切开再传入。如果直接传一整回LTP 内部会按自己的分句逻辑切分但这样你就失去了对句子边界的控制。句子边界对关系抽取非常重要关系只在单句内抽取是后面所有操作的前提。跑通之后建议先在目录里建一个entity_candidates.txt把 NER 输出的所有人名落盘统计频率。这一步能帮你快速发现 LTP 对《红楼梦》文本的识别习惯哪些人名是稳定识别的哪些是拆开的后面做别名合并才有依据。3.2 人物实体的「别名黑洞」宝玉、宝二爷、怡红公子怎么合并《红楼梦》的人名情况比新闻语料复杂得多同一个角色有多种称呼这是实体链接和知识图谱落库前必须解决的问题。最典型的是贾宝玉全名「贾宝玉」常出现但书里更多叫他「宝玉」王夫人叫他「二爷」或「宝二爷」诗社活动里他又叫「怡红公子」「绛洞花主」。黛玉也有「林黛玉」「黛玉」「颦儿」「潇湘妃子」几种叫法。如果不对齐别名图谱里会出现十几个节点一个「贾宝玉」挂着「宝玉」这个节点另一个「宝二爷」又连着黛玉那边的边。整个图看上去节点数量膨胀实际信息量却变低了。我之前项目就吃过这个亏第一版图里有四百多个节点人工一查三分之一是同一个人。解决办法是建立别名映射表用规范名统一。我一般直接在项目里放一个 python 字典alias_map { 宝玉: 贾宝玉, 宝二爷: 贾宝玉, 怡红公子: 贾宝玉, 绛洞花主: 贾宝玉, 黛玉: 林黛玉, 颦儿: 林黛玉, 潇湘妃子: 林黛玉, }逻辑说明这个映射表不是一次性写完的。先把 NER 的实体候选按频率排序人工挑出高频别名再逐个映射到规范名。映射的原则是「只合并确定指代同一人的称呼」拿不准的先不合并宁缺毋滥。在实际处理时我还会加一道自动规则如果某个称谓在文本中同时和多个规范名关联就先把它挂为「未对齐」节点等问答系统需要时再人工核对。这类歧义称谓比预想的多「太太」有时候指王夫人有时候指邢夫人不能盲目合并。3.3 关系抽取用 SRL 和句法规则拼出「谁—对—谁—做了什么」有了人物实体下一步是关系抽取。LTP 提供语义角色标注SRL这是抽取「谁对谁做了什么」最省力的路径。常见做法是把 SRL 输出中的ARG0当作施事ARG1当作受事谓词动词作为关系类型拼成三元组def extract_triples_from_srl(ltp, sentence): seg, hidden ltp.seg([sentence]) srl ltp.srl(hidden) triples [] for item in srl[0]: pred_idx item[0] roles item[1] arg0 roles.get(ARG0) arg1 roles.get(ARG1) if arg0 and arg1: head .join(seg[0][arg0[0]:arg0[1]]) rel seg[0][pred_idx] tail .join(seg[0][arg1[0]:arg1[1]]) triples.append((head, rel, tail)) return triples逻辑说明srl返回每个谓词对应的语义角色列表ARG0和ARG1是角色在句子里的起止下标。把下标的词拼接出来就得到「贾宝玉」「林黛玉」这样的实体文本。这里的rel是动词本身类似「叫」「喜欢」「是」后续要映射成图谱里的关系类型。参数说明SRL 抽出来的三元组噪音很大。动词「是」会产出「贾宝玉是贾母的孙子」这种正确三元组也会产出「林黛玉是贾宝玉的表妹」这类靠上下文才能判断的表述。对于问答系统我建议把关系类型做一个归一化把「叫」「唤」「称作」归为「别称」把「喜欢」保留为「喜欢」把「是...的...」按亲属词表映射为「亲属」。用一个字典维护关系映射不用追求把所有动词都归对先把高频动词覆盖住。3.4 关系过滤与置信度阈值别把所有共现都当关系LTP 的 SRL 会把很多「伪关系」抽出来。比如「贾宝玉见林黛玉走来」SRL 可能把「见」的施事标成贾宝玉、受事标成林黛玉产出「贾宝玉-见-林黛玉」这个三元组。从语法看它没错但从知识图谱的角度「见」不是一种值得建边的稳定关系。给关系定置信度的做法是按「关系类型 谓词」统计频次再人工定阈值。像「喜欢」「是亲戚」这类关系频次高、语义稳定直接保留「见」「说」「看」这类通用感知动词单次出现不建边只有同一对实体间的同类型关系累计出现超过三次才考虑建边。这里可以加一条规则所有动词先进候选池按人物对聚合最后只把聚合后频次前 20% 的关系写入图谱。这一步也是在为后面的问答系统清理路面。知识图谱里如果塞满了几千条「见」「说」这种动作边回答「贾宝玉的表妹是谁」时会混入大量无关路径精度会很差。把噪音挡在图谱构建阶段永远比在问答阶段做排除要省力。4. 从三元组到问答与可视化建模、Cypher 导入、前端力导向图4.1 人物关系的数据模型节点、边、属性怎么设计把三元组导入 Neo4j 前要先定数据模型这一步决定后面问答查询好不好写。常见的做法是两层设计节点只有一类Label 为Person属性里存规范名canonical_name、别名列表aliases、首次出现回目first_chapter边的关系类型按语义分常见的有亲属、主仆、喜欢、朋友等。关系类型不要超过十种否则问答映射会失控。设计模型时有一条原则把「人名」和「称谓」分开存。比如「王夫人」这个节点属性里存aliases: [太太, 王夫人]但图谱的边永远指向规范名。这样在问答系统里用户说「太太」也能通过别名属性命中王夫人节点。另一个设计点是为关系边加weight属性。这个权重不是共现次数而是关系抽取时同一对人物之间有效三元组的去重后频次。权重的作用有两个可视化时控制边的粗细问答排序时作为多答案的置信度参考。4.2 Cypher 批量导入与去重MERGE 的正确用法数据清洗完导入 Neo4j 常用LOAD CSV加MERGE的方式。我这里先给节点建唯一约束再用 CSV 批量建边CREATE CONSTRAINT person_name IF NOT EXISTS ON (p:Person) ASSERT p.canonical_name IS UNIQUE;LOAD CSV WITH HEADERS FROM file:///relations.csv AS row MERGE (a:Person {canonical_name: row.source}) MERGE (b:Person {canonical_name: row.target}) MERGE (a)-[r:REL {type: row.relation_type}]-(b) SET r.weight toFloat(row.weight);逻辑说明MERGE是查重加创建的组合操作节点和边都按指定的键去重。第一次跑导入时别用CREATE否则同一个「贾宝玉-亲属-贾母」边会重复创建图里出现多条平行边查询时 count 出来的数量是虚高的。参数说明relations.csv里的source和target必须是别名映射后的规范名这一步务必在 Python 里提前做完不要在 Cypher 里去处理别名。relation_type也要提前归一化比如把「表兄妹」「表妹」等文本统一成「亲属」。权重是浮点数用toFloat转换否则 CSV 里的数字会被当作字符串存进属性排序时会按字典序排出现「10」小于「9」的荒唐结果。导入完成后我一般会跑一条验证查询按关系类型统计边数检查是否有某个关系类型数量异常偏高。如果「喜欢」类边数超过三千条大概率是规则太宽把「笑道」之类的动词也收进去了得回头调过滤条件。4.3 可视化Neo4j Browser 排除法加 ECharts 力导向图Neo4j Browser 自带的可视化适合图结构探索不适合直接对外展示因为人物一多图会乱成一团。我一般的做法是两层结合开发检查时用 Browser 查询局部关系最终展示页面用 ECharts 的力导向图。ECharts 力导向图的数据结构是节点列表加边列表option { title: { text: 《红楼梦》人物关系图 }, tooltip: {}, series: [{ type: graph, layout: force, roam: true, label: { show: true, position: right }, force: { repulsion: 200, edgeLength: 100 }, data: nodes, links: links }] };逻辑说明nodes数组的元素长这样{ id: 贾宝玉, category: 0 }links数组的元素是{ source: 贾宝玉, target: 林黛玉, value: 5 }。这里的source和target直接复用 Neo4j 里的规范名。force.repulsion控制节点间的斥力大小数值越大图越松散edgeLength控制边长度数值越小关系紧密的节点越靠近。参数说明把全量人物一次性塞进 ECharts页面会很卡。常见做法是按关系权重或回目范围做筛选比如只展示与指定人物直接相连的一度、二度关系。演示页面上给一个「核心人物 层数」的交互比一上来画全量图效果要好得多。可视化阶段最容易踩的坑是「把图做得好看但没法讲」。我建议在节点上多挂一个属性人物首次出现的回目。这样前端可以按回目做时间轴。从「元春省亲」到「黛玉焚稿」人物关系网络的变化本身就是一条叙事线索。4.4 问答系统的主链路从自然语言问句到 Cypher 查询问答系统的实现不需要复杂先做模板匹配再上图谱查询这是知识图谱问答最可靠的第一步。问句类型常见的就三种关系查询「贾宝玉的母亲是谁」、属性查询「林黛玉住在哪里」、关系路径查询「贾宝玉和林黛玉是什么关系」。我的做法是先用规则把问句里的实体抽出来再识别意图最后拼 Cypher。典型代码长这样import re intent_patterns { relation: re.compile(r(.?)的(.?)是谁), relation_path: re.compile(r(.?)和(.?)是什么关系), } def answer_question(question, alias_map, graph): entity extract_entity(question, alias_map) for intent, pattern in intent_patterns.items(): match pattern.search(question) if not match: continue if intent relation: rel_type match.group(2) cypher ( MATCH (p:Person {canonical_name: $name}) -[r:REL {type: $rel}]-(q:Person) RETURN q.canonical_name AS answer ) return graph.run(cypher, nameentity, relrel_type) return 这个问题我暂时答不上来但我可以给你看相关段落。逻辑说明extract_entity这一步很关键它先从问句里找别名映射表内的词再回退到 NER。如果省略这一步用户问「二爷的母亲是谁」时图谱会查不到实体因为库里只存了「贾宝玉」没有「二爷」这个节点。参数说明意图正则的写法要保守。「X 的 Y 是谁」这种问法是中文里最稳定的句式先把它覆盖住「X 和 Y 是什么关系」是第二优先。更复杂的问句比如「谁喜欢林黛玉」模板匹配不上需要用反向查询的模板单独处理。先做到「十句答对八句」再逐步加模板比一上来就上语义解析框架要稳妥。5. 常见问题排查LTP、Neo4j、问答三层的高频翻车记录5.1 宝玉和宝二爷是两个节点别名合并失效现象导入 Neo4j 后查询「贾宝玉」节点发现还有一个「宝二爷」节点两者各自连着不同的边「宝二爷」的边还不少。原因角色别名映射表建得太晚。NER 抽出来的是「宝二爷」直接作为规范名写进了 CSV而别名表只覆盖了「宝玉 → 贾宝玉」没有覆盖「宝二爷」。解决把别名表从硬编码字典换成外部配置文件跑 NER 后先对全部实体候选做一次频率统计再按频率从高到低人工核对别名。对小说类项目别名覆盖率至少要达到实体总量的 90% 以上再入库。5.2 SRL 把「他」抽成施事代词导致三元组飘了现象关系三元组里出现了一批「他-喜欢-林黛玉」这样的边抽象的「他」成了实体节点图谱里多了很多无意义的代词节点。原因SRL 的语义角色标注会给人称代词分配 ARG0 角色但代词本身没有消解到具体人物。解决抽取后增加一道代词过滤凡是 head 或 tail 命中「他、她、它、之」等代词集合整个三元组先丢弃。不要试图做全量指代消解那个坑太深。如果想要追回这部分信息可以保留句子上下文在问答环节把含代词的句子作为相关段落返回。5.3 把整回文本直接喂给 LTP长文本上下文漂移现象某几个回目的 NER 结果明显异常「贾母」被标成地名「王夫人」被分成「王/夫人」。原因不是模型坏了而是把整回文本作为一个字符串传给了ltp.seg。LTP 内部虽然会分句但长段落里的标点不规范、引号嵌套把句边界搞乱了实体识别的上下文也跟着错。解决强制自己的预处理先按句切开以句子列表传入。分句时保留「」和引号内的内容不要粗暴按句号把人物对话切断。截断的对话会导致「宝玉笑道」后面跟的内容失去主语关系抽取直接从源头少数据。5.4 pyltp 和 LTP 4.x 的 API 混着抄版本地狱现象代码报错TypeError: __init__() got an unexpected keyword argument path或者模型加载路径永远不对。原因网上教程多数是 pyltpLTP 3.x的写法需要手动下模型文件再传给SegmentorLTP 4.x 改用统一LTP类模型自动下载。两个版本的 API 完全不是一回事。解决项目初始化时统一锁定一个版本。新项目直接用 LTP 4.x 的from ltp import LTP。如果项目里已经有 pyltp 代码就不要中途混用。另外离线环境要提前准备模型离线包否则每次初始化都触发下载。5.5 「共现次数」被当成了「关系强度」图变成了统计图现象导入图谱后贾宝玉和秦钟之间出现一条很粗的边因为他们在同一章共现多次但实际关系是「同窗朋友」强度并没有那么高。原因把共现权重视为关系强度混入了不参与建边的场景描写。同一场景里出现的两个人不一定有真实关系。解决边的weight只用「有效关系三元组」的频次不用共现统计。三元组里的谓词和关系类型映射必须是人工维护的白名单白名单上没有的动作谓词一律不进权重计算。可视化上要降低对权重颜色的依赖优先展示关系类型权重只作辅助参考。6. 进阶怎么验证问答效果以及下一步值得做的两个方向图谱和问答跑通后下一步不是急着加功能而是做结果验证。我习惯在人力允许的范围内抽一百个问答对做黄金标注每一对标「问题、预期答案、系统答案、是否命中」。这个批注表非常小但能直接算出问答命中率也能量化每次改规则到底是变好还是变差。样本问题预期答案系统答案命中贾宝玉的母亲是谁王夫人王夫人是林黛玉的表哥是谁贾宝玉贾宝玉是谁喜欢林黛玉贾宝玉、北静王等贾宝玉部分命中晴雯是哪个房的丫鬟怡红院未返回否验证时我会把「部分命中」也单独算一列因为问答系统的边界本来就不该追求全量覆盖。系统答不出的问题如果能回退到「返回相关段落」体验上并不差。真正要避免的是答错且笃定那比答不出来更伤可信度。后续值得做的方向有两个。一是给关系加时间维度把每对人物的建边回目记录下来这样能回答「大观园初建时谁和谁交往最密」这类带时序的问题。二是把人物实体链接到公开知识库做消歧比如「贾政」这种名字在别的书里也有有了实体链接未来把红楼梦知识图谱和其他数据集合并时就不容易串人物。我现在接到任何新语料第一件事永远是抽十个句子做人工标注用标注结果反推流程的哪一步需要调。这个习惯救了我好几次也推荐你试试。希望帮到你。本文还有配套的精品资源点击获取