
簡介這是一份基于知識圖譜的心理咨詢智能問答系統完整項目適合計算機、人工智能、電子信息等相關專業的在校生用于畢業設計、課程設計或初期項目演示也適合想學習知識圖譜與問答系統結合開發的開發者進階參考。資源共2000個文件壓縮包29.42MB主體為1804個Python源碼文件另有大量文本說明、配置文件、HTML頁面、C擴展頭文件、JSON/XML數據以及Cypher查詢腳本等覆蓋后端問答邏輯、圖譜數據構建、前端展示與運行環境配置等多個模塊。已有77人學習下載。通過該項目可掌握從心理咨詢領域知識抽取、圖譜存儲到基于圖譜的語義匹配與答案生成的完整實現思路同時附帶文檔說明與作者遠程教學支持便于快速跑通并二次擴展。代碼注釋與運行說明齊備下載后可直接對照文檔啟動服務。1. 知識圖譜驅動的心理咨詢問答從「關鍵詞匹配」走向「關系推理」傳統 FAQ 客服遇到「最近總是半夜醒來白天昏昏沉沉」這種描述大概率答非所問因為「半夜醒來」和「失眠」在字面上完全不同。心理咨詢領域的問題恰恰高度口語化用戶很少直接說出術語而知識圖譜的實體-關系結構能把「早醒、易醒、入睡困難」歸到「睡眠障礙」之下再沿「應對策略」這條邊找到「睡眠衛生」建議。下面要展開的是一條可復現的完整鏈路設計心理領域本體用規則和輕量模型從文本抽實體關系導入 Neo4j 存圖再實現意圖識別、實體鏈接和查詢生成落地成一個能跑起來的小型智能問答系統。標題里的「源碼文檔說明」對應的是可獨立運行的 Python 項目結構和一份能講清數據來源、實體字典、啟動參數的項目說明文檔。這條鏈路適合正在做垂直領域知識圖譜構建的同學也適合想給現有客服系統加關系推理能力的后端工程師。2. 先搭骨架心理領域圖譜的本體設計與初始數據準備2.1 實體與關系的頂層設計先想清楚「用戶會怎么問」知識圖譜項目最容易翻車的地方是一上來就到處抓數據。心理領域本身邊界模糊「情緒」「癥狀」「障礙」經常混著說如果不對實體類型做約束后面每條查詢都可能踩到類型不明的邊上。我一般先把用戶真實問題收集 200 條左右哪怕是從公開問答、客服日志里手工整理再反向設計本體。這里給出一個最小可用的本體設計。實體類型不用多六類足夠起步實體類型說明例子Symptom癥狀/表現失眠、早醒、心慌、手抖Emotion情緒狀態焦慮、抑郁、煩躁Disorder心理障礙廣泛性焦慮障礙、抑郁癥Strategy應對策略/干預方式認知行為療法、正念呼吸Scale心理量表PHQ-9、SAS、GAD-7Population人群屬性大學生、產后女性、老年人關系類型圍繞問答路徑來定不是學術本體里那種嚴格分類。核心關系就 5 種HAS_SYMPTOM 表示 Disorder 到 Symptom 的包含關系比如抑郁癥常伴早醒SYMPTOM_OF 是它的反向冗余查詢時省去方向判斷RELIEVED_BY 連接癥狀或情緒到應對策略是求助建議類問題的主干RECOMMEND_SCALE 連接癥狀到篩查量表RISK_FOR 表示障礙之間的風險傳導比如長期失眠會增加抑郁風險。從用戶問法倒推關系設計是知識圖譜構建里比較省力的一條路。用戶問「焦慮怎么辦」實際在查 Emotion 到 Strategy 的緩解路徑問「失眠是不是抑郁癥」需要的是 Symptom 到 Disorder 的歸屬路徑再加障礙描述兩條路徑的組合。把可能問法寫成一棵樹每種問法對應一條圖路徑本體就基本穩定了。2.2 用結構化數據準備第一批三元組本體設計完之后先用 Python 腳本準備首批三元組輸出成 TSV 給后續導入用。這一步的目的是把「已確認的知識」先沉淀下來形成可校驗的基線數據為后續規則抽取和人工審核提供參照。# build_kg.py # 定義首批三元組輸出為 Neo4j 可導入的 TSV triples [ # (head, relation, tail, head_type, tail_type) (失眠, RELIEVED_BY, 認知行為療法, Symptom, Strategy), (失眠, RELIEVED_BY, 睡眠限制療法, Symptom, Strategy), (早醒, SYMPTOM_OF, 抑郁癥, Symptom, Disorder), (焦慮, RELIEVED_BY, 正念呼吸, Emotion, Strategy), (廣泛性焦慮障礙, HAS_SYMPTOM, 坐立不安, Disorder, Symptom), ] # 實體表和關系表分開輸出導入圖數據庫只需要兩個文件 entities {} for h, r, t, ht, tt in triples: entities.setdefault(h, ht) entities.setdefault(t, tt) with open(entities.tsv, w, encodingutf-8) as f: f.write(name\ttype\n) for name, typ in entities.items(): f.write(f{name}\t{typ}\n) with open(relations.tsv, w, encodingutf-8) as f: f.write(head\trelation\ttail\n) for h, r, t, _, _ in triples: f.write(f{h}\t{r}\t{t}\n)注意這里的關系方向保留了兩條冗余邊HAS_SYMPTOM 和 SYMPTOM_OF 同時存在。因為在問答環節用戶可能從「這是什么病」問也可能從「這個癥狀說明什么」問方向冗余可以少寫一半的查詢分支。生產環境可以把這段腳本改成讀取一個kg.csv掛到 CI 里做完整性檢查比如檢查實體表里每個關系兩端是否都有對應節點。2.3 實體歸一與別名擴展心理領域術語口語化程度極高。用戶說「睡不好」「睡不著」「入睡困難」不完全是一個意思但在圖譜里都要能落到「失眠」或至少落到「睡眠障礙」這類父級節點上。所以除了本體表還必須維護一張別名表。# aliases.py # 別名表口語表達 - 標準實體名 ALIASES { 失眠: [睡不著, 睡不好, 入睡難, 夜不能寐], 早醒: [總是醒得早, 凌晨兩三點醒, 天沒亮就醒], 焦慮: [莫名心慌, 坐立不安, 靜不下來, 容易緊張], 抑郁: [開心不起來, 對什么都沒興趣, 活著沒意思], } # 反向索引供后續實體鏈接模塊使用 ALIAS_INDEX {} for entity, names in ALIASES.items(): for name in names: ALIAS_INDEX.setdefault(name, entity) ALIAS_INDEX.setdefault(entity, entity) # 標準名本身也要能命中這段代碼是后面實體鏈接的召回基礎。進階做法是把別名也建成節點用 ALIAS_OF 關系掛到標準實體下那一條 Cypher 就能做近義詞匹配。第一版先放在內存字典里等別名條數超過 5000 再考慮入圖或落到 Redis。3. 從非結構化文本抽取知識實體識別與關系抽取的工程取舍3.1 領域字典 AC 自動機最小成本的 NER 方案前面那份結構化數據只能覆蓋已經規范的條目真正讓圖譜有增量能力的是從咨詢記錄、科普文章、問答社區文本里抽取新的三元組。做知識圖譜構建的人都知道最常見的入門做法是先用領域字典做實體識別而不是立刻上 BERT。心理領域術語比較固定加上口語別名后一個幾千詞的字典能覆蓋大部分實體表達。我一般用pyahocorasick構建 AC 自動機做多模式匹配一次掃描能命中所有字典詞速度上完全夠用。# ner.py import ahocorasick def build_automaton(vocab): # vocab: {失眠: Symptom, 焦慮: Emotion, ...} a ahocorasick.Automaton() for word, etype in vocab.items(): a.add_word(word, (word, etype)) a.make_automaton() return a def extract_entities(text, automaton): hits [] # automaton.iter 返回 (end_index, (word, entity_type)) for end_idx, (word, etype) in automaton.iter(text): start_idx end_idx - len(word) 1 hits.append({start: start_idx, end: end_idx, word: word, type: etype}) # 按位置合并重疊結果優先保留最長匹配 merged [] for h in sorted(hits, keylambda x: (x[start], -(x[end] - x[start]))): if merged and h[start] merged[-1][end]: continue merged.append(h) return mergedAC 自動機的優勢是構建好后掃描是線性的對長文本尤其劃算。需要注意字典詞之間的重疊比如「焦慮」和「廣泛性焦慮障礙」同時存在時應該保留更長匹配否則同一位置會抽出兩個互相矛盾的實體。上面代碼用「按開始位置排序、重疊時跳過后續短詞」的方式做了簡單處理。3.2 關系抽取先上規則模板別急著訓練模型實體抽出來之后關系抽取是更麻煩的一環。預訓練關系模型在通用領域尚可在心理領域沒有像樣的監督數據時效果未必比規則好。第一版我通常用 spaCy 的中文依存句法加規則模板# relation_extract.py import spacy nlp spacy.load(zh_core_web_sm) def extract_symptom_of(sent): # 只處理系表結構X 是/為 Y 的表現、癥狀、征兆 doc nlp(sent) for token in doc: if token.text in {是, 為}: subj [w for w in token.lefts if w.dep_ in {nsubj, top}] obj [w for w in token.rights if w.dep_ in {attr}] if subj and obj: yield (subj[0].text, SYMPTOM_OF, obj[0].text)這條規則覆蓋面很窄但它立住了「模板 依存關系」這個框架。實際工程中模板會寫成 JSON 配置文件判斷條件包括觸發動詞、依存關系、兩側實體類型約束比如「緩解」只能連接 Symptom/Emotion 到 Strategy。把這個函數改成讀取配置文件里的一組模板就能在不改代碼的情況下持續加規則。3.3 什么時候值得上 BERT 微調以及怎么退回去當標注數據超過 1000 條、且模板 F1 連續一兩周提不上去時才值得做序列標注微調。常見做法是標注 BIO 格式用 transformers 加載中文預訓練模型跑 token 分類。數據量從 1000 漲到 5000F1 提升如果不到 2 個點說明領域特征已接近上限這時候止損繼續用「字典 模板 人工審核」的組合更劃算。無論用哪種方式抽出來的三元組都要過一輪人工審核。我通常按天導出待審核文件格式是三列head|relation|tail一條條過錯誤的直接刪行并在旁邊標注原因。這個審核文件本身就是后續模型微調的天然訓練集不要丟棄。4. 把三元組交給圖數據庫Neo4j 導入與查詢設計4.1 標簽與關系類型映射讓圖結構貼近問答邏輯實體表和關系表準備好以后導入 Neo4j 之前要先把 schema 定下來。節點標簽直接用實體類型關系類型用大寫連字符。尤其要注意的是name屬性必須在同類型節點內唯一否則實體鏈接時會出現一對多歧義。// schema.cypher CREATE CONSTRAINT symptom_name IF NOT EXISTS FOR (n:Symptom) REQUIRE n.name IS UNIQUE; CREATE CONSTRAINT emotion_name IF NOT EXISTS FOR (n:Emotion) REQUIRE n.name IS UNIQUE; CREATE CONSTRAINT disorder_name IF NOT EXISTS FOR (n:Disorder) REQUIRE n.name IS UNIQUE; CREATE CONSTRAINT strategy_name IF NOT EXISTS FOR (n:Strategy) REQUIRE n.name IS UNIQUE; CREATE INDEX scale_name_idx IF NOT EXISTS FOR (n:Scale) ON (n.name);這里用REQUIRE ... IS UNIQUE是 Neo4j 4.4 之后的語法如果還在用 3.x要改成CREATE CONSTRAINT ON (n:Symptom) ASSERT n.name IS UNIQUE。把實體類型寫進約束名出問題時能快速定位是哪張節點表排查效率高不少。4.2 用 LOAD CSV 做可重復的批量導入批量導入的第一選擇是 LOAD CSV它比neo4j-admin import更靈活支持增量追加也方便從 Python 代碼里觸發。我這里用「先清空再導入」的冪等寫法方便反復重跑// import.cypher :auto USING PERIODIC COMMIT 500 LOAD CSV FROM file:///entities.tsv AS row FIELDTERMINATOR \t WITH row WHERE row[0] name CALL apoc.create.node([row[1]], {name: row[0]}) YIELD node RETURN count(*); // 關系導入按 head 和 tail 的名稱匹配兩端節點 :auto USING PERIODIC COMMIT 500 LOAD CSV FROM file:///relations.tsv AS row FIELDTERMINATOR \t WITH row WHERE row[0] head MATCH (h {name: row[0]}) MATCH (t {name: row[2]}) CALL apoc.create.relationship(h, row[1], {}, t) YIELD rel RETURN count(*);這段腳本依賴 APOC 插件沒裝的話可以改回MATCH ... CREATE的形式但會慢不少。特別注意WHERE row[0] name是為了跳過 TSV 表頭。導入完成后做一次連通性抽查比如以「失眠」為中心展開兩跳看返回的相鄰節點是否符合預期如果結果為空優先檢查文件名和 import 目錄權限Neo4j 只允許讀取dbms.directories.import配置目錄下的文件。4.3 把「用戶問法」翻譯成圖查詢圖譜設計得再好落不到查詢上都是零。設計 Cypher 時我始終帶著用戶原話來寫而不是拿標準實體測試。用戶說「我焦慮得睡不著怎么辦」分詞出來至少兩個實體焦慮、睡不著。那么查詢就要覆蓋兩條路徑// 方案 A先取焦慮的緩解策略 MATCH (n:Emotion {name: $emotion})-[:RELIEVED_BY]-(s:Strategy) RETURN s.name AS strategy LIMIT 5;實際問答系統里一條用戶問句會映射出多條候選路徑最后按「路徑覆蓋實體數」排序。這個排序邏輯放在 Python 層因為 Cypher 里做會比較繞。參數$emotion由實體鏈接模塊傳入永遠不要用字符串拼接的方式把用戶輸入直接塞進查詢。5. 問答系統核心鏈路意圖識別、實體鏈接與答案生成5.1 意圖識別先分對「用戶在干嘛」問答系統最外層的分類是意圖識別。心理咨詢場景下意圖不需要太多五類夠用求助建議典型問法是「怎么辦」「怎么緩解」癥狀了解典型問法「是什么」「為什么」量表推薦問「要不要做測試」「怎么評估」緊急干預檢測到自傷或極端情緒傾向直接轉人工不生成答案寒暄不在系統能力范圍內用固定話術回應。實現上不需要上大模型。幾百條帶標注的問法就能訓練一個輕量 fastText 分類器或者直接用關鍵詞正則先頂著# intent.py import re INTENT_PATTERNS { 求助建議: [怎么辦, 怎么緩解, 如何改善, 有什么辦法, 幫幫我], 癥狀了解: [是什么, 為什么, 怎么回事, 算不算], 量表推薦: [量表, 自測, 測試, 評估一下, 評分], 緊急干預: [想自殺, 不想活了, 活著沒意思, 傷害自己], 寒暄: [你好, 在嗎, 嗨], } def detect_intent(text): for intent, patterns in INTENT_PATTERNS.items(): for p in patterns: if re.search(p, text): return intent return 求助建議 # 領域默認意圖默認意圖設成求助建議是因為心理咨詢場景里「怎么辦」是頻次最高的一類問題。如果拿不準直接問一句澄清也不是不行比給出一個錯誤的確定答案體驗更好。5.2 實體鏈接從口語表達落到圖譜節點實體鏈接分兩步先召回、再消歧。召回用前面維護的別名表把文本里命中的口語別名映射到標準實體消歧則利用圖結構信息。這里給一個按節點度數排序的消歧方案# link.py def link_entities(text, alias_index, driver): hits list({entity for alias, entity in alias_index.items() if alias in text}) if len(hits) 1: return hits ranked [] for name in hits: # 度數越高說明該實體在圖譜中和其它實體關聯越緊密 result driver.execute_query( MATCH (n {name: $name})-[r]-() RETURN count(r) AS degree, namename, ) ranked.append((result[0][degree], name)) ranked.sort(reverseTrue) return [name for _, name in ranked]注意driver.execute_query是較新驅動的寫法如果用的是 4.x 老版本改成session.run(...).single()[degree]即可。暴力遍歷別名表效率不高但第一版數據量小時勝在簡單。消歧依據也不只有度數還可以加「實體類型與意圖的相容性」求助建議意圖下優先選 Strategy癥狀了解意圖下優先選 Symptom這個交叉約束能明顯改善鏈接準確率。5.3 查詢編排與模板化答案生成意圖和實體都確定后進入查詢編排。每個意圖對應一組 Cypher 模板模板參數由實體鏈接結果填充。如果命中多個實體就按順序依次查詢取第一條有結果的路徑。# qa.py def answer_question(text, driver, alias_index, automaton): intent detect_intent(text) if intent 緊急干預: return 你目前的狀態聽起來很難受。請先聯系身邊信任的人并盡快撥打當地心理援助熱線系統不做應急響應。 if intent 寒暄: return 我還不太會閑聊你可以聊聊失眠、焦慮、情緒低落這些話題。 entities link_entities(text, alias_index, driver) candidates [] for entity in entities: if intent 求助建議: result driver.execute_query( MATCH (n {name: $name})-[:RELIEVED_BY]-(s) RETURN s.name AS name LIMIT 5, nameentity, ) if result: candidates.append(f針對「{entity}」可以嘗試) candidates [f- {r[name]} for r in result] elif intent 癥狀了解: result driver.execute_query( MATCH (n {name: $name})-[r]-(d) WHERE type(r) IN [SYMPTOM_OF, HAS_SYMPTOM] AND d:Disorder RETURN DISTINCT d.name AS disorder, nameentity, ) if result: candidates.append(f「{entity}」常見于) candidates [f- {r[disorder]} for r in result] elif intent 量表推薦: result driver.execute_query( MATCH (n {name: $name})-[:RECOMMEND_SCALE]-(s) RETURN s.name AS scale LIMIT 3, nameentity, ) if result: candidates.append(建議關注以下量表) candidates [f- {r[scale]} for r in result] if not candidates: return 這塊超出了我目前能回答的范圍換個說法試試或者把情況描述得更具體一些。 return \n.join(candidates)這個函數刻意把查詢和答案生成耦合在一起為的是讓人一眼看懂鏈路。實際工程里查詢模板要拆成獨立配置模塊方便在不改代碼的情況下調整語句答案模板單獨維護文案更新不觸發發版。所有 Cypher 一律用$name參數不能直接拼接字符串這能省掉大量轉義麻煩。5.4 兜底策略澄清、風險轉交與體驗兜底沒有命中的時候最忌諱的是重復播報同一句「抱歉」。更合理的做法是引導用戶補信息提取到部分實體但沒有足夠關系時反問「你是想了解怎么緩解還是想評估一下嚴重程度」沒有提取到任何實體時給出幾個常用入口比如「可以聊聊失眠、焦慮、情緒低落、人際關系壓力」風險語句命中后除了返回固定文案還要把對話原文寫入高風險日志表標記為需要人工回訪。這些兜底邏輯決定了系統第一天上線的體驗。問答系統的口碑往往不是來自答對的那 80%而是來自沒答對時有沒有讓人不舒服。6. 用「路徑回溯」評估問答效果并把源碼整理成可交付狀態6.1 評測集設計準確率之外更要看路徑命中率知識圖譜問答的評估不能只看最終答案對不對還要看中間環節有沒有走錯。我按「意圖、實體、路徑、答案」四個維度逐條標注測試集每一條問法都寫清楚期望的圖路徑。問法期望意圖期望實體期望路徑備注我睡不好怎么辦求助建議失眠Symptom → RELIEVED_BY → Strategy別名要能映射焦慮是一種病嗎癥狀了解焦慮Emotion → SYMPTOM_OF → Disorder需要復合推理我需不需要做測試量表推薦(null)直接返回量表引導話術無實體也要能答每次跑完記錄三個指標意圖正確率、實體鏈接正確率、答案路徑命中率。路徑命中率比端到端準確率更有診斷價值——答案錯了但路徑對了多是文案問題路徑錯了才是圖譜或鏈接的問題。6.2 在問答日志里加一條 trace排錯效率翻倍排查線上問題時光看用戶原話和返回答案不夠還得知道中間發生了什么。我給每個問答請求生成 trace_id把意圖、鏈接到的實體、執行的 Cypher、命中的路徑數、答案模板編號按 JSON Lines 格式追加進日志log_entry { trace_id: trace_id, question: question, intent: intent, linked_entities: entities, cypher: executed_queries, path_hits: path_hits, answer_tpl: template_ids, } # 寫入 answers_trace.jsonl行級別 JSON方便 grep 和接入日志分析系統實際代碼里就是每次回答前初始化一個 dict在鏈路各個節點往里塞字段最后統一落盤。排查時拿用戶原話搜日志馬上能看到是意圖分錯了、實體沒鏈接上還是圖譜里根本沒有這條路徑不用再靠猜。6.3 源碼目錄結構與文檔說明至少要有這些內容標題里帶著「源碼文檔說明」交付時目錄結構要讓人拿到就能跑。最小可用結構是psy_qa/ ├── data/ # 實體表、關系表、別名表、評測集 ├── scripts/ # 導入腳本、審核腳本、構建腳本 ├── kg/ # 本體定義、NER 與關系抽取管道 ├── server/ # 問答接口、意圖識別、答案生成 ├── cypher/ # schema、import、查詢模板 ├── tests/ # 評測集和回歸腳本 └── README.mdREADME 里最容易被忽略的三塊數據來源說明哪些是人工整理、哪些是規則抽取、審核到什么程度環境依賴精確到 Python 版本和 Neo4j 版本一條從空庫開始跑到第一次問答成功的完整命令序列。把這三塊寫清楚比把源碼注釋寫得像小說有用得多。新接手的人照著命令跑通一次再改起來才有底氣。提示把人工審核記錄也提交進倉庫它是圖譜迭代最重要的依據。注意圖譜規模超過 5 萬三元組后實體鏈接別再遍歷內存字典先把別名索引落到 Redis 或 Elasticsearch 里否則單條問答延遲會明顯超過 200ms。本文還有配套的精品資源點擊獲取