How to Implement ACT-R Scoring in a Retrieval System
Before You Start
This guide assumes you have an existing retrieval system that returns candidate results from a vector store. ACT-R scoring works as a reranking layer, so you do not need to replace your vector database. You need the ability to store additional metadata with each memory (access timestamps, entity links, confidence values) and the ability to run a post-retrieval scoring function before returning results to the caller.
If you are using Adaptive Recall, cognitive scoring is built in and runs automatically on every retrieval call. This guide is for developers who want to understand the mechanics or implement the scoring logic in their own systems.
Step-by-Step Implementation
Each memory record needs fields beyond content and embedding. Add an array of access timestamps (every time this memory is retrieved or updated, append the current time), a list of entity connections (extracted from the content), a confidence score (starting at a default value, updated through consolidation), and a creation timestamp. The access history is critical because base-level activation is computed directly from it.
{
"id": "mem_abc123",
"content": "JWT tokens should use RS256 for production APIs",
"embedding": [0.023, -0.041, ...],
"created_at": "2026-04-15T10:30:00Z",
"access_times": [
"2026-04-15T10:30:00Z",
"2026-04-18T14:22:00Z",
"2026-05-01T09:15:00Z"
],
"entities": ["JWT", "RS256", "API security", "authentication"],
"confidence": 7.5,
"corroboration_count": 2
}Base-level activation B for a memory at the current time t is calculated from its access history. For each access time ti, compute the time difference (t - ti) in seconds, raise it to the power of negative d (the decay parameter, typically 0.5), and sum all contributions. Then take the natural logarithm of the sum. This gives you a single activation value that increases with more frequent and more recent accesses.
import math
import time
def base_level_activation(access_times, decay=0.5):
now = time.time()
total = 0.0
for t_access in access_times:
age = now - t_access
if age < 1:
age = 1 # floor to avoid division by zero
total += age ** (-decay)
if total <= 0:
return -float('inf')
return math.log(total)For each candidate memory, check whether any of its entities overlap with entities in the query or with entities in the highest-activated memories already in the candidate set. For each shared entity, add a spreading activation bonus. The bonus should decay with graph distance: direct entity matches (depth 1) get full weight, connections through an intermediate entity (depth 2) get half weight.
def spreading_activation(memory, query_entities, entity_graph, max_depth=2):
spread = 0.0
memory_entities = set(memory['entities'])
# depth 1: direct overlap with query entities
direct = memory_entities.intersection(query_entities)
spread += len(direct) * 1.0
# depth 2: entities connected to query entities in the graph
if max_depth >= 2:
for qe in query_entities:
neighbors = entity_graph.get(qe, [])
indirect = memory_entities.intersection(neighbors)
spread += len(indirect) * 0.5
return spreadConfidence ranges from 0 to 10 in Adaptive Recall's model. A freshly stored memory starts at a default of 5.0. Memories that are corroborated by other sources gain confidence (up to 10.0), while memories that contradict established knowledge lose confidence. Normalize the confidence score to a 0-1 range and use it as a multiplier on the combined activation score.
def confidence_weight(confidence_score, max_confidence=10.0):
# normalize to 0.5 - 1.0 range so low confidence
# still allows retrieval, just at reduced rank
return 0.5 + (confidence_score / max_confidence) * 0.5Blend vector similarity, base-level activation, spreading activation, and confidence into a single score. The weights determine how much each factor influences the final ranking. A typical starting configuration gives vector similarity 40% weight, base-level activation 30%, spreading activation 20%, and confidence 10%.
def cognitive_score(memory, query_embedding, query_entities,
entity_graph, weights=None):
if weights is None:
weights = {
'similarity': 0.40,
'activation': 0.30,
'spreading': 0.20,
'confidence': 0.10
}
sim = cosine_similarity(query_embedding, memory['embedding'])
bla = base_level_activation(memory['access_times'])
sa = spreading_activation(memory, query_entities, entity_graph)
cw = confidence_weight(memory['confidence'])
# normalize activation to 0-1 range for blending
bla_norm = 1.0 / (1.0 + math.exp(-bla)) # sigmoid
score = (weights['similarity'] * sim +
weights['activation'] * bla_norm +
weights['spreading'] * min(sa / 5.0, 1.0))
score *= cw
return scoreCreate a test set of queries where you know which memories should rank highest. Run your scoring function and check whether the expected results appear in the top positions. Adjust the decay rate first (lower values preserve old memories longer, higher values favor recency), then tune the score weights to emphasize the dimensions that matter most for your use case. Customer support systems typically benefit from higher recency weight, while research systems benefit from higher confidence weight.
Performance Considerations
The scoring function runs after vector search has narrowed candidates, so it operates on tens of items rather than the full memory store. For a candidate set of 50 memories, the activation calculations take under 1 millisecond. Spreading activation requires graph lookups, which add 5 to 20 milliseconds depending on graph size and depth. The total overhead stays under 40 milliseconds for most configurations.
To minimize latency, precompute base-level activation values and update them incrementally when memories are accessed. Store the precomputed values alongside each memory and refresh them periodically through a background process. This reduces retrieval-time computation to a lookup rather than a full recalculation from access history.
What Comes Next
Once basic scoring is working, you can add memory lifecycle management to automatically consolidate, archive, or forget memories based on their activation levels. The reflect tool in Adaptive Recall handles this, detecting contradictions, merging related memories, and updating confidence scores through an evidence-gated process.
Skip the implementation and use cognitive scoring out of the box. Adaptive Recall runs the full ACT-R pipeline on every retrieval call.
Get Started Free