
简介面向自然语言处理与知识图谱方向的学习者资源包以neo4j图数据库为存储核心结合Langchain框架与千问大模型完整演示了知识图谱的自动生成与自然语言问答流程。包内共256个文件以228个jar依赖包为主体涵盖neo4j服务及图谱操作所需库另含bat/ps1启动脚本、conf配置文件、txt说明文档与xml配置等压缩包约142.75MB。其中特别提供了neo4j安装包与安装说明并配有用于构建人物关系知识图谱的文档方便新手从零搭建环境、建立实体关系数据。通过Langchain对自然语言的解析、千问大模型对语义的理解读者可复现“提问—图谱检索—自然语言回答”的完整链路还能根据自身需求改造问答逻辑。资源目前已有189人学习下载适合想快速上手知识图谱大模型应用、并希望掌握图数据库落地细节的开发者。1. 项目起因与技术选型思路大概在半年前做知识库问答时我遇到一个特别实际的问题用向量数据库加 RAG 的方式回答“这个项目里有多少人参与”“A 和 B 之间是什么业务关系”这类问题时效果非常不稳定。向量召回擅长找“相似片段”但一旦你需要的是一张确定的关系网它就会把文档里原本分散的实体和关系打碎语言模型拿到的上下文是割裂的生成结果自然开始“自由发挥”。这种场景下用户要的不是一篇相似段落而是明确的实体、明确的关系、明确的路径。所以当时我给自己定了两个目标第一用大模型自动从文本中抽取实体和关系构建可查询的 Neo4j 知识图谱第二在知识图谱上实现自然语言简单问答用户用中文直接提问系统自动把问题转成图查询最后生成可读答案。这个项目做完之后我对大模型和知识图谱的结合方式有了非常具体的理解今天把整个链路、关键代码和踩过的坑都写出来给同样在做类似方向的朋友一个可以直接参照的模板。技术选型上流程编排用 LangChain模型用的是千问大模型qwen-plus 和 qwen-max 都试过图数据库用 Neo4j整体架构可以拆成两条链路。离线链路原始文本经千问模型抽取实体和关系再写入 Neo4j 形成知识图谱在线链路用户自然语言问题输入 LangChain经过“问题理解 图查询生成 子图召回 模型组织答案”这几个步骤输出最终回答。1.1 为什么不用纯向量检索硬扛先解释一下为什么非要把知识图谱引进来。向量数据库的核心能力是语义相似度检索它能告诉你“哪些文本片段和这个问题像”但它没法回答“这个关系存在还是不存在”。比如知识库里有十份合同每份合同都提到了供应商和客户用向量检索可以找到包含“供应商”字样的段落却很难让模型跨段落推理出“甲公司到底同时是哪些项目的供应商”。这样的问题本质是关系推理不是文本相似度匹配。知识图谱恰好把文本隐式的结构化信息显式存下来一旦关系入图这类问题就变成了单纯的图查询准确率会明显提升。1.2 千问模型与 LangChain 的契合度选择千问模型有两个原因。一个是它的中文实体和关系抽取能力在同价位模型里表现比较稳对长文本的指令跟随也不错另一个是百炼平台提供了 OpenAI 兼容接口可以直接用 LangChain 的 ChatOpenAI 类接入省去封装一层自定义 LLM 的工作。LangChain 本身有 Neo4jGraph、LLMGraphTransformer 等现成组件但实际用下来直接调底层 API 反而更可控——稍后我会详细说。2. 环境搭建与模型接入的细节这个部分看起来基础但很多坑其实都藏在环境里。我的运行环境是 Windows 11 加 Python 3.10Neo4j 用的是 Desktop 版本。如果你用 Docker 跑 Neo4j 也可以但要注意端口映射和内存配置。2.1 Neo4j 数据库的准备Neo4j Desktop 安装之后创建一个本地数据库设置好用户名和密码记住两个端口7474 是 HTTP 端口用来打开浏览器管理界面7687 是 Bolt 端口Python 驱动通过它连接数据库。我习惯在创建数据库之后立刻改一下默认配置把初始内存调大一点否则后面写入大批量关系时会出现内存溢出。启动数据库后建议先打开浏览器输入http://localhost:7474确认 Cypher 查询能正常执行。这一步特别重要因为后面所有代码里的连接错误都是先在浏览器里排除的效率高很多。2.2 Python 依赖安装清单pip install langchain pip install langchain-community pip install langchain-openai pip install neo4j pip install pandas需要注意langchain-openai 这个包我们不是真的用来接 OpenAI而是用它走 OpenAI 兼容协议接千问。如果你只用百炼的 HTTP 接口也可以不装这个包但用 LangChain 统一管理对话和解析会方便很多尤其是后面要挂 Agent 或做链式调用的时候。2.3 千问大模型的接入配置接入千问的代码很简单核心是设置 base_url 和 model 名称。from langchain_openai import ChatOpenAI qwen_llm ChatOpenAI( modelqwen-plus, api_key你的百炼API-KEY, base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1, temperature0 )有一个细节必须提醒抽取实体和关系时temperature 必须设置为 0。我一开始用默认的 0.8同一个文本跑两次抽出来的实体名称不一致直接导致图谱里出现大量重复节点。这不是模型笨而是生成式模型在自由作答时本身就有随机性知识图谱构建场景需要的是稳定输出所以所有结构化抽取任务的 temperature 都设成 0。3. 大模型生成知识图谱从文本到实体关系到 Cypher这一步是整个项目的核心。目标是把一段杂乱文本变成 Neo4j 里的节点和关系。网上很多教程会直接推荐 LangChain 的 LLMGraphTransformer但我实际对比之后最后还是自己写了提示词。原因有两个第一LLMGraphTransformer 的输出格式是固定的它对关系类型的定义比较宽泛容易出现大量无意义关系第二我希望实体类型完全受控方便后面做问答时准确生成 Cypher。3.1 提示词设计让模型输出稳定的 JSON我给模型设计了两段提示词一段负责抽取实体一段负责抽取关系。实体抽取提示词的核心思路是先定义允许出现的实体类型再要求模型返回 JSON 数组。比如针对项目文档我定义了“人”“公司”“项目”“技术”“产品”五类实体针对医学文献又会换成“疾病”“药物”“症状”“检查方法”等类型。实体类型越明确后面的图结构越干净。from langchain_core.prompts import PromptTemplate from langchain_core.output_parsers import JsonOutputParser entity_prompt PromptTemplate( input_variables[text], template 你是一个信息抽取专家请从下面的文本中抽取实体。 实体类型只允许是人、公司、项目、技术、产品。 要求 1. 实体名称必须是文本中出现的原始名称或标准简称。 2. 不要合并多个实体不要扩写。 3. 返回 JSON 数组格式为 [{name: 实体名称, type: 实体类型}] 文本 {text} 只输出 JSON不要任何解释。 )这里的关键不是让模型自由发挥而是把边界卡死。我曾经不加实体类型限制结果模型抽出“数字化转型”“合作关系”这种抽象概念当实体图谱质量立刻下降。3.2 关系的抽取与实体的合并抽取关系时我需要模型输出三元组包含 source、target、relation 三个字段同时要求关系和实体名称保持严格一致。如果模型输出的是实体抽取时不同的名称后面写图就关联不上所以我专门在提示词里强调“实体名称必须和上一轮抽取结果完全一致”。实际操作中抽取完成之后还不能直接写数据库必须要做一个实体合并清洗。方法不复杂把同一实体的别名归到标准名称下比如“阿里云”和“阿里云计算有限公司”合并为“阿里云”。这一步可以用简单的字符串匹配加人工规则不必上复杂的实体对齐算法。def merge_entities(entities): alias_map { 阿里云: 阿里云, 阿里云计算有限公司: 阿里云, 千问大模型: 千问, } merged set() for ent in entities: name alias_map.get(ent[name], ent[name]) merged.add((name, ent[type])) return [{name: n, type: t} for n, t in merged]这个合并步骤千万别省。不合并的话同一个真实实体在图谱里会有三四个节点后续问答按名称匹配时就会查不到或查漏。3.3 写入 Neo4j 的完整代码实体和关系都抽好之后用 Neo4jGraph 写入。我先把所有实体节点写入再写入关系同时建立唯一性约束避免后续重复导入产生脏数据。from langchain_neo4j import Neo4jGraph graph Neo4jGraph( urlbolt://localhost:7687, usernameneo4j, password你的密码 ) # 建立唯一性约束 graph.query(CREATE CONSTRAINT entity_name IF NOT EXISTS FOR (n:Entity) REQUIRE n.name IS UNIQUE)写入节点时我分类型打标签比如 Person、Company、Project、Tech、Product而不是全部堆到 Entity 一个标签下。这样在问答阶段生成 Cypher 时可以按标签过滤查询效率高很多。for ent in entities: label ent[type] graph.query( fMERGE (n:{label} {{name: $name}}) RETURN n, {name: ent[name]} )写入关系时关系类型尽量用名词短语比如“参与”“负责”“使用”“合作”。这些关系名会直接影响后面 Text-to-Cypher 的生成质量如果关系全是“related_to”这种含糊类型问答时模型也不知道该用哪个。for rel in relations: graph.query( MATCH (a {name: $source}), (b {name: $target}) MERGE (a)-[r:HAS_RELATION {type: $relation}]-(b) , {source: rel[source], target: rel[target], relation: rel[relation]} )这里用了 MERGE 而不是 CREATE就是为了防止重复写入。理论上这里还可以把关系类型作为真实的边标签我前期为了方便统一用了 HAS_RELATION后来在问答阶段发现把关系类型作为边标签会更利于 Cypher 生成后面会详细说。4. 自然语言简单问答的实现知识图谱建好之后重头戏是问答。所谓“简单问答”我把它限定为用户用一句中文问出和图中实体、关系直接相关的问题系统从图中查到答案再用千问组织成一句话回复。比如“谁参与了那个智慧园区项目”“千问模型用到了哪些技术”“A 公司和 B 公司是什么合作关系”这些问题不涉及复杂多跳推理但正好是向量 RAG 不擅长的。4.1 两种问答路线的对比我在项目里实际对比了两种问答实现方式。第一种是 LangChain 自带的 GraphCypherQAChain它内部会把用户问题转成 Cypher然后在图上执行最后把查询结果交给模型生成答案。第二种是自己写一个调度函数分“识别实体、生成 Cypher、执行查询、生成答案”四步走。两种方案各有优劣。GraphCypherQAChain 的优点是接入快十几行代码就能看到效果缺点是对 schema 复杂一点的图它的转译成功率明显下降。比如我的图里有人、公司、项目、技术四类节点关系类型也很多GraphCypherQAChain 经常生成一个带不存在的属性名或者关系类型的 Cypher执行直接报错。所以我最后采用的是自己写调度逻辑虽然代码量多了一些但每一步都能控制排错也容易。4.2 问题解析与实体匹配用户输入问题之后我首先做实体识别和实体链接从问题中提取可能出现的实体名然后在 Neo4j 里做模糊匹配。这一步我直接用千问模型完成把“找出问题中提到的所有实体名”作为提示比用传统 NER 工具更适合业务里的随意命名。from sklearn.feature_extraction.text import TfidfVectorizer这里有个更轻的方式先用模型抽出候选实体名再去图里用CONTAINS模糊匹配。比如用户问“智慧园区项目有哪些公司参与”模型抽出“智慧园区项目”然后我执行这个查询。MATCH (p:Project) WHERE p.name CONTAINS 智慧园区 RETURN p.name如果返回多个候选实体就取名称相似度最高的那个。这一步能避免用户问“智慧园区”时匹配不到全称“智慧园区数字化改造项目”。4.3 Text-to-Cypher 的生成与执行实体确认之后我把图的 schema、实体名、用户问题一起丢给千问让它生成 Cypher。提示词里明确给出节点标签和关系类型的完整列表同时要求只输出 Cypher 语句。cypher_prompt PromptTemplate( input_variables[schema, question, entities], template 你是一个 Neo4j 专家请根据以下信息生成 Cypher 查询。 图结构说明 {schema} 用户问题{question} 问题中涉及的实体{entities} 要求 1. 只能使用上面给出的标签和关系类型。 2. 只输出 Cypher 语句不要输出其他内容。 3. 如果信息不足输出 MATCH (n) RETURN n LIMIT 1占位结果。 )schema 我会先通过代码从图里动态拉出来避免手工维护。schema graph.get_schema()这个动态 schema 非常重要。因为一旦后续调整了实体类型或关系类型问答链路不需要改代码它自动感知新的图结构。千问模型对 Cypher 的生成能力比较稳定尤其是简单查询。实测下来单实体、单/双关系跳级的查询正确率能做到八成以上。生成的 Cypher 在 Neo4j 上执行后会得到一个子图或一张结果表。这个结果再连同原问题交给千问生成自然语言答案。answer_llm ChatOpenAI( modelqwen-max, temperature0.3 ) answer answer_llm.invoke( f用户问题{question}\n f从知识图谱中查询到的结果{query_result}\n 请用自然语言简洁回答用户问题不要编造结果中不存在的信息。 )4.4 问答结果的组织与兜底实际使用中还有一个必须考虑的兜底逻辑如果 Cypher 执行结果为空怎么答复用户。有两种情况一种是用户问的问题超出图谱覆盖范围另一种是 Cypher 写错了。我现在的做法是执行结果为空时再加一个固定提示让模型不要硬编内容直接回复“知识库中暂时未找到相关信息”。这一点很重要因为大模型面对空结果时特别容易自己编造答案也就是“幻觉”。限定空结果回复模板之后问答系统至少不会一本正经地胡说八道。5. 实测效果、踩坑记录与优化思路5.1 问答效果实测对比我拿一小段技术合作文档做了测试文档里提到了某公司推出了千问大模型另外两家中标企业分别负责数据平台和客户端开发。构建图谱后我问了三个问题。第一个问题“千问大模型是什么公司推出的”答案很快直接从Product节点的HAS_OWNER关系里查到。第二个问题“智慧园区项目里有哪些参与方”这个如果用向量 RAG需要模型从多个段落里拼凑参与方名单容易漏。在知识图谱里就是一条MATCH (p:Project {name:智慧园区项目})-[:HAS_PROJECT]-(c:Company)就能拿全速度也比召回加分块快很多。第三个问题“A 公司和 B 公司是什么关系”这其实是知识图谱最拿手的场景直接查两个节点之间是否有边以及边的类型回答非常确定。我还对比了同一个知识库用纯向量检索的效果回答第一类简单单跳问题时两者差距不大但回答第二类“列举参与方”的问题时向量检索经常漏一个公司名而图查询可以保证完整枚举。在这个场景下知识图谱的价值非常明显。5.2 碰到的三个比较有代表性的坑第一个坑是实体重复导致的问题。最初我建图时没有建立唯一约束也没做实体合并结果同一个“千问”节点在库里出现了三次提问“千问大模型是什么公司推出”时系统查询结果里出现多个相似节点答案变得混乱。解决办法就是在建图阶段做合并加唯一约束从源头避免脏数据。第二个坑是关系类型命名随意。最初我为了省事把所有关系都命名为 related_to结果问答阶段让模型生成 Cypher 时它根本不知道 related_to 里存的是什么关系生成出的边类型完全匹配不上。后来我把关系名称改成“参与”“开发”“使用”“合作”这种语义明确的词转译成功率明显提升。如果有时间更推荐把关系类型直接定义为 Neo4j 的边标签比如(a)-[:PARTICIPATES_IN]-(b)这样查询和生成都会更直接。第三个坑是 Cypher 生成时模型“过度发挥”。千问在生成 Cypher 时偶尔会自己造一些属性名比如虚拟出company.name但图中根本没有这个属性。后来我在提示词里明确要求“只能使用 schema 中给出的标签、属性和关系类型”并且把动态 schema 塞进提示词这类报错基本消失了。5.3 后续还能怎么优化这个项目做完我个人的感受是它非常适合作为一个“知识增强问答”的基础框架后续可以往几个方向扩展。一个是多跳推理通过 LangChain Agent 实现多步查询比如“参与智慧园区项目的人里有多少人同时参与了另一个项目”这需要把上一步查询结果作为下一步的输入。另一个是图与向量的混合召回先通过向量检索找到相关文本再用大模型抽取实体去图谱里查关系这样既保留了语义检索的宽度也保留了图谱关系的精度。6. 项目源码的模块划分思路最后简单说下代码组织。整个项目我分了四个模块这个划分现在回头看还是很顺手。llm_utils.py负责千问模型的统一初始化和调用graph_builder.py负责文本导入、实体关系抽取、合并和 Neo4j 写入qa_pipeline.py负责问题实体识别、Cypher 生成、查询执行、答案生成schema_utils.py负责从 Neo4j 动态拉取和缓存图结构信息。模块之间只通过函数调用和简单的数据结构传递没有引入复杂的框架。如果后续要考虑多人维护或者上线部署可以再加一层任务队列因为图谱构建阶段如果文本量特别大单线程跑会很慢可以改成先并行抽取再批量写入的方式。我个人在实际项目中的最大体会是LangChain 在这个链路里真正的价值不是提供了某个“一键建图”的黑盒组件而是它把模型调用、结构化输出、数据库交互这些环节串成了一个可以随时替换零件的过程。你将来的项目如果要从千问换到其他模型或者从 Neo4j 换到其他图数据库只需要替换对应的组件整体流程不会推翻重写。这也是这类工程最有意思的地方你搭的不是一个一次性脚本而是一条可以不断加长、改道的流水线。本文还有配套的精品资源点击获取