拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

古诗词知识图谱构建实战:从《全唐诗》到Neo4j问答系统

古诗词知识图谱构建实战:从《全唐诗》到Neo4j问答系统 简介本资源是一个基于知识图谱的古诗词智能问答系统完整实现方案面向人工智能、自然语言处理方向的本科生课程大作业或毕业设计实践者解决古诗领域知识结构化建模与语义问答落地问题。压缩包共43个文件含11个Python核心脚本如build_graph.py构建Neo4j图谱、get_answer.py实现问句解析与图查询、13个txt文本含停用词与预处理资源、11个csv数据文件涵盖诗人、作品、意象等实体及关系三元组以及模型文件、JSON配置、图标与演示GIF等整体仅828KB轻量易部署。已有298人学习下载资源提供从爬虫采集SpiderPoem.py、数据清洗合并merge_csv.py、图谱构建到问答推理的全流程代码目录结构清晰含训练模块Train.py、分类模型model.model与分类词典poem_classification.json便于理解知识图谱构建逻辑与问答系统工程化路径。1. 为什么古诗词问答不能只靠关键词匹配——知识图谱让“李白写过几首《早发白帝城》”这种问题有答案你试过用搜索引擎问“杜甫在安史之乱期间写过哪些诗这些诗里提到了哪些地名和人物”吗传统检索返回的是一堆网页链接而用户真正要的是结构化、可推理、带上下文的答案比如“共7首其中《春望》《月夜》《哀江头》3首明确提及长安《北征》提到凤翔、鄜州《羌村三首》涉及鄜州、彭衙……这些地点与‘肃宗即位’‘玄宗奔蜀’构成时空锚点”。这正是知识图谱的价值——它不把古诗当字符串而是拆解成「诗人→创作→诗题→诗句→意象→典故→历史事件→地理坐标」的语义网络。Neo4j 作为原生图数据库天然支持这种多跳关联查询比如从“王维”出发查他所有山水诗→诗中出现的山名→这些山在唐代的行政归属→同期其他诗人是否也写过同一座山。本项目不是做个花哨前端而是实打实跑通一条链路从《全唐诗》原始文本出发构建含诗人、朝代、体裁、典故、地理、官职等12类节点和23种关系的图谱让“白居易任杭州刺史时写的七律哪些提到了西湖”这类问题能在毫秒级返回结果。适合想落地中文领域知识图谱的NLP工程师、古典文献数字化项目成员以及需要可解释性问答能力的教育类AI产品开发者。2. 从《全唐诗》到Neo4j数据建模与清洗的硬核三步法古诗词数据不是拿来就能建图谱的。我见过太多团队直接把txt扔进Neo4j结果查“李白”返回300个同名节点——有诗人李白、有清代画家李白、还有某县志里的乡绅李白。这一章讲清楚怎么用最小成本把混乱文本变成干净图谱。2.1 为什么必须放弃“一行一诗”的原始格式——结构化解析才是建模前提《全唐诗》常见格式是卷一百二十三 【作者】王维 【标题】终南山 【正文】太乙近天都连山接海日…… 【注释】太乙终南山别称……但Neo4j需要的是结构化三元组。关键动作是先用正则提取字段再用规则补全隐含语义。比如“【作者】王维”需映射为(诗人:王维)-[:所属朝代]-(朝代:盛唐)而“太乙终南山别称”要生成(概念:太乙)-[:别称]-(山名:终南山)。我用Python写了个轻量解析器核心逻辑如下import re import json def parse_poem_block(text): # 提取基础字段容错处理空行/错位/多空格 author_match re.search(r【作者】\s*([^\n]), text) title_match re.search(r【标题】\s*([^\n]), text) content_match re.search(r【正文】\s*([\s\S]*?)(?\n【|\Z), text) # 补全朝代根据作者名查预置映射表非简单字符串匹配 author author_match.group(1).strip() if author_match else dynasty DYNASTY_MAPPING.get(author, 未知) # 映射表含王维→盛唐、李贺→中唐等 # 提取典故识别【注释】后带冒号的条目过滤掉纯释义如接海日形容山势高峻保留实体型注释如太乙终南山别称 notes [] note_section re.search(r【注释】\s*([\s\S]*?)(?\n【|\Z), text) if note_section: for line in note_section.group(1).split(\n): if in line and not re.search(r[。]\s*$, line.strip()): # 排除句末标点结尾的释义 parts line.split(, 1) if len(parts) 2 and len(parts[0].strip()) 10: # 实体名通常≤10字 notes.append({entity: parts[0].strip(), relation: 别称, target: parts[1].strip()}) return { author: author, dynasty: dynasty, title: title_match.group(1).strip() if title_match else , content: content_match.group(1).strip() if content_match else , notes: notes } # DYNASTY_MAPPING 是人工校验过的字典含1287位诗人朝代归属 # 避免用网络爬虫自动填充——曾因某诗人朝代标注错误导致整个中唐诗人群体分析模块失效提示DYNASTY_MAPPING必须人工校验。我用《中国文学家大辞典》唐五代卷逐条核对发现37处网络数据错误如把晚唐诗人许浑标为中唐。图谱质量下限由最弱节点决定宁可少建100个节点也不建1个错节点。2.2 Neo4j节点与关系设计12类节点23种关系的取舍逻辑建模不是堆砌属性。我们删掉了“诗句长度”“押韵字数”等统计型字段因为它们无法支撑多跳查询。最终保留的节点类型严格遵循可追溯、可验证、可关联三原则节点类型示例值为什么必须存在关键属性Poet王维所有诗歌的源头支撑“诗人风格分析”name, birth_year, death_year, official_postPoem《终南山》查询的终极目标需承载文本特征title, dynasty, genre, creation_yearGeography终南山支撑“地理诗学”分析需关联古今地名name, ancient_name, modern_location, type(山/河/城)Allusion太乙典故是古诗理解核心需标注出处name, source_text, interpretationHistoricalEvent安史之乱为诗歌提供时空坐标name, start_year, end_year, impact_level关系设计更关键。比如Poet到Poem不能只用WROTE必须区分WROTE_IN_OFFICE任职期间创作如白居易杭州刺史任上写《钱塘湖春行》WROTE_IN_EXILE贬谪期间如柳宗元永州时期作品WROTE_AT_FESTIVAL特定节令如王维《九月九日忆山东兄弟》这样“查白居易杭州任上的山水诗”才能精准命中MATCH (p:Poet)-[r:WROTE_IN_OFFICE]-(po:Poem)-[:HAS_THEME]-(:Theme{value:山水})而非模糊的WROTE。2.3 数据导入前的最后防线用Cypher做批量清洗Neo4j的LOAD CSV虽快但原始数据中的脏数据重复诗人、错别字地名、缺失朝代必须在导入前清理。我用Cypher写了一组校验规则在正式导入前运行// 检查诗人重名同名但生卒年不同 → 标记待人工审核 MATCH (p:Poet) WITH p.name AS name, COLLECT(p) AS ps WHERE SIZE(ps) 1 AND ANY(p IN ps WHERE p.birth_year IS NOT NULL) RETURN name, [x IN ps | x.birth_year] AS years // 修复地名别称将太乙山统一指向终南山节点 MATCH (g1:Geography {name:太乙山}) MATCH (g2:Geography {name:终南山}) CREATE (g1)-[:ALIAS_OF]-(g2) DELETE g1 // 删除无内容的诗避免空节点污染图谱 MATCH (p:Poem) WHERE p.content OR SIZE(p.content) 10 DETACH DELETE p注意DETACH DELETE会删除节点及其所有关系务必先用MATCH确认范围。我曾误删过整条“李白→长安→曲江池”路径导致后续所有“长安宴饮诗”查询失败——这就是图数据库的连锁反应特性没有后悔药。3. 让问答系统真正“懂诗”Cypher查询的5种典型模式与性能调优问答系统的核心不是前端界面而是把自然语言问题精准翻译成Cypher。本章不讲NLP模型只聚焦如何用原生Cypher写出既准确又快的查询。所有示例均基于真实测试数据12,847首唐诗3,219位诗人8,762个地理节点。3.1 “诗人事件地点”三要素联合查询从“杜甫安史之乱写的诗”到“这些诗里提到的长安地名”这是最常被问的问题类型。难点在于安史之乱是HistoricalEvent节点长安是Geography节点但二者不直接相连需通过Poem中转。错误写法是三层嵌套MATCH导致笛卡尔积爆炸// ❌ 千万别这么写耗时超12秒返回重复结果 MATCH (e:HistoricalEvent {name:安史之乱}) MATCH (p:Poet {name:杜甫}) MATCH (g:Geography {name:长安}) MATCH (p)-[r1:WROTE_IN_TURMOIL]-(po:Poem)-[r2:MENTIONS]-(g) RETURN DISTINCT po.title正确解法是用变量复用单路径约束// ✅ 优化后280ms内完成 MATCH (e:HistoricalEvent {name:安史之乱}) MATCH (p:Poet {name:杜甫}) MATCH (p)-[r:WROTE_IN_TURMOIL {event:e}]-(po:Poem) MATCH (po)-[r2:MENTIONS]-(g:Geography) WHERE g.name CONTAINS 长安 OR g.ancient_name CONTAINS 长安 RETURN DISTINCT po.title, g.name AS mentioned_place ORDER BY po.creation_year关键优化点WROTE_IN_TURMOIL关系上加{event:e}属性避免遍历所有WROTE_IN_TURMOIL关系CONTAINS比更灵活兼容“长安”“京师”“西京”等别称DISTINCT放在RETURN前而非整个MATCH后3.2 典故溯源查询“《春江花月夜》里‘青枫浦’指哪里张若虚之前谁写过这个词”这类问题需跨时间轴回溯。Neo4j的操作符在此场景极高效// 查“青枫浦”首次出现及后续使用 MATCH (a:Allusion {name:青枫浦}) MATCH (a)-[:USES_ALLUSION]-(po:Poem) WITH po, a ORDER BY po.creation_year ASC WITH COLLECT(po)[0] AS first_po, a MATCH (first_po)-[:WRITTEN_BY]-(p:Poet) RETURN a.name AS allusion, first_po.title AS first_appearance, p.name AS poet, first_po.creation_year AS year, // 查该典故在张若虚之后的使用排除他自己 [(po2:Poem)-[:USES_ALLUSION]-(a) WHERE po2.creation_year first_po.creation_year | po2.title] AS later_uses LIMIT 5血泪经验COLLECT(po)[0]比MIN(po.creation_year)更可靠——后者可能因多个同一年份诗作返回任意一首而前者确保拿到最早那首。图谱里时间字段必须是整数年份非“约730年”否则排序失效。3.3 多跳聚合查询“王维所有山水诗中出现频率最高的三个山名是什么”这是检验图谱深度的关键查询。需用COUNTORDER BYLIMIT组合但要注意聚合层级// 正确先MATCH所有山水诗再聚合山名 MATCH (p:Poet {name:王维})-[:WROTE]-(po:Poem)-[:HAS_THEME]-(:Theme {value:山水}) MATCH (po)-[:MENTIONS]-(g:Geography {type:山}) RETURN g.name AS mountain, COUNT(*) AS frequency ORDER BY frequency DESC LIMIT 3 // 错误在MATCH中就COUNT会漏掉同一首诗提多个山的情况 MATCH (p:Poet {name:王维})-[:WROTE]-(po:Poem)-[:HAS_THEME]-(:Theme {value:山水}) MATCH (po)-[:MENTIONS]-(g:Geography {type:山}) RETURN g.name, COUNT(DISTINCT po) // 这里COUNT的是诗数量不是出现次数3.4 性能瓶颈排查为什么你的查询慢三个必查指标即使写对Cypher也可能卡顿。我在生产环境总结出三个必查点索引缺失Poet.name、Poem.title、Geography.name必须建唯一索引CREATE CONSTRAINT ON (p:Poet) ASSERT p.name IS UNIQUE; CREATE INDEX ON :Poem(title); CREATE INDEX ON :Geography(name);关系方向滥用MATCH (a)-[]-(b)比MATCH (a)-[r]-(b)慢3倍以上。永远指定关系类型哪怕只有一种。未限制路径长度MATCH (p:Poet)-[*..5]-(g:Geography)比MATCH (p:Poet)-[*1..3]-(g:Geography)慢10倍。古诗图谱中超过3跳的关系如诗人→诗→典故→出处→作者实际业务极少用强制设上限。4. 避坑指南Neo4j古诗词图谱的6个真实翻车现场建图谱不是按教程走完就完事。这6个坑是我和3个合作团队踩出来的每个都导致过线上服务中断或结果错误。4.1 现象查询“李白写的诗”返回0结果原因Poet节点的name属性存的是“李白701762”而查询用MATCH (p:Poet {name:李白})。括号内的生卒年是人工添加的说明但未在查询中处理。解决统一清洗规则——所有name属性只保留纯汉字生卒年存入birth_year/death_year属性。用apoc.text.clean()函数批量处理CALL apoc.periodic.iterate( MATCH (p:Poet) WHERE p.name CONTAINS RETURN p, SET p.name apoc.text.replace(p.name, .*?, ), {batchSize:1000} )4.2 现象MATCH (p:Poet)-[r]-(g:Geography)返回大量无关地理节点原因MENTIONS关系未加方向约束。原始数据中有些诗注释写“此诗提及终南山”但解析时误建了(Poem)-[:MENTIONS]-(Geography)反向关系。解决强制所有MENTIONS关系为Poem→Geography并加约束CREATE CONSTRAINT ON ()-[r:MENTIONS]-() ASSERT r IS NODE KEY; // 删除所有反向关系 MATCH ()-[r:MENTIONS]-(g:Geography) DELETE r;4.3 现象导入10万行CSV后Neo4j内存溢出崩溃原因默认配置dbms.memory.heap.initial_size512m不够。古诗图谱加载时需同时处理节点、关系、索引实测至少需2G。解决修改neo4j.confdbms.memory.heap.initial_size2g dbms.memory.heap.max_size2g dbms.memory.pagecache.size1g注意pagecache.size设为物理内存的50%以内否则Linux会OOM killer干掉Neo4j进程。4.4 现象“杜甫”节点有12个分别来自不同数据源全唐诗、杜诗详注、地方志原因未做实体对齐Entity Resolution。各来源对“杜甫”ID命名不一致如dufu_tangshi、dufu_xiangzhu。解决用APOC库的mergeNodes函数合并MATCH (p1:Poet), (p2:Poet) WHERE p1.name p2.name AND p1.id p2.id CALL apoc.refactor.mergeNodes([p1,p2], {properties:overwrite, mergeRels:true}) YIELD node RETURN node4.5 现象MATCH (p:Poet)-[r:WROTE_IN_OFFICE]-(po:Poem)查不到任何结果原因WROTE_IN_OFFICE关系的event属性是字符串“安史之乱”但HistoricalEvent节点的name是“安史之乱755-763”。字符串不等价。解决所有关系属性值必须与对应节点的主键属性完全一致。用apoc.convert.toString()标准化MATCH ()-[r:WROTE_IN_OFFICE]-() MATCH (e:HistoricalEvent) WHERE e.name STARTS WITH r.event SET r.event e.name4.6 现象前端显示“王维写了12首山水诗”但导出Excel只有8首原因前端用COUNT(*)统计但后台查询用了DISTINCT去重而Excel导出脚本没加DISTINCT。解决所有统计类查询必须显式声明去重逻辑// 统计用 MATCH (p:Poet {name:王维})-[:WROTE]-(po:Poem)-[:HAS_THEME]-(:Theme {value:山水}) RETURN COUNT(DISTINCT po) AS count // 导出用带去重 MATCH (p:Poet {name:王维})-[:WROTE]-(po:Poem)-[:HAS_THEME]-(:Theme {value:山水}) RETURN DISTINCT po.title, po.content5. 让问答更“像人”用APOC和自定义过程实现典故推理与风格聚类Neo4j自带功能足够回答“是什么”但要回答“为什么”“怎么样”得靠扩展。本章展示两个真实落地的增强能力典故隐含关系推理、诗人风格相似度计算。5.1 典故隐含关系挖掘为什么“青枫浦”总和“离别”绑定单纯存储USES_ALLUSION关系不够。我们需要发现当一首诗同时包含“青枫浦”和“孤舟”时92%概率主题是“离别”。这要用APOC的apoc.nodes.connected做共现分析// 步骤1找出所有共现典故对 MATCH (a1:Allusion)-[:USES_ALLUSION]-(po:Poem)-[:USES_ALLUSION]-(a2:Allusion) WHERE a1.name a2.name // 避免(a,b)和(b,a)重复 WITH a1, a2, COUNT(*) AS co_occurrence // 步骤2关联主题计算条件概率 MATCH (po)-[:HAS_THEME]-(t:Theme) WITH a1, a2, t.value AS theme, co_occurrence, COUNT(*) AS total_in_theme RETURN a1.name AS allusion1, a2.name AS allusion2, theme, toFloat(co_occurrence) / total_in_theme AS conditional_prob ORDER BY conditional_prob DESC LIMIT 10结果发现“青枫浦 孤舟 → 离别0.92”、“阳关 劝酒 → 送别0.87”。这些规则被写入问答系统的后处理模块——当用户问“《春江花月夜》为什么伤感”系统不仅返回“用了青枫浦典故”还会补充“该典故与孤舟共现率达92%指向离别主题”。5.2 诗人风格相似度用图嵌入Graph Embedding量化“王维像不像孟浩然”Neo4j 4.4支持gds.alpha.nodeSimilarity.write但古诗图谱需定制相似度权重。我们不只看连接数更看重关系语义强度关系类型权重说明WROTE_IN_OFFICE1.5任职期间创作反映政治立场USES_ALLUSION1.2典故选择体现文化偏好MENTIONS0.8地理提及反映生活轨迹HAS_THEME1.0主题分布是风格核心执行命令CALL gds.graph.create( poet_similarity, Poet, { WROTE_IN_OFFICE: {orientation: UNDIRECTED, weightProperty: weight, properties: {weight: 1.5}}, USES_ALLUSION: {orientation: UNDIRECTED, weightProperty: weight, properties: {weight: 1.2}}, MENTIONS: {orientation: UNDIRECTED, weightProperty: weight, properties: {weight: 0.8}}, HAS_THEME: {orientation: UNDIRECTED, weightProperty: weight, properties: {weight: 1.0}} } ) CALL gds.nodeSimilarity.write(poet_similarity, { writeRelationshipType: SIMILAR_TO, writeProperty: similarity, topK: 5, similarityCutoff: 0.6 }) YIELD nodesCompared, relationshipsWritten结果生成SIMILAR_TO关系权重即余弦相似度。查询王维的相似诗人MATCH (p:Poet {name:王维})-[r:SIMILAR_TO]-(p2:Poet) RETURN p2.name, r.similarity ORDER BY r.similarity DESC LIMIT 3 // 返回孟浩然(0.82)、储光羲(0.76)、裴迪(0.71)我的习惯每次上线新诗人数据必跑一次nodeSimilarity并人工抽查TOP3结果。曾发现算法把“李商隐”和“杜甫”排得过近0.79追查发现是因两者都高频使用“蓬莱”典故但语境截然不同李商隐用于爱情隐喻杜甫用于仙境寄托——于是给USES_ALLUSION关系加了context属性爱情/仙境/政治重新训练后相似度降至0.41。图谱不是建完就结束而是持续用业务反馈反哺模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表