☰
基于Neo4j知识图谱与规则匹配的肝病问答系统构建实战
2026/10/9 20:56:36 网站建设 项目流程

简介:面向知识图谱、自然语言处理及医疗信息化方向的开发者,这份资源提供了基于Neo4j和规则匹配的肝病问答系统完整代码与数据集,可直接运行,省去环境搭建与数据清洗的繁琐工作。项目以垂直医疗网站为数据源,通过爬虫脚本采集结构化信息,构建了包含7类4.4万实体、11类约30万关系的医药知识图谱,覆盖8000多种疾病,其中与肝病相关的多达200余种,并利用Neo4j图数据库存储与Cypher查询实现规则式问答。压缩包内共28个文件,主要包含9个Python脚本(用于数据爬取、实体构建、问题分类与答案检索)、8个TXT词典与规则文件、5个XML配置、3个JSON数据及说明文档,总大小15.77MB,目录层级简洁、模块划分清晰,便于对照学习。目前已有1047人学习下载,适合希望快速上手知识图谱构建、Neo4j图数据库应用以及规则匹配问答系统开发的中高级学习者,也可作为相关课程设计与毕业设计的参考蓝本。

1. 肝病问答系统为什么选“知识图谱+规则匹配”,而不是直接上大模型问答

基于 Neo4j 知识图谱和规则匹配的肝病问答系统,乍一看像课程设计里的 Demo,但真正接触过医疗信息化预研后你会发现,它恰好是当下最能落地的形态。很多团队一开始都想直接上大模型问答,最后被可解释性、离线部署和标注成本劝退——医生或患者问的不是“漂亮话”,而是“这个答案是从哪条路径查出来的”。

这个系统解决的是:让用户用自然语言问“肝硬化吃什么药”“乙肝要做哪些检查”,系统从预先构建的肝病知识图谱中把关联实体查出来,再用规则模板拼成可读回答。代码和数据可以放在同一个工程目录里直接跑通,不需要训练模型,也不需要 GPU,一台普通笔记本就能运行。

适合谁?想入门知识图谱问答的中级工程师、做医学信息课程设计的学生,以及想在项目里用一句话交互替代菜单点选的小团队。下面的章节会按“图谱怎么建 → 问句怎么解析 → 接口怎么封装 → 踩了哪些坑 → 怎么验证”的顺序展开,中间会给完整可复现的代码片段。

2. 从肝病医疗数据到 Neo4j 知识图谱:实体、关系与 Cypher 导入

首先要决定图谱里放什么。肝病问答系统不是把病历文本全量塞进图数据库,而是抽出医生问诊中最高频的实体。常见做法是维护五类核心节点:疾病、症状、检查、药物、食物;如果涉及并发症,就把并发症建模成疾病与疾病之间的关系,而不是单独一类节点。这样后面写规则匹配时,查询方向会非常统一。

2.1 实体与关系设计:肝病领域的图模型长什么样

我一般会把实体属性控制在“够用就好”。疾病节点需要 name、category、icd_code;症状节点需要 name 和描述;检查节点需要 name 和参考范围;药物节点需要 name 和适应症;食物节点需要 name 和 attribute(宜吃或忌吃)。不需要把医学知识库的全部字段搬进来,否则导入数据时会先被字段清洗累死。

关系集建议收敛成六个方向,全部沿“疾病 → 关联内容”方向:

关系类型起点终点对应问句
HAS_SYMPTOM疾病症状“肝硬化有哪些症状?”
NEED_CHECK疾病检查“乙肝需要查什么?”
USE_DRUG疾病药物“脂肪肝吃什么药?”
GOOD_FOR疾病食物“乙肝宜吃什么?”
AVOID_FOOD疾病食物“肝硬化忌口什么?”
HAS_COMPLICATION疾病疾病“肝硬化会并发什么病?”

这个方向设计很关键。如果并发症关系弄反,写成(腹水)-[:COMPLICATION_OF]->(肝硬化),那么回答“肝硬化有哪些并发症”时就要写反向查询<-[:COMPLICATION_OF]-,每个意图的方向都不一样,后面的 Cypher 模板会越写越乱。统一成“疾病 → 关系 → 目标”后,所有模板都是同一种形态:

MATCH (d:疾病 {name: $disease})-[:关系类型]->(target:目标类型) RETURN target.name AS answer

