Knowledge Graphs and Spreading Activation: How Context Surfaces
Knowledge Graphs and Spreading Activation
Vector search finds similar documents. Knowledge graphs find connected concepts. Together, they enable proactive context.
The Spreading Activation Model
In cognitive psychology, spreading activation explains how thinking of one concept primes related concepts:
Think: "dog"
Activates: pet → animal → bark → leash → walk → park
Activation spreads along associative links, weakening with distance. This is how context surfaces naturally.
Our Graph Structure
pub struct KnowledgeGraph {
nodes: HashMap<NodeId, Node>,
edges: HashMap<(NodeId, NodeId), Edge>,
}
pub struct Node {
id: NodeId,
content: String,
entity_type: EntityType, // Person, Concept, Event, etc.
activation: f32,
}
pub struct Edge {
source: NodeId,
target: NodeId,
relation: RelationType, // causes, contains, similar, etc.
strength: f32,
}
Entity Extraction
When a memory is stored, we extract entities:
fn extract_entities(text: &str) -> Vec<Entity> {
let ner_results = ner_model.predict(text);
ner_results.iter().map(|r| Entity {
text: r.text.clone(),
entity_type: classify_type(&r.label),
confidence: r.score,
}).collect()
}
Then we create/update graph nodes and edges between co-occurring entities.
Spreading Activation Algorithm
fn spread_activation(seed_nodes: &[NodeId], depth: usize) -> Vec<(NodeId, f32)> {
let mut activations: HashMap<NodeId, f32> = HashMap::new();
// Initialize seed nodes
for node in seed_nodes {
activations.insert(*node, 1.0);
}
// Spread for N iterations
for _ in 0..depth {
let mut new_activations = HashMap::new();
for (node, activation) in &activations {
for edge in self.edges_from(*node) {
let spread = activation * edge.strength * DECAY_FACTOR;
if spread > ACTIVATION_THRESHOLD {
*new_activations.entry(edge.target).or_insert(0.0) += spread;
}
}
}
for (node, act) in new_activations {
*activations.entry(node).or_insert(0.0) += act;
}
}
activations.into_iter().sorted_by_key(|(_, a)| OrderedFloat(-a)).collect()
}
Proactive Context Retrieval
When the user asks a question, we:
1. Extract entities from the query
2. Spread activation from those entities
3. Retrieve memories connected to activated nodes
4. Fuse with vector search results
fn proactive_context(query: &str) -> Vec<Memory> {
let query_entities = extract_entities(query);
let query_nodes = self.find_nodes(&query_entities);
let activated = spread_activation(&query_nodes, 3);
let graph_memories = activated.iter()
.flat_map(|(node, _)| self.memories_for_node(*node))
.collect();
let vector_memories = self.vectors.search(query, 10);
fuse_and_rank(graph_memories, vector_memories)
}
Example
Query: "How should I handle database errors?"
Entities extracted: [database, errors]
Spreading activation from "database":
Spreading from "errors":
Combined context surfaces: PostgreSQL error handling best practices, the project's existing retry logic, and relevant logging patterns.