简介:一套基于知识图谱的百科知识问答平台毕业设计源码,后端采用Python/Django,前端为HTML/CSS/JS,数据库使用MySQL与Neo4j,附说明文档、LW笔记和汇报PPT,适合毕业设计、课程设计或知识图谱问答入门实践。压缩包共444个文件,总大小约171MB,主要包含py源码、html/css/js前端页面、jar依赖库、Neo4j图数据库存储文件、sql脚本、docx/pptx文档及bat/ps1环境脚本,目录结构清晰,便于按模块学习。已有63人浏览学习。资料从知识建模、数据存储到问答解析提供了完整工程,内置数据库和依赖可直接运行,能帮助理解Django项目结构、知识图谱构建方法及前端交互逻辑,也可作为答辩展示和二次开发的基础。
1. 拿到这套百科问答源码,先弄清楚它到底解决了什么
很多刚接触 Django 的人会误以为“知识图谱问答平台”是一个需要大量机器学习参与的 NLP 项目,实际上拿到这套源码你会看到,核心并不是训练模型,而是把百科知识结构化之后,用一套确定性匹配规则把用户的自然语言问题映射成图数据库查询。这套项目的前后端是完整的:Django 负责路由、鉴权和业务逻辑,MySQL 存结构化元数据和用户行为,知识图谱部分则负责承载实体和关系的三元组,前端用 Django 模板语言(即标题里说的 html 部分)直接渲染页面,不需要单独搭 Vue 或 React 前端。
对谁有参考价值?首先是正在做课程设计或者毕业设计的学生,这套源码里带了说明文档、LW(论文)和 PPT,结构上就是按答辩材料组织的;其次是刚入手 Django+MySQL 这套组合、想看看“一个完整项目如何分层”的后端初学者。我自己拿到这种项目包的第一反应不是急着跑起来,而是先看它的 ER 图和查询模板是怎么设计的,因为知识图谱问答的效果上限在数据建模时就定死了,后面写的视图函数只是在消费这个模型。
这套平台能做的事情,用一句话概括:用户输入“李白是哪个朝代的诗人”,系统解析出实体“李白”和属性“朝代”,去图数据库里查出对应关系,再把答案以自然语言句子返回到页面上。整套流程不涉及训练模型,也不依赖外部 API,属于典型的“规则在前、数据为王”的落地实现。你不需要一台带 GPU 的服务器,普通笔记本把 MySQL 和 Neo4j(或项目里用的图数据库)装好就能跑。
2. Django 项目的骨架拆解:从目录结构到 MySQL 数据建模
2.1 拿到项目压缩包后我通常先看这四个文件
一套完整的 Django 百科问答项目,解压之后你应该先按这个顺序去读代码,而不是急着python manage.py runserver。
requirements.txt:确认 Django 版本、MySQL 驱动(mysqlclient 还是 pymysql)、图数据库驱动(neo4j 的neo4jPython 驱动或py2neo);settings.py里的DATABASES和INSTALLED_APPS:看它连的是本地 MySQL 还是远程,App 拆了几个;urls.py主路由:看接口是按index、search、qa这样分的还是按 App 分的;- 图数据库初始化脚本(通常是
kg_data/或onto_data/下的 .cypher 或 .csv 文件):这是整个项目的数据源头。
my_qa_platform/ ├── manage.py ├── knowledge_app/ # 核心应用 │ ├── models.py # ORM 模型:Question/History 等 │ ├── views.py # 视图:首页渲染、问答接口、搜索 │ ├── utils/ │ │ ├── query_parser.py # 问句解析模板 │ │ └── kg_client.py # 图数据库连接与查询 │ ├── templates/ │ │ ├── index.html │ │ └── result.html ├── static/ # CSS/JS/图片 ├── manage.py └── requirements.txt这个结构里最值得关注的是utils/query_parser.py和kg_client.py—— 前者是把“用户问的一句中文”转成“图查询模板”的地方,后者封装了所有对知识图谱的读写操作。项目里大部分的业务逻辑都会汇聚在这两个文件里。
2.2 MySQL 表结构设计:为什么问句历史要单独建表
百科问答平台里,知识本身不放在 MySQL 里,但有两类数据必须放:一类是平台运行数据,比如用户提问历史、错误日志;另一类是辅助语料,比如同义词映射表和意图分类规则。我见过很多初学者把实体属性直接塞进 MySQL,结果图数据库反而成了装饰品,这是典型的建模失误。
CREATE TABLE question_history ( id INT AUTO_INCREMENT PRIMARY KEY, user_query VARCHAR(500) NOT NULL, answer_text TEXT, is_hit BOOLEAN DEFAULT FALSE, parse_template VARCHAR(100), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE synonym_map ( id INT AUTO_INCREMENT PRIMARY KEY, original_term VARCHAR(100) NOT NULL, mapped_entity VARCHAR(100) NOT NULL, category VARCHAR(50) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;上面这段建表语句是给 Django 的models.py做参考的。question_history用来记录每一次问答,is_hit字段很关键——它标记这条问题是否在图谱中匹配到了答案,后续优化查询模板时直接按这个字段筛数据即可。synonym_map表是同义词归一化的基础,“李白”和“诗仙”指向同一个实体,靠的就是这张表中mapped_entity字段的约束。utf8mb4字符集在这里是必选项,因为百科数据里有生僻字和特殊符号,utf8mb4才能完整覆盖。
在 Django 的models.py中,对应的 ORM 写法是:
from django.db import models class QuestionHistory(models.Model): user_query = models.CharField(max_length=500) answer_text = models.TextField(null=True, blank=True) is_hit = models.BooleanField(default=False) parse_template = models.CharField(max_length=100, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "question_history"db_table显式指定表名,避免 Django 自动加前缀导致和手动创建的 SQL 表对不上。另外,Django 默认不建synonym_map这张表的 ORM 映射——你是可以把它当成原始表用objects.raw()去查的,但更规范的做法是为它也建一个模型类。手动建表后让 Djangoinspectdb反向生辰模型代码,是一条省事的路径。
2.3 创建 App 并注册:Django 项目里最常见的卡点
热词里有一条是“django创建app”,这正是这套项目里最常见的实操起点。进入虚拟环境后,在项目根目录执行:
python manage.py startapp knowledge_app这条命令会在根目录下生成knowledge_app/文件夹,包含views.py、models.py等骨架文件。注意:startapp 之后必须去settings.py里的INSTALLED_APPS手动注册,否则 Django 不认这个 App,迁移和模板加载都会出问题。
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'knowledge_app', # 新注册的 App ]对 Python 环境还不熟悉的同学,建议先查pip list | grep -i django(Linux/macOS)或pip freeze | findstr django(Windows)确认当前环境里已安装 Django。另外热词里提到“python django搭建web项目”,对应的初始化命令是django-admin startproject my_qa_platform .,注意结尾的点表示在当前目录生成,避免嵌套一层同名文件夹。
3. 知识图谱的构建与查询:从三元组到 Cypher 语句
3.1 百科知识图谱的数据模型怎么定
知识图谱的核心是三元组(实体, 关系, 属性),对应到图数据库里就是两个节点(实体)和一条边(关系),属性的值可以挂在节点上作为节点的 property。百科类问答平台最常见的模型是:
- 实体节点:人物、地点、作品、朝代、事件;
- 关系边:出生于、属于哪个朝代、代表作、作者是、发生于;
- 节点属性:出生日期、逝世日期、简介、别称。
CREATE (lb:Person {name: '李白', dynasty: '唐', birth_year: 701}) CREATE (tc:Poem {title: '静夜思', genre: '五言绝句'}) CREATE (lb)-[:WROTE {year: 726}]->(tc)这个 Cypher 片段演示了节点和关系的创建。lb和tc是变量的别名,Person和Poem是标签(相当于类型),花括号里是属性键值对。-[:WROTE]->表示从lb指向tc的一条有向边。year是边的属性,可以记录这段关系发生的时间。
在生产项目里,数据量不可能靠手写 Cypher 完成,常见做法是准备 CSV 文件,再用LOAD CSV批量导入。我通常把实体文件和关系文件分开:
LOAD CSV WITH HEADERS FROM 'file:///entities.csv' AS row CREATE (:Entity {name: row.name, type: row.type, intro: row.intro})file:///指向 Neo4j 安装目录下的import文件夹。WITH HEADERS表示首行是列名,AS row把每行数据映射成一个 map 对象,后续直接取row.name、row.type这种字段。这块儿的坑在于 CSV 文件路径和字符编码,utf-8无 BOM 是底线,Windows 上用 Excel 另存的 CSV 容易带 BOM 头导致第一个字段名解析错乱,处理方式是用 VS Code 重新保存成 UTF-8。
在kg_client.py里,封装一个通用查询函数是整个后端最值得复用的部分:
from neo4j import GraphDatabase class KGClient: def __init__(self, uri, user, password): self.driver = GraphDatabase.driver(uri, auth=(user, password)) def query(self, cypher, params=None): with self.driver.session() as session: result = session.run(cypher, params or {}) return [record.data() for record in result] def close(self): self.driver.close()GraphDatabase.driver()的作用是建立一个连接池,with self.driver.session()是上下文管理器,会在执行完自动归还连接。record.data()能把 Cypher 返回的记录直接转成 Python 字典,前端模板遍历时非常方便。这里的uri通常是bolt://localhost:7687,user/password是 Neo4j 初始化时设置的。
3.2 问句如何映射成查询:一套不依赖 NLP 的解析方案
百科问答平台里最核心的技术点是“自然语言问题到 Cypher 的转换”。不引入复杂模型的前提下,常见做法是用模板匹配 + 词槽填充。先把常见问题分成几类意图:属性查询、关系查询、列表查询、计数查询。
patterns = [ { "intent": "attribute_query", "template": r"(.+?)是(.+?)(的|是)(什么|谁|哪里)", "cypher": "MATCH (n {name: $entity}) RETURN n.$attr AS answer" }, { "intent": "relation_query", "template": r"(.+?)和(.+?)是什么关系", "cypher": "MATCH (a {name: $e1})-[r]-(b {name: $e2}) RETURN type(r) AS answer" } ]用re模块去匹配用户输入,命中哪个正则就往对应的 Cypher 模板里填空。比如“李白的代表作是什么”,正则会把$entity填成“李白”,$attr填成“代表作”,然后交给图数据库执行。这里的正则表达式需要严谨处理中文标点,(的|是)这种多分支匹配要放在最前面,否则贪婪模式会把整句话吃掉。
对于“李白”“诗仙”这种同义实体,在查询前先查synonym_map表做归一化:
def normalize_entity(raw_entity): cursor = connection.cursor() cursor.execute("SELECT mapped_entity FROM synonym_map WHERE original_term = %s", (raw_entity,)) row = cursor.fetchone() return row[0] if row else raw_entitynormalize_entity()里先查 MySQL 再走图数据库,这种“先关系库后图库”的两段式查询是这类问答平台的标准姿势。如果原词能映射到标准实体就替换,映射不到就当扩展实体查,保证查询不漏数据。
3.3 属性名的映射矛盾:图谱里叫 dynasty,用户问“朝代”
一个非常隐蔽但高频出现的问题是,图数据库里的 property 名称是英文(比如dynasty),而用户问句里是中文“朝代”。解决方案是维护一张属性名映射表:
attr_alias = { "朝代": "dynasty", "出生年份": "birth_year", "代表作": "famous_work", "别称": "alias" }先normalize_attr()把中文属性名转成图里的真实属性名,再拼进 Cypher。这套思路比在 Neo4j 里做全文检索要稳,因为属性数量是可穷举的,一张字典就能覆盖。每次问答系统答不上来,优先查是不是属性名映射漏了——这是排错的第一优先事项。
4. 前后端链路打通:Django 视图、模板渲染和问答接口实战
4.1 视图函数如何把图谱数据填进 html 模板
在 Django 里,views.py中的函数接收request,返回render渲染后的页面。对问答平台来说,最关键的是qa视图——它接收 GET 参数、调解析器、查图库、把结果放进模板上下文。
from django.shortcuts import render from .utils.query_parser import parse_query from .utils.kg_client import KGClient def qa_view(request): question = request.GET.get('q', '').strip() if not question: return render(request, 'qa_page.html', {'error': '请输入问题'}) kg = KGClient('bolt://localhost:7687', 'neo4j', 'your_password') try: plan = parse_query(question) result = kg.query(plan['cypher'], plan['params']) if result: answer = result[0].get('answer', '未找到答案') else: answer = '数据库中没有对应答案,换个问法试试' except Exception as exc: answer = f'查询异常:{exc}' kg.close() return render(request, 'qa_result.html', {'question': question, 'answer': answer})request.GET.get('q', '')是从 URL 里取查询参数,比如访问/qa?q=李白是哪个朝代的,这里就拿到了“李白是哪个朝代的”。parse_query返回一个字典,里面包含拼好的cypher和params,这样视图层不用关心解析细节。try...except是必须的——图库连接超时或者 Cypher 语法错误都属于运行期异常,不捕获的话用户会看到 500 页面。
在模板qa_result.html里,展示答案的方式是:
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>问答结果</title> </head> <body> <h2>你的问题:{{ question }}</h2> <div class="answer-box"> {% if error %} <p style="color: red;">{{ error }}</p> {% else %} <p>{{ answer }}</p> {% endif %} </div> <a href="/">返回继续提问</a> </body> </html>{{ question }}和{{ answer }}是 Django 模板变量,由视图函数里render的第三个参数(字典)填充。{% if error %}是模板标签,用来做条件渲染。这里的关键点是,视图函数里的 key(比如question)必须和模板里的变量名完全一致,否则渲染出来是空白。
4.2 首页与搜索:一个平台不只一个页面
百科问答平台的完整体验应该是:首页展示热门实体和入口,用户点实体名可以跳转到明细页。Django 里给这套设计两个路由,在urls.py里配置:
from django.urls import path from . import views urlpatterns = [ path('', views.index_view, name='index'), path('qa', views.qa_view, name='qa'), path('entity/<str:name>', views.entity_detail, name='entity_detail'), ]<str:name>是 Django 路径转换器,它会截取 URL 中对应位置的一段字符串作为name参数传给entity_detail视图。entity_detail里做的事是:拿 name 去图谱里查实体属性,找到就渲染详情页,找不到跳转到 404 页。
首页视图则可以做实体推荐:从图库里随机取几个节点展示到页面上,让用户一眼就知道这个平台能问什么。
def index_view(request): kg = KGClient('bolt://localhost:7687', 'neo4j', 'your_password') hot_entities = kg.query("MATCH (n:Entity) RETURN n.name AS name LIMIT 10") kg.close() return render(request, 'index.html', {'entities': hot_entities})LIMIT 10限制返回条数,避免首页渲染卡顿;kg.close()放在return之前是因为图数据库连接是资源占用型对象,不关闭会占满连接池。热词里有“html一键返回顶部算法”,这种纯前端脚本放在static/js/里由模板引用,与后端视图无关,但可以加在首页右侧做长页面锚点定位。
4.3 Django ORM 的历史记录落库与查询
问答结果返回的同时,把用户问题和是否命中写入question_history表,是后续优化问句模板的数据来源。
from .models import QuestionHistory def qa_view(request): # ... 前面拼接 answer 的逻辑 ... QuestionHistory.objects.create( user_query=question, answer_text=answer, is_hit=(answer != '数据库中没有对应答案,换个问法试试') ) # ... 渲染模板 ...objects.create()是 Django ORM 里最直接的插入方式,内部会自动执行 INSERT 语句并返回新对象。存储历史后,后续可以写一个管理命令:“统计没有命中的问题,按高频词找出未覆盖的问法”,这是知识图谱问答平台持续迭代的常规手段。
5. 知识图谱只显示 25 个标签的坑与处理
热词里有一句“知识图谱只显示25个标签”,这是 Neo4j Browser 的默认显示策略,不是数据问题。浏览器端的图谱可视化默认只渲染前 25 个标签节点,防止页面卡死。解决思路有两个:一是在 Cypher 里LIMIT自己控制返回数量,二是到 Neo4j Browser 的设置里调大Max nodes阈值。
但要注意,前端页面如果接了ECharts 或 D3.js做图谱可视化,这个 25 的限制就不是 Neo4j 的事了,而是你的 Python 后端传给前端的数据量。推荐的做法是在视图层做分页或按实体类型过滤:
def graph_data(request): kg = KGClient('bolt://localhost:7687', 'neo4j', 'your_password') entity_type = request.GET.get('type', 'Person') cypher = f"MATCH (n:{entity_type})-[r]->(m) RETURN n.name AS source, type(r) AS relation, m.name AS target LIMIT 100" data = kg.query(cypher) kg.close() return JsonResponse({'nodes': data})LIMIT 100是硬上限,避免接口被前端拖死。type(r)返回关系类型字符串,前端拿到source、relation、target三元组后直接喂给 ECharts 的 graph 系列即可。需要人工遍历大量节点时,不用一次全量拉取,按实体类型拆分请求,配合前端懒加载体验更好。
6. 优化问句匹配的三板斧:属性归一化、关系扩展和数据回灌
问答平台上线后你会发现,模型跑通只是第一步,真正决定好不好用的是匹配率和答案质量。我通常会做三轮优化。
第一板斧是属性归一化。用户不会按图谱设计者的命名来提问,“生卒年”“寿命”“活了多少岁”都可能指同一组属性。把这类表达收集起来,扩充attr_alias字典即可。每次遇到答不上来的问题,把问题文本记进question_history表,定期统计is_hit=False的记录,识别高频未命中模式。
第二板斧是关系扩展。初始图谱可能只有一级关系,比如“李白写过的诗”。但用户可能问“杜甫写过哪些诗”,这个图谱里若没有WROTE边的反转查询索引,就需要在 Cypher 里处理反向遍历:
MATCH (p:Person {name: $entity})-[:WROTE]->(poem) RETURN poem.title AS answer如果想让平台回答“为李白写过诗的人有哪些”,那就是反方向遍历MATCH (p:Person {name: $entity})<-[:WROTE]-(other)。这两种方向都建索引,可以显著提高查询效率——Neo4j 里的索引要手动建:
CREATE INDEX entity_name IF NOT EXISTS FOR (n:Entity) ON (n.name)IF NOT EXISTS保证可重复执行,不会因索引已存在而报错。CREATE INDEX ... FOR语法是 Neo4j 4.x 之后的写法,老版本用CREATE INDEX ON :Entity(name),换版本过了记得核对语法。
第三板斧是数据回灌。把用户问过但没有答案的问题作为新知识补充入口,设计一个简易审核后台,运营人员可以把新三元组通过页面直接写进图库。回灌的代码路径是:
def add_triple(request): head = request.POST.get('head') relation = request.POST.get('relation') tail = request.POST.get('tail') kg = KGClient('bolt://localhost:7687', 'neo4j', 'your_password') cypher = f"MERGE (a {{name: $head}}) MERGE (b {{name: $tail}}) MERGE (a)-[r:{relation}]->(b)" kg.query(cypher, {'head': head, 'tail': tail}) kg.close() return JsonResponse({'status': 'ok'})MERGE的语义是“存在就匹配,不存在就创建”,比CREATE更适合数据回灌,不会重复造节点。这个接口放在 Django 的管理员页面或自定义 action 里均可,关键点是relation需要通过白名单校验,不能直接拼接用户输入,否则图库会被注入恶意关系名。最后别忘了使用python manage.py runserver时,修改模板和静态文件是不需要重启服务的,但修改views.py和新建文件后需要按 Ctrl+C 重启,开发过程中注意这个差别,可以少浪费很多等待时间。
本文还有配套的精品资源,点击获取