简介:这份资源是面向高校学生与知识图谱初学者的一套农作物病虫害智能问答系统完整项目,适用于毕业设计、课程设计或知识图谱入门实践,帮助解决农业领域病虫害识别与防治问答的落地问题。压缩包共673个文件,约4.9MB,以555个Python源码为核心,辅以exe可执行程序、txt说明、pyc缓存、png与jpg图示、js与xml配置等,覆盖从数据构建到系统运行的完整链路。项目以neo4j图数据库存储农作物、病虫害、症状与防治方法等实体关系,结合Python的自然语言处理与图查询能力,实现问题理解、知识检索、答案生成与呈现的问答流程。已有4774人学习下载,读者可借此掌握知识图谱建模、图数据库查询与问答系统开发的整体思路,并参考目录结构快速搭建可运行环境,理解各模块协作方式,为后续二次开发或论文撰写提供实践基础。
1. 从一次田间误诊说起:这套知识图谱问答系统到底能干什么
去年帮一个做农业技术推广的朋友看他们内部的知识库,发现一个很尴尬的事:农户拍张病叶照片发到群里,三个技术员能给出三种不同的农药方案。不是他们水平不行,是水稻稻瘟病和胡麻叶斑病在早期叶片上的症状确实像,光靠人眼和记忆去翻资料,翻车概率不低。后来我就想,能不能把「症状描述→可能病害→推荐用药」这条链路做成一个能对话查询的系统,而不是让人去翻那本几百页的病虫害图鉴。
这套「基于知识图谱的农作物病虫害智能问答系统」就是干这个的。它把作物、病害、症状、农药、防治方法这些实体用图结构串起来,用户用自然语言问「水稻叶片有褐色斑点是什么病」,系统解析意图后去图谱里查路径,返回候选病害和对应的防治建议。适合两类人:一类是想拿它做农业信息化课程设计或毕设的学生,另一类是手里有植保数据、想快速搭一个问答原型的开发者。技术栈上,后端常见做法是 Python + Neo4j 存图,前端给一个轻量 Web 界面,中间夹一层意图识别和查询模板。
2. 系统架构与数据建模:图谱怎么建、问答链路怎么走
2.1 整体分层结构
这个项目我一般会拆成四层来看,从下往上分别是数据层、图谱层、问答层和交互层。数据层负责原始病虫害资料的清洗和结构化,图谱层把结构化数据灌进图数据库,问答层做意图识别和查询转换,交互层就是用户看到的输入框和结果卡片。
project/ ├── data/ # 原始数据与清洗脚本 │ ├── raw/ # 病虫害文本、表格 │ └── processed/ # 三元组 CSV ├── kg_build/ # 图谱构建 │ ├── entity_extract.py # 实体抽取 │ ├── relation_extract.py # 关系抽取 │ └── import_neo4j.py # 导入图数据库 ├── qa_engine/ # 问答核心 │ ├── intent.py # 意图分类 │ ├── template.py # 查询模板 │ └── answer.py # 答案生成 ├── web/ # 前端与接口 │ ├── app.py # Flask 入口 │ └── templates/ └── config.py # 数据库连接等配置这个目录结构不是死的,但分层逻辑建议保留。原因很简单:图谱构建和问答推理是两件独立的事,混在一起写,后面改一个实体类型就要动半个项目。我见过有人把所有逻辑塞进一个main.py,加到三百行以后自己都不敢改。
2.2 实体与关系设计
图谱的骨架是实体类型和关系类型。农作物病虫害这个场景,核心实体就那么几类:作物、病害、症状、农药、防治方法。关系上,作物和病害之间是「易感」,病害和症状之间是「表现为」,病害和农药之间是「推荐用药」,农药和防治方法之间可以挂「施用方式」。
| 实体类型 | 示例 | 说明 |
|---|---|---|
| 作物 | 水稻、小麦、玉米 | 问答的入口锚点 |
| 病害 | 稻瘟病、锈病 | 核心查询目标 |
| 症状 | 褐色斑点、叶片卷曲 | 用户描述的主要输入 |
| 农药 | 三环唑、戊唑醇 | 防治方案输出 |
| 防治方法 | 喷雾、拌种 | 补充说明 |
关系设计上有个血泪经验:别一上来就追求关系类型丰富。我一开始加了「并发」「相似」这类关系,结果查询模板复杂度直接翻倍,维护成本远超收益。后来砍到四类核心关系,问答准确率反而稳了。常见做法是先用最小可用关系集跑通链路,再按实际查询需求增量加关系。
2.3 问答链路:从一句话到一条图谱路径
用户输入「小麦叶子发黄是什么病」,系统要做的第一件事不是查图谱,而是搞清楚这句话在问什么。意图分类我一般用规则加轻量模型的方式:先匹配关键词(作物名、症状词),匹配不上再走分类模型兜底。
# qa_engine/intent.py import jieba from config import CROP_LIST, SYMPTOM_LIST def parse_question(text): """解析用户问题,抽取作物和症状关键词""" words = jieba.lcut(text) crop = None symptoms = [] for w in words: if w in CROP_LIST: crop = w if w in SYMPTOM_LIST: symptoms.append(w) # 意图类型:症状查病、病害查药、作物查病 if crop and symptoms: intent = "symptom_to_disease" elif "用什么药" in text or "怎么治" in text: intent = "disease_to_drug" else: intent = "unknown" return {"crop": crop, "symptoms": symptoms, "intent": intent}这段代码的逻辑很直白:分词后做词典匹配,抽出作物和症状两个关键槽位,再根据槽位组合判断意图。参数上,CROP_LIST和SYMPTOM_LIST是从图谱里导出的实体列表,建议放在config.py里统一维护,别硬编码在函数里。jieba.lcut用精确模式就行,这个场景不需要全模式。注意一点:症状词要做同义词归一,比如「黄叶」和「叶片发黄」得映射到同一个标准词,否则图谱里查不到。
意图拿到之后,走查询模板生成 Cypher 语句。这一步是整个问答系统的黑匣子所在,模板写得好不好直接决定召回率。
# qa_engine/template.py TEMPLATES = { "symptom_to_disease": """ MATCH (c:作物 {name: $crop})-[:易感]->(d:病害) WHERE any(s IN $symptoms WHERE (d)-[:表现为]->(:症状 {name: s})) RETURN d.name AS disease, d.description AS desc LIMIT 5 """, "disease_to_drug": """ MATCH (d:病害 {name: $disease})-[:推荐用药]->(p:农药) RETURN p.name AS drug, p.usage AS usage """ }模板里的$crop、$symptoms是参数占位,用参数化查询而不是字符串拼接,一方面是安全,另一方面 Neo4j 对参数化查询有执行计划缓存,重复查询会快不少。any(...)这个写法是 Cypher 里的列表推导,意思是「只要有一个症状匹配上就算命中」,比逐个 OR 拼接简洁得多。LIMIT 5 是防止症状描述模糊时返回过多结果,实际用的时候可以按匹配症状数量排序。
3. 环境搭建与图谱导入:从零把系统跑起来
3.1 依赖安装与 Neo4j 准备
跑这个项目需要 Python 3.8 以上和 Neo4j 4.x 或 5.x。Neo4j 可以用 Docker 起,省得配环境配到怀疑人生。
# 启动 Neo4j 容器 docker run -d \ --name agri-kg \ -p 7474:7474 -p 7687:7687 \ -e NEO4J_AUTH=neo4j/your_password \ neo4j:5.15 # 安装 Python 依赖 pip install neo4j jieba flask pandas7474是 Neo4j Browser 的端口,浏览器里可以可视化看图谱;7687是 Bolt 协议端口,Python 驱动走这个。NEO4J_AUTH设成自己的密码,别用默认的neo4j/neo4j,第一次登录会强制改密码,脚本里没处理的话会直接报错。依赖就四个核心包,pandas用来读 CSV,flask提供 Web 接口,没有多余的重型框架。
3.2 三元组数据导入
图谱构建的输入一般是 CSV 格式的三元组文件,头实体、关系、尾实体三列。导入脚本要做两件事:建约束和批量写入。
# kg_build/import_neo4j.py from neo4j import GraphDatabase import pandas as pd from config import NEO4J_URI, NEO4J_USER, NEO4J_PWD driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PWD)) def create_constraint(tx): """给作物和病害的 name 建唯一约束,避免重复节点""" tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (c:作物) REQUIRE c.name IS UNIQUE") tx.run("CREATE CONSTRAINT IF NOT EXISTS FOR (d:病害) REQUIRE d.name IS UNIQUE") def import_triples(tx, rows): """批量导入三元组""" for _, row in rows.iterrows(): tx.run( f"MERGE (h:{row['head_type']} {{name: $head}})" f"MERGE (t:{row['tail_type']} {{name: $tail}})" f"MERGE (h)-[:{row['relation']}]->(t)", head=row['head'], tail=row['tail'] ) if __name__ == "__main__": df = pd.read_csv("data/processed/triples.csv") with driver.session() as session: session.execute_write(create_constraint) session.execute_write(import_triples, df) print(f"导入完成,共 {len(df)} 条三元组")这里有几个关键点。第一,MERGE而不是CREATE,保证重复导入不会产生重复节点,这个在调试阶段反复跑脚本时特别重要。第二,约束必须在导入前建,否则大数据量下 MERGE 会全表扫描,慢到你以为程序卡死了。第三,head_type和tail_type来自 CSV 列,用 f-string 拼进 Cypher 是无奈之举,因为 Neo4j 不支持参数化标签,但这两个值必须来自受控的枚举,不能直接吃用户输入。我一般会在读 CSV 后加一层校验,类型不在白名单里就跳过并打日志。
3.3 启动 Web 服务验证链路
图谱灌完之后,起一个 Flask 服务把问答链路串起来,先验证能不能通,再谈优化。
# web/app.py from flask import Flask, request, jsonify from qa_engine.intent import parse_question from qa_engine.answer import query_graph app = Flask(__name__) @app.route("/ask", methods=["POST"]) def ask(): text = request.json.get("question", "") parsed = parse_question(text) if parsed["intent"] == "unknown": return jsonify({"answer": "没听懂,换个说法试试"}) result = query_graph(parsed) return jsonify({"answer": result}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)接口就一个/ask,收 JSON 格式的问题,返回答案。debug=True只在开发阶段开,上线要关掉,不然有安全风险。query_graph函数负责根据意图选模板、执行 Cypher、把结果拼成自然语言。测试的时候用 curl 打一发:
curl -X POST http://localhost:5000/ask \ -H "Content-Type: application/json" \ -d '{"question": "水稻叶片有褐色斑点是什么病"}'如果返回空结果,先别怀疑代码,去 Neo4j Browser 里手动跑一遍 Cypher,八成是图谱里根本没有对应数据,或者症状词没归一导致匹配不上。
4. 避坑与常见问题:那些让我加班到凌晨的坑
4.1 症状词不归一,查询永远返回空
现象是用户问「叶子发黄」,图谱里存的是「叶片黄化」,Cypher 匹配不上,返回空列表。原因在于实体抽取阶段没有做同义词映射,每个来源的表述都直接入库了。解决办法是建一张同义词表,在parse_question之后、生成 Cypher 之前做一次归一化。我一般会在config.py里维护一个SYNONYM_MAP字典,把常见口语表述映射到标准实体名。这个表不用一次建全,跑一段时间日志,把没命中的查询捞出来补充就行。
4.2 意图分类太粗,答非所问
现象是用户问「稻瘟病怎么预防」,系统返回了推荐农药,但用户要的是预防措施。原因是意图分类只区分了「症状查病」和「病害查药」,没有覆盖「预防」「传播途径」这类问法。解决思路是先把意图类型列全,再给每类写模板。我一般会拿几十条真实问句做一轮人工标注,看看实际问法分布,别拍脑袋定意图类别。意图数量控制在 5 到 8 个比较合适,太少覆盖不全,太多模板维护不过来。
4.3 图谱导入时节点重复
现象是同一个病害在图里出现多个节点,查询结果重复。原因是导入脚本用了CREATE而不是MERGE,或者约束没建成功。排查方法是先在 Neo4j Browser 里跑MATCH (d:病害) RETURN d.name, count(*),看有没有 count 大于 1 的。解决就是删库重来,先建约束再导入。这里有个后悔药:导入前先备份,或者用单独的测试库跑一遍,确认没问题再灌正式库。
4.4 中文分词把作物名切碎
现象是「小麦锈病」被 jieba 切成「小麦」和「锈病」,但「锈病」不在症状词典里,导致槽位抽取失败。原因是自定义词典没加载。解决办法是在parse_question之前用jieba.load_userdict加载作物名和病害名词典。词典文件每行一个词,可以带词频和词性。这个坑很隐蔽,因为分词结果打印出来看着挺合理,但槽位就是抽不到。
4.5 Neo4j 连接超时
现象是 Flask 跑一段时间后报连接超时。原因是驱动实例创建后长期不用被回收,或者容器资源不够。解决方法是把 driver 做成单例,在应用启动时创建,别每次请求都新建。另外检查 Docker 容器的内存限制,Neo4j 默认堆内存不大,图谱节点多了容易 OOM。我一般会给容器加-e NEO4J_dbms_memory_heap_max__size=2G这类参数,按数据量调整。
5. 二次开发:从能跑到好用,我踩过的改造路线
5.1 把规则意图升级成模型分类
规则匹配在问法规范时够用,但用户不会按你的词典说话。改造方向是加一个文本分类模型,用标注数据训练,规则作为兜底。我一般会先用规则跑一批日志,人工标注意图,积累几百条后训一个轻量模型(比如 TextCNN 或 BERT 微调),替换掉parse_question里的 if-else。改造时保留规则分支,模型置信度低的时候回退到规则,这样上线风险可控。
# 改造后的意图分类骨架 def parse_question_v2(text): rule_result = rule_based_parse(text) if rule_result["intent"] != "unknown": return rule_result # 规则没命中,走模型 model_intent = model_predict(text) if model_intent["confidence"] > 0.7: return model_intent return {"intent": "unknown"}这个渐进式改造的好处是,模型没训好之前系统不会比原来差。confidence阈值设 0.7 是个经验值,太低会引入噪声,太高模型等于没用。验证方法是拿一批没参与训练的测试问句跑对比,看规则、模型、混合三种策略的准确率。
5.2 答案生成从模板拼接到自然语言
现在返回的是图谱字段直接拼接,读起来生硬。改造方向是加一层答案模板引擎,按意图类型选不同的表述模板,把查询结果填进去。再进一步可以接一个文本生成模型,但那个成本高,建议先把模板做细。我一般会为每个意图写 2 到 3 个表述变体,随机选一个,避免每次回答都一模一样。
5.3 图谱增量更新与版本管理
病虫害数据会更新,农药推荐会调整,图谱不能建一次就不管了。改造方向是加一个增量导入接口,支持按批次更新三元组,同时记录版本。常见做法是在节点上加update_time属性,查询时只取最新版本。另外建议把三元组 CSV 纳入版本管理,每次更新打 tag,出问题能回滚。这个习惯我是被坑出来的——有一次误删了一批农药关系,没有备份,只能重新从原始资料抽,花了一整天。
5.4 验证改造效果的方法
改完之后怎么知道变好了?我一般会建一个小型评测集,50 到 100 条问题,覆盖各类意图,人工标注正确答案。每次改造跑一遍,看命中率和答案准确率的变化。别凭感觉说「好像好点了」,数据不会骗人。评测集要定期补充线上没答对的问题,保持和真实分布一致。
从那以后我每次改问答链路,都强制先跑一遍评测集再上线,哪怕只改了一个模板。希望这套拆解能帮到你,少走点我走过的弯路。
本文还有配套的精品资源,点击获取