这样设计还有一个好处:以后要加新意图,只需要加一组关系和一句模板,不需要改查询框架。

2.2 用 Cypher 把结构化数据灌进 Neo4j:三条常用语句

很多人一上来就用CREATE逐条建节点,几百条数据没问题,几千条就失去耐心。正确姿势是把数据整理成 CSV,用LOAD CSV批量导入,配合MERGE做幂等写入。首先是建约束和索引,我习惯把约束写在导入脚本最前面:

CREATE CONSTRAINT FOR (d:疾病) REQUIRE d.name IS UNIQUE; CREATE CONSTRAINT FOR (s:症状) REQUIRE s.name IS UNIQUE; CREATE CONSTRAINT FOR (m:药物) REQUIRE m.name IS UNIQUE;

逻辑说明:REQUIRE ... IS UNIQUE是 Neo4j 5.x 的约束语法,等价于老版本的CREATE CONSTRAINT ON ... ASSERT ... IS UNIQUE。约束存在时,重复MERGE不会产生重复节点。这里给每个带name的实体类型建唯一约束,也能让后续按名字查询时直接走索引。

接着导入疾病节点:

LOAD CSV WITH HEADERS FROM 'file:///disease.csv' AS row MERGE (d:疾病 {name: trim(row.name)}) SET d.icd_code = row.icd_code, d.category = row.category;

参数说明:file:///disease.csv是相对于 Neo4j 安装目录下import文件夹的路径;row.name对应 CSV 的表头。trim函数用来去首尾空格,中文数据里经常有肉眼看不见的空格,不去掉会把“乙型肝炎”和“乙型肝炎 ”当成两个节点。CSV 必须存成 UTF-8 编码,否则会出现乱码节点。

导入关系时,先MATCH两端点再MERGE关系:

LOAD CSV WITH HEADERS FROM 'file:///relations.csv' AS row MATCH (a:疾病 {name: row.source}) MATCH (b:药物 {name: row.target}) MERGE (a)-[:USE_DRUG {evidence: row.evidence}]->(b);

这段代码的逻辑是:先用MATCH定位两个实体节点,然后MERGE关系。row.source是关系起点名称,row.target是终点名称;evidence属性用来记录答案来源,比如“XX指南第X章”。如果某个MATCH找不到对应节点,整条MERGE不会执行,所以必须先完成实体导入,再导关系。关系表在整理时建议直接用权威名,不要在这里做同义词映射,把映射放到查询层。

2.3 索引与约束:没有索引的图谱查询等于全库扫

约束已经给name建了唯一索引,但问答模板里还会用到category这类属性过滤,比如“肝硬化在哪个科室看”。如果科室是疾病节点的属性,就需要额外建索引:

CREATE INDEX FOR (d:疾病) ON (d.category);

建完索引后,可以用PROFILE验证是不是真的走了索引:

PROFILE MATCH (d:疾病 {name: '肝硬化'})-[:USE_DRUG]->(m:药物) RETURN m.name;

逻辑说明:如果执行计划里出现NodeIndexSeek而不是NodeByLabelScan,说明索引生效。NodeByLabelScan等于把该类型所有节点扫一遍,数据量到十万以上时,每次问答都会有明显卡顿。这里的关键是:索引不要贪多,只给查询模板里频繁出现的过滤条件建;属性索引在写入时会增加开销,小数据量下反而没必要。

到这一步,图谱已经能查了。但用户不会写 Cypher,所以下一步要把自然语言问句转换成查询计划。

3. 规则匹配的问法解析:从自然语言问句到查询计划

问答系统里最复杂的不是 Cypher,而是把口语变成意图和槽位。肝病场景下,用户会问“乙肝吃什么药”“慢性乙型肝炎的治疗药物有哪些”“得了肝硬化要注意什么饮食”,句式变化很大。如果完全依赖训练好的命名实体识别模型,标注成本会很高;而规则匹配恰好适合领域窄、句式相对固定、对可解释性要求高的场景。

3.1 意图识别与槽位抽取:不用 NER 也能做到的方案

先把意图收敛成六类:drug_query、symptom_query、check_query、food_query、complication_query、general_query。意图识别我习惯用“正则 + 意图表”,而不是机器学习,因为每一条规则都能被审计、被测试。

下面的 Python 代码是一组核心意图模式:

