向量检索适合非结构化文本的语义匹配,知识图谱适合实体关系明确的多跳查询,两者都能作为 RAG 的检索环节。文章以邮件助手为例,展示先用图谱缩小候选范围、再按语义排序的组合方案。
在整个 AI 系统设计系列中,我们会一直以 Mailmind 为例。这是一款虚构的 AI 邮件助手,可以读取收件箱、对邮件进行分流处理,并起草回复。
阅读前建议先看:《RAG 为什么存在:上下文问题》和《读懂 Embedding 与向量搜索》。
RAG(retrieval-augmented generation,检索增强生成)指的是找到相关材料,交给模型,让模型据此回答问题。查找材料的方式可以是关键词搜索、向量搜索、数据库查询,也可以是图遍历。本文比较的是大家争论最多的两种检索方式:向量搜索和知识图谱遍历。两者都可以为 RAG 提供材料。向量搜索使用 Embedding,从非结构化文本中回答与语义有关的模糊问题;图遍历则回答精确的多跳问题,查明特定实体之间如何关联。我们的 AI 收件箱助手 Mailmind 会将两者结合起来:先用图谱缩小候选范围,再用向量搜索,在这个范围内按语义相关性排序。
搜索很快,图遍历也很快。但一旦遇到对方擅长的问题,它们就会失灵。更聪明的系统,并不是索引更好的那个,而是知道什么时候该用哪种查找方式的那个。
经过上一期关于生产级 prompt 工程的介绍,你已经能让 Mailmind 保持稳定一致的行为。但即便指令写得再好,Agent 也只能根据它能找到的信息作答。Mailmind 究竟了解收件箱里的哪些信息?当问题超出聊天窗口最近几条消息的范围时,它又该如何找到正确的记忆?
向量搜索根据语义检索文本片段,而不是根据字面上的词语。当你能描述自己需要什么,却说不出它的具体名称时,向量搜索就是合适的选择。
可以把它想象成一个为想法建立索引,而不是为关键词建立索引的搜索引擎。你输入“客户因为错过截止日期而感到不满的邮件”,它就能找出一些完全没有提到“不满”或“错过截止日期”的邮件。这些邮件可能用“这比预期花的时间更长”之类的表达传达了不满。这种能力来自文本的存储和搜索方式。
Mailmind 会把每封邮件拆成多个相互重叠、每个包含几百个 token 的文本块。邮件到达时,每个文本块都会被转换成一个 Embedding,也就是一组捕捉其语义的数字,而且只需转换一次。这些 Embedding 存储在向量数据库中。给定任意向量,向量数据库都能找出与它最接近的邻居。收到问题后,Mailmind 会将问题转换成 Embedding,再让向量数据库找出向量距离最近的文本块,通常使用余弦相似度进行衡量。排名靠前的结果会作为上下文交给 LLM(large language model,大语言模型)。RAG 这个名称来自 Lewis 等人的研究:他们将一个预训练的神经检索器与文本生成器组合起来,检索器在 Wikipedia 的向量索引上进行检索,并将检索器的查询端与生成器一起微调(Lewis 等人,2020 年,《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》)。如今的处理流程沿用了同样的思路,只是使用现成的 Embedding 模型和向量数据库,通常不需要训练任何东西。
这种检索路径非常适合开放式探索。不过,对于精确事实,它就不合适了。接下来几节会介绍应该用什么来替代。
在考虑图谱之前,先注意一点:很多“结构性”问题根本不是图谱问题。邮件本身就带有元数据,包括发件人、收件人、会话、日期和标签。下面三种查找方式,几乎可以覆盖所有情况:
关系型数据库也能通过多次 join 沿着这样的关系链查询。图数据库则把关系链查询作为一种自然的操作方式,而且随着关系链变长,查询仍然容易阅读。
知识图谱回答的是现实世界中的特定实体如何相互关联。你询问事物之间的关系,它就沿着由事实构成的网络查找,给出精确答案。
可以想象一张地图,上面标着收件箱中的人员、项目、会话、邮件和订单。你可以沿着路径从一个点走到另一个点,并数清中间经过了几步。Mailmind 用节点(Person、Project、Thread、Email、Order)和关系来构建这张地图,关系包括 WORKS_ON、SENT、IN_THREAD、ABOUT、ORDERED 和 REPLIED_TO。
当用户问“Falcon 项目中的哪些人,在过去七天里给我发过邮件,而我还没回复?”时,图谱会从 Falcon 项目节点出发,沿 WORKS_ON 边找到 Person 节点,再沿 SENT 边找到他们在这段时间内发出的邮件,然后排除所有已经收到你回复的邮件。这就是图遍历,通常使用 Cypher 这样的查询语言编写:
MATCH (:Project {name: "Falcon"})<-[:WORKS_ON]-(p:Person)-[:SENT]->(e:Email)
WHERE e.sent_at >= datetime() - duration("P7D")
AND NOT EXISTS { (:Email {from_me: true})-[:REPLIED_TO]->(e) }
RETURN DISTINCT p.name
用文字描述就是:从 Falcon 出发,找到所有参与该项目的人,再找到他们过去七天内发送的邮件,只保留你尚未回复的邮件,最后返回发件人。数据库不需要扫描每一行数据,而是沿着边查找。因此,它的成本模型和失效方式都与向量搜索不同。本系列后续的《面向 AI 的图数据库:当关系本身就是数据》会介绍图数据库的工作原理。
只要图谱本身正确,答案就是精确的:如果某个人从未与 Falcon 建立关联,或者同一个人以两个名字重复出现,查询就无从得知真实情况。而如果你让图谱查找“类似投诉的会话”,它会什么都找不到。这种失败直接暴露了结构化数据的局限,也解释了 Mailmind 为什么要组合多种查找方式。
向量搜索不擅长精确事实。“Sam 有没有回复 Stratos 会话?”并不是一个语义问题,而是一次事实查找:该会话中是否存在一封发件人为 Sam 的邮件?向量搜索可能检索到一个文本块,其中 Sam 在另一个语境中提到了 Stratos,模型随后就可能错误地推断 Sam 已经回复。使用元数据查询,或者检查是否存在 REPLIED_TO 边,就能得到明确的“是”或“否”。
图谱则不擅长处理描述性问题。“找到发件人因配送问题而感到不满的邮件”会查询失败,因为“不满”既不是节点,也不是关系。图谱中没有任何边将 Emotion 与 Email 连接起来。图谱无法从原始文本中计算语气,因此,即使正确的邮件确实存在,它也什么都找不到。
graph TD
A["User question"] --> B["Vector search"]
A --> C["Graph traversal"]
B --> D["Misses exact facts: did Sam reply?"]
C --> E["Misses meaning: upset tone"]
向量搜索与图谱覆盖了记忆的两个不同维度。一个维度是结构:谁对谁做了什么。另一个维度是语义:这是一条什么样的消息。元数据则覆盖每封邮件上直接记录的事实。把它们组合起来,Mailmind 就能回答同时涉及这三类信息的问题。
Mailmind 不会在向量搜索和图谱之间二选一。它会把问题中的结构性部分交给图谱,得到精确的候选列表,再使用向量搜索,在这个列表内按语义相关性排序。图谱处理“谁、哪里、何时”,向量搜索处理“什么样的”。
来看这个问题:“给我看看本月 Falcon 项目中讨论预算的会话。”流程如下:
图谱把搜索空间从整个收件箱缩小到确切相关的会话。随后,向量搜索补上关键词过滤可能漏掉的内容:预算可能被称作“支出”“资金分配”,或者“资金还能支撑多久”。
flowchart LR
A["User question"] --> B{"Structural filter?"}
B -->|Yes| C["Graph traversal: narrow to candidate threads"]
B -->|No| D["Vector search over all chunks"]
C --> E["Vector search over stored chunks, filtered to candidates"]
D --> F["Rank by similarity"]
E --> F
F --> G["LLM with top results"]
这套技术栈还有一处隐患:如果同一个人被存成两个独立节点,上述流程就无法正常工作。实体消歧,也就是知道“Sam K.”和“Samuel Kim”其实是同一个人,才能保证图谱准确可靠。本系列后续的《实体消歧:同一事物,多种名称》会介绍这个问题。
问:如果我已经有向量数据库,为什么还要加图谱?
很多时候,你并不需要。“Sam 在四月发来的邮件”这样的精确事实,可以通过许多向量数据库都支持的元数据过滤器查出,也可以使用普通数据库。只有当问题需要串联多层关系时,图谱才值得引入。例如,“四月里,我给哪些 Falcon 项目的联系人发过邮件?”这个问题需要先知道谁参与了 Falcon 项目,才能继续查看邮件。图谱直接存储这些关系,只需一次遍历就能精确回答,前提是实体已经被正确提取并完成消歧。
问:知识图谱能否存储完整邮件正文,从而完全不使用向量搜索?
你可以把文本作为节点属性存储,但图遍历无法判断两段文字有多相似。如果用户问“找出语气与这封投诉邮件相似的所有会话”,遍历无法比较语气,只能什么都不返回,或者把所有内容都返回。你仍然需要向量搜索按语义打分。一些图数据库现在也内置了向量索引,Neo4j 关于向量索引的文档就介绍了这种能力:直接在图存储内部运行同样的相似度搜索。
问:Mailmind 如何让知识图谱与实时邮件流量保持同步?
每封新邮件都会触发一条小型处理流水线:先使用 NER(named entity recognition,命名实体识别)模型提取实体,再进行身份消歧,最后将节点和关系 upsert 到图谱中。更新和删除则更棘手。Mailmind 采用微批处理方式,从预写日志中重放事件。图谱不是一份静态导出数据,而是持续更新、反映收件箱状态的数据视图。
问:构建知识图谱难道不需要严格的 schema 吗?
现代属性图数据库允许你结合无 schema 的灵活性与轻量约束。Mailmind 的图谱定义了少量核心节点标签(Person、Project、Thread、Email、Order、Meeting)和关系类型,你可以在不停机的情况下添加新的标签和类型。在电子邮件地址等身份字段上设置唯一性约束,可以让图谱保持实用,又不至于限制后续扩展。
问:看起来很简单的问题,向量搜索什么时候也会失败?
当答案取决于某个具体事实或关系时,向量搜索可能检索到同时提及两个实体的文本块,但这些文本并没有证明两者之间存在所需关系。例如,“Alice 有没有回复战略文档会话?”可能会检索到一封 Alice 在其他语境中提到战略文档的邮件,模型随后就可能声称她做出了实际并不存在的回复。使用元数据查找,或者检查图谱中是否存在 REPLIED_TO 边,就能明确回答。
用户问 Mailmind:“找出本周的会话,要求它们的语气与这封因退款延迟而感到沮丧的投诉邮件相似。”你运行了一次纯知识图谱查询,却没有得到任何有用结果。为什么图谱什么都查不到?应该怎样才能真正回答这个问题?
答案: 图谱知道时间戳、参与者和订单。“沮丧的语气”并没有存储在任何地方,它是文本本身的特征,而遍历只能沿着边查找,所以没有可以匹配的内容。要回答这个问题,先用日期过滤器将候选范围缩小到本周的会话。这只是普通的元数据过滤,不需要图谱。如果愿意,可以利用图谱提高那些与退款订单有关的会话的排序权重,因为这封投诉涉及退款;但不要排除其他会话,因为用户问的是语气,而不是退款。然后,为投诉邮件生成 Embedding,在本周已存储的文本块 Embedding 上运行向量搜索,返回包含最高分文本块的会话。Embedding 能捕捉语义和一部分语气,因此很适合做第一轮筛选;如果想得到更准确的信号,可以让模型对候选列表中的沮丧情绪进行标注。过滤器让搜索范围保持较小,向量搜索则把“相似语气”转化成排序。
下一篇:《幻觉检测:信任,但要验证》。它会介绍,即便 Agent 已经找到正确的记忆,如何识别它仍然编造出来的事实。
如果你希望每周都读到这样的拆解,了解真实系统底层究竟如何运作,欢迎订阅 Internals Decoded,网站是 internalsdecoded.com。
《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》(Lewis 等人,2020 年)
《向量索引》(Neo4j Cypher Manual)
本文最初发表于 Internals Decoded,以通俗的对话方式解释 AI 内部原理。
如需采取进一步行动,可以考虑屏蔽此人和/或举报滥用行为。