Agent教程21:知识图谱🕸,让AI🤖学会联想

1 阅读12分钟

ScreenShot_2026-03-16_071626_940.png

这是我的专栏《春哥的Agent通关秘籍》系列文章的第 21 篇。

希望系统性跟着我一起学AI-Agent编码的同学可以关注一下我的这个专栏。


联想并不是人类独有的高级能力。但它确实更高级。

一、语义检索的局限

RAG向量检索,虽然能超越文本进行语义上的模糊搜索,但如果在逻辑上存在超越基础语义的关联式联想,那么RAG向量检索可能就力所不及了。

让我们思考以下场景。

假设,你的RAG向量库存了三条向量:

  • “小李精通 Python。”
  • “小李目前在用 n8n 搭建自动化工作流。”
  • “n8n的 Code 节点支持Python 和 JS 脚本”

现在,假设小李提问:“帮我在 n8n 写一段脚本,实现xxxx”

此时,传统的RAG检索思路,要么直接用这句话去检索,要么提前处理一下,用更为精炼的语言去检索。

但,核心内容通常都是“n8n”、“写脚本”、“实现xxx”这些从原问题中提取出来的语义。

检索结果可能会包含:

  • “小李目前在用 n8n 搭建自动化工作流。”
  • “n8n的 Code 节点支持Python 和 JS 脚本”

但几乎很难命中到“小李精通 Python。” 这条记忆信息,从而在给出脚本语言时,可能会错误地选中“JS”作为脚本语言。

而咱们人类实际的记忆方式其实是这样的:

  • 小李要用n8n脚本实现xxx
  • n8n的 Code 节点支持Python 和 JS 脚本
  • 小李擅长Python
  • 那好,我用Python写一段脚本给小李吧

对,这就是人类记忆的联想检索模式,能把“n8n脚本”和“小李擅长Python”关联起来。

通过代码也能实现这种关联记忆吗?

能的兄弟!包能的。

这就是AI记忆领域最强大的杀手锏之一:知识图谱记忆(Knowledge Graph Memory)。

二、图数据库与 Cypher语法

如上面的例子所说,这个世界上的数据不仅仅是 Excel 表格里冷冰冰的行与列,它们更是错综复杂的关系网。为了处理这种“网状”数据,图数据库(Graph Database) 和它的专属查询语言 Cypher 应运而生。

想象一下,当你在会议室的白板上给同事讲解业务逻辑时,你会怎么画?

你大概率会画几个圆圈(代表实体,比如人、商品、公司),然后用带箭头的线条(代表关系,比如购买、认识、入股)把它们连起来。

这就是图数据库的底层逻辑。它不存表格,它直接把你在白板上画的“圆圈和线条”存进硬盘里。

  • 圆圈 = 节点(Node)

  • 线条 = 关系(Relationship)

图数据库应用而生,相比于传统数据库,它有什么特点?

  • 传统数据库更擅长通过索引搜索具体的数值,而在连表查询Left Join时,在多表关联时极其容易因为笛卡尔积的原因,导致查询时间爆炸。

  • 图数据库恰恰相反,它非善于善于从一个实体关联到另外的实体,速度极快,在多层实体关联场景下,对传统数据库进行降维打击。

列举几个最常见的例子:

  • 社交网络与推荐:从海量数据里找到你的3度好友(朋友的朋友的朋友),放到【你可能认识的人】里。
  • 金融侦察与反诈骗:从资金海量的转移过程中,它善于顺着各种资金流向,找到诈骗集团真正的账户。
  • 各类拓扑结构评估,比如信息系统的架构等影响评估等。

因而,也衍生出了许多种能支持各类场景的图数据库出来,比如:Neo4jMemgraphKùzuFalkorDB 等。

其中在各类图数据库中,使用最广的一种查询语法,名为:Cypher

你可以把它为理解为 图库里的 SQL

Cypher 语法的核心是两个概念:

  1. 节点(Node)。使用小括号包裹,写法如下:

    (p:Person {name: "小李"})
    

    数据库就知道你要找一个类型是 Person,且名字叫“小李”的节点。

  2. 关系(Relationship)。使用如下结构:-[]->

    -[:KNOWS]->
    

    当你写下 -[:KNOWS]->,数据库就知道这是一条名叫“认识”的带方向的线。

通过这两个概念,就能够拼凑出一条具备语义的查询语句:

