一开始我们试过用分类模型来判断 query 该走哪条路径,后来发现用几条简单的规则,比如 query 里有明确的人名/组织名就走知识图谱,否则走向量检索,效果反而更稳定。工程上的简单往往比算法上的精巧更可靠。
不是二选一的问题,而是看你的数据长什么样、用户怎么问。
我做过三个 RAG 项目,分别用过纯向量检索、纯知识图谱、以及两者混合的方案。最后得出的经验是:选错了技术路线,后面怎么调优都很难救回来,但选对了,很多问题根本不用调就自然解决了。
先搞清楚你的数据和查询模式
讨论技术选型之前,先回答两个问题:
第一,你的知识库里,实体之间的关系重要吗?
如果做的是企业内部的制度问答,用户问"出差报销的流程是什么",这类问题的答案基本就是一段文本,实体之间的关系不复杂。
这种场景向量数据库就够用了,把文档切块、做 embedding、检索最相似的几个 chunk、扔给 LLM 生成答案,流程清晰,工程量也不大。
但如果做的是医疗领域的问答,用户问"某个药物和某个药物能不能一起吃",这个问题的答案不在某一段文本里,而是分散在药物说明书、临床指南、不良反应数据库等多个来源中,需要把这些关系串起来才能回答:
药物A → 作用于 → 靶点X 药物B → 抑制 → 酶Y 酶Y → 代谢 → 药物A这种场景,知识图谱的优势就很明显了。
第二,你的用户是怎么提问的?
如果用户的问法比较模糊、口语化,比如"那个之前开会提到的关于降本的方案,具体内容是什么来着",这种查询靠关键词匹配几乎不可能命中,但向量检索的语义理解能力可以很好地处理。
如果用户的问法是结构化的、精确的,比如"张三在 2024 年 Q3 负责了哪些项目",这种查询需要精确匹配实体(张三)和关系(负责),知识图谱的图查询会比向量检索更准。
向量数据库适合什么场景
向量数据库的核心能力是语义相似度匹配。把一段文本转成一个高维向量,查询的时候把用户的问题也转成向量,然后在向量空间里找最近的几个邻居。
它的优势在于:
对非结构化文本友好,文档、邮件、聊天记录、会议纪要,这些东西很难提取出干净的实体关系,但做 embedding 很方便。
语义理解能力强,用户说"降本增效"和文档里写的"成本优化措施",虽然字面不一样,但 embedding 之后的向量距离是很近的。
工程落地快,现在主流的向量数据库(Milvus、Pinecone、Weaviate、Qdrant)都比较成熟,配合 LangChain 或 LlamaIndex,一两天就能搭出一个可用的 RAG pipeline。
# 一个典型的向量检索 RAG pipeline(伪代码) from langchain.vectorstores import Milvus from langchain.embeddings import OpenAIEmbeddings # 1. 文档切块 + embedding chunks = text_splitter.split_documents(documents) vectorstore = Milvus.from_documents(chunks, OpenAIEmbeddings()) # 2. 检索最相似的 chunk query = "出差报销的流程是什么" relevant_chunks = vectorstore.similarity_search(query, k=5) # 3. 扔给 LLM 生成答案 answer = llm.generate(cnotallow=relevant_chunks, questinotallow=query)但它有几个明显的不足,我在项目里踩过一个大坑:向量检索对关系型问题几乎无能为力。
举个真实例子。我们做了一个企业知识库,里面有很多项目文档。用户问:
"A 项目的负责人同时还负责了哪些其他项目?"
这个问题需要两步推理:第一步,找到 A 项目的负责人是谁;第二步,找到这个人还负责了哪些项目。
向量检索能做的是,把这个问题的 embedding 和所有文档 chunk 的 embedding 做比较,召回和 A 项目语义最相近的几段文本。但这些文本里大概率只提到了 A 项目本身的信息,并不会包含"这个负责人同时还管着 B 项目和 C 项目"这种跨文档的关联信息。
我们当时试了几种补救措施:
- 增大召回数量:top-k 从 5 调到 20
- HyDE:先让 LLM 生成假设性回答再检索
- Multi-query:把原始问题拆成多个子查询分别检索
有一定改善,但终究是在语义匹配的框架里打补丁,治标不治本。
另一个坑是数值和时间的精确匹配。向量检索本质上是模糊匹配,用户问"2024 年第三季度的销售额",它可能召回 2023 年或者第二季度的内容,因为这些文本在语义上确实很相近。
我们后来的做法是在向量检索之外加了一层metadata filter,把年份、季度这些结构化字段单独提取出来做精确过滤,再在过滤后的结果集里做向量排序。这个方案管用,但等于你自己又手工补了一部分结构化检索的能力。
# 加了 metadata filter 的向量检索 results = vectorstore.similarity_search( query="第三季度的销售额", k=5, filter={ "year": 2024, "quarter": "Q3" } )知识图谱适合什么场景
知识图谱的核心是把知识表示成三元组,实体-关系-实体,然后通过图查询语言做多跳推理。
它的强项在于:
多跳推理,"A 项目的负责人还负责了哪些项目",这种问题在图上就是两跳查询,结果精确且完整:
// Cypher 查询:两跳推理 MATCH (p:Project {name: "A项目"})<-[:负责]-(person)-[:负责]->(other:Project) WHERE other.name <> "A项目" RETURN person.name, collect(other.name) AS other_projects关系的可解释性。向量检索返回的是"最相似的文本片段",用户看不到推理过程。知识图谱返回的是一条清晰的推理路径:
张三 → 负责 → A项目 张三 → 负责 → B项目 张三 → 负责 → C项目可解释性强很多。在金融、医疗、法律这些合规要求高的领域,这个特性有时候是硬需求。
数据更新的精确性。如果某个项目换了负责人,在知识图谱里改一条边就行了。在向量数据库里,你得找到所有提到这个关系的 chunk,重新做 embedding,还得确保旧的 chunk 被正确删除或更新,操作上容易出错。
知识图谱的门槛
但知识图谱的门槛也不低!
最大的成本在知识抽取。原始数据大概率是非结构化的文档,要把它们变成三元组,需要做实体识别(NER)、关系抽取(RE)、实体消歧。这三步里每一步都可能引入误差。
我们做过一个尝试,用 GPT-4 从技术文档里自动抽取三元组。效果怎么说呢,简单的关系比如"某人属于某部门"抽得还行,准确率大概在 85% 左右。但复杂一点的关系,比如"某系统在某条件下依赖某服务的某个接口",准确率直接掉到 60% 以下。
# 用 LLM 自动抽取三元组(伪代码) prompt = """ 从以下文档中抽取实体和关系,输出为三元组格式: (实体1, 关系, 实体2) 文档内容: {document_text} """ # 简单关系:(张三, 属于, 技术部) → 准确率 ~85% # 复杂关系:(订单系统, 在高并发场景下依赖, 支付网关的回调接口) → 准确率 <60%抽取质量不行,后面的图查询结果自然也不可靠。
我们最后的做法是半自动化:LLM 先做初步抽取,然后由领域专家 review 和修正。这个人工成本是不低的,一万篇文档大概需要两个人花三周时间做质量审核。所以如果你的知识库规模大、更新频繁,纯知识图谱的方案在维护成本上可能会让你吃不消。
还有一个问题是schema 设计。知识图谱需要预先定义实体类型和关系类型,这个 schema 设计得不好,后面很难改。我们在一个项目里,一开始没把"临时负责"和"正式负责"区分成两种关系类型,结果后面业务方要查"正式负责人是谁"的时候,发现数据里混着一堆临时代理的关系,只能回去改 schema 重新导数据。
混合方案
说实话,我做的三个项目最终都走向了某种程度的混合方案,区别只在于混合的方式不同。
方案一:向量检索为主,知识图谱做补充
适合知识库以非结构化文档为主、但有一部分核心实体关系需要精确查询的场景。
具体做法是:先用向量检索召回相关文档片段,同时从用户 query 中提取实体,去知识图谱里查相关的结构化信息(比如实体的属性、关联实体),然后把两部分结果一起塞给 LLM 做生成。
# 方案一:向量检索为主 + 知识图谱补充(伪代码) def hybrid_retrieve(query): # 1. 向量检索:召回相关文档 vector_results = vectorstore.similarity_search(query, k=5) # 2. 实体提取:从 query 中抽取实体 entities = extract_entities(query) # e.g., ["A项目"] # 3. 图谱查询:查关联的结构化信息 graph_results = [] for entity in entities: related = knowledge_graph.query( f"MATCH (n)-[r]->(m) WHERE n.name='{entity}' RETURN n,r,m" ) graph_results.extend(related) # 4. 合并上下文,交给 LLM context = merge(vector_results, graph_results) return llm.generate(cnotallow=context, questinotallow=query)我们在一个内部知识库项目里用的就是这个方案。向量检索负责回答"这个东西是什么"、"怎么用"这类问题,知识图谱负责回答"谁负责的"、"依赖了哪些组件"、"上下游是什么"这类关系型问题。路由层根据 query 的类型决定走哪条路径,或者两条都走然后合并结果。
实际效果是,相比纯向量检索,整体的答案准确率提升了很多,主要提升来自于那些关系型问题,之前纯向量方案基本答不对的,加了知识图谱之后大部分能答对了。
方案二:知识图谱为主,向量检索做兜底
适合领域知识已经有比较完善的本体结构、但又不可能把所有知识都塞进图谱的场景,比如医疗、金融。
用户 query 先尝试转成图查询,如果图谱里能查到结果,直接返回;如果查不到(可能是图谱覆盖不全,或者 query 太模糊没法转成图查询),再 fallback 到向量检索。
# 方案二:知识图谱为主 + 向量检索兜底(伪代码) def graph_first_retrieve(query): # 1. 尝试将自然语言转成 Cypher cypher = text_to_cypher(query, schema=graph_schema, few_shots=examples) # 2. 语法校验 ifnot validate_cypher(cypher): return fallback_to_vector(query) # 3. 执行图查询 graph_results = knowledge_graph.execute(cypher) # 4. 结果为空则兜底 ifnot graph_results: return fallback_to_vector(query) return llm.generate(cnotallow=graph_results, questinotallow=query)这个方案的核心难点在Text-to-Cypher这一步。让 LLM 把自然语言问题翻译成Cypher查询,对 schema 的理解能力要求很高,而且很容易生成语法正确但语义错误的查询。
我们的做法是给 LLM 提供完整的schema定义和几个few-shot example,同时对生成的 Cypher 做语法校验和结果合理性检查,比如返回结果为空的时候自动 fallback。
方案三:Graph RAG
这是最近比较热的一种思路,微软开源的 GraphRAG 项目把这个概念带火了。核心思想是用 LLM 从文档中自动构建社区级别的知识图谱,然后在检索时利用图的社区结构做多层次的摘要和检索。
我试过这个方案,说说真实感受:
全局性问题效果好,比如"这个代码库整体的架构是怎样的",它能把分散在多个文档里的信息通过图的社区结构汇总起来,比纯向量检索强
缺点也很明显,构建索引的成本极高,token 消耗是纯向量方案的几十倍,而且对于细节性的点查询,某个函数的参数是什么,效果反而不如简单的向量检索
所以GraphRAG 更适合需要跨文档总结和全局视角的场景,不太适合做精确的事实查询。
怎么选型?一张表说清楚
几个容易踩的坑
最后说几个我自己踩过的具体的坑:
1. 别在向量检索上死磕多跳推理
我见过有团队用很复杂的 prompt chain 试图在纯向量检索的基础上实现多跳推理,先检索一次拿到中间结果,把中间结果再作为 query 检索第二次,如此反复。
能跑通,但召回的噪音会随着跳数指数级放大,三跳以上基本不可用。这种场景就老老实实用知识图谱。
2. 知识图谱的冷启动比你想象的慢
如果你的领域没有现成的本体结构可以复用,从零开始设计 schema、抽取三元组、做实体消歧,光前期准备工作就可能要一两个月。别低估这个时间。
3. 向量数据库的 embedding 模型选型很关键
我们早期用的是通用的 sentence-transformers 模型,后来换成了在领域数据上 fine-tune 过的 embedding 模型,召回率提升不少。如果你的领域有大量专业术语,通用模型的效果可能会打折扣。
4. 混合方案里的路由层不要过度设计
一开始我们试过用分类模型来判断 query 该走哪条路径,后来发现用几条简单的规则,比如 query 里有明确的人名/组织名就走知识图谱,否则走向量检索,效果反而更稳定。工程上的简单往往比算法上的精巧更可靠。