import re intent_patterns = [ (r"(吃|用|服用|用些什么|治疗).*(药|药物|药品)", "drug_query"), (r"(症状|表现|出现什么感觉)", "symptom_query"), (r"(检查|化验|检测项目|需要查什么)", "check_query"), (r"(饮食|什么能吃|不能吃|注意|忌口)", "food_query"), (r"(并发|引起|导致|诱使).*(什么病|并发症|疾病)", "complication_query"), (r"(是什么|什么是|啥是|定义)", "general_query"), ] def match_intent(question): for pattern, intent in intent_patterns: if re.search(pattern, question): return intent return None

逻辑说明:按列表顺序从上到下匹配,命中即返回。顺序很重要,“吃什么药”虽然也含“什么”,但drug_query排在比较靠前,所以先命中。如果你的词表更细,建议按“更长更具体模式优先”排序。参数说明:.*默认是贪婪匹配,问句里出现多个“什么”时容易匹配到后半句,可以把.*改成.*?变成懒匹配,但在这个场景下影响不大。

槽位抽取是从问句里抠出疾病名。最稳妥的方法是维护一张肝病名词表,做最长词优先匹配:

disease_table = ["慢性乙型肝炎", "乙型肝炎", "乙肝", "肝硬化", "脂肪肝", "酒精性肝病", "药物性肝损伤", "原发性肝癌"] def extract_disease(sentence): for name in disease_table: if name in sentence: return name return None

注意这里最长词优先:顺序不能把“乙肝”放在“乙型肝炎”前面,否则“乙型肝炎患者能喝牛奶吗”会先抽出“乙肝”,把病名抽错。这个低级错误会导致后续所有模板查不到节点。

3.2 从规则模板到 Cypher 模板:覆盖高频问句

拿到意图和疾病名后,下一步是查表生成 Cypher。这里可以用 Python 字典维护模板:

cypher_templates = { "drug_query": "MATCH (d:疾病 {name: $disease})-[:USE_DRUG]->(m:药物) RETURN m.name AS answer", "symptom_query": "MATCH (d:疾病 {name: $disease})-[:HAS_SYMPTOM]->(s:症状) RETURN s.name AS answer", "check_query": "MATCH (d:疾病 {name: $disease})-[:NEED_CHECK]->(c:检查) RETURN c.name AS answer", "food_query": """ MATCH (d:疾病 {name: $disease})-[:GOOD_FOR]->(f:食物) RETURN f.name AS answer, '宜吃' AS kind UNION MATCH (d:疾病 {name: $disease})-[:AVOID_FOOD]->(f:食物) RETURN f.name AS answer, '忌吃' AS kind """, "complication_query": "MATCH (d:疾病 {name: $disease})-[:HAS_COMPLICATION]->(c:疾病) RETURN c.name AS answer", }

逻辑说明:这里的$disease是参数占位符,执行查询时通过 Neo4j 驱动传参,而不是用字符串拼接,这样既能避免特殊字符问题,也能让 Cypher 执行计划复用。为什么不用.format把疾病名拼进查询串?因为就医问句里可能出现引号或括号,一旦拼到 Cypher 里就会语法错误甚至注入风险。常见做法是让模板统一采用参数化,到了第 4 章的接口代码里,你会看到session.run(cypher, disease=disease)。

还需要处理未命中意图或疾病名的情况。兜底思路是对节点做包含匹配:

MATCH (n) WHERE toLower(coalesce(n.name, '')) CONTAINS toLower($keyword) RETURN labels(n)[0] AS type, n.name AS name LIMIT 5

这段查询可以在规则没覆盖时,帮用户找到包含关键词的实体。但它只是兜底,不能替代规则;返回的labels(n)[0]在多标签节点上不一定可靠,但在常见单标签模型里够用。

3.3 同义词与换序容错:让规则不那么脆

肝病领域同义词非常多:“乙肝”和“乙型肝炎”、“脂肪肝”和“脂肪性肝病”、“肝硬化腹水”和“腹水”在不同语境下含义不同。对于问答系统,我建议做一个轻量级同义词归一表,在意图识别前先把用户问句里的别名替换成权威名:

synonym_map = { "乙肝": "乙型肝炎", "乙肝病毒携带者": "乙型肝炎", "乙型病毒性肝炎": "乙型肝炎", "脂肪肝": "脂肪性肝病", "肝腹水": "肝硬化腹水", } def normalize_synonym(sentence): for alias, standard in synonym_map.items(): sentence = sentence.replace(alias, standard) return sentence

