这是我的专栏《春哥的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度好友(朋友的朋友的朋友),放到【你可能认识的人】里。
- 金融侦察与反诈骗:从资金海量的转移过程中,它善于顺着各种资金流向,找到诈骗集团真正的账户。
- 各类拓扑结构评估,比如信息系统的架构等影响评估等。
因而,也衍生出了许多种能支持各类场景的图数据库出来,比如:Neo4j、Memgraph、Kùzu、FalkorDB 等。
其中在各类图数据库中,使用最广的一种查询语法,名为:Cypher。
你可以把它为理解为 图库里的 SQL。
Cypher 语法的核心是两个概念:
-
节点(Node)。使用小括号包裹,写法如下:
(p:Person {name: "小李"})数据库就知道你要找一个类型是 Person,且名字叫“小李”的节点。
-
关系(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源码:
以我们上面的例子为例,加入用户输入:“”
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 入库难、实体歧义
相比于前面两节提到的“事实”和“摘要”,图谱的入库必要要求以下几点:
- 节点识别是准确的
- 关系识别是准确的
这种准确甚至包含到大小写,Python 和python 不做好准确区分的话,甚至都会导致入库两个实体。
更遑论,用户可能在沟通时使用类似 Py 这样的简称,一不小心就实体重复。
关系也是:Like和LIKE_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已经归档不再维护了。
但还有很多选择,如 FalkorDBLite 、CogDB 等。
九、小结
这一节我们学习【知识图谱】这种可以给AI赋能联想能力的记忆结构。
demo地址在这里:github.com/zhangshichu…