DEV Community

Cover image for Andrej Karpathy said manual data ingest for AI agents is too slow. I built the fix.
VLSiddarth
VLSiddarth

Posted on

Andrej Karpathy said manual data ingest for AI agents is too slow. I built the fix.

Andrej Karpathy said manual data ingest for AI agents is too slow. I built the fix.

Last week Andrej Karpathy posted about building personal knowledge
bases for LLM agents. He described his workflow: manually indexing
source documents into a raw/ directory, writing custom search tools,
building a naive search engine over his wiki.

Then he wrote this:

"I think there is room here for an incredible new product
instead of a hacky collection of scripts."

He was right. So I built it.


The Problem He Identified

Karpathy's workflow is brilliant but it requires him to manually
curate every source. He clips articles with Obsidian Web Clipper,
downloads images locally, feeds them one-by-one to his LLM agent.

For a researcher at his level that works. For a developer building
production AI agents for clients, it doesn't scale.

Here's the specific failure mode I kept hitting:

You build a RAG pipeline. It works. A user asks about a Python library.
Your retriever finds a Stack Overflow answer with cosine similarity 0.94.
The LLM answers confidently. The user follows the advice. It breaks their project.

The Stack Overflow answer was from 2021. The library changed its API in 2023.

Your retriever did its job perfectly. Your vector store had no concept
of when that document was written. No exception was raised. No warning
was shown. The cosine similarity score told you nothing about whether
the knowledge was still true.

This is the silent failure mode of every RAG pipeline in production.
Tavily, Exa, and SerpAPI don't tell you when their results are stale.

So I built a retrieval API that does.


What I Built

Knowledge Universe is an open-source retrieval API that gives
Karpathy's LLM wiki agents something they currently don't have:
a production-grade data ingestion layer that crawls 18 knowledge
sources simultaneously, scores every result for freshness, and
returns structured documents in 3 seconds.

# Install the CLI
pip install knowledge-universe

# Get a free API key
ku signup you@email.com

# Run your first query
ku discover "transformer architecture" --difficulty 3
Enter fullscreen mode Exit fullscreen mode

Output:
Found 8 sources [2980ms]

  1. 🟢 [arxiv] Learning Novel Transformer Architecture for Time-series

    https://arxiv.org/abs/2502.13721v1
    decay=0.23 (fresh) quality=8.5/10

  2. ⚪ [kaggle] In-depth guide to Transformer architecture

    https://www.kaggle.com/code/tientd95/in-depth-guide-to-transformer-arc
    decay=0.40 (unknown) quality=4.5/10

  3. 🟢 [paperswithcode] Gradient Boosting within a Single Attention Layer

    https://arxiv.org/abs/2604.03190
    decay=0.01 (fresh) quality=7.6/10

  4. 🟠 [github] An-Jhon/Hand-Drawn-Transformer

    https://github.com/An-Jhon/Hand-Drawn-Transformer
    decay=0.68 (stale) quality=2.6/10

  5. 🟡 [semantic_scholar] Transformer+transformer architecture for image captioni

    https://doi.org/10.11591/ijai.v14.i3.pp2338-2346
    decay=0.44 (aging) quality=5.1/10

  6. 🟢 [arxiv] A Survey of Graph Transformers: Architectures, Theories

    https://arxiv.org/abs/2502.16533v2
    decay=0.23 (fresh) quality=8.9/10

  7. ⚪ [kaggle] LB 0.73 single fold transformer architecture

    https://www.kaggle.com/code/hengck23/lb-0-73-single-fold-transformer-a
    decay=0.40 (unknown) quality=4.5/10

  8. 🟠 [github] tum-pbs/pde-transformer

    https://github.com/tum-pbs/pde-transformer
    decay=0.70 (stale) quality=2.5/10

Cache hit: False | Time: 2980ms

Every result tells you not just what it found, but how much to trust it.

Live API: https://vlsiddarth-knowledge-universe.hf.space

GitHub: https://github.com/VLSiddarth/Knowledge-Universe


The Architecture

The core idea: run 18 crawlers in parallel, score everything,
return the best 8-10 results with freshness metadata attached.

Your App / Agent
          │
          ▼  POST /v1/discover