这段代码简单直接,但要注意顺序:长别名要先替换,比如“乙型病毒性肝炎”要在“乙肝”之前,否则“乙肝”先替换会把“乙型病毒性肝炎”拆坏。同义词表放在单独文件里维护,后续新增别名不需要改代码。

另一个问题是语序:“肝硬化吃什么药”和“吃什么药能治肝硬化”意思相同,但规则模板如果要求病名出现在“药”前面,第二种问法就会匹配失败。我常用一个占位技巧:先把疾病名抽出来替换成<D>,再对占位后的句子做意图匹配:

def normalize_order(question): disease = extract_disease(question) if disease: return question.replace(disease, " <D> "), disease return question, None normalized, disease = normalize_order("吃什么药能治肝硬化") # normalized = "吃什么药能治 <D> "

这样drug_query的正则依然能命中,而疾病名从原句单独取出,既不影响意图识别,也不丢失槽位。这个十几行的技巧,能覆盖相当一部分倒装问法。到这里,问句解析已经可以串起来工作,下一步要把它封装成 HTTP 服务。

4. 工程化问答服务:用 FastAPI 把 Neo4j 查询变成能直接调用的接口

规则解析和图谱查询都写好后,需要提供一个对外接口。常见做法是用 FastAPI 封装,因为异步支持好、文档自动生成、依赖少。驱动选择上,我优先使用官方 neo4j Python Driver,而不是 py2neo。新版 py2neo 对 Neo4j 5.x 的支持不够及时,官方驱动在连接池和参数化查询上更稳定。

4.1 FastAPI 接口封装:最小可运行代码

先安装依赖:pip install fastapi uvicorn neo4j。然后写一个最小接口文件:

from fastapi import FastAPI, Query from neo4j import GraphDatabase app = FastAPI() driver = GraphDatabase.driver( "bolt://localhost:7687", auth=("neo4j", "your_password"), connection_timeout=15 ) def query_graph(cypher, **params): with driver.session() as session: result = session.run(cypher, **params) return [record["answer"] for record in result] @app.get("/qa") def qa(q: str = Query(..., description="用户问句")): question = normalize_synonym(q) intent = match_intent(normalize_order(question)[0]) disease = extract_disease(q) if not disease or intent not in cypher_templates: return {"answer": "我还没学会回答这个问题,你可以试试问:肝硬化吃什么药?", "intent": intent, "disease": disease} cypher = cypher_templates[intent] answers = query_graph(cypher, disease=disease) if not answers: return {"answer": f"图谱里暂时没有找到“{disease}”的相关记录", "intent": intent, "disease": disease} return {"answer": format_answer(intent, disease, answers), "intent": intent, "disease": disease}

逻辑说明:driver在整个进程生命周期内只创建一次,后续请求复用连接池,不要在每个请求里重复创建。query_graph把 Cypher 和参数一起传给session.run,这里cypher来自模板,params里放疾病名。record["answer"]要求 Cypher 返回的列名是answer,否则取不到值。启动命令:

uvicorn main:app --host 0.0.0.0 --port 8000

这里的main:app指main.py文件里的app对象;--host 0.0.0.0允许局域网其它机器访问,如果只在本地调试,可以改成127.0.0.1。另外一个实践经验是:Neo4j 的用户名密码不要硬编码在代码里,用环境变量读取:

import os driver = GraphDatabase.driver( os.getenv("NEO4J_URI", "bolt://localhost:7687"), auth=(os.getenv("NEO4J_USER", "neo4j"), os.getenv("NEO4J_PASSWORD", "your_password")) )

参数说明:connection_timeout=15指建立连接的超时时间,单位秒。网络抖动时,15 秒内没有建立连接就直接报错,不会让服务无限等待。

4.2 回答生成与兜底策略:拼人话而不是裸列表

查询结果是药物、症状名称列表,直接返回列表不仅生硬,还容易让用户误以为系统在推荐治疗方案。我一般会根据意图拼回答模板:

