简介:一份基于OneKE模型构建知识图谱并搭建问答系统的Python源码与文档资料,面向计算机、人工智能、自动化等相关专业的毕设、课程设计或从业者进阶学习。项目完整覆盖了实体与关系抽取、图谱模式定义、数据转换、Neo4j导入及问答检索模块,全部代码均经过调试与测试,可稳定运行;整体方案清晰,答辩评分达98分,适合作为高分毕设参考。压缩包共27个文件,大小约2.87MB,以JSON(图谱schema与结果)、Python脚本(数据处理与转换)、CSV(三元组结果)、Cypher(图谱导入语句)为主,另含PNG流程图与README说明,便于理解处理和导入流程。三元组抽取、知识融合等环节均给出可运行代码和示例数据,并提供处理前后的JSON对比输出,方便逐模块调试;已有450人学习下载,文档对关键步骤有解释,既能满足新人入门,也可供进阶者在此基础上二次扩展。
1. 用 OneKE 抽三元组建图谱再搭问答系统:这条路能不能跑通
把 Python 和知识图谱放在一起,最常被问的一句话是:不手动标注,到底能不能把文本变成图谱?OneKE 这类统一抽取模型给出的答案是能——它把实体识别和关系抽取当成一个生成式任务,喂一段中文原文,直接回结构化 SPO 三元组(Subject-Predicate-Object,主语-谓语-宾语),不再需要先训一个 NER 再训一个关系分类器。
这份基于 OneKE 构建知识图谱并搭建问答系统的 Python 源码加文档,走的就是这条路:先抽取 SPO,再对齐导入 Neo4j,最后把图谱接进 RAG 问答链路。我拆完以后觉得它适合两类人:一类是毕设和课程设计要快速产出可演示系统的学生,答辩评审拿到 98 分说明整套流程是能闭环的;另一类是手里有垂直行业文本,想先建图谱再接问答、验证这条路值不值得趟的从业者。下面按我实际复现的顺序,把脚本怎么调、参数怎么改、哪些环节最容易翻车说清楚。
2. OneKE 做 SPO 抽取:SPO_trans.py 把文本变三元组的完整流程
这个项目里,抽取是整个链路的起点,也是决定图谱质量的关键。我这里花最多时间的地方就在SPO_trans.py和它的 schema 配置上,先把这部分吃透,后面 Cypher 导入和问答才不会白干。
2.1 生成式抽取和传统序列标注的差别,决定了你要不要换方案
以前做中文知识图谱,通用做法是拿 BERT 加 CRF 做序列标注,输出 BIO 标签,把实体边界标出来,然后再单独训练一个关系分类器判断实体对之间是什么关系。这一套在垂直领域效果不差,但成本摆在那里:标注语料要人工整理,模型要重新训练,换一个领域又要重新标注。对毕设项目或者中小型业务来说,时间成本往往比算力成本更敏感。
OneKE 走的不是这个路线。它属于生成式信息抽取框架,输入是一段文本,输出直接是结构化的三元组,实体边界和关系类别在一个解码阶段里同时决定。好处有两个:第一,不需要为每个领域重新训练模型,通过修改 schema 提示就能把抽取范围约束到当前业务;第二,对中文的短语边界、嵌套实体处理得比老式 BIO 模型更稳,因为它在解码时能看到全文上下文,而不是只盯着一个窗口。
在这个资源里,SPO_trans.py封装的正是这套调用逻辑。你不用关心权重怎么部署,只需要把文本准备好、把 schema 写对,然后跑脚本拿结果。项目里带有sample.json、sample.txt和几个输出 json,就是给你对照用的。
2.2 SPO_trans.py 怎么调用,输出长什么样
我实际复现时,先看的是README.md,然后直接跑抽取脚本。核心命令是:
python SPO_trans.py \ --input sample.json \ --schema SPO_schema.json \ --output SPO_output.json这里三个参数各干一件事:--input指向待抽取的样本数据,sample.json的结构一般是 JSON 数组,每个元素至少带id和text两个字段;--schema指向SPO_schema.json,它告诉 OneKE 当前任务要抽哪些实体类型和关系类型;--output是抽取结果落盘路径,也就是SPO_output.json。
如果你的机器有多块 GPU,我一般会在命令前面加环境变量指定设备,否则容易把显存占满:
CUDA_VISIBLE_DEVICES=0 python SPO_trans.py \ --input sample.json \ --schema SPO_schema.json \ --output SPO_output.json跑完之后,SPO_output.json里的数据大概是这个结构:
[ { "id": "doc-001", "triples": [ { "subject": "北京智源人工智能研究院", "relation": "发布", "object": "悟道2.0", "confidence": 0.97 }, { "subject": "悟道2.0", "relation": "参数规模", "object": "1.75万亿", "confidence": 0.89 } ] } ]注意看,输出里每个三元组都带confidence。这个置信度不是拿来做摆设的,我建议在写业务代码时设一个阈值,比如 0.85 以下的直接过滤,能明显减少低质量三元组对图谱的污染。id字段对应的是输入文本的编号,方便后续回溯是哪句话抽出来的,排查问题的时候特别有用。
2.3 sample_trans.py 和 schema 先对齐,再开工
原始文本往往不是干净的 JSON,项目里的sample_trans.py就是干这个的。它的作用是把纯文本转换成SPO_trans.py能识别的结构:
python sample_trans.py \ --input sample.txt \ --output sample.json \ --split-by-sentence--split-by-sentence这个参数值得多说一句。OneKE 这类生成式模型对输入长度有限制,把整篇几千字的文章一次性喂进去,后面的三元组大概率被截断。按句子切分后,每句单独成一条记录,既不超过模型长度限制,又保留了语义完整性。切分粒度可以选句子,也可以选段落,我一般处理知乎类长文时选段落,处理新闻短句时选句子。
接下来把SPO_schema.json打开,它是抽取效果的边界。项目里默认的配置大致长这样:
{ "entity_types": ["人物", "公司", "产品", "地点"], "relation_types": ["就职于", "发布", "研发", "投资"], "language": "zh" }entity_types限定要抽哪些类别,relation_types限定关系类型。如果你做的是医疗领域,把实体类型改成“药物、疾病、症状”,关系类型改成“适应症、副作用、禁忌”即可。这里有个小经验:relation_types 尽量用短字符串,不要带空格和标点,因为后面它会被直接映射成 Neo4j 里的关系类型名,带特殊字符会给你自己埋坑。
提示:改 schema 时不要把实体类型设得过大。之前我试过一次性抽 20 类实体,结果模型把很多名词都当作实体候选,抽出来的 SPO 散成一盘沙。控制在 5 类以内,准确率要稳得多。
3. 三元组到 Neo4j 图谱:KG_trans.py 和 Cypher 导入全流程
SPO 拿到手只是半成品,下一步要把这些三元组变成真正的图结构。这个环节牵涉到实体对齐、去重、关系类型映射,还有最关键的两条 Cypher 导入路径。项目里的KG_trans.py和KG_import.cypher是这条链路的纽带。
3.1 KG_trans.py 到底做了什么
如果直接把SPO_output.json里的三元组一条条导入 Neo4j,你会得到一张全是重复节点的图——同一家公司在十篇文档里出现十次,就会生成十个节点。KG_trans.py首要任务就是实体对齐和归并。它对subject和object做归一化处理,比如去掉繁体转简体的差异、去除空格和全角字符,然后给每个实体分配唯一的内部 ID。
运行命令如下:
python KG_trans.py \ --spo SPO_output.json \ --schema KG_schema.json \ --output KG_output.json参数含义:--spo指定上一步的抽取结果,--schema指定图谱结构定义文件,--output输出对齐后的图谱数据。这里注意KG_schema.json和SPO_schema.json是两个不同的文件,许多小白会搞混,我拆项目时做过一个对比表:
| 文件 | 作用 | 什么时候改 |
|---|---|---|
| SPO_schema.json | 定义 OneKE 抽取的实体类型和关系类型 | 换领域、调整抽取范围时 |
| KG_schema.json | 定义图谱中节点的属性、关系的方向和类型 | 设计图谱结构、决定怎么存时 |
SPO_schema管的是“抽什么”,KG_schema管的是“怎么存”。如果SPO_output.json里有关系类型研发,但KG_schema.json里定义的关系类型叫研发了,两者对不上,导入后查询就会落空。
KG_trans.py还会同时输出KG_output_handled.json和KG_output.json。我理解handled后缀的文件是经过人工修正或规则修正后的结果,当你发现某个实体在同音异形字上出现问题时,把修正逻辑加进去再重新生成 handled 版本,后续导入以它为基准。
3.2 两个 Cypher 导入脚本,实际用哪个
项目里给了两个导入文件:SPO_import.cypher和KG_import.cypher。它们的关系不是二选一,而是顺序执行:先按 SPO 原始结构建基础节点和关系,再根据 KG schema 做属性归并和类型补全。我复现时直接执行第二个文件就够了,但如果你改了 schema,两个都要同步。
看一下SPO_import.cypher里的核心语句结构:
MERGE (s:Entity {name: "北京智源人工智能研究院"}) SET s.category = "organization" MERGE (o:Entity {name: "悟道2.0"}) SET o.category = "product" MERGE (s)-[r:发布]->(o) RETURN count(r) AS created_relations这段 Cypher 的关键是MERGE而不是CREATE。CREATE无条件插入,跑十遍就会产生十份重复数据;MERGE会先匹配,存在就跳过,不存在才创建,保证幂等。SET s.category是在给实体打标签,这个category字段的值,最好在KG_schema.json里提前定义好。
执行导入的方式有两种。如果你用的是 Neo4j Desktop,直接把 cypher 文件拖进浏览器执行框就能跑;如果是社区版服务端,我一般用命令行:
cat SPO_import.cypher | cypher-shell -u neo4j -p your_password如果遇到认证问题,看下 Neo4j 的conf/neo4j.conf里dbms.security.auth_enabled是否被改过。这类坑我放在下一章细说。
3.3 导入完成怎么核对,别等问答阶段才发现图是空的
导入完成后,我做的第一件事不是写问答脚本,而是跑几条计数查询确认图谱规模:
MATCH (n:Entity) RETURN n.category AS category, count(*) AS cnt ORDER BY cnt DESC;MATCH ()-[r]->() RETURN type(r) AS relType, count(*) AS cnt ORDER BY cnt DESC;第一条告诉你每个类别下有多少实体,第二条告诉你每种关系有多少条边。拿项目里自带的KG_result.csv和SPO_result.csv对照,看两边的数字是否在合理区间。如果某个关系类型的边数是零,大概率是KG_trans.py的关系映射没覆盖到那种类型。
每章的关键行数太少,我会让这段代码后面对应一份 check。还有一点,所有节点名都用:Entity,实际开发中如果你有预案区分“人物”和“公司”,可以在 Cypher 里用:Entity统一承载,把具体类别放属性里,这样做问答检索时写查询更简单。
4. 避坑:OneKE 建图加问答最容易踩的五个坑
这个项目整体能跑通,但中间有几个环节的失败率明显高于其他部分。下面五条是我复现和改代码时最容易翻车的地方,按照我踩坑的顺序列出来,每条都按现象、原因、解决的路子说清楚。
4.1 模型跑得慢但没报错,以为是死机
现象:SPO_trans.py跑在 CPU 机器上,等了十分钟没有输出,控制台也没报错,看起来像卡死。
原因:OneKE 是生成式模型,生成三元组时要逐 token 解码,CPU 推理速度比 GPU 慢一个数量级。而且默认生成长度可能被设得很大,模型在一段长文本上反复计算,时间就拖起来了。
解决:确认代码里有没有传入max_new_tokens参数,我一般设置成 512 以内,因为一个 SPO 三元组的 token 数通常不超过 100。同时对长文本先按句切分再逐句抽取,避免单条输入过长。项目里someshell.sh是一个可参考的封装脚本,它把切分、抽取、转换成图谱数据串成一条流水线,我建议你打开它看一眼执行顺序再动代码。
4.2 同一个实体在 JSON 里两种写法,图谱出现双节点
现象:KG_output.json里“百度公司”和“百度”同时存在,导入 Neo4j 后图谱里有大量相近节点,查询路径被截断。
原因:实体对齐逻辑用的是精确字符串匹配,没有做别名归一化。“百度公司”和“百度”、“腾讯科技”和“腾讯”在文本里都可能出现,OneKE 不会帮你做实体融合,它只负责抽取。
解决:在KG_trans.py里维护一张别名到规范名的映射表,比如百度公司、百度在线都映射到百度。我一般会直接用 Python 字典或 CSV 维护这个映射,跑完 SPO 之后先过一遍映射再做对齐。
4.3 中文实体导入 Neo4j 后变成乱码
现象:在 Neo4j Browser 里看到实体名显示成??或一堆\u转义序列。
原因:导入 Cypher 文件时终端和文件的编码不一致。Windows 上默认可能是 GBK,而 Neo4j 的 cypher-shell 按 UTF-8 读取,字节流解析错位就乱码。
解决:所有脚本的输出文件统一用 UTF-8 编码写入,比如json.dump(..., ensure_ascii=False)是最容易被忽略的——如果没写ensure_ascii=False,JSON 里的中文会变成\uXXXX,Cypher 导入时 Neo4j 会当成普通字符串处理。在 Linux 上执行file -i SPO_import.cypher检查编码,看到charset=utf-8再继续。
4.4 SPO 里的关系类型和 KG_schema 定义不一致
现象:图谱导入时没有报错,但问答阶段查某个关系类型总是空结果。
原因:SPO_trans.py按SPO_schema.json抽取,得到的关系类型是中文“发布”;而KG_schema.json里用的是英文PUBLISH。KG_trans.py做关系映射时没匹配上,导致漏建了一部分边。
解决:从项目一开始就固定一套关系类型字符串,让SPO_schema.json和KG_schema.json保持一致。如果你需要存中文关系名,两个文件都用中文;如果存英文,两个都用英文。混合使用是最折磨人的。改完KG_schema.json后,把KG_output.json里所有 relation 字段扫一遍,写个小脚本统计类型分布,确认没有漏网之鱼。
4.5 问答阶段让大模型直接生成 Cypher,多跳问题频繁出错
现象:想省事,让 LLM 把用户问题直接转换成 Cypher,结果生成出来的查询语句要么语法报错、要么把节点名写错,尤其在实体名称含括号和空格时,十条里错四条。
原因:生成式模型对 Cypher 语法的精确性要求很高,一对括号、一个引号错误就会导致整条查询失败。多跳问题上,模型还要自己推断出正确的路径模式,难度更大。
解决:不要拿 NL-to-Cypher 作为主线方案。改成“先实体链接,再子图检索”的路线,把检索到的三元组拼成文本喂回大模型,由大模型做自然语言生成。核心是让模型理解图数据,而不是让模型生成图查询。
5. 从图谱到 RAG 问答:用子图检索搭建可以落地的问答链路
图谱建好之后,问答系统的设计决定了你这个项目是“能演示”还是“能实战”。我拆到的这套方案没有直接走“先向量化文档、再相似度检索”的老路,而是把知识图谱本身作为检索源,这在这类场景里通常更稳。
5.1 图谱问答和普通 RAG 的差别在哪
普通 RAG 的做法是:把文档切块、向量化、存进向量库,用户提问时召回相似度最高的几个块,拼进 Prompt 让大模型回答。这个方案适合开放域问答,但在“多跳问题”上表现很差——“谁投资了开发某个芯片的公司”这种问题,答案分散在两三条边上,单独的文本块里根本没有完整路径。
知识图谱问答的优势在于关系路径是显式存好的。从实体 A 出发,沿着关系走到实体 B,再走到实体 C,每一步都是结构化的。所以链路设计核心变成了:先定位问题里的实体,再环绕实体检索子图,最后把子图序列化成文本交给生成模型。对比一下:
| 方案 | 适用场景 | 弱项 |
|---|---|---|
| 向量检索 | 开放域、答案分散在段落中 | 多跳推理、关系判断基本靠蒙 |
| 图谱子图检索 | 结构化、关系明确、多跳问题 | 实体识别失败时直接断链 |
实际项目中,这两者可以组合使用,但主链路应该以图谱子图检索为主,向量检索只是兜底。
5.2 第一步:实体链接,先定位问题里的起点
实体链接的作用是把自然语言问题中的名词映射到图谱里的实体节点。在这个资源里,常见做法是用 jieba 分词后做词典匹配:
import jieba ENTITY_INDEX = { "智源": "北京智源人工智能研究院", "北京智源": "北京智源人工智能研究院", "悟道": "悟道2.0" } def link_entities(question: str): results = [] for token in jieba.cut(question.strip()): if token in ENTITY_INDEX: canonical = ENTITY_INDEX[token] if canonical not in results: results.append(canonical) return results这段代码里ENTITY_INDEX是别名到规范实体名的映射,函数把所有命中的实体名返回。注意做了一次去重,因为一个别名可能对应多个 token 片段。这一步看似简单,但它是整个问答链路里最容易漏答案的地方——分词工具对多字词识别不准,会在“北京智源人工智能研究院”这种长实体上拆碎。我通常会在这里再加一层:先从题目中抽取候选名词,再和 Neo4j 里的实体名做最长匹配,优先命中最长的那个。
5.3 第二步:子图检索,控制展开深度和边数
实体定位之后,从该实体出发往外展开。这里的核心参数是depth和limit,直接决定查询耗时和结果质量:
from py2neo import Graph graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) def fetch_subgraph(entity: str, depth: int = 2, limit: int = 30): cypher = f""" MATCH (n:Entity {{name: $name}})-[rels*1..{depth}]-(m:Entity) RETURN n, rels, m LIMIT {limit} """ records = graph.run(cypher, name=entity).data() return recordsdepth=2表示从节点出发最多走两步,能覆盖一对多和多跳关系;limit=30是结果上限,防止中间节点太稠密时返回几千条记录把 Prompt 撑爆。如果图谱比较大,我一般还会加上按关系类型过滤,只保留与问题关键词相关的关系,比如问题里出现“投资”,就只展开[:投资]这条边。
这一步有一层转换技巧:Neo4j 返回的数据结构比较原始,需要把每条记录转换成易读的三元组文本,再拼进 Prompt。
5.4 第三步:把子图序列化成文本,交给大模型生成回答
大模型不能直接读图数据,需要把图结构序列化成自然语言。我推荐的格式是每行一条三元组:
def triples_to_text(records) -> str: lines = [] for rec in records: n = rec.get("n") m = rec.get("m") rels = rec.get("rels") if not rels: continue rel = rels[-1] # 取路径上最后一条边 rel_type = type(rel).__name__ s_name = n.get("name", "") if isinstance(n, dict) else n.get("name") o_name = m.get("name", "") if isinstance(m, dict) else m.get("name") lines.append(f"{s_name} {rel_type} {o_name}") return "\n".join(lines)然后把它拼进 Prompt:
prompt = f"""你是知识图谱问答助手。 背景知识(知识图谱三元组): {triples_text} 用户问题:{question} 请仅根据背景知识回答,不要编造事实。如果背景知识不足以回答,请指出信息不足。"""这个 Prompt 结构有三个要点:一是明确告诉模型“仅根据背景知识回答”,减少幻觉;二是要求“信息不足就直说”,避免模型强行作答;三是三元组按行排列,让模型更容易捕捉关系结构。实测下来,这个方案比让大模型自己生成 Cypher 的稳定性高很多,“谁-关系-谁”的格式对模型非常友好。
如果问题中命中了多个实体,把每个实体对应的子图记录合并后统一去重,再一起拼进 Prompt。这一点上代码很简单,就不单独贴了,核心是在fetch_subgraph外面套一层循环。
6. 验证与进阶:让 OneKE 建图和问答项目换成自己的数据
整个项目复现完后,最重要的事情是验证链路,然后把它搬到自己的数据上。不要一开始就拿全量业务数据跑,先拿项目自带的样例数据打通,再逐步替换,能省掉大量排错时间。
6.1 用项目自带数据做验证
压缩包里有sample.txt、sample.json、SPO_test.json、KG_test.json等测试文件,验证顺序我建议严格按链路走:
先跑sample_trans.py把sample.txt转成sample.json,此时可以打开sample.json检查文本字段是否完整、是否有空行;再跑SPO_trans.py得到SPO_output.json,和SPO_test.json对比,看三元组数量和实体名是否有明显缺漏;接着跑KG_trans.py生成KG_output_handled.json,对照KG_result.csv检查实体数量和关系数量;最后执行KG_import.cypher导入 Neo4j,在浏览器里跑MATCH (n:Entity) RETURN count(n)确认节点数,再和图graph.png、example_1.png对比看结构是否一致。
SPO_output_handled.json和KG_output_handled.json这两个文件值得好好利用。它们是已经修正过的版本,如果你在跑完原始脚本后得到的结果与 handled 版本差异很大,说明你的代码版本或依赖版本与原作者不一致,需要回头排查环境问题。我复现时曾因为 Transformers 库版本太新导致抽取结果格式变化,后来 pin 了旧版本才对齐。
6.2 进阶改造的几个方向
验证完之后,如果你想把这个项目改造成自己的东西,我一般会在三个方向上面改:
自定义KG_schema.json,增加时间属性。原项目里的实体只有name和category,如果图谱想表达关系随时间的演化,就在节点上加start_time、end_time这类属性,并在KG_trans.py里从原始文本中抽取时间信息补进去。
升级实体对齐逻辑。从精确匹配升级为别名映射加拼音匹配,处理同音字、简繁体差异。这个改造对中文语料尤其重要,因为中文里同一个机构的全称、简称、旧称差异很大。
把问答链路从“只检索图谱”升级为“图谱加向量混合检索”。图谱子图检索对实体明确的问题效果好,但开放域问题容易断链。加一层向量检索兜底,当子图为空时退回向量检索,能明显提升回答的覆盖范围。
有一件事值得每个人养成习惯:每改一版 schema 或对齐逻辑,都要从头重跑一遍sample_trans.py → SPO_trans.py → KG_trans.py → Cypher 导入这条链路。从那以后我每次拿到新语料建图谱,都强制先把 sample 数据完完整整跑一遍再上业务数据,哪怕改动再小也不跳过。这个习惯帮我挡掉了很多因为 schema 改了但导入脚本没同步造成的低级事故,也让问答环节的排查范围小了很多。希望帮到你。
本文还有配套的精品资源,点击获取