┌─────────────────────────────────────────────────┐
│            Knowledge Universe API               │
│                                                 │
│  1. Cache check (Redis) ──── HIT → 200ms return │
│        │ MISS                                   │
│  2. asyncio.gather(18 crawlers, per-timeouts)   │
│        ├── arXiv          (25s timeout)         │
│        ├── CrossRef       (8s) ← [Academic]     │
│        ├── PapersWithCode (8s) ← [SOTA Models]  │
│        ├── Documentation  (3s) ← [Fast-Fail]    │
│        ├── GitHub         (8s)                  │
│        ├── StackOverflow  (6s)                  │
│        ├── HuggingFace    (8s)                  │
│        ├── Kaggle         (6s)                  │
│        ├── YouTube        (8s)                  │
│        ├── Sketchfab      (5s) ← [3D Spatial]   │
│        ├── Freesound      (5s) ← [Audio]        │
│        ├── Wikipedia      (5s)                  │
│        ├── MIT OCW        (5s)                  │
│        ├── OpenLibrary    (5s)                  │
│        ├── Podcast Index  (5s)                  │
│        ├── Libgen         (4s)                  │
│        ├── CommonCrawl    (2s) ← [Fast-Fail]    │
│        └── GH Archive     (2s) ← [Fast-Fail]    │
│        │                                        │
│  3. Semantic pre-filter (cosine sim > 0.25)     │
│  4. Quality ranker (5-dimension scoring)        │
│  5. Knowledge Decay Engine                      │
│  6. LLM reranker (all-MiniLM-L6-v2)             │
│  7. Coverage Confidence Score                   │
│  8. Cache result (Redis, 4h TTL)                │
│        │                                        │
│  Return: sources + decay_scores + confidence    │
└─────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Why asyncio.gather and not threads?

Each crawler is an async HTTP call. asyncio.gather runs all 18
simultaneously, so the total wall time equals the slowest crawler,
not the sum. The parallel ceiling is arXiv at ~2.5s for complex queries.

One lesson that cost me 5 seconds of latency: one blocking crawler
kills everything. My original Kaggle integration used the official
SDK which runs synchronous urllib3 under the hood. I wrapped it in
run_in_executor thinking that was fine. It held a thread pool slot
for 2.5 seconds on every query and pushed cold latency from 3s to 8s.

The fix: replace the SDK with direct async HTTP calls using httpx:

# Before — blocks the thread pool
async def crawl(self, topic, difficulty):
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(None, self._crawl_sync, topic, difficulty)

# After — true async, 300ms for same results
async def crawl(self, topic, difficulty):
    async with httpx.AsyncClient() as client:
        datasets = await client.get(
            "https://www.kaggle.com/api/v1/datasets/list",
            params={"search": topic, "sortBy": "usability"},
            headers={"Authorization": f"Basic {self._encoded_creds}"}
        )
        return self._parse(datasets.json(), difficulty)
Enter fullscreen mode Exit fullscreen mode

Cold latency dropped from 8.8s to 3.1s with that one change.


The Knowledge Decay Formula

This is the part that doesn't exist in any other retrieval API.

Every result gets a decay score computed from its age and source type:
decay = 1 - 0.5 ^ (age_days / half_life)
freshness = 1 - decay

Half-lives are tuned per platform based on how fast knowledge
in that domain becomes outdated:

Platform Half-life Why
HuggingFace 120 days ML model landscape changes monthly
GitHub 180 days Dependencies update constantly
YouTube 270 days Library tutorials date quickly
Stack Overflow 365 days API answers age with framework versions
arXiv 1,095 days Research papers have longer shelf life
Wikipedia 1,460 days Actively maintained, slow decay
Open Library 1,825 days Books revised infrequently

The output on every result:

"stackoverflow:59523557": {
  "decay_score": 0.986,
  "freshness": 0.014,
  "label": "decayed",
  "age_days": 2263,
  "half_life_days": 365
}
Enter fullscreen mode Exit fullscreen mode

That Stack Overflow answer from 2021 with cosine similarity 0.94?
Its freshness score is 0.014. It gets downweighted before it
reaches your LLM. The silent failure mode is no longer silent.

Fast-moving topics get an additional volatility multiplier.
Topics like "LLMs", "React", "Docker", "Claude" use a ×1.1
multiplier on the decay rate — knowledge in those domains
goes stale faster than the platform average.


The 5-Dimension Quality Ranker

Before decay is applied, each source gets a base quality score:

WEIGHTS = {
    "authority":            0.35,  # platform trust + content type
    "difficulty_alignment": 0.30,  # how well difficulty matches request
    "completeness":         0.20,  # metadata richness
    "social_proof":         0.10,  # stars, citations, views (log-scaled)
    "accessibility":        0.05,  # open access bonus
}

final_score = base_quality * decay_penalty_multiplier
Enter fullscreen mode Exit fullscreen mode