MATCH (p1:Person {name: "小李"})-[:KNOWS]->(friend:Person)-[:KNOWS]->(fof:Person)
RETURN fof.name

翻译一下这段代码:

“请帮我匹配这样一个图案:从小李 (p1) 出发,顺着认识的线 -[:KNOWS]-> 找到朋友 (friend),再顺着认识的线 -[:KNOWS]-> 找到朋友的朋友 (fof)。最后把这个人的名字给我。”

也就完成了“二度好友”的迅速查询。

是不是还挺好理解的?

这就是 Cypher 的魅力。

你不需要告诉数据库“怎么找”,你只需要向数据库“描述那个图案的长相”,底层的图引擎会自动为你规划最优的寻路路线。

三、实体语句构建

了解基础 Cypher 语法是必要的,这样才能更准确的要求LLM构建合适的JSON结构,以便完成日常的增删改查操作。

3.1. 防重插入实体

```Cypher
// 语义:图里有叫$name的 Person 就不管,没有就新建一个。
MERGE (n:Person {name: $name})
```

3.2. 实体属性更新

```Cypher
MERGE (n:Person {name: $name})
// 如果是全新创建的节点,打上出生时间戳,初始化提及次数为 1
ON CREATE SET 
    n.created_at = $current_time,
    n.mention_count = 1
// 如果是已经存在的节点,更新最后活跃时间,提及次数 +1
ON MATCH SET 
    n.last_seen = $current_time,
    n.mention_count = coalesce(n.mention_count, 0) + 1
```

3.3. 在Python语法中的示例

注意:在 Cypher 中,节点类型(Label)不能作为变量传递,必须用 Python 字符串拼接(f-string);而属性值必须作为参数(Parameters)传递,防注入且提升性能。

entity_label = "Language"  # 从大模型提取的 JSON 中获取
entity_name = "Python"

# 1. 构建 Cypher 模板
cypher_node = f"""
    MERGE (n:{entity_label} {{name: $name}})
    ON CREATE SET n.weight = 1
    ON MATCH SET n.weight = n.weight + 1
"""
# 2. 传参执行
conn.execute(cypher_node, parameters={"name": entity_name})

四、关系语句构建

核心原则:

  • 连线必须建立在已有的实体之上;
  • 连线本身不能重复生成;
  • 能够通过权重(Weight)体现关系的强弱。

4.1 先MATCH,再MERGE

绝对不要直接 MERGE 整个带两头节点的关系链,这在 Cypher 中是极其危险的,极易导致意想不到的节点重复。

正确做法:先把两头“锚定”(MATCH),再在它们之间“拉线”(MERGE)。

// ✅ 推荐的写法:先 MATCH,后 MERGE
MATCH (source:Person {name: '小李'}), (target:Language {name: 'Python'})
MERGE (source)-[r:KNOWS]->(target)

注意:如果不 MATCH 直接 MERGE,可能会导致创建多个【小李】节点和【Python】节点。

// ❌ 危险的写法:直接 MERGE 整个模式
MERGE (p:Person {name: '小李'})-[:KNOWS]->(l:Language {name: 'Python'})

4.2 记忆加深

如果用户在对话中反复强调过两百多次:“我喜欢孙燕姿”,那么和他只提过一次的“最近喜欢二手玫瑰”。

那么,在记忆中这里可能需要一些区分。

这个区分就是“权重”,我们可以再更新时,给权重属性+1。

实际上,就是把【我】【喜欢】【孙燕姿】中的【喜欢】这条线上的一个属性值不断递增。

MATCH (source:Person {name: $s_name}), (target:Tool {name: $t_name})
MERGE (source)-[r:USES]->(target)
// 第一次连线
ON CREATE SET 
    r.weight = 1, 
    r.established_at = $current_time
// 再次听到同样的连线
ON MATCH SET 
    r.weight = r.weight + 1,
    r.last_confirmed_at = $current_time

有了这个 weight 属性,未来 Agent 检索时就可以加上 WHERE r.weight > 3,自动过滤掉那些只提过一次的噪音干扰。

4.3 Python代码示例

source_label = "Person"
source_name = "小李"
target_label = "Tool"
target_name = "n8n"
relation_type = "USES"

cypher_rel = f"""
    MATCH (source:{source_label} {{name: $s_name}}), (target:{target_label} {{name: $t_name}})
    MERGE (source)-[r:{relation_type}]->(target)
    ON CREATE SET r.weight = 1
    ON MATCH SET r.weight = r.weight + 1
"""

