:從源碼到實戰(zhàn)的畢設(shè)指南)
簡介這是一套面向計算機相關(guān)專業(yè)本科生與項目實戰(zhàn)學習者的古詩詞問答系統(tǒng)源碼以知識圖譜為核心技術(shù)路線可作為畢業(yè)設(shè)計、課程設(shè)計或期末大作業(yè)的完整參考方案。項目圍繞古詩詞實體與關(guān)系構(gòu)建圖譜并實現(xiàn)自然語言問答交互適合人工智能、軟件工程等方向的學生理解知識圖譜落地流程。壓縮包共521個文件約50.58MB其中98個py文件承載核心邏輯與圖譜構(gòu)建腳本129個txt與24個json用于詩詞語料及圖譜數(shù)據(jù)存儲另有jpg、css、js、html等前端與靜態(tài)資源以及少量md說明文檔整體結(jié)構(gòu)清晰、模塊劃分明確。目前已有331人學習下載。代碼經(jīng)測試可正常運行答辯評審平均分達96.5分讀者可據(jù)此掌握圖譜構(gòu)建、問答匹配與前后端聯(lián)調(diào)思路并在此基礎(chǔ)上修改擴展功能。1. 從一份本科畢設(shè)源碼說起知識圖譜怎么把古詩詞問答做成能跑的系統(tǒng)很多同學拿到「基于知識圖譜的古詩詞問答系統(tǒng)」這個題目時第一反應(yīng)是去搜一份現(xiàn)成源碼解壓、裝依賴、跑起來然后截圖交差。但真正動手才會發(fā)現(xiàn)古詩詞這個領(lǐng)域和通用百科問答完全不是一回事一首詩有作者、朝代、體裁、意象、典故、情感標簽詩句之間還有化用、唱和、同題的關(guān)系這些關(guān)系用一張二維表根本存不下。知識圖譜的價值就在這里——它把「李白」和「《靜夜思》」和「唐代」和「五言絕句」和「思鄉(xiāng)」這些實體用邊連起來問答時沿著邊走就能推理出答案而不是靠關(guān)鍵詞匹配硬猜。這份本科畢設(shè)源碼要解決的核心問題是讓一個 Python 后端能聽懂「李白寫過哪些思鄉(xiāng)的詩」這類自然語言問題把它翻譯成對圖譜的查詢再把結(jié)果組織成人話返回。適合誰看正在做畢設(shè)、需要一套能講清楚原理又能跑通的代碼的同學也適合想入門知識圖譜構(gòu)建與問答、但被 Neo4j 和 SPARQL 勸退的開發(fā)者。下面我按「圖譜怎么建 → 問句怎么解析 → 查詢怎么執(zhí)行 → 坑在哪」的順序把這條鏈路拆開講。2. 古詩詞知識圖譜的構(gòu)建從 CSV 到 Neo4j 的實體關(guān)系落地2.1 為什么古詩詞適合用圖數(shù)據(jù)庫而不是關(guān)系表先想清楚選型理由不然后面寫代碼全是玄學。古詩詞數(shù)據(jù)的典型特征是「多跳關(guān)系密集」想知道「杜甫寫給李白的詩里提到了哪些地名」路徑是 杜甫 → 贈詩 → 李白 → 詩中地名 → 具體地點這在關(guān)系型數(shù)據(jù)庫里要 join 四張表而在圖數(shù)據(jù)庫里就是一次兩跳遍歷。另一個特征是「schema 不固定」有的詩有典故標注有的沒有圖數(shù)據(jù)庫的邊可以按需擴展不用改表結(jié)構(gòu)。常見做法是用 Neo4j 作為存儲Python 側(cè)用py2neo或官方neo4j驅(qū)動。我一般會先把原始數(shù)據(jù)整理成兩類 CSV節(jié)點文件和關(guān)系文件。節(jié)點包括詩人、詩題、朝代、體裁、意象、情感關(guān)系包括「創(chuàng)作」「屬于朝代」「屬于體裁」「包含意象」「表達情感」「贈予」「唱和」。提示本科畢設(shè)的數(shù)據(jù)量通常在幾千到幾萬條Neo4j 社區(qū)版完全夠用不要一上來就上分布式圖庫那是給自己找麻煩。2.2 用 Python 批量導入節(jié)點和關(guān)系的可復現(xiàn)腳本下面這段腳本做兩件事讀取 CSV用MERGE語句冪等地寫入節(jié)點和關(guān)系。MERGE而不是CREATE是關(guān)鍵重復執(zhí)行不會產(chǎn)生重復節(jié)點。from py2neo import Graph, Node, Relationship import csv # 連接本地 Neo4j默認 bolt 端口 7687 graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) def load_poets(path): with open(path, encodingutf-8) as f: for row in csv.DictReader(f): # MERGE 保證按 name 唯一不會重復建點 poet Node(Poet, namerow[name], dynastyrow[dynasty]) graph.merge(poet, Poet, name) def load_poems(path): with open(path, encodingutf-8) as f: for row in csv.DictReader(f): poem Node(Poem, titlerow[title], contentrow[content], genrerow[genre]) graph.merge(poem, Poem, title) # 建立 詩人-創(chuàng)作-詩 的關(guān)系 poet graph.nodes.match(Poet, namerow[author]).first() if poet: graph.merge(Relationship(poet, 創(chuàng)作, poem)) load_poets(data/poets.csv) load_poems(data/poems.csv)邏輯說明graph.merge(node, label, key)的第三個參數(shù)是唯一性約束字段必須和你在 Neo4j 里建的約束一致否則每次都會新建節(jié)點。參數(shù)上auth里的密碼是你初始化 Neo4j 時設(shè)的不是默認的neo4j第一次登錄會強制改密。Relationship的方向建議統(tǒng)一成「主體→動作→客體」比如「詩人→創(chuàng)作→詩」后面寫查詢時方向不會亂。2.3 意象和情感這類弱實體怎么掛到圖上意象明月、楊柳、孤舟和情感思鄉(xiāng)、送別、懷古不是詩本身但它們是問答的高頻入口。做法是把它們也建成節(jié)點用「包含意象」「表達情感」兩條邊連到詩上。這里有個坑意象抽取如果只靠分詞會把「明月光」切成「明月」和「光」導致誤掛。穩(wěn)妥做法是維護一個意象詞典用詞典匹配而不是純分詞。IMAGERY_DICT [明月, 楊柳, 孤舟, 落花, 流水, 鴻雁] def tag_imagery(poem_title, content): poem graph.nodes.match(Poem, titlepoem_title).first() for word in IMAGERY_DICT: if word in content: img Node(Imagery, nameword) graph.merge(img, Imagery, name) graph.merge(Relationship(poem, 包含意象, img))參數(shù)說明IMAGERY_DICT建議從 50 到 200 個高頻意象起步覆蓋《全唐詩》里出現(xiàn)頻率前 10% 的詞即可貪多反而引入噪聲。執(zhí)行完可以用一句 Cypher 驗證MATCH (p:Poem)-[:包含意象]-(i:Imagery) RETURN count(*)數(shù)字對不上就回去查詞典匹配邏輯。3. 問句解析把「李白寫過哪些思鄉(xiāng)的詩」翻譯成圖譜查詢3.1 意圖識別和實體抽取的分工問答系統(tǒng)的第一道關(guān)卡是理解用戶問了什么。古詩詞問答的意圖其實不多常見就幾類查作者的作品、查某首詩的作者或朝代、查包含某意象的詩、查表達某情感的詩、查詩人之間的關(guān)系。意圖識別可以用規(guī)則模板也可以用輕量分類模型。本科畢設(shè)階段我建議先用規(guī)則打底把準確率做穩(wěn)再考慮上模型。實體抽取要抽的是「詩人名」「詩題」「意象」「情感」「朝代」這幾類。難點在于詩人名和意象可能重疊比如「李白」是詩人但「李」也可能是姓氏泛稱。解決辦法是優(yōu)先匹配長詞并且給每類實體維護獨立詞典。import jieba POETS {李白, 杜甫, 王維, 白居易, 蘇軾} IMAGERY {明月, 楊柳, 孤舟, 落花} EMOTIONS {思鄉(xiāng), 送別, 懷古, 邊塞} def extract_entities(question): words jieba.lcut(question) result {poet: None, imagery: None, emotion: None} for w in words: if w in POETS: result[poet] w elif w in IMAGERY: result[imagery] w elif w in EMOTIONS: result[emotion] w return result邏輯說明jieba.lcut做精確模式分詞然后逐詞比對詞典。參數(shù)上詞典要按長度倒序匹配否則「白居易」可能被「白」先命中。如果問句里同時出現(xiàn)詩人和情感比如「李白寫過哪些思鄉(xiāng)的詩」兩個槽位都要填上查詢時用 AND 連接。3.2 意圖模板和槽位的映射表把意圖和槽位組合成查詢模板是這套系統(tǒng)最核心的設(shè)計。下面這張表是我實際用的映射關(guān)系可以直接抄。意圖觸發(fā)詞示例槽位對應(yīng) Cypher 片段查作品寫過、有哪些詩poet(p:Poet)-[:創(chuàng)作]-(poem)查作者誰寫的、作者是誰title(poem)-[:創(chuàng)作]-(p:Poet)查意象含、出現(xiàn)、描寫imagery(poem)-[:包含意象]-(i)查情感表達、抒發(fā)emotion(poem)-[:表達情感]-(e)查關(guān)系和、贈、唱和poet1, poet2(p1)-[:贈予]-(p2)注意觸發(fā)詞要覆蓋口語變體比如「寫過」和「寫了」都要能命中否則用戶換個說法系統(tǒng)就啞了。3.3 從槽位到 Cypher 的拼裝與參數(shù)化拼 Cypher 字符串時千萬不要用 f-string 直接插用戶輸入那是注入漏洞。正確做法是用參數(shù)化查詢把槽位值作為參數(shù)傳進去。def build_query(intent, slots): if intent 查作品 and slots[poet] and slots[emotion]: return ( MATCH (p:Poet {name: $poet})-[:創(chuàng)作]-(poem)-[:表達情感]-(e:Emotion {name: $emotion}) RETURN poem.title AS title, poem.content AS content ), {poet: slots[poet], emotion: slots[emotion]} if intent 查作者 and slots.get(title): return ( MATCH (poem:Poem {title: $title})-[:創(chuàng)作]-(p:Poet) RETURN p.name AS author ), {title: slots[title]} return None, None邏輯說明函數(shù)返回 Cypher 語句和參數(shù)字典兩部分執(zhí)行時用graph.run(cypher, **params)。參數(shù)說明$poet這種占位符由驅(qū)動負責轉(zhuǎn)義用戶輸入再離譜也不會破壞語句結(jié)構(gòu)。如果意圖和槽位組合沒有對應(yīng)模板返回None上層給用戶一個「暫時沒理解」的兜底回復而不是硬查。4. 查詢執(zhí)行與答案生成讓返回結(jié)果像人話4.1 執(zhí)行查詢并處理空結(jié)果拿到 Cypher 和參數(shù)后執(zhí)行并判斷結(jié)果??战Y(jié)果在古詩詞問答里很常見可能是圖譜里確實沒有也可能是槽位抽錯了。兩種情況要區(qū)分對待如果實體在詞典里但圖譜查不到提示「圖譜中暫無相關(guān)記錄」如果實體根本沒抽出來提示「沒聽懂換個說法試試」。def answer(question): intent detect_intent(question) slots extract_entities(question) cypher, params build_query(intent, slots) if not cypher: return 這個問題我還沒學會換個問法試試 records graph.run(cypher, **params).data() if not records: return 圖譜里暫時沒有找到相關(guān)詩詞。 return format_answer(intent, records)參數(shù)說明graph.run(...).data()返回的是字典列表字段名和 RETURN 里的別名一致。detect_intent可以用關(guān)鍵詞命中實現(xiàn)命中多個意圖時按優(yōu)先級取第一個優(yōu)先級建議是「查關(guān)系 查作者 查作品 查意象 查情感」。4.2 答案模板與多結(jié)果截斷返回多條詩時不要一股腦全倒出來。我一般限制最多返回 5 條超出部分提示「還有 N 條可繼續(xù)追問」。答案模板按意圖區(qū)分查作品返回「詩人 共找到 N 首 詩題列表」查作者返回「《詩題》的作者是 XXX」查意象返回「包含『明月』的詩有……」并附上名句。def format_answer(intent, records): if intent 查作品: titles [r[title] for r in records[:5]] return f共找到 {len(records)} 首包括 、.join(titles) if intent 查作者: return f這首詩的作者是 {records[0][author]}。 if intent 查意象: lines [f《{r[title]}》{r[content][:20]}… for r in records[:5]] return \n.join(lines) return str(records[:5])邏輯說明截斷用切片[:5]同時把總數(shù)len(records)告訴用戶兼顧信息量和可讀性。參數(shù)上content[:20]截取前 20 個字符做摘要避免整首詩刷屏。如果要做成 Web 接口把answer函數(shù)包一層 Flask 路由即可返回 JSON。4.3 用 Flask 暴露一個最小問答接口from flask import Flask, request, jsonify app Flask(__name__) app.route(/qa, methods[POST]) def qa(): question request.json.get(question, ) return jsonify({answer: answer(question)}) if __name__ __main__: app.run(port5000)邏輯說明POST 接收 JSON字段名question返回answer。參數(shù)上端口用 5000和前端聯(lián)調(diào)時注意跨域開發(fā)階段可以加flask-cors。這個接口就是畢設(shè)演示的入口前端頁面或 Postman 都能調(diào)。5. 避坑與排查古詩詞問答系統(tǒng)最容易翻車的 5 個地方5.1 現(xiàn)象Neo4j 連不上報ServiceUnavailable原因通常是三個Neo4j 服務(wù)沒啟動、bolt 端口被防火墻攔了、密碼不對。解決順序是先neo4j status看服務(wù)再telnet localhost 7687看端口最后確認auth里的密碼是不是初始化時改過的那個。本科畢設(shè)常見錯誤是照抄教程里的默認密碼neo4j但實際第一次登錄已經(jīng)強制改密了。5.2 現(xiàn)象節(jié)點重復查「李白」出來好幾條原因是merge時唯一性字段沒對上或者 CSV 里同一詩人名字有空格差異。解決方法是先建唯一性約束CREATE CONSTRAINT FOR (p:Poet) REQUIRE p.name IS UNIQUE再重新導入。導入前用strip()清洗字段全角和半角空格都要處理。5.3 現(xiàn)象問「思鄉(xiāng)的詩」返回空但圖譜里明明有原因是情感節(jié)點沒掛上或者情感詞和用戶輸入不一致。比如圖譜里存的是「思鄉(xiāng)」用戶輸入「想家」詞典沒覆蓋。解決辦法是維護同義詞表把「想家」「思念故鄉(xiāng)」都映射到「思鄉(xiāng)」。排查時直接跑MATCH (e:Emotion) RETURN e.name看圖譜里到底有哪些情感標簽。5.4 現(xiàn)象分詞把詩人名切碎實體抽不出來原因是 jieba 默認詞典不含古詩人名。解決辦法是jieba.add_word(李白)把詩人名和意象詞都加進自定義詞典或者用jieba.load_userdict(dict.txt)批量加載。參數(shù)上自定義詞典每行「詞 詞頻 詞性」三段詞頻給高一點保證優(yōu)先切出。5.5 現(xiàn)象Cypher 查詢慢幾秒才返回原因是沒建索引或者查詢寫了笛卡爾積。解決辦法是給常用查詢字段建索引CREATE INDEX FOR (p:Poet) ON (p.name)詩題、意象名同理。另外檢查 Cypher 里有沒有漏寫關(guān)系方向?qū)е氯珗D掃描EXPLAIN一下執(zhí)行計劃看到AllNodesScan就要警惕。6. 把問答準確率再抬一截同義詞擴展與查詢回退的實操技巧系統(tǒng)能跑通之后真正拉開差距的是準確率。我踩過的最大坑是「用戶說法千變?nèi)f化詞典永遠不夠」。后來我加了一層同義詞擴展效果立竿見影。做法是維護一個synonyms.json把情感、意象、意圖觸發(fā)詞都做同義映射查詢前先歸一化。import json with open(synonyms.json, encodingutf-8) as f: SYNONYMS json.load(f) def normalize(text): for standard, variants in SYNONYMS.items(): for v in variants: if v in text: text text.replace(v, standard) return textsynonyms.json的結(jié)構(gòu)是{思鄉(xiāng): [想家, 思念故鄉(xiāng), 鄉(xiāng)愁], 明月: [月, 月亮, 月光]}。參數(shù)說明標準詞必須和知識圖譜里的節(jié)點名完全一致否則歸一化后還是查不到。變體詞按長度倒序替換避免「月」把「月亮」拆壞。第二層是查詢回退。當精確查詢返回空時不要直接說「沒找到」而是降級先去掉情感槽位只按詩人查再不行只按意象查最后給一個「你是不是想問……」的猜測。這個回退鏈能把很多「差一點就命中」的問題救回來。回退層級查詢條件適用場景第一層詩人 情感完整槽位第二層僅詩人情感詞沒匹配上第三層僅意象詩人名抽錯第四層模糊匹配詩題用戶直接報詩名驗證方法很簡單準備 50 條標注好答案的測試問句跑一遍統(tǒng)計命中率每次改詞典或模板都重跑看數(shù)字是漲是跌。我一般會把測試集和結(jié)果存成 CSV方便對比不同版本。這套東西做完本科畢設(shè)的答辯基本穩(wěn)了因為你能說清楚每一層為什么這么設(shè)計、邊界在哪。最后一個習慣每次改完圖譜或詞典先跑一遍那 50 條測試問句別憑感覺覺得「應(yīng)該沒問題」。我吃過這個虧改了一個意象詞結(jié)果把另一個情感查詢帶崩了答辯前一天才發(fā)現(xiàn)。希望幫到你。本文還有配套的精品資源點擊獲取