GraphRAG 房产推荐学习笔记:当通勤/学区/配套变成关系链问题,我怎么补齐检索能力
这是我学习 GraphRAG 的完整记录。通过 3 个递进式 demo,从零跑通 GraphRAG 的核心链路:建图 → 子图检索 → 回到证据 → 生成推荐。
前言
最近在优化房产推荐系统,发现传统 RAG 有个天然的瓶颈:它擅长"单维度匹配",但不擅长"多维度关系组合"。
比如用户问"国贸上班,通勤 40 分钟内,最好有学区,小区离地铁 1 公里以内",传统 RAG 很难同时满足这些条件。
一句话定位:
- 传统 RAG 检索的是"文本相似度"(房源描述里有没有"学区"“地铁”)
- GraphRAG 检索的是"关系链"(小区 → 学校、小区 → 地铁站 → 通勤时间)
GraphRAG 不是"更强的 embedding",而是把知识从"文本块"变成"实体-关系网络",让检索变成"在关系网络上导航"。
本文记录了我从零开始学习 GraphRAG 的完整过程,包括 3 个本地 demo 和对应的讲解。
第二部分:问题分类:硬条件 vs 关系链
这很重要。不是所有问题都需要 GraphRAG,要先判断你的问题是什么类型。
硬条件问题(传统 RAG 就够)
特征:条件明确、维度单一、可以结构化过滤
例子:
"朝阳区三居室 600万"
"北京市 2023 年新房"
"面积 100-150 平方米"
传统 RAG 怎么做:
1. 在房源描述里搜"朝阳""三居""600万"
2. 返回匹配的房源
3. 准确率:80%+
为什么够用:
– 条件都在房源描述里
– 相似度匹配就能解决
– 不需要"关系"
关系链问题(需要 GraphRAG)
特征:条件分散在多个数据源、需要"跨实体推理"
例子:
"国贸上班,通勤 40 分钟内"
→ 需要:小区 → 地铁站 → 地铁线 → 国贸 → 通勤时间
"学区房 + 地铁"
→ 需要:小区 → 学校 AND 小区 → 地铁站
"附近有医院、商场、公园"
→ 需要:小区 → POI(多个)
传统 RAG 怎么做:
1. 搜"通勤 40 分钟"
2. 可能搜到"房子 40 年房龄"(无关)
3. 准确率:40%
为什么不够用:
– 通勤时间不在房源描述里
– 需要"小区 → 地铁 → 通勤时间"的关系链
– 相似度匹配无法处理
GraphRAG 怎么做:
1. 识别实体:国贸、地铁站、小区
2. 扩展关系:国贸 ← 地铁线 ← 地铁站 ← 小区
3. 计算通勤时间
4. 返回满足条件的小区
5. 准确率:85%+
判断标准
用传统 RAG:
✅ 条件都在房源描述里
✅ 可以用关键词匹配
✅ 不需要跨数据源推理
用 GraphRAG:
✅ 条件分散在多个数据源
✅ 需要"关系链"推理
✅ 涉及"通勤/学区/配套"这种关系问题
第三部分:核心概念
GraphRAG 的 5 个核心概念
| Entity | 实体节点 | 小区、地铁站、学校、房源 |
| Relation | 实体关系 | 距离/位于/对应学区/属于 |
| Graph | 知识网络 | "小区-地铁-学校-板块"的完整网络 |
| Subgraph | 查询相关的局部图 | “望京 → 15号线 → 国贸通勤链” |
| Evidence | 回源证据 | 某房源字段/小区配套数据 |
类比理解
传统 RAG:全文搜索
用户问:"通勤 40 分钟"
系统:在所有文本里搜"40 分钟"
问题:可能搜到"房子 40 年房龄"这种无关内容
GraphRAG:关系网络导航
用户问:"通勤 40 分钟"
系统:
1. 识别实体:"国贸"(目的地)
2. 扩展关系:国贸 ← 地铁线 ← 地铁站 ← 小区
3. 计算通勤时间:小区 → 地铁站 → 地铁线 → 国贸
4. 返回满足条件的小区
优势:精准、可解释、支持多维度组合
第二部分:为什么房产推荐需要 GraphRAG
传统 RAG 的天花板
✅ 传统 RAG 擅长: 硬条件检索
用户问:"朝阳区三居室 600万"
传统 RAG:在房源描述里搜"朝阳""三居""600万"
结果:准确率 80%+
❌ 传统 RAG 不擅长: 关系链组合
用户问:"国贸上班,通勤 40 分钟内,最好有学区,小区离地铁 1 公里以内"
传统 RAG:需要同时满足 4 个条件
结果:准确率 40%(因为这些条件分散在不同数据源)
GraphRAG 的 3 个价值问题
问题 1:通勤链路
"国贸上班,通勤 40 分钟内,推荐哪些小区?"
传统 RAG:
– 搜"通勤 40 分钟"
– 可能搜到"房子 40 年房龄"
– 准确率低
GraphRAG:
– 识别实体:国贸(目的地)
– 扩展关系:国贸 ← 地铁线 ← 地铁站 ← 小区
– 计算通勤时间
– 返回满足条件的小区
– 准确率高 + 可解释
问题 2:学区链路
"想要学区+地铁,哪些小区同时满足?"
传统 RAG:
– 搜"学区"和"地铁"
– 可能返回"有学区的房源"和"靠地铁的房源"
– 但不一定是"同时满足"的
GraphRAG:
– 识别实体:学校、地铁站
– 扩展关系:小区 → 学校、小区 → 地铁站
– 求交集:同时满足两个条件的小区
– 准确率高 + 可解释
问题 3:配套链路
"附近要有公园+医院+商场,小区怎么选?"
传统 RAG:
– 搜"公园""医院""商场"
– 可能返回"有公园的房源""有医院的房源"
– 但不一定是"同时满足"的
GraphRAG:
– 识别实体:公园、医院、商场
– 扩展关系:小区 → POI(兴趣点)
– 求交集:同时满足三个条件的小区
– 准确率高 + 可解释
第三部分:3 个递进式 Demo
Demo 1:最小建图(不用 Neo4j)
目标: 跑通"关系网络"的核心概念,用最小代码实现建图
输入数据(最小三张表):
# 表 1:房源数据
houses = [
{"id": "h1", "name": "望京新城 3 居", "community": "c1", "price": 600},
{"id": "h2", "name": "望京花园 3 居", "community": "c1", "price": 580},
{"id": "h3", "name": "朝阳门 2 居", "community": "c2", "price": 500},
]
# 表 2:小区数据
communities = [
{"id": "c1", "name": "望京", "district": "d1", "subway": "s1"},
{"id": "c2", "name": "朝阳门", "district": "d2", "subway": "s2"},
]
# 表 3:配套数据(地铁、学校、POI)
subway_stations = [
{"id": "s1", "name": "望京站", "line": "l1", "distance_to_cbd": 8},
{"id": "s2", "name": "朝阳门站", "line": "l2", "distance_to_cbd": 3},
]
schools = [
{"id": "sc1", "name": "XX小学", "community": "c1"},
{"id": "sc2", "name": "YY小学", "community": "c2"},
]
pois = [
{"id": "p1", "name": "医院", "community": "c1", "type": "hospital"},
{"id": "p2", "name": "商场", "community": "c1", "type": "mall"},
]
建图代码(邻接表):
# 构建图(邻接表形式)
graph = {
# 实体节点
"entities": {
"h1": {"type": "house", "name": "望京新城 3 居", "price": 600},
"c1": {"type": "community", "name": "望京"},
"s1": {"type": "subway", "name": "望京站"},
"sc1": {"type": "school", "name": "XX小学"},
"p1": {"type": "poi", "name": "医院"},
},
# 关系边
"relations": [
("h1", "属于", "c1"), # 房源属于小区
("c1", "有地铁", "s1"), # 小区有地铁站
("c1", "对应学校", "sc1"), # 小区对应学校
("c1", "附近POI", "p1"), # 小区附近有 POI
("s1", "距离CBD", 8), # 地铁站距离 CBD 8km
]
}
关键点:
- 实体是"节点",关系是"边"
- 这就是 GraphRAG 的核心数据结构
- 不需要复杂的图数据库,用 dict 就能跑通
Demo 2:子图检索(1-hop / 2-hop)
目标: 看到 GraphRAG 的"检索对象变了"——从"文本块"变成"关系路径"
用户问题: “国贸上班,通勤 40 分钟内,推荐哪些小区?”
检索流程(用箭头链路表达):
Query: "国贸上班,通勤 40 分钟内"
↓
Step 1: 实体识别
识别出:国贸(目的地)
↓
Step 2: 子图扩展(1-hop)
国贸 ← 地铁线 ← 地铁站
↓
Step 3: 子图扩展(2-hop)
地铁站 ← 小区
↓
Step 4: 计算通勤时间
小区 → 地铁站 → 地铁线 → 国贸 = 40 分钟?
↓
Step 5: 返回候选小区
满足条件的小区列表
代码实现:
def subgraph_expansion(graph, start_entity, hops=2):
"""
从起始实体出发,扩展子图
"""
visited = set()
subgraph = {"entities": {}, "relations": []}
def dfs(entity_id, current_hop):
if current_hop > hops or entity_id in visited:
return
visited.add(entity_id)
# 添加实体
if entity_id in graph["entities"]:
subgraph["entities"][entity_id] = graph["entities"][entity_id]
# 扩展邻居
for relation in graph["relations"]:
if relation[0] == entity_id:
subgraph["relations"].append(relation)
dfs(relation[2], current_hop + 1)
dfs(start_entity, 0)
return subgraph
# 使用示例
subgraph = subgraph_expansion(graph, "c1", hops=2)
print(f"子图包含 {len(subgraph['entities'])} 个实体")
print(f"子图包含 {len(subgraph['relations'])} 条关系")
两个策略的对比:
hop=1(快速检索):
小区 → 地铁站
优点:快
缺点:链路短,可能漏掉"通勤时间"信息
hop=2(标准检索):
小区 → 地铁站 → 地铁线 → 国贸
优点:链路完整,能计算通勤时间
缺点:稍慢,但通常够用
建议:房产推荐用 hop=2
关键点:
- 子图扩展是 GraphRAG 的核心
- 不同的 hop 数对应不同的"搜索深度"
- hop=2 通常是房产推荐的最佳选择
Demo 3:回源证据 + Evidence Table
目标: 理解"可解释推荐"怎么做——每条推荐都要有证据
用户问题: “学区房 + 地铁 + 600万以内”
推荐输出:
推荐 Top3:
房源 1:望京新城 3 居
价格:600万
学区:对应 XX小学(来自学区数据库)
地铁:距离 15号线望京站 500m(来自 POI 数据)
通勤:15号线到国贸 35 分钟(来自地铁时刻表)
房源 2:望京花园 3 居
价格:580万
学区:对应 XX小学(来自学区数据库)
地铁:距离 15号线望京站 600m(来自 POI 数据)
通勤:15号线到国贸 38 分钟(来自地铁时刻表)
房源 3:朝阳门 2 居
价格:500万
学区:对应 YY小学(来自学区数据库)
地铁:距离 6号线朝阳门站 300m(来自 POI 数据)
通勤:6号线到国贸 15 分钟(来自地铁时刻表)
Evidence Table 代码:
def build_evidence_table(candidates, graph):
"""
为候选房源构建证据表
"""
evidence_table = []
for house_id in candidates:
house = graph["entities"][house_id]
community_id = None
# 找到房源所属小区
for relation in graph["relations"]:
if relation[0] == house_id and relation[1] == "属于":
community_id = relation[2]
break
if not community_id:
continue
community = graph["entities"][community_id]
evidence = {
"house_id": house_id,
"house_name": house["name"],
"price": house["price"],
"evidences": []
}
# 收集证据
for relation in graph["relations"]:
if relation[0] == community_id:
if relation[1] == "有地铁":
evidence["evidences"].append({
"type": "subway",
"value": graph["entities"][relation[2]]["name"],
"source": "POI 数据"
})
elif relation[1] == "对应学校":
evidence["evidences"].append({
"type": "school",
"value": graph["entities"][relation[2]]["name"],
"source": "学区数据库"
})
evidence_table.append(evidence)
return evidence_table
# 使用示例
evidence_table = build_evidence_table(["h1", "h2"], graph)
for evidence in evidence_table:
print(f"房源:{evidence['house_name']}")
for e in evidence["evidences"]:
print(f" {e['type']}: {e['value']} (来自 {e['source']})")
关键点:
- Evidence Table 是"可解释推荐"的关键
- 每条推荐都要有"来源"
- 这样用户能理解"为什么推荐这套房"
第五部分:现实代价(GraphRAG 也不便宜)
GraphRAG 看起来很强,但它有现实的代价。
难:数据准备要求更高
问题: GraphRAG 需要"结构化的实体和关系",不能是"乱七八糟的文本"
传统 RAG:
输入:房源描述文本(可以很乱)
处理:embedding + 相似度匹配
要求:低
GraphRAG:
输入:结构化的实体和关系
处理:图构建 + 子图扩展
要求:高
具体要求:
1. 学校数据必须结构化
❌ "附近有 XX小学"
✅ community_id → school_id(映射关系)
2. 地铁数据必须结构化
❌ "靠地铁"
✅ community_id → subway_station_id → distance(距离)
3. POI 数据必须结构化
❌ "附近有医院、商场"
✅ community_id → poi_id → poi_type(类型)
4. 通勤时间必须结构化
❌ "通勤方便"
✅ subway_station_id → destination_id → time(时间)
工程成本: 需要"数据清洗 + 结构化"的工作
难:实体消歧是必修课
问题: "望京"可能是板块、地铁站、小区名,怎么区分?
错误做法:
直接搜"望京"
结果:混淆了板块、地铁站、小区
正确做法:
1. 用词典区分
"望京" → [板块, 地铁站, 小区]
2. 用上下文消歧
"望京附近" → 板块
"望京站" → 地铁站
"望京新城" → 小区
3. 用向量排序
计算相似度,选择最相关的
工程成本:需要"实体库 + 消歧逻辑"
贵:图构建和维护成本
成本公式:
传统 RAG 成本:
= embedding 模型调用
+ 向量存储
≈ 低
GraphRAG 成本:
= embedding 模型调用
+ 实体抽取(LLM 或规则)
+ 关系抽取(LLM 或规则)
+ 图数据库存储
+ 图查询(子图扩展)
≈ 高
具体数字:
– 实体抽取:每 1000 个 chunk,需要 100 次 LLM 调用
– 关系抽取:每 1000 个 chunk,需要 100 次 LLM 调用
– 图存储:每 10000 个实体,需要 1GB 存储
– 图查询:每次子图扩展,需要 200ms
慢:图构建是离线过程
问题: 图不能实时构建,必须离线预处理
传统 RAG:
新增文档 → embedding → 立即可查询
延迟:秒级
GraphRAG:
新增文档 → 实体抽取 → 关系抽取 → 图更新 → 可查询
延迟:分钟级或小时级
工程成本:需要"离线处理 + 增量更新"的流程
第六部分:企业级迁移
从 demo 到生产,需要补 4 个模块:
模块 1:实体归一化(消歧)
问题: "望京"可能是板块、地铁站、小区名,怎么区分?
# 错误做法:直接搜"望京"
# 结果:混淆了板块、地铁站、小区
# 正确做法:实体归一化
entity_mapping = {
"望京": {
"type": "district", # 板块
"id": "d1",
"canonical_name": "朝阳区望京"
},
"望京站": {
"type": "subway", # 地铁站
"id": "s1",
"canonical_name": "15号线望京站"
},
"望京新城": {
"type": "community", # 小区
"id": "c1",
"canonical_name": "望京新城"
}
}
# 用上下文消歧
def disambiguate(entity_name, context):
"""
根据上下文判断实体类型
"""
if "地铁" in context or "站" in context:
return entity_mapping[entity_name]["subway"]
elif "小区" in context or "房源" in context:
return entity_mapping[entity_name]["community"]
else:
return entity_mapping[entity_name]["district"]
模块 2:关系的结构化表达
问题: 关系不能是"附近""相关"这种泛关系,要有具体数值
# 错误做法:关系是文本
relations = [
("c1", "附近有地铁", "s1"), # 太泛
("c1", "相关学校", "sc1"), # 太泛
]
# 正确做法:关系有具体数值
relations = [
("c1", "距离地铁", {"value": 500, "unit": "m"}), # 500 米
("c1", "对应学校", {"name": "XX小学", "distance": 1000}), # 1 公里
("s1", "通勤时间", {"destination": "国贸", "time": 35, "unit": "min"}), # 35 分钟
]
模块 3:图+向量混合
问题: 图只能处理"关系",不能处理"描述"(采光、安静、装修风格)
# 混合方案
def hybrid_retrieval(query, graph, embeddings):
"""
结合图检索和向量检索
"""
# 第 1 步:用图检索处理"关系"
# 例如:通勤、学区、地铁
graph_results = subgraph_expansion(graph, "c1", hops=2)
# 第 2 步:用向量检索处理"描述"
# 例如:采光好、安静、装修风格
vector_results = vector_search(embeddings, query)
# 第 3 步:融合结果
merged = merge_results(graph_results, vector_results)
return merged
模块 4:约束解析先行
问题: 预算、户型这些硬条件不能靠相似度,要先过滤
def constrained_retrieval(query, graph):
"""
先解析硬条件,再做检索
"""
# 第 1 步:解析硬条件
constraints = parse_constraints(query)
# 例如:{"budget": 600, "rooms": 3, "district": "朝阳"}
# 第 2 步:硬条件过滤
candidates = filter_by_constraints(graph, constraints)
# 例如:预算 ≤ 600万,户型 = 3 居,地区 = 朝阳
# 第 3 步:在候选集上做子图检索
subgraph = subgraph_expansion(graph, candidates, hops=2)
# 第 4 步:生成推荐
recommendations = generate_recommendations(subgraph)
return recommendations
第七部分:常见问题
Q1:为什么扩展出来的子图太大?
现象: hop=3 时,子图包含 1000+ 个实体,查询变得很慢
原因:
- hop 数太大
- 每个节点的邻居太多
解决方案:
# 方案 1:限制 hop 数
subgraph = subgraph_expansion(graph, start_entity, hops=2) # 不超过 2
# 方案 2:限制每个节点的邻居数
def limited_expansion(graph, start_entity, hops=2, max_neighbors=5):
"""
限制邻居数,避免子图爆炸
"""
# 对每个节点,只保留 top-5 的邻居
# 例如:按关系权重排序,只保留权重最高的 5 个
pass
# 建议:hop=2,max_neighbors=5
Q2:为什么命中实体不准?
现象: 用户问"望京",系统识别成了"地铁站"而不是"板块"
原因:
- 实体消歧不够
- 上下文理解不足
解决方案:
# 方案 1:用词典
entity_dict = {
"望京": ["district", "subway", "community"], # 多个可能
}
# 方案 2:用上下文
def entity_linking(entity_name, context):
"""
根据上下文判断实体类型
"""
if "地铁" in context or "站" in context:
return "subway"
elif "小区" in context or "房源" in context:
return "community"
else:
return "district"
# 方案 3:用向量排序
def entity_linking_with_vector(entity_name, context, embeddings):
"""
用向量相似度排序候选实体
"""
candidates = entity_dict[entity_name]
scores = []
for candidate in candidates:
score = cosine_similarity(
embeddings[entity_name],
embeddings[candidate]
)
scores.append((candidate, score))
return sorted(scores, key=lambda x: x[1], reverse=True)[0][0]
# 建议:先用词典 + 上下文,再用向量排序
Q3:为什么推荐解释看着像"编的"?
现象: 系统说"这套房靠地铁",但实际上离地铁 2 公里
原因:
- 没有回源到原始数据
- 证据不可追溯
解决方案:
# 错误做法:直接返回推荐
recommendations = ["望京新城", "望京花园"]
# 正确做法:必须回源引用
def generate_recommendations_with_evidence(candidates, graph):
"""
每条推荐都要有证据
"""
recommendations = []
for candidate in candidates:
# 找到原始数据源
evidence = []
for relation in graph["relations"]:
if relation[0] == candidate:
evidence.append({
"relation": relation[1],
"target": relation[2],
"source": "原始数据库" # 关键:标注来源
})
recommendations.append({
"house": candidate,
"evidence": evidence
})
return recommendations
# 建议:每条推荐都要有"来源"标注
第八部分:下一步计划
下一篇我会学习 Agentic RAG,用多轮澄清和任务拆解,把"买房顾问"做得更像真人。
关键点: GraphRAG 把"检索链"补齐了,但用户需求仍然很模糊。
比如用户问"帮我推荐房子",GraphRAG 无法自动澄清"你的预算是多少?""你要几居室?"这些问题。
这就是 Agentic RAG 上场的原因——它不是"更强的检索",而是"把问答变成可控的多轮流程"。
总结
什么时候用 GraphRAG?
不是"所有场景都用",而是"特定场景才用":
✅ 用 GraphRAG:
– 问题涉及"多维度关系组合"(通勤 + 学区 + 配套)
– 需要"可解释推荐"(每条推荐都要有证据)
– 传统 RAG 的准确率 < 60%
❌ 不用 GraphRAG:
– 条件都在房源描述里(用传统 RAG)
– 数据不结构化(成本太高)
– 实时性要求高(图构建太慢)
怎么快速上手?
第 1 步:从 Demo 1 开始
理解"实体-关系网络"的数据结构
第 2 步:到 Demo 2
理解"子图扩展"怎么工作
第 3 步:到 Demo 3
理解"Evidence Table"怎么做可解释推荐
第 4 步:理解代价
数据准备、实体消歧、图维护的成本
下一步怎么优化?
1. 实体归一化
区分"望京"是板块还是地铁站
2. 关系权重
不是所有关系都等价
3. 图+向量混合
关系用图,描述用向量
4. 约束解析先行
硬条件先过滤,再做子图扩展
最后一句话:
GraphRAG 解决的是"怎么查关系链"。
它把检索从"相似度匹配"升级到"关系导航"。
但它不会管理"咨询流程"——这就是下一篇 Agentic RAG 的工作。
这就是我的 GraphRAG 学习笔记。希望能帮助你快速上手!