conn.execute(cypher_rel, parameters={
    "s_name": source_name, 
    "t_name": target_name
})

五、设计“图查询智能”的结构

了解了图的概念,我们就需要开始设计,如何在Agent中合理地使用图数据库了。

那么就涉及到两个核心问题要处理:

  • 如何从用户的对话里解析出图结构
  • 如何结合上下文,从图数据库里查询出有用的图结构

一般来说,流程如下图所示:

  • 定期对聊天历史进行消息提取,生成三元组,插入图数据库,形成【知识图谱】
  • 当用户与LLM交互时,如需要查询知识图谱,则由另一子LLM生成Cypher查询语句,将查到的信息返回主LLM进行解答。

因此,此时我们其实由三个LLM Client:

  • Client 1: 和用户交互的Client
  • Client 2: 提取三元组知识
  • Client 3: 根据诉求生成Cypher查询语句

六、实现“提取三元组知识”和“生成查询语句”

这两个实现其实不复杂,无非就是SystemPrompt,输出结构设置好,条件配好,通过LLM就能完成提取。

相信看文章的你也可以轻松做到。

需要注意的是,至少需要在提示词里给好以下提示:

  • 你有哪些实体类型需要提取
  • 你有哪些关系类型需要提取
  • 输出的JSON格式/Cypher格式如何,给好定义和示例

这里就不放具体实现了,不然又是几百字的提示词,有灌水嫌疑,需要的同学可以在这里查看demo源码:

github.com/zhangshichu…

以我们上面的例子为例,加入用户输入:“”

LLM帮我们生成了如下Cypher查询语句:

MATCH (n8n:Entity {name: "n8n"})
OPTIONAL MATCH (n8n)-[:SUPPORTS]->(language:Entity)
OPTIONAL MATCH (n8n)-[:USES]->(tool:Entity)
OPTIONAL MATCH (person:Entity {entity_type: "Person"})-[:USES]->(n8n)
OPTIONAL MATCH (person)-[:KNOWS]->(person_language:Entity)
OPTIONAL MATCH (person)-[:LEARNS]->(person_learns:Entity)
OPTIONAL MATCH (person)-[:INTERESTED_IN]->(person_interest:Entity)
OPTIONAL MATCH (person)-[:WORKS_AT]->(company:Entity)
OPTIONAL MATCH (company)-[:USES]->(company_tool:Entity)
OPTIONAL MATCH (web_scraping:Entity {name: "网页抓取"})
OPTIONAL MATCH (script_writing:Entity {name: "脚本编写"})
OPTIONAL MATCH (tech_solution:Entity {name: "技术方案"})
OPTIONAL MATCH (web_scraping)-[:BUILDS_WITH]->(scraping_component:Entity)
OPTIONAL MATCH (script_writing)-[:BUILDS_WITH]->(scripting_component:Entity)
OPTIONAL MATCH (tech_solution)-[:BUILDS_WITH]->(solution_component:Entity)
RETURN
    n8n,
    collect(DISTINCT language) AS n8n_supported_languages,
    collect(DISTINCT tool) AS n8n_used_tools,
    collect(DISTINCT person) AS n8n_users,
    collect(DISTINCT person_language) AS user_known_languages,
    collect(DISTINCT person_learns) AS user_learning_topics,
    collect(DISTINCT person_interest) AS user_interests,
    collect(DISTINCT company) AS user_companies,
    collect(DISTINCT company_tool) AS company_tools,
    web_scraping,
    collect(DISTINCT scraping_component) AS web_scraping_components,
    script_writing,
    collect(DISTINCT scripting_component) AS script_writing_components,
    tech_solution,
    collect(DISTINCT solution_component) AS tech_solution_components

这个查询语句的核心逻辑在于:

  • MATCH 严格经过节点

    • MATCH (n8n:Entity {name: "n8n"}): 必须经过n8n节点
  • OPTION MATCH 可选经过节点

    OPTIONAL MATCH (n8n)-[:SUPPORTS]->(language:Entity)
    OPTIONAL MATCH (n8n)-[:USES]->(tool:Entity)
    OPTIONAL MATCH (person:Entity {entity_type: "Person"})-[:USES]->(n8n)
    OPTIONAL MATCH (person)-[:KNOWS]->(person_language:Entity)
    
    • 上面的语句的意思是:“去打听下 n8n 支持啥语言、依赖啥工具;然后再顺藤摸瓜揪出是谁在用它,以及这人自己会啥编程语言。”
    • OPTION 的意思是:有符合的就查,没符合的也没关系,不严格