Difficulty alignment is the most impactful dimension for
Karpathy's use case specifically. When you're feeding an LLM wiki
agent, you want sources matched to the agent's context level.
A research synthesis agent should get arXiv papers, not YouTube
explainers. A learning tool for beginners should get the opposite.

# difficulty gap penalty
# gap=0: score 10.0  (perfect match)
# gap=1: score 8.5   (acceptable)  
# gap=2: score 6.0   (marginal)
# gap=3: score 2.0   (nearly blocked)
Enter fullscreen mode Exit fullscreen mode

The Coverage Confidence Score

This is the feature that surprised me most when I built it.

After reranking, the API computes the average cosine similarity
between your query and the top results. If the average falls below
0.45, it warns you and suggests better queries:

"coverage_intelligence": {
  "confidence": 0.36,
  "confidence_label": "low",
  "coverage_warning": true,
  "warning_message": "Low confidence — results may not match intent",
  "suggested_queries": [
    "attention mechanism self-attention explained",
    "transformer encoder decoder architecture tutorial",
    "attention is all you need paper walkthrough"
  ]
}
Enter fullscreen mode Exit fullscreen mode

This matters for LLM wiki agents specifically. When the agent asks
an ambiguous question that doesn't match how sources are indexed,
instead of silently returning mediocre results, the API tells the
agent to rephrase. The agent can use the suggested queries directly.

Karpathy mentioned he runs "health checks" over his wiki to find
inconsistent data and impute missing data. Coverage confidence is
essentially that health check, automated and running on every query.


Performance vs Competitors

Tested against Tavily, Exa, and SerpAPI using identical queries:

Metric Knowledge Universe Tavily Exa SerpAPI
Cold latency 3.1s 5.4s 1.5s 3.5s
Cache hit 220ms N/A N/A N/A
Decay scoring
Confidence score
Difficulty ranking
Source diversity 18 typed platforms (incl. 3D & Audio) Web only Web only Google only

KU is faster than Tavily on cold queries despite hitting 18 typed
sources vs Tavily's general web index. The parallel architecture
is what makes this possible — the wall clock time equals the
slowest single crawler, not the sum of all crawlers.

Note on Exa: Exa is faster (1.5s) because it uses a single unified
search index rather than parallel crawling. The tradeoff is no decay
scoring and no source type diversity — you get whatever their index
decided to rank.


LangChain Integration — Drop-in Ready

import requests
from langchain_core.documents import Document

def get_knowledge_universe_docs(
    topic: str,
    difficulty: int = 3,
    formats: list = None,
    api_key: str = "your_key_here",
    min_freshness: float = 0.3,  # filter sources below 30% fresh
) -> list[Document]:

    formats = formats or ["pdf", "github", "html", "video", "stackoverflow"]

    resp = requests.post(
        "https://vlsiddarth-knowledge-universe.hf.space/v1/discover",
        headers={"X-API-Key": api_key},
        json={
            "topic": topic,
            "difficulty": difficulty,
            "formats": formats,
            "max_results": 10,
        },
        timeout=30,
    ).json()

    # Check coverage confidence
    cov = resp.get("coverage_intelligence", {})
    if cov.get("coverage_warning"):
        print(f"⚠️  Low confidence. Try: {cov.get('suggested_queries', [])}")

    docs = []
    decay_map = resp.get("decay_scores", {})

    for source in resp.get("sources", []):
        decay = decay_map.get(source["id"], {})
        freshness = decay.get("freshness", 0.5)

        # Filter stale sources before they reach your LLM
        if freshness < min_freshness:
            continue

        docs.append(Document(
            page_content=source.get("summary", ""),
            metadata={
                "title":         source["title"],
                "url":           source["url"],
                "platform":      source["source_platform"],
                "freshness":     freshness,
                "decay_label":   decay.get("label"),
                "quality_score": source.get("quality_score"),
                "difficulty":    source.get("difficulty"),
            }
        ))

    return docs


# Usage — drop into any existing LangChain RAG chain
docs = get_knowledge_universe_docs(
    topic="transformer architecture",
    difficulty=3,
    api_key="ku_test_...",
)

for doc in docs:
    print(f"[{doc.metadata['decay_label']}] {doc.metadata['title']}")
    print(f"  freshness={doc.metadata['freshness']:.2f}")
    print(f"  url={doc.metadata['url']}")
Enter fullscreen mode Exit fullscreen mode

For Karpathy's LLM wiki use case specifically:

