1. 为什么要把向量数据库和图数据库放在一起用
1.1 从一个真实需求说起
去年下半年我接手了一个内部知识库的改造项目,需求方给的原话是"让系统能听懂人话,还能顺着线索自己找答案"。听起来像两个需求,实际上是一件事:既要语义层面的模糊匹配,又要关系层面的精确推理。
举个具体例子。用户问"某个型号的设备在高温环境下出现异常振动,可能和哪些部件有关"。这个问题里有两层信息:第一层是"高温""异常振动"这些描述,它们和知识库里的"热应力""共振频率偏移"在字面上完全不重合,必须靠语义相似度去捞;第二层是"哪些部件有关",这需要沿着设备-部件-工况-故障模式的关系链路去走,是典型的图遍历问题。
只用向量数据库,第一层能解决,第二层就抓瞎——它返回的是一堆语义相近的文本片段,但片段之间的因果关系、层级关系、时序关系全丢了。只用图数据库,第二层很漂亮,第一层又不行——图查询依赖精确的实体名和关系类型,用户随口一句"高温下抖得厉害",根本映射不到图里的节点。
所以这套方案的核心思路就一句话:向量库负责"找得到",图库负责"理得清",大模型负责"串起来"。三者各干各最擅长的事,通过一个编排层粘合。
1.2 三个组件各自的角色定位
我把这套架构里的分工拆得比较清楚,避免后期维护时职责混乱:
| 组件 | 核心职责 | 不负责什么 |
|---|---|---|
| 向量数据库 | 语义召回、模糊匹配、多模态特征检索 | 不做多跳推理、不维护实体关系 |
| 图数据库 | 关系存储、多跳遍历、路径推理、约束校验 | 不做语义相似度计算 |
| 大模型 | 意图解析、查询改写、结果融合、自然语言生成 | 不做精确的图计算、不承担召回主责 |
这个分工不是拍脑袋定的。我试过让大模型直接读图数据库的 schema 然后生成查询语句,在小规模图上还行,一旦关系类型超过几十种、节点属性上百个,模型生成的查询准确率断崖式下跌。也试过把图数据全部拍平成文本塞进向量库,结果就是关系信息被稀释,多跳查询基本失效。
提示:不要试图让任何一个组件"全能"。向量库做关系推理、图库做语义匹配、大模型做精确计算,这三件事都是反模式的,踩过一次就知道疼。
1.3 这套架构适合什么场景
不是所有知识检索都需要上这套组合拳。我总结了几条判断标准,满足两条以上才值得投入:
- 知识之间存在显式的关系结构,比如设备层级、组织架构、工艺流程、法律条款引用
- 用户查询既有模糊描述又有精确约束,比如"类似XX的案例中涉及哪些供应商"
- 需要多跳推理才能得到答案,单跳检索覆盖不了
- 知识会持续更新,且更新时关系需要同步维护
如果只是简单的文档问答,向量库加大模型就够了,硬上图库是过度设计。反过来,如果只是固定几类关系查询,图库加规则引擎也能跑,不必上向量。这套方案的价值区间在"语义+关系"双重不确定性的场景。
2. 核心细节拆解:数据怎么组织、查询怎么走
2.1 数据双写:向量和图谱如何保持一致
这是整个项目最容易出问题的地方。同一份知识,既要进向量库做 embedding,又要进图库建节点和边,两边怎么同步?
我的做法是以图库为主数据源,向量库为派生索引。具体流程是:原始知识先经过抽取管线,产出结构化的三元组(头实体、关系、尾实体)和实体属性,写入图库;然后从图库的实体和关系文本中生成 embedding,写入向量库,并在向量库的元数据里存图库的节点 ID。
这样做的好处是:图库是"真相源",向量库随时可以从图库重建。如果 embedding 模型升级了,只需要重新跑一遍向量化,图结构不动。反过来如果以向量库为主,图关系一旦需要调整,向量库里的元数据就得跟着改,很容易出现不一致。
# 伪代码:双写管线的核心逻辑 def ingest_knowledge(raw_doc): # 第一步:抽取三元组和实体 triples, entities = extractor.extract(raw_doc) # 第二步:写入图库,拿到节点ID node_ids = graph_store.upsert(entities, triples) # 第三步:为每个实体生成embedding,带上图库ID for entity in entities: vec = embed_model.encode(entity.text + entity.context) vector_store.upsert( id=entity.id, vector=vec, metadata={"graph_node_id": node_ids[entity.id], "type": entity.type} )这里有个细节值得说:实体文本的 embedding 不能只编码实体名。比如"轴承"这个词,单独编码和"高速旋转工况下的轴承"编码,语义向量差别很大。我的做法是把实体名、实体类型、以及它在图里的直接邻居关系拼成一段描述文本再编码,这样向量里天然携带了一部分关系信息,召回时更准。
2.2 查询路由:什么时候走向量,什么时候走图
用户输入一句话进来,系统得先判断该走哪条路。我一开始想用大模型做意图分类,后来发现延迟太高,每次查询多几百毫秒。现在改成规则+轻量分类器的组合:
- 如果查询里包含明确的实体名或实体类型词,优先走图库做实体链接
- 如果查询是纯描述性的、没有可识别的实体锚点,先走向量库召回候选实体
- 如果两者都有,走混合路径:向量召回候选,再用图库做关系过滤
实际跑下来,大概 60% 的查询走混合路径,25% 纯向量,15% 纯图。这个分布和业务场景强相关,不同项目差别会很大,建议上线后埋点统计,动态调整路由阈值。
2.3 混合检索的融合策略
向量召回和图召回各自返回一批结果后,怎么合并排序?我试过三种方案:
方案一:加权分数融合。把向量相似度和图路径得分归一化后加权求和。简单,但权重很难调,不同查询类型最优权重不一样。
方案二:级联过滤。先用向量召回 Top-K,再用图关系做过滤。召回率高但精度受限于向量质量。
方案三:RRF(倒数排名融合)。不依赖分数绝对值,只看排名。这是我现在用的方案,鲁棒性最好。
RRF 的公式很简单:每个文档的最终得分等于它在各召回列表中的排名倒数之和。比如一个实体在向量召回里排第 3,在图召回里排第 1,得分就是 1/3 + 1/1 = 1.33。这个方案的好处是不需要归一化,也不用调权重,对异构召回源的融合特别友好。
注意:RRF 里的常数 k 一般取 60,这是原论文的经验值。我试过调到 10 和 100,效果都不如 60 稳。如果你的召回列表很短(少于 20 条),k 可以适当调小。
3. 实操过程:从零搭一套可跑的检索推理链路
3.1 环境准备与组件选型
向量库我选的是支持元数据过滤的方案,因为混合检索时需要按实体类型、时间范围做前置过滤。图库选的是支持属性图模型的,因为业务里的实体属性比较丰富,纯 RDF 三元组表达起来别扭。
大模型这块,意图解析和查询改写用中等规模的指令微调模型就够了,最终答案生成用大一点的模型。我实测下来,查询改写这个环节用小模型反而比大模型稳,因为大模型容易"过度发挥",把用户的原始意图改偏。
# 依赖安装(示意,具体包名按实际选型替换) pip install vector-client graph-client llm-sdk3.2 图谱 Schema 设计
Schema 设计是图库这边最关键的一步。我的原则是:实体类型宁少勿多,关系类型宁精勿滥。一开始我设计了 30 多种实体类型,后来合并到 12 种,查询和维护都轻松很多。
核心实体类型包括:设备、部件、工况、故障模式、处置措施、文档。核心关系包括:包含(设备-部件)、发生于(故障-工况)、导致(部件-故障)、处置(故障-措施)、引用(文档-实体)。
{ "entity_types": ["设备", "部件", "工况", "故障模式", "处置措施", "文档"], "relation_types": [ {"name": "包含", "from": "设备", "to": "部件"}, {"name": "发生于", "from": "故障模式", "to": "工况"}, {"name": "导致", "from": "部件", "to": "故障模式"}, {"name": "处置", "from": "故障模式", "to": "处置措施"}, {"name": "引用", "from": "文档", "to": "*"} ] }Schema 定好后,抽取管线的 prompt 里要把这个 schema 作为约束传进去,让模型只抽这几类实体和关系。不约束的话,模型会抽出各种五花八门的类型,后期图谱会变成一团乱麻。
3.3 查询编排的完整流程
一次完整的查询请求,经过这几个阶段:
第一阶段:意图解析。大模型把用户输入拆成三部分:语义查询串、实体锚点、关系约束。比如"高温下振动的设备可能是什么部件坏了",解析结果是语义串"高温环境振动异常",实体锚点"设备",关系约束"部件-导致-故障"。
第二阶段:并行召回。语义串走向量库召回候选实体,实体锚点走向量库做实体链接,同时图库根据锚点做一跳邻居扩展。
第三阶段:图推理。把向量召回的候选实体作为起点,在图库中做 2-3 跳遍历,找出满足关系约束的路径。这里要设跳数上限,不然在大图上会爆炸。我的经验是 3 跳是性价比拐点,超过 3 跳的路径噪声太大。
第四阶段:结果融合与生成。用 RRF 融合各路结果,取 Top-N 路径和实体,连同原始问题一起喂给大模型生成答案。prompt 里要明确要求模型"只基于给定路径作答,路径之外的信息不要编"。
def hybrid_query(user_input): # 1. 意图解析 parsed = llm.parse_intent(user_input, schema=GRAPH_SCHEMA) # 2. 并行召回 vec_candidates = vector_store.search(parsed.semantic_query, top_k=50) graph_neighbors = graph_store.expand(parsed.entity_anchors, hops=1) # 3. 图推理 paths = graph_store.traverse( start_nodes=[c.graph_node_id for c in vec_candidates], relation_filter=parsed.relation_constraint, max_hops=3 ) # 4. 融合排序 fused = rrf_fuse([vec_candidates, graph_neighbors, paths]) # 5. 生成答案 answer = llm.generate(user_input, context=fused[:10]) return answer3.4 参数选择的实测记录
几个关键参数我做了对比测试,记录如下:
| 参数 | 测试值 | 最终选择 | 理由 |
|---|---|---|---|
| 向量召回 Top-K | 20/50/100 | 50 | 20 漏召回,100 引入太多噪声,50 是拐点 |
| 图遍历最大跳数 | 2/3/4 | 3 | 4 跳的路径人工评估相关性低于 30% |
| RRF 常数 k | 10/60/100 | 60 | 原论文经验值,实测最稳 |
| 融合后取 Top-N | 5/10/20 | 10 | 5 信息不足,20 超出模型上下文性价比 |
这些值不是通用的,不同数据规模下要重新调。但调参的思路可以复用:先定召回率下限,再压精度,最后看生成质量。
4. 踩过的坑与排查技巧
4.1 实体链接失败导致图查询空转
最常见的故障是:用户查询里的实体名和图中的实体名对不上。比如用户说"主轴承",图里存的是"主轴轴承组件"。字面匹配失败,实体链接就断了,后面的图遍历全部空转。
我的解法是双通道实体链接:字面匹配走一路,向量相似度匹配走一路,两路结果取并集。向量匹配时用实体描述文本的 embedding,而不是实体名本身,这样"主轴承"和"主轴轴承组件"的向量距离会比较近。
实操心得:实体链接的阈值不要设太高。我一开始设 0.85,召回率惨不忍睹。后来降到 0.7,配合后续的图约束过滤,整体准确率反而上升了。因为图结构本身就是一个强约束,链接错一两个候选,图遍历会自然把它们过滤掉。
4.2 图遍历的路径爆炸
在关系密集的图上,3 跳遍历可能产生几万条路径。我遇到过一次查询返回了 4 万多条路径,直接把下游的融合排序拖垮。
解决办法有三个,我组合使用:
- 关系类型白名单:只遍历 schema 里定义的核心关系,忽略辅助关系
- 路径长度惩罚:越长的路径得分越低,在融合阶段自然沉底
- 中间节点度数限制:如果某个中间节点的度数超过阈值(比如 500),跳过该节点,避免经过超级节点
超级节点是图数据库的经典问题。一个"文档"节点可能连接了几万个实体,任何经过它的路径都会爆炸。我的做法是在 schema 层面就把这类节点标记为"不可作为中间节点",只允许作为起点或终点。
4.3 大模型"幻觉"引用不存在的路径
生成阶段最怕模型编造路径。我试过在 prompt 里写"不要编造",效果有限。后来改成结构化引用:给每条路径编号,要求模型在答案里用 [P1][P2] 的形式标注来源,然后后处理校验这些编号是否真实存在。
这个改动之后,幻觉率从大概 15% 降到了 3% 以下。剩下的 3% 主要是模型把路径内容理解错了,而不是凭空编造,性质没那么严重。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 图查询返回空 | 实体链接失败 | 检查实体名匹配和向量相似度阈值 |
| 查询延迟高 | 路径爆炸或向量库索引未优化 | 看遍历路径数、检查向量索引类型 |
| 答案答非所问 | 意图解析偏差 | 对比解析结果和原始查询 |
| 召回结果重复 | 双写不一致 | 校验图库和向量库的节点 ID 映射 |
| 新知识检索不到 | 索引未更新 | 检查双写管线的触发机制 |
4.5 增量更新的处理
知识库不可能一次建完,增量更新是常态。我的做法是图库实时写,向量库准实时写。图库的写入是同步的,保证关系立即可查;向量库的 embedding 生成有延迟,走异步队列,一般秒级到分钟级完成。
这里有个坑:如果向量库更新滞后,用户刚录入的知识可能搜不到。我的缓解方案是在向量库更新完成前,对新增实体做一个短期的"字面匹配兜底",等 embedding 写入后再切换到正常路径。
5. 效果评估与持续优化
5.1 怎么衡量这套系统好不好
单看召回率或准确率都不够,因为这是多阶段管线,每个阶段的误差会累积。我建了一套分层评估指标:
- 实体链接准确率:人工标注 200 条查询,看链接对的比例
- 路径相关性:对返回的路径做人工打分,1-5 分
- 答案忠实度:答案是否只基于给定路径,有无编造
- 端到端满意度:业务方对最终答案的采纳率
这四个指标里,我最看重答案忠实度。因为前三个再高,如果模型编造内容,业务方就不敢用。忠实度上不去,整套系统的信任就建立不起来。
5.2 持续优化的几个方向
跑了大半年,我总结了几个投入产出比最高的优化点:
第一,扩充实体别名表。这是最土但最有效的办法。把业务里常见的口语化说法、缩写、错别字都收进别名表,实体链接准确率能提升十几个百分点。
第二,优化抽取 prompt。抽取质量直接决定图谱质量。我每两周会抽样检查抽取结果,发现错误就补充到 prompt 的 few-shot 示例里。这个工作很枯燥,但回报很直接。
第三,调整融合权重。RRF 虽然鲁棒,但在特定查询类型下,适当加权能提升效果。比如纯关系查询,图召回的权重可以调高。
第四,缓存高频查询。知识库场景下,查询重复率其实不低。对高频查询做结果缓存,能显著降低延迟和成本。
5.3 一个容易被忽视的点:可解释性
这套系统相比纯向量方案,最大的隐性优势是可解释。向量检索返回的是一堆相似度分数,业务方看不懂为什么返回这些。而图路径是可视化的:A 部件导致 B 故障,B 故障发生于 C 工况,这条链路一目了然。
我在前端把推理路径画成了图,业务方能看到系统"怎么想的"。这个功能上线后,业务方的信任度明显提升,反馈问题的质量也高了——他们会说"这条路径不对,应该是另一条",而不是笼统地说"结果不准"。
6. 一些个人体会
这套架构跑下来,我最大的感受是:技术选型的难点不在单个组件,而在组件之间的边界划分。向量库和图库各自都很成熟,但把它们粘在一起,边界在哪、谁负责什么、数据怎么流转,这些才是真正花时间的地方。
另一个体会是,不要追求一步到位。我一开始想做一个全自动的抽取-建图-检索-推理闭环,结果每个环节都不够稳。后来改成半自动:抽取结果先人工审核一批,图谱质量稳定后再逐步放开。慢是慢了点,但系统能真正用起来,比一个跑不通的"全自动"强得多。
最后说个具体的:如果你的团队刚开始做这类项目,建议先用一个小而完整的场景跑通全链路,哪怕只有几百个实体。全链路的体感,比在单个组件上抠细节重要得多。链路跑通了,再逐步扩规模、加关系、调参数,心里才有底。