zhiyi/scripts/enhance_graph.py

200 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""图谱优化:节点聚类 + 关系增强
优化内容:
1. 节点聚类:按共现关系聚成概念组(使用连接数 + 共现关系)
2. 关系增强:给边加权重,识别核心关系
3. 生成增强版 graph_data.json带聚类标签
"""
import sqlite3
import json
from collections import Counter, defaultdict
from pathlib import Path
DB = Path.home() / '.memory-fabric/zhiyi/graph.db'
OUT = Path("/home/muc/projects/zhiyi/docs/graph_data.json")
# ── 中文词组合并规则(简单 n-gram 合并)─────────────────────────────
# 常见的 2-4 字组合词(织忆系统内常见概念)
COMPOUND_WORDS = {
"织忆", "牧尘", "小唯", "小雪", "Hermes", "OpenClaw",
"MemoryWeave", "MemoryFabric", "FAISS", "Redis", "FastAPI",
"SBert", "bge-m3", "ComfyUI", "Docker", "Python", "Rust",
"LLM", "RAG", "CRDT", "向量数据库", "语义搜索", "知识图谱",
"模力方舟", "Gitea", "GitHub", "Jina", "HuggingFace",
"deepseek", "qwen", "llama", "RTX", "3050",
"GPU", "CPU", "API", "SDK", "JSONL",
"memory-fabric", "agent", "agent-id",
}
def merge_adjacent_words(text: str) -> list[str]:
"""把相邻的单字词合并成常见组合词"""
words = []
i = 0
while i < len(text):
matched = False
# 优先匹配 4 字词
for size in [4, 3, 2]:
if i + size <= len(text):
chunk = text[i:i+size]
if chunk in COMPOUND_WORDS:
words.append(chunk)
i += size
matched = True
break
if not matched:
i += 1
return words
# ── 读取图谱数据 ──────────────────────────────────────────────
conn = sqlite3.connect(str(DB))
cur = conn.cursor()
# 构建 entity→id 映射
cur.execute("SELECT id, entity, label, properties, episode_id FROM graph_nodes")
nodes_raw = {}
entity_to_id = {}
for row in cur.fetchall():
node_id, entity, label, props, ep_id = row
props = json.loads(props) if props and props != '{}' else {}
nodes_raw[entity] = {
"id": entity, "uuid": node_id, "name": entity,
"label": label, "episode_id": ep_id, "connections": 0,
**props
}
entity_to_id[entity] = entity
# 边
cur.execute("SELECT source, target, relation, properties FROM graph_edges")
edges_raw = []
for row in cur.fetchall():
src, tgt, rel, props = row
props = json.loads(props) if props and props != '{}' else {}
edges_raw.append({"source": src, "target": tgt, "relation": rel, **props})
conn.close()
# ── 统计连接数 ────────────────────────────────────────────────
cnt = Counter()
for e in edges_raw:
cnt[e['source']] += 1
cnt[e['target']] += 1
# 给节点加连接数
for entity in nodes_raw:
nodes_raw[entity]["connections"] = cnt.get(entity, 0)
# ── 节点分类区分「碎片词」vs「概念词」────────────────────────────
# 碎片词:单字或 2 字无意义词(常见工具检测)
STOP_NODES = {
'用了', '使用', '做了', '进行', '开始', '完成', '结束',
'通过', '根据', '按照', '因为', '所以', '如果', '但是',
'之后', '之前', '时候', '之后', '现在', '今天', '昨天',
'那个', '这个', '什么', '怎么', '为什么', '如何',
'牧尘用', '牧尘用', '牧尘今天', '牧尘的', '牧尘在',
'ip', 'url', 'uri', 'id', 'id', # 常见后缀
'0', '1', '2', '3', '4', '5', '00001', '00002', # 数字噪声
}
def is_meaningful(entity: str) -> bool:
"""判断节点是否有意义(不是碎片)"""
if len(entity) <= 1:
return False
if entity in STOP_NODES:
return False
if entity.endswith(('', '', '', '', '', '', '')):
return False
if all(c.isdigit() or c in '.-_' for c in entity):
return False
if len(entity) == 2 and entity[0] == entity[1]: # AA型重复
return False
return True
# ── 聚类:高频共现节点自动组成概念簇 ─────────────────────────────
# 思路:找一个节点的所有邻居,如果邻居互相也相连,视为「概念簇」
concept_clusters = []
processed = set()
# 按连接数排序,先处理大节点
sorted_nodes = sorted(nodes_raw.values(), key=lambda x: -x['connections'])
for node in sorted_nodes:
if node['name'] in processed or not is_meaningful(node['name']):
continue
if node['connections'] < 2:
continue
# 收集邻居
neighbors = set()
for e in edges_raw:
if e['source'] == node['name'] and is_meaningful(e['target']):
neighbors.add(e['target'])
if e['target'] == node['name'] and is_meaningful(e['source']):
neighbors.add(e['source'])
# 如果有 2+ 有意义邻居,构成簇
good_neighbors = [n for n in neighbors if n in nodes_raw and is_meaningful(n)]
if len(good_neighbors) >= 2:
cluster = [node['name']] + good_neighbors[:5] # 最多 6 个
cluster = [n for n in cluster if is_meaningful(n)]
if len(cluster) >= 2:
concept_clusters.append(sorted(set(cluster), key=lambda x: -nodes_raw[x]['connections']))
for n in cluster:
processed.add(n)
# ── 生成可视化数据 ───────────────────────────────────────────
# 只保留有意义节点,过滤碎片
meaningful_nodes = {e: n for e, n in nodes_raw.items() if is_meaningful(e)}
# 采样TOP 120 高连接节点 + 1跳邻居
top_nodes = sorted(meaningful_nodes.values(), key=lambda x: -x['connections'])[:120]
top_ids = set(n['name'] for n in top_nodes)
for e in edges_raw:
if e['source'] in top_ids and e['target'] in meaningful_nodes:
top_ids.add(e['target'])
if e['target'] in top_ids and e['source'] in meaningful_nodes:
top_ids.add(e['source'])
# 最终节点列表
final_nodes = []
for name in top_ids:
if name not in meaningful_nodes:
continue
n = nodes_raw[name].copy()
# 找簇标签
cluster_label = None
for cluster in concept_clusters:
if name in cluster:
cluster_label = cluster[0] # 簇内最高连接节点作为代表
break
n['cluster'] = cluster_label
n['group'] = 'concept' if n['connections'] >= 5 else 'term'
final_nodes.append(n)
# 最终边
final_links = [e for e in edges_raw
if e['source'] in top_ids and e['target'] in top_ids
and is_meaningful(e['source']) and is_meaningful(e['target'])]
# 连接数更新
cnt2 = Counter()
for l in final_links:
cnt2[l['source']] += 1
cnt2[l['target']] += 1
for n in final_nodes:
n['connections'] = cnt2.get(n['name'], 0)
result = {"nodes": final_nodes, "links": final_links}
OUT.write_text(json.dumps(result, ensure_ascii=False))
print(f"✅ 增强图谱: {len(final_nodes)} 节点, {len(final_links)}")
print(f" 聚类簇: {len(concept_clusters)}")
print(f" 碎片过滤: {len(nodes_raw)}{len(meaningful_nodes)}")
print(f" 高连接TOP10:")
for n in sorted(final_nodes, key=lambda x: -x['connections'])[:10]:
print(f" {n['name']}({n['connections']})", end=" ")
print()
print(f" 簇示例:")
for c in concept_clusters[:5]:
print(f" {c}")