def format_answer(intent, disease, answers): if intent == "drug_query": return f"{disease}常用的药物有:{'、'.join(answers)}。具体用药请遵医嘱。" if intent == "symptom_query": return f"{disease}可能出现的症状包括:{'、'.join(answers)}。" if intent == "check_query": return f"{disease}常见的检查项目有:{'、'.join(answers)}。" if intent == "food_query": good = [a for a, kind in answers if kind == "宜吃"] bad = [a for a, kind in answers if kind == "忌吃"] return f"{disease}宜吃:{'、'.join(good)};忌吃:{'、'.join(bad)}。" if intent == "complication_query": return f"{disease}可能并发的疾病有:{'、'.join(answers)}。" return "、".join(answers)

在医疗问答里,回答末尾加“具体用药请遵医嘱”不是套话,而是红线。就算只是一个 Demo,也不能让输出看起来像诊断建议。回答生成还要配两级兜底:图谱没查到是一种,意图没识别是另一种。图谱没查到返回“图谱里暂时没有找到……”,意图没识别返回引导示例。两级兜底都要在日志里记录下来,方便统计哪些问法没有覆盖到。

4.3 与前端/调用方约定的响应结构

接口响应建议固定成{answer, intent, disease}三个字段,前端只渲染answer,其余字段用于调试和统计。一个实际响应示例:

{ "answer": "乙型肝炎常用的药物有:恩替卡韦、替诺福韦。具体用药请遵医嘱。", "intent": "drug_query", "disease": "乙型肝炎" }

把intent和disease返回给前端的好处是,页面可以做“猜你想问”或追问,后端也可以用同一套日志来做规则覆盖率分析。不要一开始就设计复杂的嵌套结构,问答场景返回越平越好。

如果调用方是浏览器页面,需要在 FastAPI 里加 CORS 中间件。常见做法是:

from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"], )

这里的allow_origins=["*"]只适合本地开发,生产环境必须换成具体域名。接口封装完成后,真正的问题才会显现出来:运行时的坑往往比写代码时多。

5. 运行与调优中的避坑:Neo4j连接、中文分词、模板冲突

这一章写的是我在类似系统上实际踩过的坑,每一条都用“现象 → 原因 → 解决”的顺序讲清楚。如果你完整跑通了上面的代码再看这些坑,会有一种“原来翻车在这”的感觉。

5.1 现象:Neo4j连接失败或超时

现象:调用/qa接口时返回 500,服务日志里出现ServiceUnavailable或AuthenticationError,但 Neo4j 浏览器里明明能登录。

原因:最常见的是 Bolt 端口和 HTTP 端口搞混。Neo4j 浏览器访问的是7474端口,Bolt 驱动访问的是7687端口,两者不是一个东西。其次是连接池被占满,长事务没关闭,导致后续请求拿不到连接。还有可能是密码写错,Neo4j 5.x 安装时设定的密码会被浏览器记住,但代码里用的还是旧密码。

解决:先用浏览器打开http://localhost:7474确认 Neo4j 进程正常,再从命令行用cypher-shell -u neo4j -p 密码验证认证信息。代码里增加连接参数:

driver = GraphDatabase.driver( os.getenv("NEO4J_URI", "bolt://localhost:7687"), auth=(os.getenv("NEO4J_USER", "neo4j"), os.getenv("NEO4J_PASSWORD", "your_password")), connection_timeout=15, max_connection_lifetime=3600 )

max_connection_lifetime设置连接最大存活时间,避免网络设备自动断开长期空闲连接后,驱动还在复用已被对端关闭的 socket。这个参数在容器化部署里尤其重要。

5.2 现象:中文问句里的“吃什么药”匹配不到

现象:在 Python 脚本里直接测match_intent("肝硬化吃什么药")能返回drug_query,但通过 HTTP 接口一问就走到兜底文案。

原因:这类问题通常是编码和全角半角造成的。Windows 命令行粘贴的问句可能是全角括号或全角空格;另外 FastAPI 收到的字符串虽然统一是 Unicode,但如果调用方用 GBK 编码发请求,问句到后端可能已经是乱码。还有一种情况是病名词表里没有“肝硬化”的别名,导致extract_disease返回空。

解决:在解析入口先做 Unicode 标准化和全角转半角:

import unicodedata def normalize_question(q): return unicodedata.normalize("NFKC", q).strip()

NFKC会把全角英数、全角标点转成半角,再把“xx”这类符号整理干净。正则匹配时加上re.IGNORECASE也能避免英文大小写问题。病名词表里则要维护一个尽量完整的别名清单,并在第 3.3 节里做同义词归一。

