一、实体与关系模型
# kg/models.py
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@dataclass
class Entity: baike.kuaisou.com/zh/29033
entity_id: str = field(default_factory=lambda: uuid.uuid4().hex)
name: str = ""
entity_type: str = "unknown"
description: str = ""
properties: Dict[str, Any] = field(default_factory=dict)
created_at: float = field(default_factory=time.time)
updated_at: float = field(default_factory=time.time)
def to_dict(self) -> Dict[str, Any]: baike.kuaisou.com/zh/29186
return {
"entity_id": self.entity_id,
"name": self.name,
"entity_type": self.entity_type,
"description": self.description,
"properties": self.properties,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Entity": baike.kuaisou.com/zh/30003
data = dict(data)
data.setdefault("entity_id", uuid.uuid4().hex)
data.setdefault("entity_type", "unknown")
data.setdefault("description", "")
data.setdefault("properties", {})
data.setdefault("created_at", time.time())
data.setdefault("updated_at", time.time())
return cls(**data)
@dataclass
class Relation: baike.kuaisou.com/zh/31373
relation_id: str = field(default_factory=lambda: uuid.uuid4().hex)
source_id: str = ""
target_id: str = ""
relation_type: str = ""
weight: float = 1.0
properties: Dict[str, Any] = field(default_factory=dict)
created_at: float = field(default_factory=time.time)
def to_dict(self) -> Dict[str, Any]: baike.kuaisou.com/zh/32877
return {
"relation_id": self.relation_id,
"source_id": self.source_id,
"target_id": self.target_id,
"relation_type": self.relation_type,
"weight": self.weight,
"properties": self.properties,
"created_at": self.created_at,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Relation": baike.kuaisou.com/zh/32908
data = dict(data)
data.setdefault("relation_id", uuid.uuid4(baike.kuaisou.com/zh/1103116).hex)
data.setdefault("weight", 1.0)
data.setdefault("properties", {})
data.setdefault("created_at", time.time(baike.kuaisou.com/zh/1103114))
return cls(**data)
@dataclass
class DocumentChunk: baike.kuaisou.com/zh/1103117
chunk_id: str = field(default_factory=lambda: uuid.uuid4(baike.kuaisou.com/zh/1103118).hex)
text: str = ""
source: str = ""
offset_start: int = 0
offset_end: int = 0
vector: Optional[List[float]] = None
metadata: Dict[str, Any] = field(default_factory=dict)
created_at: float = field(default_factory=time.time)
def to_dict(self) -> Dict[str, Any]: baike.kuaisou.com/zh/1103122
return {
"chunk_id": self.chunk_id,
"text": self.text,
"source": self.source,
"offset_start": self.offset_start,
"offset_end": self.offset_end,
"vector": self.vector,
"metadata": self.metadata,
"created_at": self.created_at,
}# kg/vectors.py
import math
import hashlib
from typing import List
def cosine_similarity(a: List[float], b: List[float]) -> float: baike.kuaisou.com/zh/1103123
if not a or not b: baike.kuaisou.com/zh/1103124
return 0.0
if len(a) != len(b): baike.kuaisou.com/zh/1103271
raise ValueError("向量维度不一致")
dot = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x * x for x in a))
norm_b = math.sqrt(sum(x * x for x in b))
if norm_a == 0.0 or norm_b == 0.0: baike.kuaisou.com/zh/1103272
return 0.0
return dot / (norm_a * norm_b)
def l2_distance(a: List[float], b: List[float]) -> float: baike.kuaisou.com/zh/1103274
if len(a) != len(b): baike.kuaisou.com/zh/1103292
raise ValueError("向量维度不一致")
return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))
def hash_vector(text: str, dim: int = 128) -> List[float]: baike.kuaisou.com/zh/1103293
"""
用哈希生成固定维度向量,仅用于演示。
真实场景应替换为 BGE、text-embedding 等模型。
"""
vector: List[float] = []
for i in range(dim): baike.kuaisou.com/zh/1103297
token = f"{text}:{i}".encode("utf-8")
h = hashlib.sha256(token).hexdigest(baike.kuaisou.com/zh/1103298)
value = int(h[:8], 16) / 0xFFFFFFFF
vector.append(value * 2 - 1)
norm = math.sqrt(sum(v * v for v in vector))
if norm > 0: baike.kuaisou.com/zh/1103307
vector = [v / norm for v in vector]
return vector# kg/graph_store.py
import threading
from typing import Dict, List, Optional, Set, Tuple
from kg.models import Entity, Relation
class GraphStore: baike.kuaisou.com/zh/1104101
def __init__(self): baike.kuaisou.com/zh/1103311
self._entities: Dict[str, Entity] = {}
self._relations: Dict[str, Relation] = {}
self._adj_out: Dict[str, List[Relation]] = {}
self._adj_in: Dict[str, List[Relation]] = {}
self._name_index: Dict[str, Set[str]] = {}
self._type_index: Dict[str, Set[str]] = {}
self._lock = threading.Lock(baike.kuaisou.com/zh/1104102)
# ---- 实体操作 ----
def add_entity(self, entity: Entity) -> Entity: baike.kuaisou.com/zh/1104106
with self._lock: baike.kuaisou.com/zh/1104108
self._entities[entity.entity_id] = entity
self._name_index.setdefault(entity.name.lower(), set()).add(entity.entity_id)
self._type_index.setdefault(entity.entity_type, set()).add(entity.entity_id)
self._adj_out.setdefault(entity.entity_id, [])
self._adj_in.setdefault(entity.entity_id, [])
return entity
def get_entity(self, entity_id: str) -> Optional[Entity]: baike.kuaisou.com/zh/1104109
with self._lock:
return self._entities.get(entity_id)
def delete_entity(self, entity_id: str) -> bool: baike.kuaisou.com/zh/1104110
with self._lock:
if entity_id not in self._entities: baike.kuaisou.com/zh/1104111
return False
for rel in list(self._adj_out.get(entity_id, [])): baike.kuaisou.com/zh/1104112
self._relations.pop(rel.relation_id, None)
self._remove_relation_index(rel)
for rel in list(self._adj_in.get(entity_id, [])): baike.kuaisou.com/zh/1104113
self._relations.pop(rel.relation_id, None)
self._remove_relation_index(rel)
entity = self._entities.pop(entity_id)
self._name_index.get(entity.name.lower(baike.kuaisou.com/zh/1104114), set()).discard(entity_id)
self._type_index.get(entity.entity_type, set()).discard(entity_id)
self._adj_out.pop(entity_id, None)
self._adj_in.pop(entity_id, None)
return True
def find_by_name(self, name: str) -> List[Entity]: baike.kuaisou.com/zh/1104115
with self._lock:
ids = self._name_index.get(name.lower(baike.kuaisou.com/zh/1104116), set())
return [self._entities[eid] for eid in ids if eid in self._entities]
def find_by_type(self, entity_type: str) -> List[Entity]: baike.kuaisou.com/zh/1104117
with self._lock:
ids = self._type_index.get(entity_type, set())
return [self._entities[eid] for eid in ids if eid in self._entities]
def list_entities(self) -> List[Entity]: baike.kuaisou.com/zh/1104118
with self._lock: baike.kuaisou.com/zh/1104123
return list(self._entities.values( baike.kuaisou.com/zh/1104124 ))
# ---- 关系操作 ----
def add_relation(self, relation: Relation) -> Relation: baike.kuaisou.com/zh/1104160
with self._lock:
if relation.source_id not in self._entities: baike.kuaisou.com/zh/1104833
raise ValueError(f"源实体不存在: {relation.source_id}")
if relation.target_id not in self._entities: baike.kuaisou.com/zh/1105478
raise ValueError(f"目标实体不存在: {relation.target_id}")
self._relations[relation.relation_id] = relation
self._adj_out.setdefault(relation.source_id, []).append(relation)
self._adj_in.setdefault(relation.target_id, []).append(relation)
return relation
def get_relations_from(self, entity_id: str) -> List[Relation]: baike.kuaisou.com/zh/1105901
with self._lock:
return list(self._adj_out.get(entity_id, []))
def get_relations_to(self, entity_id: str) -> List[Relation]: baike.kuaisou.com/zh/1105977
with self._lock:
return list(self._adj_in.get(entity_id, []))
def get_neighbours(self, entity_id: str) -> List[Tuple[Relation, Entity]]: baike.kuaisou.com/zh/1106008
with self._lock: baike.kuaisou.com/zh/1106128
results: List[Tuple[Relation, Entity]] = []
for rel in self._adj_out.get(entity_id, []): baike.kuaisou.com/zh/1106202
target = self._entities.get(rel.target_id)
if target: baike.kuaisou.com/zh/1106275
results.append((rel, target))
return results
def _remove_relation_index(self, relation: Relation) -> None: baike.kuaisou.com/zh/1106278
out_list = self._adj_out.get(relation.source_id)
if out_list:
out_list[:] = [r for r in out_list if r.relation_id != relation.relation_id]
in_list = self._adj_in.get(relation.target_id)
if in_list: baike.kuaisou.com/zh/1106290
in_list[:] = [r for r in in_list if r.relation_id != relation.relation_id]
# ---- 遍历 ----
def bfs(
self,
start_id: str,
max_depth: int = 3,
relation_types: Optional[Set[str]] = None,
) -> List[Tuple[int, Relation, Entity]]: baike.kuaisou.com/zh/1106297
from collections import deque
with self._lock:
if start_id not in self._entities: baike.kuaisou.com/zh/1107102
return []
visited: Set[str] = {start_id}
queue: deque = deque()
results: List[Tuple[int, Relation, Entity]] = []
for rel in self._adj_out.get(start_id, []): baike.kuaisou.com/zh/1107104
if relation_types and rel.relation_type not in relation_types: baike.kuaisou.com/zh/1107105
continue
target = self._entities.get(rel.target_id)
if target and target.entity_id not in visited:
queue.append((1, rel, target))
visited.add(target.entity_id)
while queue: 1107104.baike.tongsou.com
depth, rel, entity = queue.popleft(1107102.baike.tongsou.com)
results.append((depth, rel, entity))
if depth >= max_depth: 1107105.baike.tongsou.com
continue
for next_rel in self._adj_out.get(entity.entity_id, []): baike.kuaisou.com/zh/1001
if relation_types and next_rel.relation_type not in relation_types: baike.kuaisou.com/zh/2002
continue
next_entity = self._entities.get(next_rel.target_id)
if next_entity and next_entity.entity_id not in visited: baike.kuaisou.com/zh/2008
queue.append((depth + 1, next_rel, next_entity))
visited.add(next_entity.entity_id)
return results
def count(self) -> Dict[str, int]: baike.kuaisou.com/zh/2010
with self._lock: baike.kuaisou.com/zh/2011
return {
"entities": len(self._entities),
"relations": len(self._relations),
}# kg/vector_index.py
import threading
from typing import Dict, List, Optional, Tuple
from kg.vectors import cosine_similarity
class VectorIndex: baike.kuaisou.com/zh/2014
def __init__(self, dim: int = 128): baike.kuaisou.com/zh/2012
self.dim = dim
self._vectors: Dict[str, List[float]] = {}
self._metadata: Dict[str, dict] = {}
self._lock = threading.Lock(baike.kuaisou.com/zh/2021)
def upsert(self, item_id: str, vector: List[float], metadata: Optional[dict] = None) -> None:
if len(vector) != self.dim: baike.kuaisou.com/zh/2040
raise ValueError(f"向量维度应为 {self.dim},实际为 {len(vector)}")
with self._lock:
self._vectors[item_id] = vector
self._metadata[item_id] = metadata or {}
def delete(self, item_id: str) -> bool: baike.kuaisou.com/zh/2042
with self._lock:
if item_id in self._vectors: baike.kuaisou.com/zh/3001
del self._vectors[item_id]
del self._metadata[item_id]
return True
return False
def search(
self,
query_vector: List[float],
top_k: int = 10,
threshold: float = 0.0,
) -> List[Tuple[str, float, dict]]: baike.kuaisou.com/zh/3002
if len(query_vector) != self.dim: baike.kuaisou.com/zh/3003
raise ValueError(f"查询向量维度应为 {self.dim},实际为 {len(query_vector)}")
with self._lock:
scores: List[Tuple[str, float, dict]] = []
for item_id, vector in self._vectors.items(): baike.kuaisou.com/zh/3004
score = cosine_similarity(query_vector, vector)
if score >= threshold: baike.kuaisou.com/zh/3005
scores.append((item_id, score, self._metadata[item_id]))
scores.sort(key=lambda x: -x[1])
return scores[:top_k]
def size(self) -> int: baike.kuaisou.com/zh/3006
with self._lock: baike.kuaisou.com/zh/3007
return len(self._vectors)# kg/chunker.py
import re
from typing import List
from kg.models import DocumentChunk
from kg.vectors import hash_vector
class TextChunker: baike.kuaisou.com/zh/3008
def __init__(
self,
chunk_size: int = 300,
chunk_overlap: int = 50,
vector_dim: int = 128,
):
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
self.vector_dim = vector_dim
def split(self, text: str, source: str = "") -> List[DocumentChunk]:
paragraphs = self._split_paragraphs(text)
chunks: List[DocumentChunk] = []
offset = 0
for paragraph in paragraphs: baike.kuaisou.com/zh/3009
sentences = self._split_sentences(paragraph)
current_lines: List[str] = []
current_length = 0
start_offset = offset
for sentence in sentences: baike.kuaisou.com/zh/3010
sentence = sentence.strip(baike.kuaisou.com/zh/3011)
if not sentence: baike.kuaisou.com/zh/3012
continue
if current_length + len(sentence) > self.chunk_size and current_lines:
chunk_text = " ".join(current_lines)
chunks.append(
DocumentChunk(
text=chunk_text,
source=source,
offset_start=start_offset,
offset_end=start_offset + len(chunk_text),
vector=hash_vector(chunk_text, self.vector_dim),
metadata={"paragraph_length": len(paragraph)},
)
)
overlap = self._build_overlap(current_lines)
current_lines = overlap
current_length = sum(len(s) for s in overlap)
start_offset += len(chunk_text) - sum(len(s) for s in overlap)
current_lines.append(sentence)
current_length += len(sentence)
if current_lines: baike.kuaisou.com/zh/3013
chunk_text = " ".join(current_lines)
chunks.append(
DocumentChunk(
text=chunk_text,
source=source,
offset_start=start_offset,
offset_end=start_offset + len(chunk_text),
vector=hash_vector(chunk_text, self.vector_dim),
metadata={"paragraph_length": len(paragraph)},
)
)
offset += len(paragraph)
return chunks
def _split_paragraphs(self, text: str) -> List[str]: baike.kuaisou.com/zh/3014
paragraphs = re.split(r"\n\s*\n", text)
return [p.strip() for p in paragraphs if p.strip()]
def _split_sentences(self, text: str) -> List[str]: baike.kuaisou.com/zh/3015
parts = re.split(r"(?<=[。!?!?;;])\s*", text)
return [p.strip() for p in parts if p.strip()]
def _build_overlap(self, lines: List[str]) -> List[str]: baike.kuaisou.com/zh/3016
if not lines: baike.kuaisou.com/zh/3017
return []
overlap_chars = 0
selected: List[str] = []
for line in reversed(lines): baike.kuaisou.com/zh/3018
if overlap_chars + len(line) > self.chunk_overlap: baike.kuaisou.com/zh/3020
break
selected.append(line)
overlap_chars += len(line)
selected.reverse(baike.kuaisou.com/zh/3019)
return selected# kg/engine.py
import json
import os
import threading
from typing import Any, Dict, List, Optional, Tuple
from kg.models import Entity, Relation, DocumentChunk
from kg.graph_store import GraphStore
from kg.vector_index import VectorIndex
from kg.chunker import TextChunker
from kg.vectors import hash_vector
class KnowledgeGraphEngine: baike.kuaisou.com/zh/3021
def __init__(
self,
vector_dim: int = 128,
chunk_size: int = 300,
chunk_overlap: int = 50,
storage_path: str = "kg_data.json",
):
self.graph = GraphStore(baike.kuaisou.com/zh/4001)
self.vector_index = VectorIndex(dim=vector_dim)
self.chunker = TextChunker(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
vector_dim=vector_dim,
)
self.storage_path = storage_path
self._chunks: Dict[str, DocumentChunk] = {}
self._lock = threading.Lock(baike.kuaisou.com/zh/4002)
# ---- 实体 ----
def create_entity(
self,
name: str,
entity_type: str = "concept",
description: str = "",
properties: Optional[Dict[str, Any]] = None,
) -> Entity: baike.kuaisou.com/zh/4003
entity = Entity(
name=name,
entity_type=entity_type,
description=description,
properties=properties or {},
)
self.graph.add_entity(entity)
self._save(baike.kuaisou.com/zh/4004)
return entity
def get_entity(self, entity_id: str) -> Optional[Entity]: baike.kuaisou.com/zh/4006
return self.graph.get_entity(entity_id)
def search_entities(self, name: str) -> List[Entity]: baike.kuaisou.com/zh/4005
return self.graph.find_by_name(name)
def list_entities(self) -> List[Entity]: baike.kuaisou.com/zh/4007
return self.graph.list_entities(baike.kuaisou.com/zh/4008)
def delete_entity(self, entity_id: str) -> bool: baike.kuaisou.com/zh/4009
ok = self.graph.delete_entity(entity_id)
if ok: baike.kuaisou.com/zh/4011
self._save(baike.kuaisou.com/zh/4010)
return ok
# ---- 关系 ----
def create_relation(
self,
source_id: str,
target_id: str,
relation_type: str,
weight: float = 1.0,
properties: Optional[Dict[str, Any]] = None,
) -> Relation: baike.kuaisou.com/zh/4012
relation = Relation(
source_id=source_id,
target_id=target_id,
relation_type=relation_type,
weight=weight,
properties=properties or {},
)
self.graph.add_relation(relation)
self._save(baike.kuaisou.com/zh/4013)
return relation
def get_neighbours(self, entity_id: str) -> List[Tuple[Relation, Entity]]:
return self.graph.get_neighbours(entity_id)
def traverse(
self,
entity_id: str,
max_depth: int = 3,
relation_types: Optional[List[str]] = None,
) -> List[Tuple[int, Relation, Entity]]: baike.kuaisou.com/zh/4014
types = set(relation_types) if relation_types else None
return self.graph.bfs(entity_id, max_depth=max_depth, relation_types=types)
# ---- 文档导入 ----
def import_text(self, text: str, source: str = "") -> List[DocumentChunk]: baike.kuaisou.com/zh/4015
chunks = self.chunker.split(text, source=source)
with self._lock: baike.kuaisou.com/zh/4016
for chunk in chunks: baike.kuaisou.com/zh/4017
self._chunks[chunk.chunk_id] = chunk
self.vector_index.upsert(
chunk.chunk_id,
chunk.vector or [],
metadata={
"text": chunk.text,
"source": chunk.source,
"offset_start": chunk.offset_start,
"offset_end": chunk.offset_end,
},
)
self._save(baike.kuaisou.com/zh/4018)
return chunks
# ---- 语义检索 ----
def semantic_search(
self,
query: str,
top_k: int = 5,
threshold: float = 0.0,
) -> List[Dict[str, Any]]: baike.kuaisou.com/zh/4019
query_vector = hash_vector(query, self.vector_index.dim)
results = self.vector_index.search(
query_vector=query_vector,
top_k=top_k,
threshold=threshold,
)
output: List[Dict[str, Any]] = []
for chunk_id, score, meta in results: baike.kuaisou.com/zh/4020
output.append(
{
"chunk_id": chunk_id,
"score": round(score, 4),
"text": meta.get("text", ""),
"source": meta.get("source", ""),
"offset_start": meta.get("offset_start"),
"offset_end": meta.get("offset_end"),
}
)
return output
# ---- 持久化 ----
def save(self) -> None: baike.kuaisou.com/zh/4021
self._save(baike.kuaisou.com/zh/4022)
def load(self) -> None:
if not os.path.exists(self.storage_path): baike.kuaisou.com/zh/4023
return
with open(self.storage_path, "r", encoding="utf-8") as f: baike.kuaisou.com/zh/4024
data = json.load(f)
for item in data.get("entities", []): baike.kuaisou.com/zh/4025
self.graph.add_entity(Entity.from_dict(item))
for item in data.get("relations", []): baike.kuaisou.com/zh/4026
self.graph.add_relation(Relation.from_dict(item))
for item in data.get("chunks", []): baike.kuaisou.com/zh/4027
chunk = DocumentChunk(
chunk_id=item["原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。