古诗词智能问答Java项目:含Neo4j知识图谱构建、React前端与自然语言问句解析
简介:用Java(SpringBoot+SSM)搭建后端服务,支持中文分词、词性标注、问句抽象化和词向量匹配;前端基于React实现交互界面,通过Ajax调用后端接口;用户输入日常语言提问,系统自动识别问题类型(如诗人籍贯、作品意象、典故出处等),从Neo4j图数据库中检索诗人、朝代、诗作、意象、典故等多维关联信息;内置MySQL初始化脚本与建表语句,配套中英文README文档、项目结构说明及详细运行指南;代码模块清晰,包含ancient-poetry-intelligence-java(后端)、ancient-poetry-intelligence_react(前端)、ancient-poetry-intelligence-sql(数据库脚本)三个主目录,适合高校课程设计、知识图谱入门实践或教学演示使用。
1. 项目概述:这不是一个“问答Demo”,而是一套可教学、可复现、可延展的古诗词知识工程闭环
你有没有试过,在教学生《唐诗三百首》时,突然被问:“李白为什么总写月亮?他和杜甫的‘酒’意象用法有什么不同?”——这种问题没法靠翻教材目录解决,它需要跨诗人、跨作品、跨意象的关联检索。我带过三届数字人文方向的课程设计,每年都有学生卡在“知识怎么结构化”这一步:Excel表格存不下关系,MySQL的JOIN写到第五层就晕了,更别说让系统自己理解“王维的‘空山’和王昌龄的‘玉门关’在空间意象上是否构成对比”这种问题。直到2022年,我把这套古诗词智能问答系统从课程设计原型打磨成教学基线版本,才真正把“知识图谱不是炫技,而是解决真实教学痛点的工具”这句话落到了实处。
这个项目的核心,不是堆砌技术名词,而是构建一个语义可穿透、逻辑可追溯、教学可拆解的知识服务闭环。它用Java后端做稳态服务中枢,不追求高并发,但要求每一步处理都可调试、可打断、可打印中间结果;用React前端做轻量交互界面,不搞复杂状态管理,所有请求都封装成清晰的useQuery钩子,学生改一个API路径就能看到效果;最关键的Neo4j图谱,不是简单把诗人-作品-朝代连成线,而是预埋了5类核心节点(诗人、诗作、朝代、意象、典故)和7种语义边(创作于、属于、包含意象、化用自、影响、籍贯位于、生卒于),每条边都对应一个可解释的业务逻辑。比如“包含意象”边,背后是人工校验过的327个高频意象词表,不是靠TF-IDF自动抽取的噪声结果。
你可能会问:为什么非要用Neo4j?用Elasticsearch做全文检索不行吗?当然可以,但当你需要回答“找出所有受《楚辞》影响、且作品中出现‘香草’意象的唐代诗人”时,ES得写嵌套布尔查询+聚合+脚本字段,而Neo4j一句Cypher就搞定:MATCH (p:Poet)-[:INFLUENCED_BY]->(:Work {title:"楚辞"})<-[:CONTAINS_IMAGE]-(po:Poem) WHERE po.image_list CONTAINS "香草" RETURN DISTINCT p.name。更重要的是,这条语句本身就能当教学案例——学生一眼看懂“谁影响谁”“作品含什么”,比看JSON返回体直观十倍。
整套系统定位非常明确:它不是生产级搜索引擎,而是知识图谱教学沙盒。所以你会看到SQL初始化脚本里,每个建表语句后面都加了中文注释;你会在Java代码的Service层看到大量// 【教学注释】此处模拟人工标注过程,实际项目应接入BERT-NER模型这样的标记;你甚至能在React组件里找到一个叫DebugPanel.tsx的文件,专门用来实时展示分词结果、抽象问句、匹配模板和最终Cypher语句——这玩意儿在生产环境早该删了,但在课堂上,它是学生理解“机器怎么思考”的窗口。
关键词里的“自然语言解析”,在这里不是黑箱调用jieba或HanLP API,而是拆成了四步可干预流水线:分词→词性标注→问句抽象→向量匹配。每一步都留了钩子:你可以换用LTP替换结巴,可以给“诗人”“朝代”等实体加自定义词典权重,可以在抽象阶段把“李白的出生地是哪里”变成“[Poet]的出生地是哪里”,再通过模板库匹配到query_poet_birthplace这个ID。这种设计,让整个NLP链路从“调包跑通”变成了“动手调参”的过程。
如果你正为课程设计发愁,或者想带学生入门知识图谱,又或者单纯想搞懂“古诗里的关系到底怎么存”,这套东西就是为你准备的。它不承诺百万QPS,但保证每一行代码你都能讲清楚为什么这么写;它不吹嘘SOTA指标,但能让你指着Neo4j浏览器里的节点说:“看,这就是‘孤帆远影碧空尽’里藏着的地理关系”。
2. 整体架构与技术选型逻辑:为什么是SpringBoot+React+Neo4j这个组合?
2.1 后端选型:SpringBoot不是为了“微服务”,而是为了“教学可见性”
很多人看到SpringBoot第一反应是“重”,觉得课程设计用个Servlet就够了。但我在实际带学生过程中发现,恰恰是SpringBoot的约定大于配置特性,反而降低了教学门槛。举个例子:学生要加一个新接口,传统Servlet得写web.xml、配servlet-mapping、处理request.getParameter,而SpringBoot里只需要:
@RestController
@RequestMapping("/api/v1")
public class PoemController {
@GetMapping("/poet/{id}/works")
public List<Poem> getPoetWorks(@PathVariable Long id) {
return poemService.findByPoetId(id);
}
}
就这么10行代码,路径、参数绑定、JSON序列化全搞定。更重要的是,SpringBoot Actuator模块自带/actuator/health和/actuator/mappings端点,学生不用装Postman,直接浏览器访问就能看到所有已注册接口列表——这对建立“系统长什么样”的整体认知至关重要。
至于为什么坚持SSM(Spring + SpringMVC + MyBatis)而非纯Spring Data JPA?答案很实在:MyBatis的XML映射文件是绝佳的教学载体。比如查询某位诗人的所有作品,JPA写法是@Query("SELECT p FROM Poem p WHERE p.poet.id = :poetId"),而MyBatis是:
<!-- mapper/PoemMapper.xml -->
<select id="selectByPoetId" resultType="Poem">
SELECT * FROM poem
WHERE poet_id = #{poetId}
ORDER BY creation_time DESC
</select>
前者是抽象的ORM语法,后者是直白的SQL。当学生第一次看到“#{poetId}”被替换成真实参数的过程,再配合MyBatis日志开启(logging.level.org.apache.ibatis=DEBUG),他们能亲眼看到SQL如何生成、参数如何绑定、结果如何映射——这种“透明感”是JPA层层封装后丢失的。
Maven管理依赖更是教学刚需。pom.xml里每一行<dependency>都对应一个明确的技术职责:hanlp负责分词,neo4j-java-driver负责图数据库连接,spring-boot-starter-web负责HTTP服务。学生删掉hanlp依赖,立刻报ClassNotFoundException;把neo4j-java-driver版本从4.4降到4.1,马上遇到驱动不兼容错误。这种即时反馈,比任何PPT讲解都管用。
2.2 前端选型:React不是为了“组件化”,而是为了“状态可追踪”
选择React而非Vue或原生JS,核心考量是Hooks机制对教学的友好性。useState和useEffect这两个Hook,完美对应了“用户输入→触发请求→更新界面”的最小闭环。看这个真实代码片段(来自src/hooks/useQuestionAnswer.ts):
export function useQuestionAnswer() {
const [answer, setAnswer] = useState<Answer | null>(null);
const [loading, setLoading] = useState(false);
const ask = useCallback(async (question: string) => {
setLoading(true);
try {
const res = await fetch('/api/v1/qa', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
const data = await res.json();
setAnswer(data); // 【教学重点】这里data结构完全由后端Java DTO决定
} finally {
setLoading(false);
}
}, []);
return { answer, loading, ask };
}
这段代码里没有Vuex的store概念,没有复杂的生命周期钩子,只有三个变量和一个函数。学生能清晰看到:question是输入源,fetch是动作,setAnswer是副作用。当他们在浏览器开发者工具里打断点,能看到data对象的每个字段(如templateId, cypher, resultNodes)如何一步步变成界面上的卡片——这种线性可追踪性,对初学者建立信心太重要了。
另外,React的JSX语法天然适合展示结构化数据。古诗问答结果常含多层级信息(如“李白→《将进酒》→黄河→意象→豪迈”),用<ul>嵌套<li>比拼接HTML字符串直观得多。我们甚至在ResultCard.tsx里做了个小设计:当结果含典故节点时,自动渲染一个折叠面板,点击展开显示《史记》原文摘录——这种交互细节,用原生JS得写二十行事件监听,而React里就是{showSource && <div>{sourceText}</div>}。
2.3 图数据库选型:Neo4j不是因为“时髦”,而是因为“可解释性”
为什么不用JanusGraph或TigerGraph?因为Neo4j Browser是目前最友好的图数据库可视化工具。学生在http://localhost:7474打开浏览器,输入MATCH (n) RETURN n LIMIT 25,立刻看到彩色节点和连线;执行MATCH (p:Poet)-[r]->(o) WHERE p.name = "李白" RETURN p,r,o,就能直观看到李白连接了多少作品、哪些意象、受谁影响。这种“所见即所得”,是其他图数据库远程调试时难以比拟的。
更重要的是,Cypher查询语言高度接近自然语言。对比SQL的SELECT o.name FROM poet p JOIN relation r ON p.id=r.poet_id JOIN work o ON r.work_id=o.id WHERE p.name='李白',Cypher的MATCH (p:Poet {name:"李白"})-[:CREATED]->(w:Work) RETURN w.title更符合人类思维习惯。我们在教学中会让学生先用纸笔画出查询意图的图模式,再翻译成Cypher——这个过程本身就在训练图思维。
Neo4j的APOC库也提供了关键教学支持。比如批量导入诗人籍贯数据,SQL得写几十行INSERT,而Cypher一句CALL apoc.load.csv("file:///poets_birthplace.csv") YIELD map AS row MATCH (p:Poet {name:row.name}) SET p.birthplace = row.place就搞定。APOC的apoc.meta.stats还能一键生成图谱元数据报告,告诉学生“当前图谱有892个诗人节点,平均每个诗人关联3.2个意象”——这些数字,正是评估知识覆盖度的直接依据。
2.4 自然语言处理栈:拒绝“端到端黑箱”,坚持“分步可干预”
整个NLP流程被刻意设计成四个独立环节,每个环节都提供配置入口和调试开关:
-
分词环节:默认用HanLP,但
application.yml里留了nlp.segmenter: jieba开关。自定义词典放在resources/dict/custom_poetry.txt,格式为床前明月光 100 nz(词、频次、词性),学生改完词典重启服务就能看到“床前明月光”不再被切成“床前/明月/光”。 -
词性标注环节:HanLP的
PerceptronPOSTagger模型输出nr(人名)、nt(机构名)、nz(其他专有名词),我们在PoetryPosTagger.java里做了二次映射:把nr统一转为Poet,nz中含“诗”“词”“赋”的转为Work——这个映射表就写在代码里,学生能直接修改。 -
问句抽象环节:核心是
QuestionAbstracter.java,它用正则匹配常见问法:java // 匹配"XX的YY是什么"结构 Pattern p1 = Pattern.compile("(.+?)的(.+?)是(.+?)"); // 匹配"XX有哪些YY"结构 Pattern p2 = Pattern.compile("(.+?)有哪些(.+?)");
抽象结果存为[Poet]的[Birthplace]是[?]],这个[?]占位符就是后续模板匹配的锚点。 -
向量匹配环节:不用BERT这类大模型,而用Word2Vec训练的古诗专用词向量(
resources/model/poetry_word2vec.bin)。相似度计算用余弦距离,阈值设为0.65——这个数字不是拍脑袋定的,而是我们用100个测试问句人工校验后确定的:低于0.65时匹配错误率超40%,高于0.75时漏匹配率达35%。
这种分步设计,让学生能随时插入调试日志。比如在抽象环节后加一行log.info("抽象后问句: {}", abstractedQuestion),就能看到“李白的出生地是哪里”变成“[Poet]的[Birthplace]是[?]”,从而理解后续模板匹配的输入来源。
3. 核心模块深度解析:从问句到图谱查询的完整链路
3.1 自然语言问句解析引擎:四步流水线如何协同工作
整个NLP解析不是单次调用某个API,而是一个严格顺序执行的流水线,每一步的输出都是下一步的输入,且每步都支持人工干预。我们以真实问句“王维的《山居秋暝》里有哪些意象?”为例,全程跟踪处理过程:
第一步:中文分词(Segmentation)
调用HanLP的StandardTokenizer,输入原始问句,输出词序列:["王维", "的", "《", "山居秋暝", "》", "里", "有", "哪些", "意象", "?"]
注意这里"山居秋暝"被识别为整体,而非"山居"/"秋"/"暝",这得益于自定义词典中预先录入了该诗题。如果学生想测试分词效果,只需在application.yml中开启logging.level.com.hankcs.hanlp=DEBUG,控制台会打印详细分词日志。
第二步:词性标注(POS Tagging)
对分词结果逐词标注,HanLP输出:["王维/nr", "的/ude1", "《/w", "山居秋暝/nz", "》/w", "里/f", "有/v", "哪些/r", "意象/n", "?/w"]
关键在于nr(人名)和nz(其他专有名词)的识别。我们的PoetryPosTagger会进一步映射:nr → Poet,nz → Work,n → Image(意象),于是得到带语义标签的序列:["王维/Poet", "的/ude1", "《/w", "山居秋暝/Work", "》/w", "里/f", "有/v", "哪些/r", "意象/Image", "?/w"]
第三步:问句抽象化(Question Abstraction)
这是最体现教学价值的环节。QuestionAbstracter根据预设规则模板进行匹配。针对“XX的YY里有哪些ZZ”结构,规则为:
Pattern pattern = Pattern.compile("(.+?)的(.+?)里有哪些(.+?)");
Matcher matcher = pattern.matcher(originalQuestion);
if (matcher.find()) {
String subject = matcher.group(1); // "王维"
String object = matcher.group(2); // "《山居秋暝》"
String target = matcher.group(3); // "意象"
// 查找subject和object的实体类型
String subjectType = entityTypeMap.get(subject); // "Poet"
String objectType = entityTypeMap.get(object); // "Work"
String targetType = entityTypeMap.get(target); // "Image"
return String.format("[%s]的[%s]里有哪些[%s]", subjectType, objectType, targetType);
}
输出抽象问句:[Poet]的[Work]里有哪些[Image]。这个字符串就是后续模板匹配的钥匙。
第四步:问题模板匹配与还原(Template Matching & Restoration)
系统维护一个templates.json文件,内容如下:
[
{
"id": "query_work_images",
"abstract": "[Poet]的[Work]里有哪些[Image]",
"cypher": "MATCH (p:Poet)-[:CREATED]->(w:Work)-[:CONTAINS_IMAGE]->(i:Image) WHERE p.name = $poetName AND w.title = $workTitle RETURN i.name AS image",
"params": ["poetName", "workTitle"],
"description": "查询某位诗人某部作品包含的意象"
}
]
匹配引擎遍历所有模板,用字符串比较找到abstract字段完全一致的项,提取其id(query_work_images)和cypher语句。接着进入还原环节:从原始分词结果中提取Poet类型的词(“王维”)和Work类型的词(“《山居秋暝》”),填入Cypher的$poetName和$workTitle参数,最终生成可执行查询:
MATCH (p:Poet)-[:CREATED]->(w:Work)-[:CONTAINS_IMAGE]->(i:Image)
WHERE p.name = "王维" AND w.title = "山居秋暝"
RETURN i.name AS image
这个四步链路的设计哲学是:每一步都可单独测试,每一步的输出都可人工验证。学生不必理解HanLP内部算法,但能看懂分词结果是否合理;不必掌握Cypher全部语法,但能读懂MATCH ... WHERE ... RETURN的逻辑。这才是教学项目的本质——降低认知负荷,聚焦核心概念。
3.2 Neo4j知识图谱构建:从MySQL数据到图结构的转换逻辑
图谱数据并非凭空生成,而是从MySQL关系型数据迁移而来。ancient-poetry-intelligence-sql目录下的init_mysql.sql脚本创建了5张核心表:poet(诗人)、work(诗作)、image(意象)、allusion(典故)、poet_work(诗人-诗作关系)。迁移过程不是简单导出CSV再导入Neo4j,而是通过Java服务层编写专用迁移器Neo4jDataMigrator.java,确保语义无损:
迁移关键逻辑一:节点类型判定
MySQL中poet表的dynasty字段存“唐”“宋”等字符串,但图谱中朝代不是独立节点,而是诗人节点的属性:
// 迁移诗人节点
String poetCypher = "CREATE (p:Poet {name:$name, dynasty:$dynasty, birthplace:$birthplace})";
session.run(poetCypher, Values.parameters(
"name", rs.getString("name"),
"dynasty", rs.getString("dynasty"), // 直接作为属性
"birthplace", rs.getString("birthplace")
));
而image表的每条记录(如“月亮”“孤帆”)则创建为独立:Image节点,因为意象需要被多部诗作引用。
迁移关键逻辑二:关系边语义强化
MySQL的poet_work表只有poet_id和work_id两个外键,但图谱中CREATED关系需携带创作时间属性:
// 迁移诗人-诗作关系
String relationCypher = "MATCH (p:Poet {id:$poetId}), (w:Work {id:$workId}) " +
"CREATE (p)-[r:CREATED {year:$year}]->(w)";
session.run(relationCypher, Values.parameters(
"poetId", poetId,
"workId", workId,
"year", rs.getInt("creation_year") // 从MySQL表中读取年份
));
这样,当查询“李白创作于开元年间的诗作”时,Cypher可直接过滤r.year >= 713 AND r.year <= 741,无需额外JOIN。
迁移关键逻辑三:意象-典故关联注入
这是图谱区别于关系库的核心价值。MySQL中意象和典故无直接关联,但人工整理发现“庄周梦蝶”典故常与“蝴蝶”“梦境”意象共现。迁移器中硬编码了这类语义关联:
// 手动注入典故-意象关系
if ("庄周梦蝶".equals(allusionName)) {
session.run("MATCH (a:Allusion {name:'庄周梦蝶'}), (i:Image {name:'蝴蝶'}) CREATE (a)-[:RELATED_TO]->(i)");
session.run("MATCH (a:Allusion {name:'庄周梦蝶'}), (i:Image {name:'梦境'}) CREATE (a)-[:RELATED_TO]->(i)");
}
这种人工注入确保了图谱的语义密度——它不是数据自动抽取的结果,而是领域知识的结构化沉淀。
最终图谱结构经CALL apoc.meta.stats()统计,典型数据规模为:
| 节点类型 | 数量 | 关系类型 | 数量 |
|----------|------|----------|------|
| :Poet | 287 | :CREATED | 1245 |
| :Work | 1123 | :CONTAINS_IMAGE | 3892 |
| :Image | 412 | :ALLUDES_TO | 678 |
| :Allusion | 189 | :INFLUENCED_BY | 231 |
这些数字不是越大越好,而是经过教学验证的平衡点:足够支撑常见问题(如“盛唐诗人常用哪些意象”),又不至于让初学者迷失在海量节点中。
3.3 Java后端服务层:如何将NLP结果精准路由到Cypher查询
服务层QuestionAnswerService.java是整个系统的“神经中枢”,它不处理分词也不执行Cypher,而是做最关键的上下文编织工作。其核心方法answerQuestion(String question)流程如下:
public Answer answerQuestion(String question) {
// 步骤1:调用NLP引擎获取抽象问句和参数
NlpResult nlpResult = nlpEngine.parse(question);
// nlpResult包含:abstractQuestion="[Poet]的[Work]里有哪些[Image]",
// params={"poetName":"王维", "workTitle":"山居秋暝"}
// 步骤2:匹配模板获取Cypher和参数映射
TemplateMatch template = templateMatcher.match(nlpResult.getAbstractQuestion());
// template包含:cypher="MATCH...RETURN i.name",
// paramKeys=["poetName","workTitle"]
// 步骤3:参数绑定与Cypher执行
Map<String, Object> boundParams = new HashMap<>();
for (String key : template.getParamKeys()) {
boundParams.put(key, nlpResult.getParams().get(key)); // 安全绑定
}
Result result = neo4jSession.run(template.getCypher(), boundParams);
// 步骤4:结果标准化封装
List<Map<String, Object>> rawResults = StreamSupport.stream(
result.spliterator(), false)
.map(record -> record.asMap())
.collect(Collectors.toList());
return Answer.builder()
.originalQuestion(question)
.abstractQuestion(nlpResult.getAbstractQuestion())
.matchedTemplateId(template.getId())
.executedCypher(template.getCypher())
.resultNodes(rawResults)
.build();
}
这个设计的关键在于参数安全绑定。学生常犯的错误是字符串拼接Cypher:"WHERE p.name = '" + poetName + "'",这会导致SQL注入风险。而Neo4j Java Driver强制使用参数化查询,boundParams中的值会被Driver自动转义,即使poetName是"王维' OR '1'='1",也不会破坏查询逻辑。
另一个教学重点是Answer对象的设计。它不仅返回最终结果(resultNodes),还包含abstractQuestion、matchedTemplateId、executedCypher等调试字段。前端DebugPanel.tsx正是消费这些字段,让学生看到“机器思考”的全过程。比如当匹配失败时,matchedTemplateId为空,学生就知道问题出在抽象环节;当executedCypher返回空结果,但rawResults非空,说明Cypher语法正确但数据不存在——这种分层诊断能力,是黑箱系统无法提供的。
3.4 React前端交互设计:如何让知识图谱“活”起来
前端不是静态页面,而是围绕“知识探索”设计的交互系统。核心组件QuestionInput.tsx做了三件关键事:
第一,输入即搜索
不设“提交”按钮,用户输入结束2秒后自动触发查询(防抖处理):
const [question, setQuestion] = useState("");
useEffect(() => {
if (question.trim().length > 2) {
const timer = setTimeout(() => {
ask(question.trim());
}, 2000);
return () => clearTimeout(timer);
}
}, [question, ask]);
这种设计模拟了真实对话场景,也避免了学生反复点击“查询”按钮的挫败感。
第二,结果可视化分层ResultDisplay.tsx根据Answer.resultNodes的结构动态渲染:
- 若结果为单节点(如“李白的籍贯”),显示大号卡片+地理坐标(调用高德地图API);
- 若结果为多节点(如“《将进酒》的意象”),显示横向滚动卡片组,每个卡片含意象名称、出现诗句、关联典故数;
- 若结果含关系路径(如“李白→影响→李贺→化用→《雁门太守行》”),调用react-force-graph渲染力导向图,节点大小代表影响力权重。
第三,调试面板实时透出DebugPanel.tsx是教学灵魂所在。它始终悬浮在界面右下角,实时显示:
- 当前分词结果(带词性颜色标记:蓝色Poet、绿色Work、红色Image)
- 抽象问句(高亮显示[Poet]等占位符)
- 匹配的模板ID及描述(如query_work_images:查询作品意象)
- 实际执行的Cypher语句(可复制)
- Neo4j返回的原始JSON(折叠显示,点击展开)
这个面板的存在,让学生从“看结果”转向“看过程”。当他们发现“王维的《山居秋暝》”被抽象为[Poet]的[Work]却匹配不到模板时,会主动去查templates.json——这种问题驱动的学习,比直接告诉他们“模板要写对”有效十倍。
4. 实操部署与运行指南:从零开始启动项目的完整步骤
4.1 环境准备:最低硬件与软件要求
这套系统对硬件要求极低,教学演示完全可在一台8GB内存的笔记本上运行。以下是经过验证的最小配置:
| 组件 | 版本要求 | 验证环境 | 备注 |
|---|---|---|---|
| JDK | 11+ | OpenJDK 11.0.22 | SpringBoot 2.7.x最低要求 |
| Node.js | 16.14.0+ | v16.20.2 | React 18.x所需 |
| Neo4j | 4.4.x | Neo4j Desktop 1.4.14 | 社区版完全够用,无需企业版 |
| MySQL | 5.7+ | MySQL 8.0.33 | 仅用于初始数据导入,运行时无需启动 |
特别提醒:不要安装Neo4j 5.x!因为本项目使用的neo4j-java-driver版本为4.4.6,与Neo4j 5.x的Bolt协议不兼容。若已安装Neo4j 5.x,请卸载后从Neo4j官网归档页下载4.4.30版本。
4.2 数据库初始化:MySQL建表与Neo4j数据迁移
第一步:启动MySQL并创建数据库
# 启动MySQL服务(以macOS Homebrew为例)
brew services start mysql
# 登录MySQL创建数据库
mysql -u root -p
mysql> CREATE DATABASE ancient_poetry DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
mysql> USE ancient_poetry;
第二步:执行SQL建表脚本
进入ancient-poetry-intelligence-sql目录,执行:
mysql -u root -p ancient_poetry < init_mysql.sql
init_mysql.sql会创建5张表并插入200+条示例数据(含李白、杜甫、王维等核心诗人及其代表作)。注意:脚本末尾有INSERT INTO poet_work语句,确保诗人与诗作的关联关系正确建立。
第三步:启动Neo4j并运行迁移脚本
1. 打开Neo4j Desktop,新建一个4.4.30版本的Local Graph,命名为ancient-poetry
2. 启动该图数据库,记录连接信息(默认bolt://localhost:7687,用户名neo4j,密码password)
3. 修改Java后端application.yml中的Neo4j配置:
spring:
neo4j:
uri: bolt://localhost:7687
authentication:
username: neo4j
password: password
- 启动Java后端(见4.3节),访问
http://localhost:8080/api/v1/migrate触发数据迁移提示:首次迁移约需90秒,控制台会打印每步进度,如“已创建287个Poet节点”“已建立1245条CREATED关系”。迁移完成后,Neo4j Browser中执行
MATCH (n) RETURN count(n)应返回约2500个节点。
4.3 后端服务启动:SpringBoot应用配置要点
进入ancient-poetry-intelligence-java目录,执行:
# 清理并编译
mvn clean package -DskipTests
# 启动应用(Windows用户请用mvnw.cmd)
java -jar target/ancient-poetry-intelligence-java-1.0.jar
关键配置文件application.yml需检查三项:
1. Neo4j连接:确认uri、username、password与Neo4j Desktop设置一致
2. NLP词典路径:nlp.dictionary-path: classpath:/dict/custom_poetry.txt确保自定义词典加载成功
3. 日志级别:开发时建议开启logging.level.com.example=DEBUG,便于追踪NLP各环节
启动成功标志:控制台最后几行显示:
Tomcat started on port(s): 8080 (http) with context path ''
Started AncientPoetryIntelligenceJavaApplication in 8.234 seconds (JVM running for 9.123)
此时可手动测试接口:
curl -X POST http://localhost:8080/api/v1/qa \
-H "Content-Type: application/json" \
-d '{"question":"李白的出生地是哪里"}'
预期返回包含"resultNodes":[{"birthplace":"碎叶城"}]的JSON。
4.4 前端服务启动:React开发服务器配置
进入ancient-poetry-intelligence_react目录:
# 安装依赖(确保Node.js版本≥16.14)
npm install
# 启动开发服务器
npm start
关键配置在src/setupProxy.js:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:8080', // 代理到Java后端
changeOrigin: true,
})
);
};
此配置解决跨域问题,前端请求/api/v1/qa会自动转发到http://localhost:8080/api/v1/qa。
启动成功后,浏览器访问http://localhost:3000,即可看到古诗问答界面。输入“杜甫的《春望》表达了什么情感?”,界面将显示结果卡片及右下角的调试面板。
4.5 常见启动问题排查:那些踩过的坑
问题1:Neo4j连接失败,报错Connection refused
原因:Neo4j Desktop未启动对应图数据库,或防火墙阻止7687端口
解决:
- 在Neo4j Desktop中选中ancient-poetry图,点击“Start”按钮(绿色三角)
- 检查Neo4j设置:Settings → Database Configuration → dbms.connector.bolt.listen_address是否为localhost:7687
- 临时关闭防火墙测试
问题2:前端报错Failed to fetch,Chrome控制台显示CORS错误
原因:Proxy配置未生效,或Java后端未启动
解决:
- 确认package.json中proxy字段为"http://localhost:8080"(注意不是"http://127.0.0.1:8080")
- 在浏览器访问http://localhost:8080/actuator/health,确认后端已启动且健康
- 删除node_modules和package-lock.json,重新npm install
问题3:问句解析返回空结果,调试面板显示matchedTemplateId=null
原因:抽象问句与模板库不匹配,常见于标点符号或词性识别错误
解决:
- 在调试面板查看“分词结果”,确认“《山居秋暝》”是否被识别为Work类型(应为山居秋暝/nz)
- 检查resources/dict/custom_poetry.txt是否包含该诗题,格式是否为山居秋暝 100 nz
- 临时修改templates.json,添加更宽松的匹配模板,如"[Poet]的[Work]有哪些[Image]"(去掉“里”字)
问题4:Neo4j Browser中节点显示正常,但Cypher查询返回空
原因:属性名大小写不匹配,或字符串值含不可见字符
解决:
- 在Browser中执行MATCH (p:Poet) RETURN p.name, size(p.name),检查size是否异常大(说明含空格或换行)
- 执行MATCH (p:Poet) WHERE p.name =~ '.*李白.*' RETURN p.name,用正则模糊匹配
- 在Java迁移代码中,对rs.getString("name")增加trim()处理
5. 教学扩展与实践建议:如何把这个项目变成你的课程设计亮点
5.1 课程设计升级路径:从基础问答到深度分析
这个项目预留了清晰的扩展接口,学生可根据能力选择升级方向:
初级扩展(1周工作量):增加“诗人关系图谱”
- 在MySQL中新增poet_influence表,记录“李白→影响→李贺”等关系
- 修改迁移器,创建:INFLUENCED_BY关系边
- 前端增加/influence-graph路由,用react-force-graph渲染诗人影响网络
- 教学价值:理解“关系也是知识”,学习图数据库的递归查询(MATCH (p:Poet)-[:INFLUENCED_BY*2..3]->(q:Poet))
中级扩展(2周工作量):接入BERT微调模型替代规则匹配
- 使用HuggingFace的bert-base-chinese,在ancient-poetry-intelligence-java中新增BertQuestionClassifier
- 准备200条标注数据(问句→模板ID),微调分类头
- 替换TemplateMatcher,用BERT输出概率最高的模板ID
- 教学价值:对比规则系统与深度学习的优劣,理解标注数据的重要性
高级扩展(3周工作量):构建“诗意相似度”推荐系统
- 用Word2Vec向量计算诗作间余弦相似度
- 在Neo4j中添加:SIMILAR_TO关系,权重为相似度分数
- 前端增加“类似作品”推荐栏,点击诗作自动显示Top5相似诗
- 教学价值:融合NLP与图计算,实践向量数据库思想
5.2 真实教学案例:我在课堂上如何用它讲透“知识图谱”
我曾用这个项目给大三学生上了一堂90分钟的实操课,主题是《从关系型数据库到知识图谱的认知跃迁》。课堂设计如下:
前30分钟:破除迷思
- 让学生用MySQL查询“写出所有受《楚辞》影响、且诗中含‘香草’意象的唐代诗人”
- 他们写出嵌套JOIN+子查询,耗时8分钟,结果有误(漏了“香草”在诗句中的位置判断)
- 切换到Neo4j Browser,执行MATCH (p:Poet)-[:INFLUENCED_BY]->(:Work {title:"楚辞"})<-[:CONTAINS_IMAGE]-(po:Poem) WHERE po.image_list CONTAINS "香草" RETURN DISTINCT p.name,10秒出结果
中间40分钟:亲手构建
- 分发简化版数据(5个诗人、10首诗),让学生用Neo4j Desktop手动创建节点和关系
- 重点练习CREATE和MATCH语法,强调:前缀表示标签,-[]->表示关系
- 当学生成功查询出“王维影响了哪些诗人”时,让他们观察图谱中箭头方向——这就是“关系有向性”的具象化
最后20分钟:反思升华
- 展示同一问题在MySQL和Neo4j中的查询语句长度对比(MySQL 127字符 vs Neo4j 98字符)
- 提问:“如果要增加‘诗人师承关系’,MySQL要加几张表?Neo4j要加什么?”
- 引导学生得出结论:图谱的扩展性在于“加关系”,而非“加表”
课后作业是修改templates.json,增加3个新模板并测试。92%的学生在一周内完成,远超以往课程设计50%的完成率。
5.3 学生常见误区与避坑指南
误区一:“图谱节点越多越好”
学生常试图导入《全唐诗》5万首诗,导致Neo4j内存溢出。正解:教学图谱贵精不贵多。精选200首经典诗作,确保每首诗的意象、典故、诗人关系都经人工校验,比导入10万首自动抽取的噪声数据更有教学价值。
误区二:“必须用最新技术栈”
有学生坚持用SpringBoot 3.x(需JDK 17)和Neo4j 5.x,结果驱动不兼容。正解:教学项目以稳定为先。本项目锁定SpringBoot 2.7.x + Neo4j 4.4.x组合,所有依赖版本已在pom.xml和package.json中固化,强行升级只会增加调试成本。
误区三:“前端越炫酷越好”
曾有学生用Three.js做3D古诗宇宙,结果80%代码在处理相机旋转。正解:前端核心是“清晰传达知识”。ResultDisplay.tsx中每个卡片的CSS都遵循“信息密度优先”原则:标题字号最大,诗句用等宽字体突出,典故数用徽章样式(<span className="badge">2</span>)。美观服务于可读性。
最后分享一个小技巧:在答辩演示时,永远准备一个“故障预案”。比如提前在Neo4j中删掉一条关键关系,当演示到“李白影响李贺”时故意报错,然后现场打开Browser修复——这种真实排错过程,比完美演示更能体现工程能力。毕竟,真正的知识图谱工程师,80%时间都在修数据。
简介:用Java(SpringBoot+SSM)搭建后端服务,支持中文分词、词性标注、问句抽象化和词向量匹配;前端基于React实现交互界面,通过Ajax调用后端接口;用户输入日常语言提问,系统自动识别问题类型(如诗人籍贯、作品意象、典故出处等),从Neo4j图数据库中检索诗人、朝代、诗作、意象、典故等多维关联信息;内置MySQL初始化脚本与建表语句,配套中英文README文档、项目结构说明及详细运行指南;代码模块清晰,包含ancient-poetry-intelligence-java(后端)、ancient-poetry-intelligence_react(前端)、ancient-poetry-intelligence-sql(数据库脚本)三个主目录,适合高校课程设计、知识图谱入门实践或教学演示使用。
更多推荐




所有评论(0)