5.3 现象:规则模板之间互相冲突

现象:用户问“肝硬化会导致什么病”,系统返回“肝硬化是什么”的定义,而不是并发症列表。

原因:意图正则里general_query的(是什么|什么是)模式放在了complication_query的(导致).*(什么病)前面,而“会导致什么病”中恰好包含“什么”,于是general_query先命中。这是规则匹配系统的通病:正则模式之间没有互斥。

解决:调整意图表的顺序,把更长、更具体的模式放在前面。更靠谱的做法是给每个意图正则增加“正向关键词 + 负向排除”:

intent_patterns = [ (r"(吃什么药|用药|服用|治疗药物)", "drug_query"), (r"(并发|引起|导致|诱发).*(什么病|并发症)", "complication_query"), (r"(是什么|什么是|啥是|定义)", "general_query"), ]

这里把complication_query提到general_query之前,并给drug_query增加更具体的短语,减少歧义。如果模板继续增多,可以改成“先算每个意图的命中得分,取最高分”的方案,而不是简单顺序匹配。

5.4 现象:查询同义词节点时返回空结果

现象:图谱里节点名是“乙型肝炎”,用户问“乙肝吃什么药”,接口返回“图谱里暂时没有找到……”。但直接在图谱浏览器里查“乙型肝炎”有数据。

原因:图谱节点只维护了权威名,而用户问的是“乙肝”这个别名,模板生成的 Cypher 使用{name: '乙肝'}去匹配,自然匹配不到。这说明同义词归一只在问句解析层做了,查询层没有兼容别名。

解决:除了做同义词归一,建议给实体节点增加alias属性,在导入时把别名写进去:

MERGE (d:疾病 {name: '乙型肝炎'}) SET d.alias = ['乙肝', '乙型病毒性肝炎']

查询时用WHERE d.name = $disease OR $disease IN d.alias:

MATCH (d:疾病) WHERE $disease IN coalesce(d.alias, [d.name]) MATCH (d)-[:USE_DRUG]->(m:药物) RETURN m.name AS answer

这里的coalesce防止没有alias属性的节点报错。注意,这个写法在d.name和d.alias同时存在时,走name索引的效率会下降,因为IN查询不一定能用上索引;如果问题比较严重,可以把别名也建成独立节点,并加ALIAS_OF关系,但这属于大实体的调整,小数据量下用coalesce就够。

6. 验证回答质量的三个技巧:从接口日志到图谱路径回看

系统能跑通不意味着回答正确。我每次搭建完都会用三个技巧做验证,能快速找到规则盲区和图谱数据缺失。

第一个技巧是准备一张标准问题集,至少覆盖六个意图,每个意图写三条不同说法,共十八条以上。把这些问题批量调用接口,统计意图识别正确率和图谱返回空结果的比例。这比随机点几个问题靠谱得多。标准问题集里的每一条都记录预期意图,接口返回的intent字段会和预期比对,错一条就立刻能定位是正则的问题还是模板的问题。

第二个技巧是查看接口返回的 Cypher。在开发阶段保留响应里的cypher字段,然后拿到 Neo4j Browser 里手动执行。如果回答错了,先看 Cypher 有没有查错关系方向;如果 Cypher 正确但结果不对,再去查图数据。这一步能快速区分“规则写错”和“数据缺失”。我经常在浏览器里用一条链式查询核对回答路径,例如回答是“肝硬化常用的药物有:A、B”,就执行:

MATCH (d:疾病 {name: '肝硬化'})-[:USE_DRUG]->(m:药物) RETURN d.name, m.name

看返回的路径是否真实存在于图谱中,有没有把关系方向搞反。第三个技巧是看日志里的兜底触发次数。把“我还没学会回答这个问题”和“图谱里暂时没有找到”统计出来,排在前面的问法就是下一轮要补充的规则。这个习惯帮我避免了很多“看着能答,实际一问就哑火”的问题。

最后说一个教训:规则匹配问答系统本质上不是模型项目,而是配置管理项目。同义词表、意图正则、Cypher 模板都要版本化,每加一条新问法,就把这条问法加进标准问题集里做回归。这样做看似很笨,却是让系统长期可维护最快的路。这个方向不需要大算力和大规模标注,值得投入;希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询