Neo4j 与 LLM:图数据库赋能的知识问答
环境准备与基础概念
在开始将 Neo4j 与大型语言模型(LLM)结合之前,需要理解二者各自扮演的角色以及协作的基础设施。Neo4j 是一个属性图数据库,数据以节点(Node)、关系(Relationship)和属性(Property)的形式存储,擅长表达高度互联的数据和复杂语义网络。LLM 则负责理解自然语言、生成答案并推理上下文。
为什么图数据库能增强 LLM 的知识问答
LLM 虽然具备强大的泛化能力,却面临知识截止、幻觉、缺乏领域专有知识等挑战。图数据库从三个层面弥补这些缺陷:
- 结构化知识存储:将企业文档、业务实体及其关系建模为图,构成可查询的知识图谱。
- 精确的上下文检索:基于图的遍历与模式匹配,检索出与用户问题高度相关的子图,作为 LLM 的提示上下文。
- 可解释的推理路径:关系本身就是推理单元,答案生成后可追溯至图中的具体路径,增强透明度和可信度。
技术栈选型
推荐组合:
- 图数据库:Neo4j(社区版或 AuraDB 免费实例)
- LLM 访问:OpenAI API、Azure OpenAI,或本地模型通过 LangChain、LlamaIndex 封装
- 编排框架:LangChain(提供 GraphCypherQAChain)或 LlamaIndex(KnowledgeGraphIndex)
- 编程语言:Python 3.10+
知识图谱构建:从非结构化文本到 Neo4j
知识问答系统的起点是构建一张包含领域知识的知识图谱。以下流程将原始文档转化为 Neo4j 中的节点和关系。
实体与关系抽取流水线
典型流水线有两种构建方式:基于 LLM 的一体化抽取,或使用传统 NLP 结合规则。
方式一:利用 LLM 直接抽取三元组
通过提示工程引导 LLM 从文本片段中抽取 (头实体, 关系, 尾实体) 三元组。提示模板示例:
从以下文本中提取所有实体和它们之间的关系,以JSON列表返回,每个元素包含 head, relation, tail 三个字段。
文本:{chunk_text}
处理长文档时,先按段落或固定 Token 长度切分文本块,对每个块调用 LLM,聚合结果并去重。Python 伪代码:
import openai
from neo4j import GraphDatabase
def extract_triplets(text_chunk):
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": f"提取三元组: {text_chunk}"}]
)
# 解析返回的JSON三元组列表并返回
return parse_json(response.choices[0].message.content)
# 写入 Neo4j
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
with driver.session() as session:
for head, rel, tail in triplets:
session.run("""
MERGE (h:Entity {name: $head})
MERGE (t:Entity {name: $tail})
MERGE (h)-[r:RELATION {type: $rel}]->(t)
""", head=head, rel=rel, tail=tail)
方式二:预定义模式 + 实体链接
如果领域边界清晰,可先设计本体(节点标签和关系类型),然后用命名实体识别(NER)模型(如 spaCy)抽取实体,再通过 LLM 对实体对分类关系。此方法更可控,适合精度要求高的场景。
图数据质量优化
- 实体归一化:对相似名称(如“Apple”与“Apple Inc.”)进行实体对齐,可利用字符串相似度和 LLM 判断。
- 关系细化:将通用关系(RELATION)替换为领域特有类型,如
:ACTED_IN、:WORKS_FOR。 - 属性嵌入:为节点生成文本向量(OpenAI Embeddings),存储为节点属性,支持后续语义搜索。
知识问答实现模式
将 Neo4j 的查询能力与 LLM 结合,主要分为三种实现模式,复杂度递增。
模式一:文本到 Cypher(Text2Cypher)
最通用的方法,让 LLM 将用户自然语言问题翻译为 Cypher 查询,执行查询后,将结果提供给 LLM 合成最终答案。
核心架构:GraphCypherQAChain
LangChain 内置了此模式,封装了提示、查询生成、结果处理和答案合成。
from langchain.chains import GraphCypherQAChain
from langchain_community.graphs import Neo4jGraph
from langchain_openai import ChatOpenAI
graph = Neo4jGraph(
url="bolt://localhost:7687",
username="neo4j",
password="password"
)
chain = GraphCypherQAChain.from_llm(
llm=ChatOpenAI(model="gpt-4", temperature=0),
graph=graph,
verbose=True
)
chain.run("谁导演了电影《盗梦空间》?")
该链的底层工作原理:
- Schema 注入:自动获取 Neo4j 数据库中所有节点标签、关系类型和属性键,构造提示前缀。
- Cypher 生成:LLM 根据 Schema 和用户问题生成查询。
- 查询执行:在 Neo4j 中执行,捕获错误并可能让 LLM 自我修正。
- 答案合成:将查询结果与原始问题一同提交给 LLM,生成自然语言回答。
限制与优化
- 复杂问题生成错误:可通过少样本提示(Few-shot)提供问题-Cypher 对示例,显著提升生成准确率。
- 只读安全:在提示中严格声明只允许
MATCH、RETURN等只读操作,避免数据篡改。 - 性能:限制生成查询返回的记录数,使用
LIMIT。
模式二:图增强检索生成(Graph RAG)
Graph RAG 不依赖 LLM 生成数据库查询,而是先基于用户问题从图中检索相关的结构化知识,再把这些知识块注入 LLM 上下文。
检索器设计
检索分为两步:实体链接与子图抽取。
- 实体链接:使用向量相似度搜索(利用节点上的文本嵌入属性)或全文搜索,找到问题中提及的实体节点。
MATCH (e:Entity) WHERE e.embedding IS NOT NULL WITH e, gds.similarity.cosine(e.embedding, $question_embedding) AS score ORDER BY score DESC LIMIT 5 RETURN e.name, score - 子图抽取:从已链接的实体出发,沿关系扩展1-2跳,构成局部子图。可基于规则或使用图算法(如PageRank)过滤重要关系。
上下文组装与生成
将子图序列化为人类可读的文本格式(如“实体A -[关系]-> 实体B”),与用户问题拼接后送入 LLM。
context = "知识图谱片段:\n"
for r in subgraph_relations:
context += f"{r['source']} -- {r['type']} --> {r['target']}\n"
prompt = f"根据以下知识回答问题:\n{context}\n问题:{question}\n回答:"
answer = llm.predict(prompt)
Graph RAG 的优势在于避免了 Text2Cypher 可能的翻译错误,尤其适合图模式复杂、但问题偏向于局部信息查询的场景。缺点是当需要聚合统计(如“有多少导演与克里斯托弗·诺兰合作过?”)时可能检索不全,需回退到 Cypher 模式。
模式三:智能体循环(Agentic Loop)
对于需要多步推理、多工具协作的复杂问题,可将 Neo4j 封装为一个工具,由 LLM 智能体(Agent)决策何时调用图工具,并结合其他工具(如计算器、搜索引擎)完成回答。
使用 LangChain 的 create_openai_tools_agent:
from langchain_core.tools import tool
from langchain_community.graphs import Neo4jGraph
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
graph = Neo4jGraph(...)
@tool
def query_knowledge_graph(question: str) -> str:
"""用于查询电影和导演相关知识的图数据库。输入为自然语言问题,返回相关信息。"""
# 内部可以调用 Text2Cypher 或 Graph RAG
return graph.query(question_to_cypher(question))
llm = ChatOpenAI(model="gpt-4-turbo", temperature=0)
tools = [query_knowledge_graph]
agent = create_openai_tools_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
agent_executor.invoke({"input": "莱昂纳多·迪卡普里奥演过的电影中,哪一部评分超过9.0且导演也参演了?"})
智能体能够自主拆分问题、多次调用图工具,甚至结合外部数据,但延迟较高,成本也需注意。
实战案例:构建电影知识问答应用
为具体说明,我们用一个公开电影数据集构建一个问答系统。数据集来自MovieLens或IMDB,包含电影、演员、导演、评分等。
数据建模与导入
| 节点标签 | 属性 |
|---|---|
| Movie | title, released, imdbRating |
| Person | name, born |
| Genre | name |
关系:
(:Person)-[:ACTED_IN {roles}]->(:Movie)(:Person)-[:DIRECTED]->(:Movie)(:Movie)-[:IN_GENRE]->(:Genre)
假设已有 CSV 文件,使用 Cypher 的 LOAD CSV 或 Neo4j ETL 工具导入。
实现并对比两种问答方式
我们实现 Text2Cypher 和 Graph RAG 两种模式,在相同测试集上对比效果。
- Text2Cypher 实现:直接复用 GraphCypherQAChain,携带包含少样本的提示。
- Graph RAG 实现:
- 使用 OpenAI Embeddings 为每个 Person 和 Movie 节点生成描述嵌入(由 LLM 生成一句话描述)。
- 检索时,将问题嵌入,通过向量索引找到最相关的实体。
- 从这些实体出发,执行如下查询获取1跳子图:
MATCH (n)-[r]->(m) WHERE id(n) IN $entity_ids RETURN n.name, type(r), m.title - 将子图文本化后与问题一同提交给 LLM。
评测与观察
对于聚合统计类问题(如“诺兰电影的均分是多少”),Text2Cypher 正确率高但可能出现语法错误;Graph RAG 因缺乏全局聚合能力无法直接回答,需结合 Cypher。对于事实型问题(如“莱昂纳多演过恋爱类电影吗”),Graph RAG 更稳定,因为始终能提供相关背景。实际生产环境中,通常采用“先 Graph RAG 检索,若置信度不足则尝试 Text2Cypher”的混合路由策略。
生产化注意事项
- 图索引优化:为常用查询属性创建索引(
CREATE INDEX movie_title FOR (m:Movie) ON (m.title)),为向量搜索使用 Neo4j 向量索引(5.x以上支持)。 - 读取副本:高并发问答场景使用只读副本分离负载。
- 权限控制:LLM 生成的 Cypher 必须由只读用户执行,并通过参数化查询防止注入。
- 缓存:对常见问题或相似图查询结果进行缓存,降低成本。
- 监控与反馈:记录问答日志,收集用户反馈,建立 “问题-Cypher” 黄金数据集,用于微调小模型或提示优化。
总结与进阶方向
Neo4j 与 LLM 的结合为知识问答赋予了结构化思维和精确检索能力,避免了纯生成式模型的幻觉和冷知识盲区。本文介绍的三种模式从简单到复杂覆盖了大部分场景,起步可从 Text2Cypher 开始,逐步根据痛点引入 Graph RAG 和智能体。
进阶学习路径:
- 探索 Neo4j 图数据科学库(GDS)进行社区发现和图嵌入,辅助问题理解。
- 使用 LangGraph 构建更精细的图问答状态机,支持多轮对话澄清。
- 在本地部署微调的代码生成模型(如 CodeLlama)专门完成 Cypher 翻译,降低 API 成本和延迟。
通过持续迭代知识图谱质量与问答架构,你将能够打造出深度理解领域知识的下一代智能应用。