1. 从诗词图谱大模型这个组合说起做毕业设计选题的时候很多人第一反应是做个管理系统或者爬个数据做个可视化大屏稳妥、好过、查重也容易。但如果你恰好对自然语言处理有点兴趣又不想做那种一眼就能看出是模板套壳的题目那中华古诗词知识图谱可视化 智能问答 情感分析这个方向其实是个相当能打的选题。它同时踩中了知识图谱、大模型应用、NLP、情感计算这几个当前比较热的技术点答辩的时候老师问起来也有东西可讲不至于三句话就被问穿。我自己完整跑过一遍这个链路从数据采集、实体抽取、图谱构建到问答系统接入大模型、情感分析模型训练中间踩的坑不算少。这篇就把整个项目的技术选型、核心实现、以及那些文档里不会写的经验尽量讲透。适合正在做类似毕设的同学也适合想入门知识图谱和NLP应用落地的开发者。读完你应该能清楚数据从哪来、图谱怎么建、问答怎么接、情感分析怎么做、以及每一步为什么这么选。先说清楚这个系统到底在做什么。简单讲它把古诗词相关的结构化知识诗人、朝代、诗作、意象、流派、地点等抽出来建成一张知识图谱让李白和杜甫是什么关系哪些诗写了月亮这类问题可以通过图查询回答同时接一个大模型把自然语言问题转成图谱查询或者直接生成答案再单独训练一个情感分析模型判断一首诗是豪放、婉约、悲凉还是闲适。三块拼起来就是一个能查、能问、能分析的诗词知识系统。关键词里出现的 Python、大模型、知识图谱、NLP 这几个词基本就是这个项目的技术骨架。下面我按实际开发顺序拆开讲每一块都会说清楚选型理由和实操细节。2. 数据从哪来诗词语料的采集与清洗2.1 语料来源的选择与取舍古诗词数据不像新闻语料那样每天更新它的特点是总量有限、质量要求高、结构相对规整。常见的来源有几类公开的诗词数据库、古籍数字化项目、以及一些整理好的开源数据集。我当时的做法是优先用现成的结构化程度高的数据集再补充少量爬取。为什么不建议纯爬虫从零搞因为诗词文本的版权和准确性问题很麻烦网上流传的版本经常有错字、漏字甚至张冠李戴。你辛辛苦苦爬了几万首结果发现作者标错、朝代混乱后面图谱构建全是脏数据。所以我的建议是以高质量开源数据集为主爬虫只用来补充元数据比如诗人的生平简介、地理信息。一个典型的诗词数据字段应该包含这些字段名说明是否必需title诗题必需author作者必需dynasty朝代必需content正文必需genre体裁诗/词/曲建议tags标签如思乡边塞可选translation译文可选appreciation赏析可选字段越全后面图谱能抽的关系越多。但要注意字段多不代表质量高很多数据集的赏析字段是机器生成的读起来一股AI味这种宁可不要否则情感分析模型会被带偏。2.2 清洗环节最容易忽略的三件事数据清洗这一步很多人觉得就是去重、去空值其实远不止。我踩过的坑主要集中在三个地方。第一是异体字和通假字。古诗词里沈和沉、澹和淡经常混用如果你不做归一化图谱里会出现沈约和沉约两个节点查询的时候漏掉一半结果。我的处理方式是建一个映射表把常见异体字统一到标准写法。第二是作者名消歧。历史上重名的人不少比如多个王维、多个李煜。光靠名字没法区分得结合朝代和生平信息。简单做法是用作者名朝代作为唯一标识复杂一点可以引入生卒年。第三是标点和断句。不同来源的诗词标点风格不一致有的用全角逗号有的用半角有的干脆没有标点。这会影响后面的分词和情感分析。统一转成标准中文标点是清洗的必做项。import re def clean_poem_text(text): # 统一标点 text text.replace(,, ).replace(., 。) text text.replace(?, ).replace(!, ) # 去除多余空白 text re.sub(r\s, , text) # 异体字归一示例实际需要更完整的映射表 variant_map {沈: 沉, 澹: 淡} for k, v in variant_map.items(): text text.replace(k, v) return text这段代码看着简单但映射表要慢慢积累。我的经验是先把数据跑一遍统计出高频的异常字符再针对性补映射比一上来就写几百条规则高效得多。2.3 数据量级与分布的把控毕设项目不需要追求百万级数据但也不能太少。我的建议是至少覆盖 5000 到 10000 首诗并且朝代分布要相对均衡。如果全是唐诗那宋词相关的查询就答不上来图谱会显得很单薄。统计分布的时候重点看三个维度朝代分布、作者分布、体裁分布。如果某个作者的诗占了总量的 30%那图谱会严重偏向这个人问答系统问别的诗人就容易出问题。这时候要么补充数据要么在采样时做加权平衡。3. 知识图谱怎么建本体设计与 Neo4j 落地3.1 先想清楚本体再动手建图知识图谱这东西最怕的就是上来就写代码建节点建到一半发现关系设计不合理推倒重来。所以第一步一定是本体设计也就是先想清楚这个领域里有哪些实体类型它们之间有哪些关系。古诗词领域的本体我最后定下来的是这样几类实体和关系实体类型包括诗人Poet、朝代Dynasty、诗作Poem、意象Imagery、流派School、地点Location、官职Official。关系类型包括诗人-所属朝代BELONGS_TO、诗人-创作诗作WROTE、诗作-包含意象CONTAINS、诗人-属于流派MEMBER_OF、诗作-提及地点MENTIONS、诗人-担任官职HELD。这个设计不是拍脑袋来的而是从用户可能问什么问题倒推的。比如用户会问李白写过哪些带月亮的诗那就需要诗人-诗作和诗作-意象两条关系会问苏轼属于哪个流派就需要诗人-流派关系。本体设计的核心原则是关系要能支撑查询而不是为了好看。3.2 Neo4j 建模的实操细节选 Neo4j 是因为它对图查询的支持最成熟Cypher 语法也好上手社区资料多毕设答辩时老师也认。安装和启动这里不展开重点讲建模时容易出问题的地方。第一个坑是节点去重。Neo4j 默认不会自动去重你如果直接CREATE同一个诗人会被创建多次。正确做法是用MERGE它会先查再建。// 创建诗人节点用 MERGE 避免重复 MERGE (p:Poet {name: 李白, dynasty: 唐}) SET p.birth 701, p.death 762 // 创建诗作节点 MERGE (poem:Poem {title: 静夜思, author: 李白}) SET poem.content 床前明月光疑是地上霜。举头望明月低头思故乡。 // 建立关系 MATCH (p:Poet {name: 李白}) MATCH (poem:Poem {title: 静夜思}) MERGE (p)-[:WROTE]-(poem)第二个坑是索引没建查询慢到怀疑人生。数据量上万之后不建索引的MATCH会全图扫描。一定要给常用查询字段建索引CREATE INDEX poet_name IF NOT EXISTS FOR (p:Poet) ON (p.name); CREATE INDEX poem_title IF NOT EXISTS FOR (po:Poem) ON (po.title); CREATE INDEX imagery_name IF NOT EXISTS FOR (i:Imagery) ON (i.name);第三个坑是意象抽取的粒度。古诗词里的意象非常丰富月明月月光蟾宫其实都指向月亮。如果每个词都建一个节点图谱会爆炸且查询困难。我的做法是建一个意象同义词表把变体归一到标准意象比如明月皓月玉盘都归到月。3.3 实体抽取规则 模型双管齐下诗人、朝代、诗题这些是结构化字段直接映射就行。真正难的是意象、地点、流派这些需要从正文里抽的实体。我的方案是规则和模型结合。规则部分用词典匹配维护一个意象词典月、花、酒、剑、柳、雁等和地点词典长安、洛阳、江南、塞北等用 Aho-Corasick 算法做多模式匹配速度快。模型部分用 BERT 做命名实体识别处理词典覆盖不到的边界情况。import ahocorasick def build_imagery_automaton(imagery_dict): A ahocorasick.Automaton() for idx, word in enumerate(imagery_dict): A.add_word(word, (idx, word)) A.make_automaton() return A def extract_imagery(text, automaton): results [] for end_index, (idx, word) in automaton.iter(text): results.append(word) return list(set(results))实测下来词典匹配能覆盖 80% 以上的常见意象剩下的用模型补。这样既保证了速度又保证了召回。要注意的是意象抽取要考虑上下文比如月在明月几时有里是意象在三月里是时间不能一刀切。简单做法是加否定词表复杂做法是上序列标注模型。4. 大模型接入让问答系统真正会说话4.1 为什么不能只靠图谱查询知识图谱擅长精确查询但用户不会用 Cypher 语法提问。用户问的是李白和杜甫谁更厉害这种模糊问题图谱答不了。这时候就需要大模型做两件事一是把自然语言转成图谱查询NL2Cypher二是对图谱返回的结果做自然语言组织。还有一种情况是图谱里根本没有的知识比如这首诗表达了什么感情这就要靠大模型直接生成。所以大模型在这个系统里不是可有可无的装饰而是连接用户和图谱的桥梁。4.2 本地部署还是调用 API这是毕设里绕不开的选择。调用 API 的好处是省事、效果好但有两个问题一是要花钱二是答辩现场网络不一定稳。本地部署的好处是可控、离线可用但对硬件有要求。我的建议是优先考虑本地部署小参数模型比如 7B 级别的模型用 4-bit 量化后消费级显卡8G 显存以上就能跑起来。这样答辩时不怕断网也不用担心额度。如果硬件实在不够再考虑 API 方案但一定要做好降级处理——API 调不通时能回退到纯图谱查询。本地部署用 Ollama 或者 llama.cpp 都行Ollama 更省心。启动之后通过 HTTP 接口调用import requests def ask_llm(prompt, modelqwen2:7b): url http://localhost:11434/api/generate payload { model: model, prompt: prompt, stream: False } response requests.post(url, jsonpayload) return response.json().get(response, )4.3 NL2Cypher 的提示词设计让大模型把自然语言转成 Cypher关键在提示词。提示词里必须包含图谱的 schema 信息否则模型不知道有哪些节点和关系。我的提示词模板大概是这样你是一个 Neo4j Cypher 查询生成器。已知图谱包含以下结构 节点Poet(name, dynasty), Poem(title, content), Imagery(name), Dynasty(name) 关系(Poet)-[:WROTE]-(Poem), (Poem)-[:CONTAINS]-(Imagery), (Poet)-[:BELONGS_TO]-(Dynasty) 请把下面的问题转成 Cypher 查询只输出查询语句不要解释 问题李白写过哪些包含月亮的诗实测下来7B 模型在 schema 清晰的情况下简单查询的转换准确率能到 70% 左右复杂查询多跳、聚合会明显下降。所以我的做法是加一层校验生成的 Cypher 先做语法检查再试执行失败就回退到预设的模板查询。这样虽然不够智能但保证了系统不会直接崩。提示提示词里一定要明确只输出查询语句否则模型会加一堆解释文字解析起来很麻烦。另外模型对中文实体名的处理不如英文稳定必要时可以在提示词里给出几个示例few-shot。4.4 问答结果的组装与兜底图谱查询返回的是结构化数据直接丢给用户太生硬。我的做法是把查询结果再喂回大模型让它组织成自然语言。比如查询返回静夜思、月下独酌、古朗月行模型输出李白写过不少与月亮相关的诗比如《静夜思》《月下独酌》《古朗月行》……。这里要注意幻觉问题。模型可能会编造不存在的诗所以组装结果时必须把图谱返回的原始数据作为约束提示词里明确只能基于以下信息回答不要编造。如果图谱没查到结果就直接告诉用户没有找到相关诗作不要硬编。5. 情感分析模型从标注到训练5.1 情感标签体系怎么定古诗词的情感分类不像电商评论那样只有正负两极。我最后定的是六类豪放、婉约、悲凉、闲适、思乡、爱国。这个分类参考了传统的诗词风格划分也兼顾了标注的可操作性。标签体系定得好不好直接决定模型能不能训出来。如果类别之间边界模糊比如悲凉和思乡经常重叠标注一致性就会很差。我的经验是先做小规模试标找三五个人标 200 首看一致性。如果某两类的混淆率超过 30%就要考虑合并或者重新定义。5.2 标注数据的获取策略纯人工标注 5000 首诗工作量太大毕设周期扛不住。我的策略是弱监督 人工校验。先用规则给一部分诗打上初始标签比如含剑马沙场的打豪放含泪愁断肠的打悲凉。然后用这些弱标注数据训一个初始模型再用模型去预测剩下的人工只校验置信度低的部分。这样能把人工标注量压缩到 1000 首左右同时保证整体质量。要注意的是弱标注的规则不能太粗否则初始模型偏差太大后面越训越歪。5.3 模型选型与训练细节情感分析本质是文本分类BERT 系列是首选。我用的是中文预训练模型做微调输入是诗词正文输出是六分类。from transformers import BertTokenizer, BertForSequenceClassification import torch tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels6 ) def predict_emotion(text): inputs tokenizer(text, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): outputs model(**inputs) pred torch.argmax(outputs.logits, dim1).item() return pred训练时有几个细节值得说。第一是最大长度诗词一般不长128 足够设太大浪费显存。第二是学习率微调 BERT 用 2e-5 到 5e-5 比较稳太大容易训崩。第三是类别不平衡如果某类样本特别少要用加权损失或者过采样。实测下来在 5000 条标注数据上BERT 微调能到 75% 左右的准确率。这个数字不算高但诗词情感本身就主观能到这个水平已经可用。如果想再提升可以试试用大模型做数据增强或者引入意象特征作为辅助输入。5.4 情感分析结果的可视化光有分类结果不够毕设要好看得做可视化。我的做法是单首诗显示情感雷达图整个数据集显示情感分布饼图再按朝代、作者做交叉分析。比如唐代豪放诗占比 vs 宋代豪放诗占比这种对比图在答辩时很加分。可视化用 ECharts 或者 Pyecharts 都行前端直接嵌。要注意的是图表要能交互鼠标悬停显示具体数值点击能下钻到具体诗作这样才显得系统完整。6. 系统集成与那些文档不会写的坑6.1 前后端怎么串后端用 Flask 或 FastAPI 都行我用的 FastAPI因为异步支持好调大模型接口时不阻塞。前端用 Vue 或者直接 HTML ECharts。接口设计上主要分四类图谱查询、智能问答、情感分析、数据统计。图谱查询和情感分析是同步接口直接返回结果。智能问答因为要调大模型耗时较长建议做成流式返回用户体验好很多。FastAPI 的StreamingResponse可以搞定。6.2 性能与体验的平衡系统跑起来之后最影响体验的是大模型的响应速度。7B 模型在消费级显卡上生成 100 字大概要 3 到 5 秒。如果用户每问一句都等这么久体验很差。我的优化手段有三个一是缓存常见问题把高频问答对存起来命中直接返回二是限制生成长度问答场景不需要长篇大论设 max_tokens 为 200 足够三是异步处理前端先显示正在思考结果出来再渲染。6.3 答辩时最容易被问的问题根据我的经验答辩老师最可能问这几个图谱为什么用 Neo4j 不用关系型数据库大模型为什么选这个参数规模情感分析的准确率怎么评估的标注数据怎么保证质量这些问题要提前准备好答案。比如第一个核心回答是诗词实体间是多对多关系图数据库在关系查询上天然优于关系型数据库多跳查询性能差距明显。第二个回答要结合硬件条件和效果权衡不要只说因为显存不够。6.4 项目还能怎么扩展如果时间充裕这个系统还有不少可扩展的方向。比如加入多模态给诗配图或者做诗意图生成比如加入推荐功能根据用户喜欢的诗推荐相似作品比如把问答从单轮扩展到多轮支持上下文追问。我个人觉得最有价值的是多轮对话因为用户问诗词往往是递进的问完李白写过哪些月亮诗可能接着问其中哪首最有名。单轮问答满足不了这种需求多轮才能真正像个助手。最后分享一个实操小技巧整个项目开发过程中一定要把数据处理的每一步都存中间结果。清洗后的数据、抽取的实体、构建的图谱、训练的模型每一步都落盘。这样出了问题可以快速定位不用从头跑。我当初就是因为没存中间结果改一个清洗规则重跑了三个小时血的教训。另外代码一定要模块化数据清洗、图谱构建、模型训练、接口服务分开写别全塞一个文件里。答辩的时候老师看代码结构清晰是加分项。Git 提交记录也要规范别最后一天一次性提交那样一看就是赶出来的。