通过这种语句,能形成非常广泛且发散的联想联调。

查询结果如下:

[{'_id': {'offset': 2, 'table': 0}, '_label': 'Entity', 'name': 'n8n', 'entity_type': 'Tool', 'properties': None}, [{'_id': {'offset': 3, 'table': 0}, '_label': 'Entity', 'name': 'JavaScript', 'entity_type': 'Language', 'properties': None}, {'_id': {'offset': 1, 'table': 0}, '_label': 'Entity', 'name': 'Python', 'entity_type': 'Language', 'properties': None}], None, [{'_id': {'offset': 0, 'table': 0}, '_label': 'Entity', 'name': '小李', 'entity_type': 'Person', 'properties': '{}'}], [{'_id': {'offset': 1, 'table': 0}, '_label': 'Entity', 'name': 'Python', 'entity_type': 'Language', 'properties': None}], [{'_id': {'offset': 4, 'table': 0}, '_label': 'Entity', 'name': 'Rust', 'entity_type': 'Language', 'properties': '{}'}], None, None, None, None, None, None, None, None, None]

这一大串JSON什么鬼?其实翻译一下很简单:

"图谱查明是 小李 正在使用 n8n(该工具支持 JS 和 Python),并且他本人刚好懂 Python(最近还在学 Rust)。"

咦?通过联想,小李在当前场景更适合Python的联想居然就有了,再把这些信息给主LLM做参考,整个记忆联调就通顺了!

七、图谱的缺陷

如果三元组知识图谱的关联能力那么强,为什么它在大家讨论记忆方案时,并不是最热门最常见的技术呢?

任何技术都有它的优势与劣势,上面我们谈到了知识图谱的优势。

下面我们也有必要了解它的缺陷,让它流行程度上不那么高。

7.1 入库难、实体歧义

相比于前面两节提到的“事实”和“摘要”,图谱的入库必要要求以下几点:

  • 节点识别是准确的
  • 关系识别是准确的

这种准确甚至包含到大小写,Pythonpython 不做好准确区分的话,甚至都会导致入库两个实体。

更遑论,用户可能在沟通时使用类似 Py 这样的简称,一不小心就实体重复。

关系也是:LikeLIKE_USES一不小心,会产生两个关系。

而准不准确这点,非常看LLM的脸色,不稳定。

因此显然不如“事实”和“摘要”皮实可靠。

7.2 记忆更新与冲突覆盖极其痛苦

还记得我们在向量库(RAG)里是怎么做记忆更新的吗?

  • 把旧的 Chunk 删掉,塞一个新的 Chunk 进去就行了。

但在图谱里,修改一个事实牵一发而动全身:

假设小李说:“我不用 Python 了,我现在全栈转 Node.js。” 在图谱里更新这个动作需要:

  • 查找到 (小李) 节点。

  • 查找到它和 (Python) 节点之间的所有关系线。

  • 删除 [CURRENTLY_USING] 连线(但还要考虑是否保留 [USED_IN_PAST] 连线作为历史经验)。

  • 创建新的 (Node.js) 节点。

  • 建立新的连线。

如果涉及复杂的层级关系,这种图结构的级联更新(Cascading Updates)在工程代码里写起来非常容易出 Bug。

八、结合与互补

让我们理解向量与图谱两种记忆方式,不难发现:

  • 纯图谱太“轴”(容错率极低,构建成本极高)
  • 纯向量太“傻”(没有逻辑观,容易产生幻觉)

所以最新的趋势中,是把它们缝合起来:

  • 用图谱做骨架:约束核心的业务逻辑、技术栈拓扑。

  • 用向量做血肉:把大段的文本、模糊的表述转化为 Embedding,挂载在图节点上。

  • 检索时:先用向量的“模糊匹配”搞定实体消歧(查找到正确的入口节点),然后再顺着图谱的“精确连线”进行逻辑推理。

demo中用到的Kuzu其实就支持这种方式,不过非常可惜的是Kuzu已经归档不再维护了。

但还有很多选择,如 FalkorDBLiteCogDB 等。

九、小结

这一节我们学习【知识图谱】这种可以给AI赋能联想能力的记忆结构。

demo地址在这里:github.com/zhangshichu…