简介:这套基于大模型与知识图谱的知识库问答实践资源,面向正在探索AI大模型落地的开发者、算法工程师,以及需要构建企业级问答系统的技术团队。资源共190个文件,包含56个Python脚本(负责知识抽取、模型调用与问答逻辑)、62个JSON配置(图谱数据与参数设定)、13个Vue前端页面及配套js/css,另有txt说明、md文档、ipynb示例与部署脚本;整体压缩包约47.09MB,结构清晰,便于对照学习。内容覆盖从知识图谱构建、大模型接口集成到问答界面展示的完整链路,既可作为二次开发脚手架,也能当作技术方案拆解样本。目前已有182人学习下载,适合希望快速上手大模型+知识图谱组合应用,并想结合实际代码理解落地方案的读者。
1. 基于大模型与知识图谱的知识库问答,先解决“为什么是这两者结合”
实际做知识库问答项目时,最常见的失败不是模型回答得不流畅,而是模型把两个同名不同义的概念混在一起,或者用训练语料里的旧知识覆盖了知识库里的最新事实。向量检索能缓解一部分,但问题一旦落到“甲和乙之间隔了几层关系”“哪些实体满足某个组合条件”这类多跳查询,相似度检索就显得力不从心。基于大模型+知识图谱的知识库问答,解决的是“把知识先组织成图,再让大模型去理解问题、查询图谱、组织答案”这条完整链路。它适合需要精确引用、多跳推理和答案可解释的政企知识库、设备维修问答、合规审查等场景。本标题讲的就是这类系统的落地路径,从GraphRAG选型、图谱构建、Cypher生成,到最后的验证方法。如果你拿到的是zip压缩包,解压后通常就是数据、图谱构建脚本、问答服务接口这几块,下文按这套结构展开。
2. 选型:从普通RAG到知识图谱问答的三条路线
2.1 纯向量检索的失效边界:多跳问题为什么答不准
先看一个典型问题:“负责D-102设备维修的张工,上个月处理过哪些故障?”这个问题需要两步推理:先找到张工和D-102之间的负责关系,再顺着张工找出所有处理过的故障。向量检索的做法是把整句话编码成向量,去知识库的向量索引里找最相似的文本块,它并不会执行“实体到实体”的路径遍历。
用词向量算相似度时,“张工”“D-102”“故障”这几个词可能分别命中了不同的文本块,但这三个词在同一段文本里共现的概率不高。于是模型只能凭上下文猜,猜错了就出现幻觉。这不是模型能力不够,而是检索机制没有路径推理能力。基于大模型+知识图谱的知识库问答,核心改进正是把检索对象从“文档”换成“图”,让问题可以在实体关系网络上遍历。
2.2 GraphRAG:用图结构补上关系遍历
GraphRAG是目前最常见的大模型+知识图谱落地形态。它和普通RAG的差别在于,检索阶段不再只做向量相似度匹配,而是先从库里定位实体节点,再沿着关系边扩展子图,把子图内的三元组信息拼成文本片段,最后交给大模型生成答案。
- 局部检索:从问题中抽取实体名,在Neo4j中用Cypher找到该实体周围1到2跳的子图,再序列化为文本。
- 全局检索:针对整个图谱预先做社区划分,对每个社区生成摘要,问题到来时按主题匹配相关社区摘要。
- 混合检索:向量检索负责找相关文本块,知识图谱负责校验实体关系和多跳路径,两者结果合并后统一作为大模型上下文。
局部检索适合“这个设备的故障原因是什么”这类单实体问题,全局检索适合“哪些故障类型占比最高”这类需要跨社区统计的问题。实际项目里两种模式可以都实现,根据问题类型路由。
2.3 三种问答方案对比与本地部署选型
| 方案 | 检索方式 | 多跳能力 | 可解释性 | 部署成本 |
|---|---|---|---|---|
| 纯向量RAG | 向量相似度 | 弱 | 中 | 低 |
| GraphRAG局部检索 | 图谱子图序列化 | 强 | 高 | 中 |
| GraphRAG全局检索 | 社区摘要 | 中 | 高 | 高 |
本地部署通常采用“ollama起大模型服务 + bge-m3做向量化 + Neo4j存图谱 + 自写Python服务编排”的组合。这套组合的好处是全部组件都能跑在内网,数据不出域。大模型用qwen2.5这类参数适中的开源模型,向量模型用bge-m3,Neo4j负责图谱存储。如果你的环境里已经跑了ollama,问答服务的代码只需要通过OpenAI兼容接口调用本地模型,不需要额外引入推理框架。
提示:GraphRAG构建图谱的离线抽取环节最消耗算力,建议用GPU机器跑这一段;在线问答阶段的大模型推理,CPU也能勉强应付小并发,但延迟会明显上升,生产环境建议GPU。
3. 用大模型做实体和关系抽取,把语料变成图谱
3.1 为什么选大模型抽取而不是传统NLP工具
构建知识图谱的第一步是信息抽取,也就是从原始文档里提取实体、关系和属性。传统做法是用HanLP或LTP这类NLP工具做分词、词性标注、依存句法分析,再根据规则匹配实体关系。这套方案在垂直领域的效果强依赖领域词典和规则模板,每换一个业务域就要重新写一批规则,维护成本很高。
大模型做抽取的核心优势是少样本和可定制schema。我在项目中用大模型做知识抽取,通常只需要在提示词里定义实体类型和关系类型,给两到三个示例,模型就能在垂直领域语料上给出可用的结构化结果,不需要针对每个新领域写规则。这种方式特别适合设备维修、医疗、法律这类实体类型相对固定的垂直场景。
3.2 一个可复制的抽取提示词与JSON输出
下面这段代码用OpenAI兼容接口调用本地部署的qwen模型,从一段设备维修记录中抽取实体和关系:
import json from openai import OpenAI client = OpenAI( base_url="http://localhost:11434/v1", # ollama的兼容端点 api_key="ollama" ) EXTRACT_PROMPT = """你是知识图谱构建助手。请从下面的文本中抽取实体和关系。 实体类型:设备、故障、方案、负责人。 关系类型:产生、处理、负责。 输出格式为JSON: {"entities": [{"name": "实体名", "type": "设备|故障|方案|负责人"}], "relations": [{"source": "实体名", "target": "实体名", "type": "产生|处理|负责"}]} 约束: - 如果没有符合要求的关系,relations返回空数组 - 实体名使用原文中的名称,不要改写 文本: {text}""" def extract_entities(text): resp = client.chat.completions.create( model="qwen2.5:7b", messages=[{"role": "user", "content": EXTRACT_PROMPT.format(text=text)}], temperature=0, # 抽取任务必须低随机性 max_tokens=1024, response_format={"type": "json_object"} ) return json.loads(resp.choices[0].message.content)这段代码有三个关键参数:temperature=0保证多次抽取同一文本结果稳定,response_format强制模型输出JSON方便直接解析,max_tokens=1024给足生成空间避免长文本截断。抽取逻辑用EXTRACT_PROMPT约束了schema,模型输出就只会包含这四类实体和三类关系,入库时不需要再做字段映射。
注意:
response_format参数不是所有本地模型服务都支持,如果ollama版本或模型不支持JSON模式,就在提示词里强调“只输出JSON,不要加任何解释”,再用正则兜底提取JSON块。
3.3 Schema设计与Neo4j入库
抽取完成后的结构化数据要写入图数据库。实际项目中,图谱的schema设计对后续Cypher查询的编写影响很大。节点标签和关系类型需要提前固定,否则大模型在生成Cypher时容易用错关系名。常见的schema设计如下:
| 节点标签 | 主要属性 | 关系类型 | 关系方向 |
|---|---|---|---|
| 设备 | name, model, location | 产生 | 设备→故障 |
| 故障 | name, level, occurred_at | 处理 | 故障→方案 |
| 方案 | name, steps, owner | 负责 | 负责人→方案 |
| 负责人 | name, department | 处理 | 负责人→故障 |
写入Neo4j时,用MERGE而不是CREATE,避免重复导入产生重复实体。下面给出入库代码:
from neo4j import GraphDatabase driver = GraphDatabase.driver("neo4j://localhost:7687", auth=("neo4j", "your-password")) def load_to_neo4j(data): with driver.session() as session: # 先合并实体节点 for ent in data["entities"]: label = ent["type"] session.run( f"MERGE (n:{label} {{name: $name}})", name=ent["name"] ) # 再合并关系 for rel in data["relations"]: session.run( """MATCH (a {name: $source}), (b {name: $target}) MERGE (a)-[r:{rel_type}]->(b)""".format(rel_type=rel["type"]), source=rel["source"], target=rel["target"] )这里用带参数的Cypher而不是拼接字符串,一是防止特殊字符破坏查询语法,二是利用Neo4j的查询缓存提升重复执行性能。MERGE的原理是先匹配再创建,节点按name属性去重。如果同一份文档需要更新,先对已有实体执行MATCH验证,再用SET更新属性,不要直接删除重建,否则会丢失已经建立的关系边。
3.4 抽取质量自检:图谱稀疏和冗余问题
图谱建完后必须做质量检查。常见问题有两类:一是抽取结果过于稀疏,一个大型语料只抽出了几十个实体,说明提示词里的实体类型定义太严格,或者文本长度超过模型上下文导致截断;二是实体名不一致,“张工”和“张伟”两个写法指向同一个人,需要做实体对齐。检查稀疏度用下面的Cypher统计各类节点数量:
MATCH (n) RETURN labels(n), count(*); MATCH ()-[r]->() RETURN type(r), count(*);如果发现某类关系数量异常少,对比原始语料和抽取提示词,看是文本过长截断了还是关系类型定义不合适。实体对齐可以先在抽取提示词里加一条指令“把同一实体的不同表述统一为规范名称”,再用向量模型聚合同名实体的属性相似度做半自动合并。这一步不追求完美,做到“同一实体在图上只有一个节点”即可。
4. 知识库问答的实现:自然语言转Cypher并生成答案
4.1 先把问题翻译成查询
在线问答阶段的核心思路是让大模型把自然语言问题翻译成Cypher查询,在知识图谱中执行查询拿到结构化结果,再把结果序列化给大模型生成最终答案。这样“查询”是精确执行的,“生成”才是模型发挥的地方,幻觉空间被压缩到最小。
import re from neo4j.exceptions import CypherSyntaxError CYPHER_PROMPT = """你是Neo4j查询专家。根据下面的问题生成Cypher查询。 节点标签:设备、故障、方案、负责人。 关系类型:产生、处理、负责。 示例: 问题:哪个负责人负责编号为D-102的设备? Cypher:MATCH (p:负责人)-[:负责]->(e:设备 {name: 'D-102'}) RETURN p.name 问题:{question} Cypher:""" def generate_cypher(question): resp = client.chat.completions.create( model="qwen2.5:7b", messages=[{"role": "user", "content": CYPHER_PROMPT.format(question=question)}], temperature=0, # 查询生成需要确定性 max_tokens=256 ) cypher = resp.choices[0].message.content.strip() cypher = re.sub(r"^```cypher|^```|```$", "", cypher).strip() return cypherfew-shot示例是让模型生成正确Cypher的关键。提示词里给出的示例必须覆盖实际会用到的关系方向和属性名,否则模型会猜测标签名称。实际项目中常见的问题是模型生成的Cypher用了不存在的属性名,比如把name写成title,这时要在提示词里补充“属性名仅使用:name, model, level”,约束越具体,生成越稳定。
4.2 查询执行与无结果时的递归扩展
Cypher生成后执行查询,但经常会遇到“查到了”和“没查到”两种情况。没有结果的原因可能是图谱里确实没有对应关系,也可能是模型生成的Cypher路径方向反了或实体名匹配不上。下面这段代码展示执行和允许深度的递归扩展示例:
def run_question_answer(question, max_depth=2): # 第一轮:正常生成Cypher并执行 cypher = generate_cypher(question) try: with driver.session() as session: records = session.run(cypher).data() except CypherSyntaxError: records = [] # 第一轮失败或不完整:递归扩展实体周围子图 if not records and max_depth > 0: entity_names = extract_entity_names(question) with driver.session() as session: records = session.run( """MATCH path = (n)-[r*1..{depth}]-(m) WHERE n.name IN $names RETURN path, n, r, m LIMIT 30""".format(depth=max_depth), names=entity_names ).data() return records or None递归扩展的思路是以问题中出现的实体名称为种子节点,扩展1到2跳范围内的所有关系,作为一个子图上下文。限制条件是必须设置LIMIT,避免中心节点连接边数过多时结果爆炸。max_depth=2在绝大多数知识库场景够用,超过2跳后路径数量呈指数增长,会超出大模型的上下文窗口。
4.3 答案生成与引用溯源
拿到查询结果后,需要把图结构序列化成文本再交给大模型。Cypher返回的data()结构是嵌套字典,直接拼到提示词里会占用大量token,先做一个文本化处理:
SERIALIZE_TEMPLATE = "实体{source}通过关系{rel}连接到实体{target}" def graph_result_to_text(records): lines = [] for rec in records: for path_item in rec.get("path", []): lines.append(SERIALIZE_TEMPLATE.format( source=path_item.start_node["name"], rel=path_item.type, target=path_item.end_node["name"] )) return "\n".join(lines[:50])答案生成的提示词需要明确要求模型只使用图谱信息,并且标注引用来源:
ANSWER_PROMPT = """你是一个知识库问答助手。请根据下面的图谱信息回答用户问题。 图谱信息: {graph_text} 约束: - 只依据图谱信息回答,图谱中没有的信息回答“知识库中未收录” - 在答案末尾用括号标注所引用的实体名称 问题:{question} 答案:"""这里的可解释性来自“实体名引用标注”,用户可以看到答案对应图谱中的哪些节点。最终生成的回答在形式上与普通RAG相同,但底层依据的是精确的图查询结果。
4.4 关键参数与推理依赖
在线问答服务的实际调优集中在下面几个参数上:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| temperature | 0(Cypher生成)/ 0.2(答案生成) | 查询必须确定,答案可以略自由 |
| max_tokens | 256(Cypher)/ 512(答案) | 防止查询被截断、答案过长 |
| 图谱遍历深度 | 1~2 | 超过3跳路径数量指数增长 |
| 单次结果上限 | 30条 | 防止结果截断或超上下文 |
| 超时时间 | 30秒 | 查询超时直接走兜底检索 |
答案生成阶段可以用top_p=0.9适当增加多样性,但Cypher生成阶段需要保持top_p=1.0配合temperature=0,确保同样的问题每次都生成一样或等价的查询语句。如果需要在服务层面避免重复查询,可以用Redis缓存问题到答案的映射,key用问题的哈希值,命中缓存直接返回。
5. 三个真实坑:知识图谱问答的边界与验证
5.1 实体消歧:同名实体必须分
知识图谱里最容易出的问题就是两个同名实体被MERGE成了同一个节点。比如“李工”在设备A的维修记录里出现过,又在方案B的评审记录里出现过,但实际上是两个人。解决办法是在schema里给每个实体增加业务唯一键,设备用设备编号,人员用工号,而不是用name做唯一约束。如果历史数据已经混了,用Cypher找出同名节点的数量:
MATCH (n) RETURN n.name, count(*) ORDER BY count(*) DESC LIMIT 10;对数量大于1的同名节点,人工核对后补充唯一属性再重新MERGE。这个问题必须前置解决,否则问答阶段查出的“李工”无法确定是哪一个。
5.2 图谱陈旧:先查图再问模型
知识图谱的内容有生命周期,设备报废、人员离职、流程变更都会让图谱失真。如果图谱说“某设备在运行”,但真实状态是已停用,大模型基于错误图谱生成答案,就变成了系统性幻觉。应对方式是让大模型在答案末尾标注“以上信息依据某年某月更新的知识库”,同时定期用源系统数据对关键节点做属性校验。大模型的训练知识不能替代图谱的实时数据。
5.3 用Cypher回归集做自动验证
知识库问答上线前,最好准备一组回归测试用例,每条用例包含问题、期望答案中应出现的实体名。每次修改提示词或图谱schema后,都要跑一遍回归集:
QA_CASES = [ {"question": "D-102设备的故障由谁处理?", "expected": ["张工"]}, {"question": "设备D-102使用什么方案解决过热故障?", "expected": ["风冷改造"]}, ] def run_regression(): passed = 0 for case in QA_CASES: answer = run_question_answer(case["question"]) if any(name in answer for name in case["expected"]): passed += 1 else: print(f"FAIL: {case['question']}") print(f"passed={passed}/{len(QA_CASES)}")回归集设计原则:覆盖单实体查询、多跳查询、无结果查询和模糊实体名四种类型。无结果查询用来检查兜底逻辑,模糊实体名用来检查对齐效果。这四类用例全部通过,再考虑升级图谱的抽取规则或调整提示词约束。
本文还有配套的精品资源,点击获取