简介一款基于 Python3 的本地化知识图谱标注工具源码面向知识图谱构建、NLP 工程与算法研究人员用于解决文本中命名实体和关系的高效联合标注问题。工具通过自定义快捷键和背景色区分实体与关系类型支持撤销、取消标注及文本格式化可导入文本文件、自定义实体与关系模板并将标注结果导出为五元组 CSV 等下游任务数据便于衔接知识图谱构建或机器学习训练。压缩包为 zip 格式共 7 个文件包含 2 个 Python 主程序、实体与关系配置文件、requirements.txt 依赖清单及项目配置整体仅 7KB结构精简适合快速部署和二次开发。目前已有 65 人学习下载。读者可从源码中直接查看核心标注交互逻辑、配置加载方式、颜色模块与标注文件解析思路获得一套可运行的本地标注工具基础实现对需要搭建轻量标注环境、评估标注工具实现方案或在此基础上扩展一键标注与网页部署功能的开发者是一份具备直接参考价值的源码包。1. 知识图谱标注工具源码包与其自己写爬虫攒三元组不如先把标注这件事做对做过实体识别和关系抽取的人都有同感模型调参是玄学但数据标注是体力活。市面上现成的标注平台要么太贵要么绑死云端要么标签体系改不动。这份知识图谱标注工具源码包解决的就是“私有化部署 自定义标签体系 离线标注”这件事。它不依赖外部标注平台前端标注界面可以直接在浏览器里跑后端提供实体识别、关系标注、属性填充的完整流程。适合这类人需要构建特定领域知识图谱的开发者、在做信息抽取但被数据标注卡住的团队、想给客户交付一套可二次开发的标注系统的软件工程师。我第一次拿到这套源码时的感受是功能不多不少但工程结构很诚实该有的都有了。2. 标注体系设计实体、关系、属性三要素决定标注效率知识图谱标注和图像框选标注完全是两回事。图像标注只需要拉一个框写上类别知识图谱标注要求在文本或表格数据里同时完成实体边界、关系方向、属性键值这三类操作。这套源码把这套体系拆分成了可配置的schema驱动结构改配置就能切换领域不需要改动页面代码。2.1 数据模型SPO三元组标注的底层结构知识图谱的底层表达是SPO三元组也就是Subject主语、Predicate谓语、Object宾语。这套标注工具的数据模型围绕这个结构设计每完成一次标注就生产一个SPO实例。在源码里标注记录的存储结构大致是这样一个JSON形态{ id: anno_1024, text: 某公司成立于2005年总部位于某市高新区。, spo_list: [ { subject: { text: 某公司, entity_type: ORG, offset: [0, 3] }, predicate: 成立时间, object: { text: 2005年, entity_type: DATE, offset: [5, 10] } } ] }这段结构里最关键的是offset字段它记录了实体在原文中的起止位置标注工具导出的结果下游模型可以直接消费。源码里对这部分的处理逻辑是前端根据用户鼠标框选的文字动态计算offset后端负责校验重复标注和实体类型冲突。还有一个容易被忽视的设计——谓词predicate没有绑定在固定的实体类型上而是通过schema配置来约束比如成立时间这个谓词只允许ORG类型作为主语这样能减少无效标注。2.2 Schema配置如何用一份JSON换一套标签体系这套工具的核心扩展点是schema.json它定义了整套标注任务的标签规则。换领域只需要改这个文件不需要动前端组件。文件结构大概是{ entity_types: [ {id: ORG, name: 组织机构, color: #2E86AB}, {id: PERSON, name: 人物, color: #A23B72}, {id: DATE, name: 日期, color: #F18F01} ], relation_types: [ { id: found_time, name: 成立时间, subject_type: [ORG], object_type: [DATE] }, { id: headquarter, name: 总部位于, subject_type: [ORG], object_type: [LOC] } ], attributes: { ORG: [简称, 全称, 别称], PERSON: [职位, 国籍] } }schema里同时定义了实体类型、关系类型和属性字段关系还做了subject类型和object类型的约束。标注界面的下拉框、实体高亮颜色、关系连线语义全部由这份JSON驱动。字段比较多看的时候留意几个关键参数subject_type表示该关系允许哪些实体类型作为主语object_type表示宾语类型attributes是附加的实体元数据标注项。在实际使用时建议把与领域无关的关系如相关这种泛化谓词全部从schema里删掉会让标注一致性提升一个档次。3. 前端标注器与后端工程源码包怎么在本地跑起来拿到源码包之后第一步是把它跑起来。这套项目采用的是前后端分离结构前端是标注工作台后端是数据服务和导出接口。3.1 环境准备与项目目录结构解读先看一下源码包的目录组织方式这决定了后续改哪里。典型的结构是按功能分模块的annotation-tool/ # 源码包根目录├── frontend/ │ ├── src/ │ │ ├── components/ │ │ ├── pages/ │ │ ├── store/ │ │ └── main.js │ └── package.json ├── backend/ │ ├── app.py │ ├── models/ │ ├── services/ │ └── utils/ ├── configs/ │ └── schema.json └── README.md后端看起来是基于Python的Flask框架写的app.py是入口文件models目录处理数据持久化services目录放业务逻辑utils目录放导入导出工具。前端用的是Vue单页应用方案main.js是前端入口components目录是标注面板相关的组件。 这种目录结构对二次开发很友好——改前端配色和标注交互就去frontend/src/components改数据导出格式就去backend/services换领域标签只动configs/schema.json。如果对Flask不熟也不用怕后端逻辑很薄主要是几个HTTP接口和本地文件读写核心标注态都在前端维护。 ### 3.2 启动步骤后端起服务、前端起开发服务器 启动流程不复杂本地跑起来只需要两个终端窗口。先启动后端 bash cd annotation-tool/backend pip install -r requirements.txt python app.py --host 127.0.0.1 --port 5001requirements.txt里锁定了依赖版本。启动成功后终端会输出监听地址看到Running on http://127.0.0.1:5001就说明后端就绪。这里host地址和port参数值得说明一下host设成127.0.0.1只允许本机访问如果要部署到局域网让团队多人标注就要改成0.0.0.0port默认5001是为了避开Flask开发服务器常见的5000端口占用问题如果被占用可以改成5002或6001。再启动前端开发服务器cd annotation-tool/frontend npm install npm run servenpm install会把Vue及其依赖装到node_modules目录。npm run serve起来之后默认监听8080端口浏览器访问http://localhost:8080就能看到标注工作台的登录页。如果要连接后端接口前端代码里有一处环境变量需要确认// frontend/src/api/http.js const BASE_URL process.env.VUE_APP_API_BASE || http://127.0.0.1:5001/api; export default BASE_URL;这里环境变量VUE_APP_API_BASE如果没配置默认指向本地5001端口。如果你修改了后端端口这里要对齐否则前端调用接口会全部404。3.3 核心标注流程从打开文本到产出三元组标注器的主界面通常分成三块左侧是文本列表中间是待标注文本区右侧是标注操作面板。一个完整的标注操作流程是在左侧列表选择一条待标注文本中间区域会显示原文。用鼠标在文本区选中一个词或短语系统自动弹出实体类型选择浮层。选择实体类型比如ORG、PERSON、DATE选中的文字会以对应颜色的高亮显示。点击已标注的实体再点击右侧面板的添加关系按钮进入关系标注模式。选择另一个实体作为宾语再从关系下拉框里选谓词即可生成一组SPO。如需给实体补充属性选中实体后右侧面板的属性块会展示schema里约定的属性字段。整个流程的设计意图是让标注者把注意力集中在文本理解上而不是维护标签ID表。源码里对实体去重做了处理同一段文字被重复标注时后端会返回冲突提示标注者可以选择复用已有实体或强制新建。关系标注那块还有一个细节不同类型的实体在右侧面板会有不同的待选谓词列表这是前端根据schema.json里的subject_type和object_type动态过滤的结果有效避免了标出产品-成立时间这种不合逻辑的关系。说一下回退机制。标注过程中不可避免会标错工具提供了单步撤销和历史记录两种方式CtrlZ撤销上一步操作历史记录面板可以回到任意历史节点。这套实现用的是前端状态快照栈每一次标注操作都会压栈一份当前文本的全部标注状态所以撤销的代价很小。4. 避坑清单标注工具项目五个最常见的翻车现场这套源码整体质量不错但不代表没有坑。以下五条是我实际使用或审查类似项目时遇到的高频问题每一条都对应了具体场景有一条算一条。4.1 offset计算错位标注结果错一个字全盘皆输现象从标注工具导出的实体offset与实际文本位置对不上模型训练时实体错位F1直接崩到个位数。原因前端计算offset用的是JavaScript的字符串索引但浏览器对中文和emoji的处理并不一致。中文按UTF-16码元计算索引时生僻字和特殊符号会占两个码元位置导致偏差。解决在后端引入字节级对齐校验前端提交标注记录时后端重新从原文中截取offset对应的文本片段与标注的实体文本做比对。这个工具源码里已经有类似的validate_offsets函数建议不要跳过后端的校验逻辑直接入库# backend/services/validator.py def validate_offsets(text, entity_text, start, end): # 按码点取原文片段而不是按字节索引 sliced text[start:end] if sliced ! entity_text: # 自动尝试前后偏移修正 for delta in (-1, 1, -2, 2): if text[startdelta:enddelta] entity_text: return startdelta, enddelta return None return start, end这个修正逻辑是逐级尝试前后偏移量能覆盖大多数由码元差异导致的错位问题。如果传回了None说明实体文本和原文差异太大就不能静默接受要提示标注者重新标注。4.2 实体嵌套与重叠标注冲突现象标注人名时把某科技公司创始人张三整段标成PERSON但某科技公司同时也被标成了ORG两个标注区域发生重叠保存时报冲突。原因实体嵌套是知识图谱标注里的天然难点。人物实体包含组织实体或者组织名包含人名前端如果只判断区域重叠就拒绝保存会让标注者很沮丧但如果不校验就会产生脏数据。解决在数据模型层区分完全重叠和部分重叠两类情况。完全重叠说明标注者误操作直接阻止部分重叠则允许并标记为嵌套关系导出时自动展开成普通SPO。源码里在后端标注接收接口做了分级处理# backend/services/annotation_service.py def check_overlap(spo, existing_annotations): new_region set(range(spo[subject][offset][0], spo[subject][offset][1])) for anno in existing_annotations: old_region set(range(anno[subject][offset][0], anno[subject][offset][1])) if new_region old_region: return exact_duplicate if new_region old_region: return partial_overlap return ok遇到exact_duplicate直接给前端返回409状态码前端弹出提示partial_overlap则允许入库。这条逻辑对于标注包含关系如机构名嵌套在人物履历里特别重要。4.3 关系方向标反谓词语义被搞反现象导出的三元组出现某市高新区(主语) - 总部位于(谓词) - 某公司(宾语)这种倒挂关系模型训练后关系抽取精确率惨不忍睹。原因标注者在选择主语和宾语时没有严格按照句子语义顺序。尤其当主语和宾语在文本中出现的顺序与语义方向不一致时比如某公司的总部位于某市高新区前端如果引导不够很容易把文本先出现的实体默认当主语。解决在关系标注模式下前端默认按文本先后顺序填充主语和宾语但提供交换按钮让标注者手动反转。源码里已经在关系创建弹窗上加了交换功能关键是养成习惯每标注完一组SPO扫一眼右侧面板里主语-谓词-宾语的展示顺序把倒挂的当场换过来。4.4 批量导入数据编码混乱现象从数据库或Excel导出的文本数据丢进标注系统后某些字符变成乱码尤其是引号和特殊符号。原因文件编码不一致。UTF-8的文本和GBK的文本混在一起后端统一按UTF-8处理GBK文件里的中文字符就会全部变问号。解决在导入入口增加编码探测和转换层# backend/services/importer.py import chardet def read_text_file(path): raw open(path, rb).read() detect chardet.detect(raw) encoding detect.get(encoding, utf-8) if encoding ! utf-8: return raw.decode(encoding).encode(utf-8).decode(utf-8) return raw.decode(utf-8)源码的importer模块里已经有相似的编码兼容逻辑如果你拿到的是早期版本且没有这段代码一定要自己补上否则中文字符的标注任务会变成一场灾难。4.5 多人标注冲突同一个实体出现多个版本现象两个人标注同一份文本一个人把某公司标成ORG另一个人漏标了或者标成PERSON导入模型训练数据时同一实体在不同样本里的标签不一致。原因这是标注一致性管理问题。源头在于标注任务没有锁定机制同一份文本可以被多人同时领取并各自保存。解决在任务分配层加状态机文本进去验证队列后就进入标注中状态只有提交质检后才能进入已完成状态其他人不能再打开。源码的后端预留了task_status字段通过更新状态字段来控制任务流转。实际使用时建议再加入一条硬约束每次提交前系统自动统计当前标注结果与已完成文本的实体类型分布差异超过阈值就强制让标注者复核。5. 标注数据导出与复用让清洗后的结果无缝喂给模型标注的最终目的是生产训练数据。很多标注工具导出的数据格式单一喂给模型前还要写一堆转换脚本。这套源码的导出模块做了功能分层数据可以直接对接主流序列标注框架。5.1 导出格式选型BIO、JSONL和三元组CSV源码的导出模块支持三种格式BIO序列标注格式、JSONL行式数据和CSV三元组表。三种格式对应不同的下游任务选型依据可以按这张表来导出格式数据结构适用场景示例用途BIO每行一个字符/词B-前缀表示实体开头序列标注模型训练BiLSTM-CRF、BERT-NER微调JSONL每行一个完整标注对象生成式模型、关系抽取联合模型输入文本SPO列表CSV扁平的SPO三元组行图数据库导入、SQL存储Neo4j批量导入、知识图谱构建三种格式导出的数据总量一致但组织方式完全不同。实体识别类的模型训练推荐BIO格式关系抽取类的推荐JSONL直接喂给联合解码模型最终入库和查询用CSV最方便。这个工具前端导出按钮会调用后端对应接口如果要在导出时做字段过滤直接改services/export_service.py里的字典映射即可。5.2 三元组去重与冲突合并的清洗逻辑标注过程中会不可避免地产生重复三元组和冲突三元组。比如同一句话被两个人标注一个标了某公司-成立时间-2005年另一个标了某公司-成立时间-2005年5月如果不做清洗训练数据里会出现两个答案。源码里有一个utils/dedup.py模块处理优先级为# backend/utils/dedup.py def merge_spo_annotations(annotation_list): merged {} for item in annotation_list: key (item[subject][text], item[predicate], item[object][text]) if key not in merged: merged[key] item else: # 冲突时取置信度更高的一条 old_item merged[key] if item.get(confidence, 0.5) old_item.get(confidence, 0.5): merged[key] item return list(merged.values())置信度字段在标注界面上有一个可选的打分控件标注者可以给每条关系打1-5分。如果没有标注confidence字段就保留先提交的记录。这里要提醒一下导出CSV前最好跑一遍这个去重逻辑否则冗余三元组会造成统计结果虚高后面做图数据库查询会看到一坨重复边。5.3 标注进度统计与质检抽样质检是标注工序里最容易被跳过的一环。这套工具提供一个简单的进度统计接口按任务、按标注者两个维度输出统计结果。常用的指标是有效标注率即质检通过的标注数占总数比例。后端接口实现方式是聚合查询标注记录表# backend/services/statistics.py def compute_quality_metrics(db, task_id): total db.fetch_count(SELECT COUNT(*) FROM annotations WHERE task_id?, (task_id,)) passed db.fetch_count(SELECT COUNT(*) FROM annotations WHERE task_id? AND statuspassed, (task_id,)) conflict db.fetch_count(SELECT COUNT(*) FROM annotations WHERE task_id? AND statusconflict, (task_id,)) return {total: total, passed_rate: passed / total, conflict_count: conflict}可以使用状态字段status来标记每条标注记录是否通过质检这个字段在导入时可被丢弃不会污染模型训练数据。6. 用一致性检查脚本给标注结果做最后一道体检标注做完了但导出前还有一个值得养成的习惯跑一遍数据一致性检查。我从实际项目里总结出一个小技巧就是把检查项量化成脚本输出用数字来判断这批数据是否达到可以进入训练环节的标准。下面这段脚本可以放在后端utils目录里直接执行# backend/utils/quality_check.py import json def run_quality_checks(jsonl_path): report {} with open(jsonl_path, r, encodingutf-8) as f: lines [json.loads(line) for line in f if line.strip()] report[total_samples] len(lines) spo_count [len(item.get(spo_list, [])) for item in lines] report[avg_spo_per_sample] round(sum(spo_count) / max(len(lines), 1), 2) report[empty_sample_count] len([c for c in spo_count if c 0]) # 检查主语遗漏与宾语遗漏 missing_subject 0 missing_object 0 for item in lines: for spo in item.get(spo_list, []): if not spo.get(subject) or not spo.get(subject, {}).get(text): missing_subject 1 if not spo.get(object) or not spo.get(object, {}).get(text): missing_object 1 report[missing_subject] missing_subject report[missing_object] missing_object # 检查关系类型是否合法从schema读取合法谓词表 schema json.load(open(configs/schema.json, encodingutf-8)) valid_predicates {r[id] for r in schema[relation_types]} illegal_predicate 0 for item in lines: for spo in item.get(spo_list, []): if spo.get(predicate) not in valid_predicates: illegal_predicate 1 report[illegal_predicate] illegal_predicate return report if __name__ __main__: report run_quality_checks(exported_annotations.jsonl) print(json.dumps(report, ensure_asciiFalse, indent2))这段脚本不复杂但每项指标都有实际意义。total_samples让你知道数据集体量avg_spo_per_sample过低说明标注者可能漏标了大量关系empty_sample_count过高意味着文本质量差或任务分配有问题missing_subject和missing_object是SPO结构完整性底线任何一条不为0都必须手工修复illegal_predicate是从schema合法表里校验出来的脏数据通常是schema版本更替后老标签残留导致的。跑完后报告如果四项异常都是0这批数据就算过了我自己的验收线。我给这套工具补过一些别的检查逻辑比如实体类型分布统计和同一实体的标签一致性检查都是基于这段脚本扩的。从那以后我每次做标注项目导出前都强制走一遍这个体检流程不再依赖肉眼抽查。标注工具做得再好数据质量始终是知识图谱项目的地基。希望这份体检脚本能帮你在交付前拦住最后一波脏数据。希望帮到你。本文还有配套的精品资源点击获取