# Give your LLM wiki agent a tool that does the manual ingest
# he described — automatically, with freshness scoring

wiki_sources = get_knowledge_universe_docs(
    topic="mixture of experts routing algorithms",
    difficulty=5,           # researcher-level sources only
    min_freshness=0.5,      # only recent sources go into the wiki
    formats=["pdf", "github"]  # papers and implementations
)

# Feed directly to your wiki agent
for source in wiki_sources:
    agent.ingest_to_wiki(source)
Enter fullscreen mode Exit fullscreen mode

The manual raw/ directory collection Karpathy describes is now
three lines of code.


Things That Didn't Work

MinHash LSH deduplication misses near-identical titles.
Wikipedia returns both "Neural network" and "Neural network
(machine learning)" as separate articles. After normalization
they differ, so both pass deduplication. Fixed with a
parenthetical-stripping step before the hash comparison.

Per-crawler timeouts were the wrong abstraction initially.
I started with a global 8s timeout for all crawlers.
CommonCrawl and GHArchive always time out at 8s with 0 results,
wasting the full 8s on every query. Setting them to 2s fast-fail
dropped the parallel ceiling from 8s to 3s.

Semantic Scholar blocked Hugging Face IP addresses.
I originally used Semantic Scholar for academic papers. In late 2024, they changed their policy and started throwing 403 Forbidden errors for server-to-server requests from Hugging Face Spaces free tiers.

The Fix: I ripped it out and integrated CrossRef. They index 150M+ scholarly works, have a fully open API (CC0 metadata), and actively encourage programmatic access via their "polite pool" (just pass your email in the User-Agent). It gave me access to IEEE, ACM, and Nature papers that arXiv misses, with zero rate-limit blocks.

Lesson: profile each crawler individually before setting any
global timeout.

_is_stale() was silently killing cache hit rate.
The function checked if a cached result was older than 80% of
the TTL (14,400s × 0.8 = 11,520s). Any query between 3.2 and
4 hours old triggered a full cold re-crawl even though Redis
still had the result. Cache hit rate was 25%. One-line fix:
use the full TTL. Hit rate went to 50%+ immediately.

The shared model singleton took too long to notice.
Both LocalLLMReranker and CoverageConfidenceScorer were
independently loading all-MiniLM-L6-v2 (90MB, ~300MB RAM).
Loading twice pushed HuggingFace Spaces free tier (2GB limit)
near the ceiling and added ~500ms to first requests. Fixed with
a module-level singleton:

# src/integrations/shared_model.py
_model = None
_model_lock = threading.Lock()

def get_shared_model():
    global _model
    if _model is not None:
        return _model
    with _model_lock:
        if _model is None:
            from sentence_transformers import SentenceTransformer
            _model = SentenceTransformer("all-MiniLM-L6-v2")
    return _model
Enter fullscreen mode Exit fullscreen mode

Both classes now call get_shared_model(). Model loads once.
Shared embeddings from the reranker pass directly to the
confidence scorer — zero extra encode() calls per request.


Try It Now

# Install the Python SDK
pip install knowledge-universe

# Or Node
npm install knowledge-universe

# Get a free API key (500 calls/month, no credit card)
ku signup you@email.com

# Query the live API directly
curl -X POST https://vlsiddarth-knowledge-universe.hf.space/v1/discover \
  -H "X-API-Key: ku_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "topic": "transformer architecture",
    "difficulty": 3,
    "formats": ["pdf", "github", "html", "video"],
    "max_results": 10
  }'
Enter fullscreen mode Exit fullscreen mode

Live API + Swagger docs:

https://vlsiddarth-knowledge-universe.hf.space

GitHub (MIT licensed):

https://github.com/VLSiddarth/Knowledge-Universe


What's Next

Two things I'm actively building:

1. Streaming results — return sources as they arrive from
each crawler rather than waiting for all 18. The first 3 results
could be in your agent pipeline within 800ms. You see something
immediately; the pipeline enriches as more crawlers complete.

2. /v1/monitor webhook alerts — register a topic and a
webhook URL. Knowledge Universe checks that topic daily. When
freshness drops below a threshold or a significantly better
source appears, it pushes an update to your endpoint. Your
RAG pipeline stays current without polling.

If you're building LLM agents that need external knowledge —
whether it's Karpathy's wiki pattern, a production RAG pipeline,
or something in between — I'd genuinely like to hear what breaks.

What's your current approach to handling source freshness in
retrieval? Drop it in the comments.


Tags: python, ai, machinelearning, webdev

Top comments (0)