1. 从两个数据库的“各管一摊”说起向量数据库和图数据库这两年在大模型应用圈子里几乎是绕不开的两个词。但真正把两者放在一起协同工作的项目目前还不多见。我最近花了几周时间完整跑通了一套“图数据知识检索与关联推理”的智能应用方案核心思路就是用向量数据库负责语义召回用图数据库负责关系推理两者通过大模型串联起来形成一个既能“模糊理解”又能“精确推理”的检索增强系统。这套方案解决的核心问题是传统的关键词检索只能匹配字面向量检索能理解语义但丢失了实体之间的结构关系而图数据库擅长处理多跳关系却无法直接理解自然语言查询。把三者结合起来用户可以用自然语言提问系统先通过向量检索找到语义相关的实体节点再在图数据库中沿着关系边做多跳推理最后把结构化的推理路径交给大模型生成可读的答案。适合谁来参考如果你正在做知识库问答、企业文档检索、智能客服、风控关系分析这类场景并且已经接触过向量检索或图数据库中的至少一个那这套方案可以直接拿来改。如果你完全没碰过图数据库建议先花半天时间了解一下属性图模型和Cypher查询语言的基本概念否则后面的实操部分会有些吃力。我下面会从整体设计思路开始拆然后逐步深入到每个核心环节的实现细节包括参数选择、性能调优、踩过的坑以及最终跑通后的效果对比。整个方案的技术栈是向量数据库用Milvus图数据库用Neo4j大模型用本地部署的开源模型你也可以换成任何兼容OpenAI接口的模型服务嵌入模型用BGE-M3。所有组件都是可替换的核心在于协同的架构思路而不是某个特定产品。2. 整体架构设计与选型考量2.1 为什么不是“一个数据库搞定所有”很多人第一反应是能不能只用一个数据库比如用支持向量索引的图数据库或者用带图功能的向量数据库。我试过几条路线结论是能做但不好用。向量数据库的核心优势在于高维向量的近似最近邻搜索它的索引结构如HNSW、IVF是为稠密向量检索优化的你让它去处理多跳关系查询性能会断崖式下跌。反过来图数据库的存储和遍历是为关系设计的你硬塞一个高维向量字段进去做相似度搜索要么全表扫描要么索引效率极低。所以我的选择是让专业的工具做专业的事。向量数据库负责“从海量非结构化数据中找到语义相关的候选集”图数据库负责“在候选集基础上做精确的关系推理和路径发现”。两者之间通过实体ID做关联大模型负责最后的自然语言生成和查询意图解析。这个架构的关键在于“实体对齐”——向量数据库里存的每条向量对应一个实体或一个文本块这个实体在图数据库里也有对应的节点。用户查询时先从向量库召回相关实体拿到实体ID列表再去图库做扩展推理。2.2 数据流向的完整链路整个系统的数据流向可以拆成两条线写入线和查询线。写入线是这样的原始文档经过分块处理后每个文本块通过嵌入模型生成向量存入向量数据库同时抽取文本中的实体和关系构建图结构存入图数据库。这里有个关键设计——向量数据库中的每条记录都带一个entity_id字段图数据库中的每个节点也有一个entity_id属性两者通过这个字段做映射。查询线更复杂一些用户输入自然语言问题先经过大模型做意图解析提取出查询中的关键实体和关系意图。然后拿这些关键实体去向量数据库做语义检索召回一批相关实体。接着用这些实体ID去图数据库做多跳查询找出它们之间的关联路径。最后把原始问题、召回的相关文本块、图查询返回的关系路径一起塞给大模型生成最终答案。这个链路里最容易被忽视的是“意图解析”这一步。如果大模型没能正确提取查询中的实体和关系意图后面的检索和推理都会跑偏。我的做法是给大模型一个few-shot提示模板里面包含几个典型的查询解析示例让模型输出结构化的JSON包含entities、relations、query_type三个字段。2.3 选型对比为什么是Milvus加Neo4j向量数据库我选的是Milvus原因有几个。第一它的HNSW索引在召回率和延迟之间平衡得很好我实测在100万条768维向量的规模下top-10召回延迟稳定在10毫秒以内。第二它支持标量字段过滤我可以在向量检索的同时加上entity_type之类的过滤条件减少后续图查询的候选集大小。第三它的分区功能让我可以按业务域把数据分开查询时只扫相关分区。图数据库选Neo4j主要是因为它对Cypher的支持最成熟社区资料也最丰富。另一个考虑是Neo4j的APOC库提供了很多图算法和路径扩展的便捷函数做多跳推理时省了不少事。如果你对性能有极致要求也可以考虑用NebulaGraph或TigerGraph但Neo4j的生态和文档对新手最友好。嵌入模型用BGE-M3主要是看中它对中文和英文的混合支持都很好而且支持多粒度句子级和段落级的嵌入。如果你主要处理英文数据换成text-embedding-3-large或e5-large也可以但要注意维度变化后需要重建向量索引。大模型这块我用的是本地部署的Qwen2.5-14B-Instruct通过vLLM做推理加速。选它的原因是中文理解能力强而且14B的规模在单张A100上就能跑起来延迟可以接受。如果你没有本地GPU资源换成任何兼容OpenAI接口的云端模型服务也可以只需要改一下API地址和密钥。3. 核心细节解析与实操要点3.1 实体抽取与对齐整个系统的地基实体抽取的质量直接决定了后续检索和推理的上限。我的做法是用大模型做few-shot实体抽取而不是用传统的NER模型。原因很简单传统NER模型只能识别预定义的实体类型人名、地名、机构名等而知识库场景中往往需要抽取领域特定的实体比如产品型号、技术术语、业务流程节点等。具体操作上我设计了一个提示模板让大模型从每个文本块中抽取实体和关系输出JSON格式。提示模板的核心部分是这样的EXTRACTION_PROMPT 从以下文本中抽取所有实体和它们之间的关系。 实体类型包括人物、组织、产品、技术、事件、地点。 关系类型包括任职于、开发了、依赖于、发生于、属于。 输出格式为JSON { entities: [{name: 实体名, type: 实体类型}], relations: [{source: 源实体, target: 目标实体, type: 关系类型}] } 文本内容 {text} 这里有个坑大模型有时候会编造不存在于原文中的实体或者把同一个实体写成不同的名称。我的解决办法是在抽取之后加一步“实体归一化”——用嵌入模型计算实体名称之间的相似度把相似度超过0.9的实体合并为同一个并保留一个标准名称作为entity_id。实体对齐的另一个关键是确保向量数据库和图数据库中的entity_id完全一致。我的做法是在写入阶段就用同一个ID生成规则对实体名称做标准化处理去空格、转小写、去除特殊字符然后取SHA256哈希的前16位作为entity_id。这样无论数据先写入哪个库ID都是一样的。3.2 向量索引的参数调优Milvus的HNSW索引有几个关键参数需要根据你的数据规模和查询模式来调。我踩过的坑是一开始用默认参数召回率只有70%左右很多相关实体根本没被召回来。核心参数是M和efConstruction。M控制每个节点的最大连接数值越大索引越稠密召回率越高但内存占用也越大。efConstruction控制构建索引时的搜索深度值越大构建越慢但索引质量越好。我的经验值是对于100万条以内的数据M32、efConstruction256是一个不错的起点。如果数据量超过500万可以把M提到48或64。查询时的ef参数也很关键它控制搜索时的候选集大小。ef越大召回率越高但延迟也越高。我实测下来ef128时top-10召回率能到95%以上延迟在15毫秒左右。如果对延迟极其敏感可以降到ef64召回率大概损失3到5个百分点。还有一个容易忽略的点是向量归一化。BGE-M3输出的向量默认是归一化的但如果你换用其他模型一定要确认是否需要手动做L2归一化。Milvus的IP内积距离在向量归一化后等价于余弦相似度如果没归一化就直接用IP结果会完全不对。3.3 图Schema设计与多跳查询优化图Schema的设计要平衡“表达力”和“查询效率”。我的Schema比较简单节点标签主要有Entity带entity_id、name、type三个属性关系类型有RELATES_TO带relation_type和weight两个属性。这种设计的优点是通用性强任何领域的数据都能塞进去缺点是查询时无法利用关系类型的索引所有关系都走同一种边。如果你对查询性能有更高要求可以把关系类型拆成独立的边类型比如WORKS_AT、DEVELOPS、DEPENDS_ON等。这样查询时可以只遍历特定类型的边速度会快很多。但代价是Schema变得不灵活新增关系类型需要改Schema。多跳查询是图数据库的强项但跳数不是越多越好。我实测下来2到3跳的查询在延迟和召回之间平衡得最好。4跳以上的查询延迟会显著增加而且返回的路径数量爆炸式增长很多路径其实是噪音。我的做法是给查询加一个LIMIT子句限制返回的路径数量同时按路径长度排序优先返回短路径。Cypher查询的一个优化技巧是用MATCH时尽量指定节点标签和关系类型避免全图扫描。比如MATCH (a:Entity)-[r:RELATES_TO]-(b:Entity)比MATCH (a)-[r]-(b)快很多因为前者可以利用标签索引。4. 实操过程与核心环节实现4.1 环境搭建与依赖安装先说一下我的环境配置Ubuntu 22.04Python 3.10Milvus 2.4用Docker Compose部署Neo4j 5.15社区版vLLM 0.4.2。如果你用云端服务Milvus和Neo4j都有托管版本可以跳过本地部署这一步。Milvus的Docker Compose部署很简单官方仓库里有现成的配置文件。下载后执行docker compose up -d就行。需要注意的是Milvus对内存要求比较高建议至少给16GB内存否则索引构建阶段容易OOM。Neo4j的部署更简单下载社区版解压后修改conf/neo4j.conf把dbms.default_listen_address改成0.0.0.0然后bin/neo4j start就行。默认的用户名和密码都是neo4j第一次登录会强制改密码。Python依赖主要是这几个pip install pymilvus2.4.0 neo4j5.15.0 openai1.12.0 sentence-transformers2.5.1sentence-transformers用来加载BGE-M3模型如果你用API方式做嵌入可以换成对应的SDK。4.2 数据写入流程的完整实现数据写入分三步文本分块、向量化与图抽取、双写。文本分块我用的是固定长度加重叠的策略每块512个token重叠128个token。重叠的目的是避免实体被切分到两个块里导致抽取不完整。分块工具用的是LangChain的RecursiveCharacterTextSplitter它对中文的分割效果比简单的按字符切分好很多。向量化这一步BGE-M3的加载方式是from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-m3) vectors model.encode(texts, normalize_embeddingsTrue, batch_size32)注意normalize_embeddingsTrue这个参数它确保输出的向量是L2归一化的这样Milvus里用IP距离就等价于余弦相似度。图抽取用前面提到的提示模板调用大模型API。这里有个性能优化的点不要一条一条地调把多个文本块拼成一个batch一次请求处理10到20个块能显著减少API调用次数。但要注意batch太大会超出模型的上下文窗口我的经验是每个batch的总token数控制在3000以内。双写的时候要注意事务性。我的做法是先写图数据库拿到所有节点的entity_id列表再写向量数据库。如果向量数据库写入失败图数据库里的数据不会回滚但因为有entity_id做关联后续可以补写。如果对一致性要求极高可以用一个简单的任务队列做重试。4.3 查询链路的逐步拆解查询链路是整个系统最复杂的部分我把它拆成四个步骤。第一步是意图解析。用户输入“A公司和B公司之间有什么业务往来”大模型需要输出{ entities: [A公司, B公司], relations: [业务往来], query_type: relationship }这一步的提示模板里我放了5个few-shot示例覆盖了实体查询、关系查询、路径查询三种类型。实测下来few-shot能把解析准确率从60%左右提到85%以上。第二步是向量召回。拿解析出的实体名称去向量数据库做检索每个实体召回top-20的相关实体。这里有个技巧不要只用实体名称做查询把原始问题也作为一个查询向量这样能召回一些名称不匹配但语义相关的实体。第三步是图推理。拿召回实体的entity_id列表去图数据库做多跳查询。Cypher语句大概是这样的MATCH path (a:Entity)-[r:RELATES_TO*1..3]-(b:Entity) WHERE a.entity_id IN $entity_ids AND b.entity_id IN $entity_ids RETURN path, length(path) AS hops ORDER BY hops ASC LIMIT 50这个查询会找出所有在3跳以内能连通的路径按跳数排序。LIMIT 50是为了防止路径爆炸。第四步是答案生成。把原始问题、向量召回的相关文本块、图查询返回的路径描述一起塞给大模型让它生成最终答案。提示模板里要明确告诉模型优先使用图路径中的关系信息向量召回的文本块作为补充上下文。4.4 性能实测与调优记录我在一个包含5万条实体、12万条关系、50万条向量记录的数据集上做了完整测试。硬件配置是单张A100 40GBMilvus和Neo4j跑在同一台机器上。端到端延迟的拆解大概是意图解析200毫秒向量召回15毫秒图推理80毫秒答案生成1.5秒。总延迟在1.8秒左右其中大模型生成占了绝大部分。如果对延迟敏感可以把答案生成换成流式输出首token延迟能降到300毫秒以内。召回率方面我用100个手工标注的查询做了测试。向量召回的top-20召回率是92%图推理的路径召回率是78%。两者结合后的最终答案准确率是85%。剩下的15%错误主要来自实体抽取阶段的遗漏和意图解析的偏差。一个明显的性能瓶颈是图推理阶段。当entity_ids列表很长超过50个时Cypher查询会变得很慢。我的优化方法是先用向量相似度对召回实体做二次排序只取top-10的实体ID去做图查询。这样虽然会损失一点召回率但延迟从800毫秒降到了80毫秒性价比很高。5. 常见问题与排查技巧实录5.1 实体对齐失败的典型表现与修复最常见的问题是向量数据库召回了实体但用entity_id去图数据库查不到对应的节点。这通常是因为实体归一化阶段出了问题。比如“某科技公司”和“某科技公司有限公司”被当成了两个不同的实体生成了不同的entity_id。排查方法是在向量召回后打印出召回实体的entity_id和name然后手动去图数据库里查这些ID是否存在。如果不存在说明归一化规则需要调整。我的修复方案是在归一化阶段加一个“别名表”把已知的别名映射到标准名称然后再生成entity_id。另一个常见问题是大小写和空格导致的ID不一致。比如“Machine Learning”和“machine learning”生成了不同的哈希。解决办法是在生成entity_id之前统一做lower()和strip()处理并且把连续空格替换成单个空格。5.2 图查询返回空路径的排查思路图查询返回空结果通常有三个原因。第一实体ID确实不在图库里这是实体对齐问题参考上一条。第二关系类型不匹配比如你查的是RELATES_TO但实际存储的是RELATED_TO。第三跳数限制太严格两个实体之间实际需要4跳才能连通但你只查了3跳。排查顺序建议是先确认实体ID存在再确认关系类型拼写正确最后逐步放宽跳数限制。我一般会先用一个最简单的查询MATCH (n:Entity {entity_id: xxx}) RETURN n确认节点存在然后再加关系条件。还有一个隐蔽的坑是Neo4j的索引没建。如果entity_id字段没有索引MATCH (n:Entity {entity_id: xxx})会做全表扫描数据量大的时候慢到无法接受。建索引的语句是CREATE INDEX entity_id_index FOR (n:Entity) ON (n.entity_id)5.3 大模型生成答案偏离事实的抑制方法大模型在生成答案时有时候会“脑补”一些图路径和文本块里都没有的信息。这个问题在知识库场景下很致命因为用户期望的是基于事实的回答。我的抑制方法是双管齐下。第一在提示模板里明确要求“只使用提供的上下文信息如果上下文不足以回答问题直接说‘根据现有信息无法回答’”。第二在生成之后加一个“事实校验”步骤用另一个大模型调用检查生成的答案中的每个事实陈述是否能在上下文中找到依据。这个校验步骤会增加一次模型调用但能把事实性错误率降低60%以上。另一个技巧是给大模型提供结构化的上下文而不是一堆散乱的文本。比如把图路径格式化成“A公司 --[投资]- B公司 --[合作]- C公司”这样的形式模型更容易理解关系方向生成答案时也不容易搞反。5.4 常见问题速查表问题现象可能原因排查方法修复方案向量召回为空嵌入模型未归一化检查向量模长是否接近1加normalize_embeddingsTrue图查询超时entity_id无索引用EXPLAIN看执行计划建索引答案偏离事实提示模板约束不够检查生成内容与上下文的重合度加事实校验步骤实体重复归一化规则不完善统计entity_id数量与预期对比加别名表和标准化处理端到端延迟高图查询跳数太多打印各阶段耗时限制跳数和候选实体数6. 几个容易被忽视的实操心得第一个心得是关于嵌入模型的。BGE-M3虽然支持多粒度但我在实际使用中发现对短实体名称做嵌入时加上一些上下文信息效果更好。比如不要只嵌入“A公司”而是嵌入“A公司一家专注于某领域的企业”。这样召回的实体更准确因为向量里包含了类型信息。第二个心得是关于图数据库的写入性能。Neo4j的写入速度在单条插入时很慢我一开始逐条CREATE每秒只能写几十条。后来改成用UNWIND批量写入速度提升了20倍以上。批量写入的Cypher模板大概是UNWIND $batch AS row MERGE (a:Entity {entity_id: row.source_id}) MERGE (b:Entity {entity_id: row.target_id}) MERGE (a)-[r:RELATES_TO {relation_type: row.rel_type}]-(b)注意用MERGE而不是CREATE这样可以避免重复节点和重复边。第三个心得是关于大模型提示模板的迭代。我一开始把提示模板写得很复杂塞了很多规则和约束结果模型反而容易困惑。后来精简成“角色定义任务描述输出格式两个示例”的结构效果反而更好。提示工程的核心不是堆规则而是让模型清楚地知道你要什么。第四个心得是关于监控。这套系统涉及三个组件任何一个出问题都会导致端到端失败。我在每个环节都加了日志和耗时统计并且用Prometheus加Grafana做了一个简单的监控面板。这样当用户反馈“查不到结果”时我能快速定位是向量召回的问题、图查询的问题还是大模型生成的问题。这套方案我目前跑了三个月累计处理了大约两万次查询整体稳定。最大的收获是向量和图不是竞争关系而是互补关系。向量负责“广撒网”图负责“精捕捞”大模型负责“把鱼做好端上桌”。三者各司其职才能做出真正好用的知识检索与推理应用。
阅读完成 · 觉得有帮助?