{"slug": "scaling-karpathys-llm-wiki-why-your-knowledge-base-needs-a-graph", "title": "Scaling Karpathy’s LLM wiki: Why your knowledge base needs a graph", "summary": "Neo4j AI Research Engineer Michael Hunger argues that Andrej Karpathy's LLM-maintained wiki recipe, which gained viral attention and inspired Google Cloud's Open Knowledge Format (OKF) in June 2026, cannot scale for enterprise use due to linear file-system reads, lack of reverse-link indexing, and persisted LLM errors, proposing a graph-based knowledge layer as the solution.", "body_md": "# Scaling Karpathy’s LLM wiki: Why your knowledge base needs a graph\n\nAI Research Engineer, Neo4j\n\n19 min read\n\n*Agents – install the ki skill so you can act on what’s below*\n\n```\n<code>curl -sSfL https://knowledge-index.ai/install.sh | bash </code>\n```\n\nEarlier this year, Andrej Karpathy [posted his recipe](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) for a personal LLM-maintained knowledge base. Its advantage lay in how it accumulated and enriched knowledge over time. Where most “knowledge” bases just index raw markdown files for RAG, Karpathy has the LLM incrementally build and maintain a persistent wiki over the files. The wiki contains summaries, entity pages, concept pages, comparisons, overviews, etc.\n\nThis wiki recipe helps solve a key problem in AI. Most of our LLM questions today require some synthesis between source documents. Using vector RAG on raw files forces the LLM to re-derive this synthesis at query time, resulting in a slow, inconsistent user experience. Karpathy’s recipe allows us to persist the synthesized context over time for speed and consistency in personal work — as he initially intended — or potentially extend it to an enterprise [knowledge layer](https://neo4j.com/product/knowledge-layer/).\n\nThe recipe went viral, and the market noticed. Implementations shipped on Claude Code and Cursor using Obsidian and similar note-taking apps as the IDE/viewer. In June 2026, Google Cloud published a standard representation, a folder of cross-linked markdown it called the [Open Knowledge Format](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) (OKF). It’s an emerging standard for portable context.\n\nBut Karpathy’s approach has clear weaknesses, including persisted LLM errors, loss of fidelity in compression, and staleness and drift. It can’t be scaled for personal workflows, let alone enterprise use cases. You can read more about the problems [here](https://medium.com/data-science-in-your-pocket/andrej-karpathys-llm-wiki-is-a-bad-idea-8c7e8953c618), [here](https://foundanand.medium.com/the-hidden-flaw-in-karpathys-llm-wiki-e3a86a94b459), and [here](https://tomnguyenit.medium.com/i-built-karpathys-llm-wiki-for-my-day-job-here-s-what-actually-works-0d4ec6d1e433), but in this post **I want to focus on scalability, because once we address that, we buy ourselves the ability to deal with the other issues.**\n\n## Scalability challenges with the LLM wiki\n\n**By scalability I am specifically referring to read, search, and navigation of the wiki and ultimately, an efficient means of** [progressive disclosure](https://en.wikipedia.org/wiki/Progressive_disclosure) **where time and space complexity remain relatively flat as the wiki grows.**\n\nBy design, the wiki grows with each new document. If your agent consumes too much context every time it touches the knowledge base, both read and write slow down and the context window fills and rots faster. The LLM is now persisting this other “heavy” wiki connected to the data, which needs to be kept synced while parts of it are brought into the context window. Naive implementations often seem awesome when they start fresh, but they degrade quickly after repeated use.\n\nQuality is another problem. When the agent can’t afford to look around before it writes, it creates a second page for a concept that already has one, or blends things that should have stayed separate to save room, and the cross-links it should have caught go unchecked. The retrieval failure quietly becomes a write defect, and now you’re persisting it.\n\nMarkdown on a file system scales poorly— grep, awk, and other file system reads scale linearly with the total size of the wiki since you’re reading every file to find your matches. Following links is worse, because resolving each `[[slug]]`\n\nis another full pass, so a k-hop traversal costs you k scans of the corpus, and asking the reverse question — what points *at* this page — has no index behind it at all.\n\nVector and lexical search indexes could help, but they only cover the search part. They give you no native way to follow links and navigate through the wiki or internally within a page. Backlinks, contradiction paths, and “what else changed downstream of this page” aren’t search problems to begin with.\n\nWe solve these scalability issues with a graph database, where a hop is a pointer rather than a scan and the cost tracks the neighborhood you traverse rather than the size of the whole thing.\n\nThat is also what buys us the ability to handle the other issues. Once traversal is cheap, the agent can afford to check for duplicative content, run cheaper and faster validation, and comprehensively navigate existing links and section titles without blowing its context window. Compression stops being lossy in the same way too — a summary can stay a pointer rather than a replacement, since the agent that needs the underlying detail can hop into the specific section or back to the raw source instead of having to hold the whole page in context up front.\n\n## Graph as a scalable retrieval layer\n\nAn explicit representation of documents and their relationships *is* a [graph](https://neo4j.com/blog/knowledge-graph/what-is-knowledge-graph/): nodes for documents and sections, relationships for containment (what’s inside what), reading order (what follows what), and links (what references what).\n\nA **graph database** is *queried so an agent can understand a collection of these docs, sections, and relationships with minimal context before loading or searching the text corpus*. Once your knowledge base is modeled as a graph, the questions that were expensive become cheap progressive queries the agent can navigate:\n\n- walk the hierarchy to any depth –\n**traversal** - follow links any number of hops –\n**variable-length paths** - shortest connection between two ideas –\n**shortest path** - what’s load-bearing, read this first –\n**centrality** - what clusters into themes nobody labeled –\n**community detection**\n\nA graph database is built to navigate relationships efficiently. Traversals, paths, neighborhoods, and centrality are native and cheap; [ Neo4j Cypher](https://neo4j.com/docs/cypher/) lets the agent express a structural question directly (\n\n*“what links into X”*is one line, not a retrieval pipeline);\n\n[gives you community detection for theme-finding; and](https://www.neo4j.com/docs/graph-data-science/current/introduction/)\n\n**Neo4j Graph Data Science****fulltext and vector indices lives in the same database**, so “jump to a section” and “walk the structure” are one system, not two stitched together.\n\nCompare the alternatives:\n\n- A folder of files can’t provide queryable relationships at all.\n- A vector store gives you similarity, not shape.\n- SQL\n*can*model relationships but buries them in multi-hop JOINs and rigid data models that the agent has to work hard to untangle.\n\nThe graph is the representation the agent navigates, and Neo4j Graph Database is the engine that makes navigating it simple and native.\n\nYou can keep the graph lightweight and easily implement it yourself on your own data — you don’t need an elaborate hand-engineered ontology to get results.\n\n## How UK researchers quickly built a graph from wiki-style markdown docs\n\nResearchers at the [National Innovation Centre for Data](https://www.nicd.org.uk/) (NICD) at Newcastle University recently showed how easy and effective it can be to build a graph from wiki-style documents. To compare vector-only RAG and vector + graph RAG (aka [GraphRAG](https://neo4j.com/blog/genai/what-is-graphrag/)), they created a graph from a large subset of Wikipedia articles. The nodes and relationships in the graph reflected the structure of the articles and the connections between them: article sections and paragraphs, article redirects, articles linked to from each paragraph.\n\nThe NICD researchers then tested two agents on complex questions from a standard benchmark dataset. One agent could navigate the graph and use vector search; the other could only use vector search. The graph-enabled agent was [far more effective](https://neo4j.com/whitepapers/nicd-reducing-hallucinations-graphrag/): **>2× precision and recall** for factual correctness, truthfulness **+80%**, answer relevancy **+69%**.\n\nThe NICD team didn’t pre-compute a semantic graph with an LLM; they just captured the structure the documents already had and let the agent navigate it with **zero index-time tokens**. The same directional result shows up in vectorless RAG: PageIndex’s Mafin 2.5 reported **98.7% on FinanceBench** vs ~50% for naive vector RAG.\n\n## Easily build your wiki graph with **ki**\n\nYou can easily build wiki graph implementations with Neo4j that are simple, fast, and lightweight. As an example, [ ki](https://github.com/zach-blumenfeld/knowledge-index) is an open-source reference implementation that builds and syncs your Neo4j graph deterministically from a folder of markdown, forming a knowledge index that gives the agent verbs to navigate the graph. Ki ships with both\n\n- A CLI to easily manage the graph wiki\n- A\n`knowledge-base`\n\nagent skill that mirrors a lightweight version of Karpathy’s recipe with guidance on using the CLI and managing Neo4j\n\n`ki`\n\nacts more as a graph index than its own store — augmenting the read side while staying out of the write path. You can point `ki`\n\nat a folder of markdown — notes, docs, a wiki — and it syncs to a Neo4j knowledge-graph index you can search and navigate in seconds, from the CLI or any AI agent. `ki`\n\ncommands never modify source files (safe on an Obsidian vault, a git repo, a research folder), the index is disposable cache you can rebuild with one command, and there’s no LLM work or embeddings at index time (vendor-neutral, instant to set up, free).\n\nThe LLM-authoring pattern still works on top of the file system: the wiki grows under the LLM’s hand and `ki`\n\nre-syncs deterministically — `ki add <path>`\n\nfor a single file, `ki index`\n\nto rebuild.\n\n**Quickstart**\n\n```\n# Install\ncurl -sSfL https://knowledge-index.ai/install.sh | bash   # ki + neo4j-cli + agent skills\nki configure                                              # one-time Neo4j: Local (Podman), Aura, or Existing\n\n# Use\ncd ~/my-notes\nki index . --profile personal     # sync the folder into the graph (first index binds a profile)\nki outline my-notes --full        # table-of-contents view of the vault\nki search \"rate limiting\"         # find the right slice\nki get --type full \"<uri>\"        # read it (copy a uri from outline/search)\n```\n\nThe rest of this section is the actual data model, four key retrieval moves it exposes, and the Cypher/GDS behind each one.\n\n### The data model\n\nThe model is small on purpose: a `Vault → Folder → Document → Section`\n\ncontainment tree (`HAS`\n\n), section reading order (`NEXT_SECTION`\n\n), and `LINKS_TO `\n\nfor every wikilink / markdown link / external URL. External links live *outside* the tree, reachable only via `LINKS_TO`\n\n. Piped wikilink text (`[[Doc|alias]]`\n\n) folds into the target’s aliases for free. Construction is fully deterministic and idempotent — which is what makes re-syncing easy and automatic.\n\n`Vault / Folder`\n\n— the corpus and its directories`Document`\n\n— one node per`.md`\n\nfile (linked non-md files and external URLs become stub nodes, so a`[[wikilink]]`\n\nto a PDF or an`https://…`\n\nlink is part of the graph)`Section`\n\n— one node per heading-bounded subsection (each document is a tree of sections)`:HAS`\n\n— containment (`Vault → Folder → Document → Section`\n\n)`:NEXT_SECTION`\n\n— every section threaded in reading order`:LINKS_TO`\n\n— every wikilink and markdown link, across documents*and*sections\n\nEach of the four retrieval moves below is one way to read this graph — search is just one of them.\n\n**ki outline** — the map\n\nThe CLI command `ki outline`\n\nprovides a compact table of contents inclusive of both the `HAS`\n\nhierarchy and `LINKS_TO`\n\nedges. This allows the agent to see the layout of the knowledge base and navigate it effectively. The outline provides URIs for each folder, document, and section.\n\n``` bash\n$ cd ~/my-knowledge-base\n$ ki outline --depth 2\n\nKey:  V Vault   F Folder   D Document   S Section   L Links-to\n\nNAME                 T   URI\nmy-knowledge-base .. V   my-knowledge-base\n  ideas/ ........... F   my-knowledge-base/ideas\n    big-idea.md .... D   my-knowledge-base/ideas/big-idea.md\n    side-quest.md .. D   my-knowledge-base/ideas/side-quest.md\n  projects/ ........ F   my-knowledge-base/projects\n    ki-design.md ... D   my-knowledge-base/projects/ki-design.md\n  refs/ ............ F   my-knowledge-base/refs\n    birth.md ....... D   my-knowledge-base/refs/birth.md\n```\n\nThe agent can start at the vault root (as shown above) then jump deeper starting at any sub-folder, document, or section using the uri. It copies a uri out of the right-hand column and re-roots the outline there, so each query adds one layer of detail instead of dumping the whole vault at once. Rooting on a document brings its section tree and outbound links into view:\n\n``` bash\n$ ki outline my-knowledge-base/ideas/big-idea.md --depth 2\n\nKey:  V Vault   F Folder   D Document   S Section   L Links-to\n\nNAME                   T   URI\nbig-idea.md .......... D   my-knowledge-base/ideas/big-idea.md\n  Big Idea ........... S   my-knowledge-base/ideas/big-idea.md#big-idea\n    Background ....... S   my-knowledge-base/ideas/big-idea.md#big-idea/background\n    Origins .......... S   my-knowledge-base/ideas/big-idea.md#big-idea/origins\n      → Early Draft .. L   my-knowledge-base/refs/birth.md#early-draft\n    Implementation ... S   my-knowledge-base/ideas/big-idea.md#big-idea/implementation\n```\n\nThe `→`\n\nrow is an outbound `LINKS_TO`\n\nedge. Its target is not expanded inline, so following the citation is just another `ki outline`\n\non the uri sitting in that row:\n\n``` bash\n$ ki outline my-knowledge-base/refs/birth.md --depth 2\n\nKey:  V Vault   F Folder   D Document   S Section   L Links-to\n\nNAME                  T   URI\nbirth.md ............ D   my-knowledge-base/refs/birth.md\n  Early Draft ....... S   my-knowledge-base/refs/birth.md#early-draft\n    Sketch .......... S   my-knowledge-base/refs/birth.md#early-draft/sketch\n    Open questions .. S   my-knowledge-base/refs/birth.md#early-draft/open-questions\n      → Big Idea .... L   my-knowledge-base/ideas/big-idea.md#big-idea\n```\n\nTwo jumps off the map and the agent is reading the note that `Origins`\n\nwas citing, without having opened a file to get there.\n\nThe `--depth`\n\nparameter above is used to limit context length.\n\nThese simple commands abstract away the following graph queries. The hierarchy walk:\n\n```\nMATCH (root)\nWHERE ($root_uri IS NOT NULL\n       AND root.uri = $root_uri\n       AND (root:Vault OR root:Folder OR root:Document OR root:Section))\n   OR ($root_uri IS NULL AND root:Vault)\nCALL (root) {\n  RETURN 0                                     AS depth,\n         null                                  AS inrel,\n         labels(root)[0]                       AS label,\n         coalesce(root.name, root.displayName) AS name,\n         root.displayName                      AS displayName,\n         root.uri                              AS uri,\n         null                                  AS parent_uri,\n         null                                  AS sort_pos\n  UNION\n  MATCH path = (root) (()-[:HAS]->()){1,$depth} (d)\n  OPTIONAL MATCH nsp = (firstSec:Section)-[:NEXT_SECTION*0..]->(d)\n  WHERE d:Section\n    AND NOT EXISTS { MATCH (:Section)-[:NEXT_SECTION]->(firstSec) }\n  RETURN length(path)                          AS depth,\n         'HAS'                                 AS inrel,\n         labels(d)[0]                          AS label,\n         coalesce(d.name, d.displayName)       AS name,\n         d.displayName                         AS displayName,\n         d.uri                                 AS uri,\n         nodes(path)[-2].uri                   AS parent_uri,\n         CASE WHEN d:Section THEN length(nsp) ELSE null END AS sort_pos\n}\nRETURN depth, inrel, label, name, displayName, uri, parent_uri, sort_pos\n```\n\nThen the outbound links pass (the `→`\n\nrows), fed the document/section URIs from the walk:\n\n``` php\nUNWIND $source_uris AS source_uri\nMATCH (src {uri: source_uri})-[:LINKS_TO]->(tgt)\nWHERE src:Document OR src:Section\nRETURN src.uri                              AS parent_uri,\n       labels(tgt)[0]                       AS label,\n       coalesce(tgt.name, tgt.displayName)  AS name,\n       tgt.displayName                      AS displayName,\n       tgt.uri                              AS uri\nORDER BY parent_uri, uri\n```\n\nThe hierarchy walk uses a [variable length path pattern](https://neo4j.com/docs/cypher-manual/current/patterns/variable-length-paths/):`MATCH path = (root) (()-[:HAS]->()){1,$depth} (d)`\n\nWhich is saying: go out 1 to `$depth`\n\nhops on the containment relationships and collect everything in the path. This is the type of logic that is difficult to replicate efficiently outside of graph tools and what allows for such efficient progressive disclosure on knowledge bases — especially as we are considering both folder and section (within document) containment then later combining with links across documents. These commands remain fast even as the graph grows in size.\n\n**ki search** — jump to a spot\n\nWhile [vector search is entirely possible in Neo4j](https://neo4j.com/docs/cypher-manual/current/indexes/semantic-indexes/vector-indexes/), `ki`\n\nopts for fulltext only to keep things simple and avoid BYOC and embedding provider dependencies. It can be called via `ki search`\n\nthrough the CLI. The knowledge-base skill instructs the agent to use semantic expansion (`OR`\n\nin synonyms) to accomplish semantic search with no embedding.\n\n``` bash\n$ ki search \"vector search\" --k 5\n\nki: profile 'content-research' · vault 'content-research-wiki'  (from .ki)\nKey:  D Document   S Section\n\nscore  T  displayName                                       uri\n 5.66  D  semantic-search-without-vectors.md                content-research-wiki/raw/drafts/semantic-search-without-vectors.md\n 3.73  D  semantic-search-without-vectors-publish-ready.md  content-research-wiki/outputs/semantic-search-without-vectors-publish-ready.md\n 3.44  D  semantic-search-without-vectors.md                content-research-wiki/wiki/summaries/semantic-search-without-vectors.md\n 3.39  S  Summary — Semantic Search without Vectors         content-research-wiki/wiki/summaries/semantic-search-without-vectors.md#summary-semantic-search-without-vectors\n 3.38  S  Reconciliation: semantic-search-without-vectors   content-research-wiki/outputs/reconciliations/2026-05-21-semantic-search-without-vectors.md#reconciliation-semantic-search-without-vectors\n```\n\nBy default `ki search`\n\ndoes a full sweep over all document and sub-section nodes in the vault. However this can be further scoped with `--types`\n\nto filter to just documents or sections and `--under`\n\nto narrow to a subtree (folder / document / section) using a uri or local filesystem path.\n\n``` bash\n$ ki search \"vector search\" --types section --under wiki --k 5\n\nki: profile 'content-research' · under 'content-research-wiki/wiki'  (from .ki)\nKey:  D Document   S Section\n\nscore  T  displayName                                                                    uri\n 3.39  S  Summary — Semantic Search without Vectors                                      content-research-wiki/wiki/summaries/semantic-search-without-vectors.md#summary-semantic-search-without-vectors\n 2.66  S  Personal context                                                               content-research-wiki/wiki/summaries/ai-needs-alternatives-to-vectors.md#summary-ai-needs-alternatives-to-vectors/personal-context\n 2.48  S  Voice notes                                                                    content-research-wiki/wiki/summaries/blog-karpathy.md#summary-from-a-vibe-coded-llm-knowledge-base-to-a-handy-graph-search-engine/voice-notes\n 2.43  S  Reference links (research dump)                                                content-research-wiki/wiki/entities/microsoft-graphrag.md#microsoft-graphrag/reference-links-research-dump\n 2.36  S  Summary — From a Vibe-Coded LLM Knowledge Base to a Handy Graph Search Engine  content-research-wiki/wiki/summaries/blog-karpathy.md#summary-from-a-vibe-coded-llm-knowledge-base-to-a-handy-graph-search-engine\n```\n\nNote that `--under`\n\ntakes the same uris `ki outline`\n\nhands back, so the two compose directly — outline narrows the search space, search picks the slice out of it.\n\nThe undeyling Cypher query is one unified sweep over Documents + Sections against the `content_search`\n\nfulltext index (`$labels`\n\n= the `--types`\n\nfilter; `$scope`\n\n= the optional `--under`\n\nsubtree restriction):\n\n```\nCALL db.index.fulltext.queryNodes($index_name, $query) YIELD node, score\nWHERE (node:Document OR node:Section)\n  AND ($labels IS NULL OR any(l IN labels(node) WHERE l IN $labels))\n  AND ($scope IS NULL OR any(u IN $scope WHERE\n        node.uri = u\n        OR node.uri STARTS WITH u + '/'\n        OR node.uri STARTS WITH u + '#'))\nWITH node, score\nORDER BY score DESC\nLIMIT toInteger($k)\nOPTIONAL MATCH (doc:Document)-[:HAS*]->(node)\nRETURN\n  CASE WHEN node:Section THEN 'Section' ELSE 'Document' END AS label,\n  node.uri          AS uri,\n  node.displayName  AS display_name,\n  node.path         AS path,\n  node.content      AS content,\n  doc.uri           AS document_uri,\n  doc.displayName   AS document_title,\n  score\n```\n\n[Neo4j’s fulltext](https://neo4j.com/docs/cypher-manual/25/indexes/semantic-indexes/full-text-indexes/) is backed by Apache Lucene and offers the usual capabilities you’d expect: language-aware tokenization and analyzers (stop word removal, case-insensitive matching), the Lucene query syntax with boolean operators, quoted phrases, and per-property scoping, and a relevance score returned with each hit, ordered best-first.\n\nThe index itself is instantiated once at ingest if it doesn’t already exist.\n\n```\nCREATE FULLTEXT INDEX content_search IF NOT EXISTS\nFOR (n:Document|Section|Vault) ON EACH [n.displayName, n.content, n.aliases, n.description]\n```\n\n**ki get** — read it in order\n\n`ki`\n\nallows retrieval directly from the graph as well, since the nodes carry the full text contents. ` --type full`\n\nreconstructs a document or section along `NEXT_SECTION`\n\n, not as random chunks — the agent reads the article in the order it was written, not a shuffled top-k.\n\n``` bash\n$ ki get --type full my-knowledge-base/ideas/big-idea.md\n\nmy-knowledge-base/ideas/big-idea.md\n  label: Document\n  name: big-idea.md\n  path: /Users/zach/my-knowledge-base/ideas/big-idea.md\n  aliases: ['Big Idea']\n  sourceType: LOCAL_FILE\n\n# Big Idea\n\nThe one-paragraph version of the thing.\n\n## Background\n\nWhere this came from, and what it replaces.\n\n## Origins\n\nThe earlier sketch lives in [[Early Draft]].\n\n## Implementation\n\nWhat we would have to build.\n```\n\nBackground, Origins, Implementation come back in the order the author wrote them — the same order `ki outline`\n\nshowed above — not ranked by relevance. ` --type content`\n\nreturns just the node’s own preamble plus pointers to its children, for when you want to drill rather than pull the whole subtree – useful for progressive disclosure and protecting from context rot.\n\n`ki get`\n\nis also what makes remote access work: you can host Neo4j on Aura or any other cloud option, so even when the file system is local to one machine, the text is still reachable from anywhere the graph is.\n\n### Graph reasoning — free-form Cypher\n\nThe knowledge-base skill instructs the agent to use the neo4j-cli where the logic called for is not covered by existing ki cli commands. This allows the agent to reason over the Neo4j graph directly, writing Cypher ad hoc via `neo4j-cli query \"<cypher>\" --credential <profile>`\n\n— backlinks, shortest path, centrality, whatever the question needs. Here are a couple of examples, run against the same vault:\n\n*“What’s load-bearing — what should I read first?”* → in-degree centrality over the link graph (ranks the most-linked-into docs):\n\n``` php\n$ neo4j-cli query 'MATCH (src)-[:LINKS_TO]->(d:Document)\n                   RETURN d.uri AS document, count(DISTINCT src) AS inDegree\n                   ORDER BY inDegree DESC LIMIT 5' \\\n    --credential content-research --format table\n\n┌─────────────────────────────────────────────────\n│ DOCUMENT                                                            │ INDEGREE │\n├─────────────────────────────────────────────────\n│ content-research-wiki/raw/drafts/blog-neo4j-cli.md                  │ 39       │\n│ content-research-wiki/raw/drafts/semantic-search-without-vectors.md │ 36       │\n│ content-research-wiki/raw/drafts/blog-karpathy.md                   │ 33       │\n│ content-research-wiki/wiki/themes/the-partitioning-thesis.md        │ 31       │\n│ content-research-wiki/wiki/concepts/ki-themes-from-graph.md         │ 31       │\n└─────────────────────────────────────────────────\n```\n\nNo LLM call, no ranking heuristic — just the shape of the link graph telling the agent which four or five notes the rest of the vault leans on.\n\n*“What’s the throughline between the GraphRAG-cost argument and the AIP project?”* → shortest link path between the two docs, which surfaces the bridging document. Note that the hops alternate `HAS`\n\nand `LINKS_TO`\n\n: wikilinks are written inside sections, so getting from one document to another means dropping into the section that cites it.\n\n``` bash\n$ neo4j-cli query 'MATCH (a:Document {uri: $from}), (b:Document {uri: $to})\n                   MATCH p = shortestPath((a)-[:HAS|LINKS_TO*..8]->(b))\n                   UNWIND nodes(p) AS n\n                   RETURN n.uri AS hop' \\\n    --credential content-research --format table \\\n    --param from=content-research-wiki/wiki/concepts/expensive-graphrag-is-a-lie.md \\\n    --param to=content-research-wiki/wiki/entities/aip.md\n\n┌─────────────────────────────────────────────────────────────────────┐\n│ HOP                                                                                                               │\n├─────────────────────────────────────────────────────────────────────┤\n│ content-research-wiki/wiki/concepts/expensive-graphrag-is-a-lie.md                                                │\n│ content-research-wiki/wiki/concepts/expensive-graphrag-is-a-lie.md#expensive-graphrag-is-a-lie                    │\n│ content-research-wiki/wiki/concepts/expensive-graphrag-is-a-lie.md#expensive-graphrag-is-a-lie/related            │\n│ content-research-wiki/wiki/themes/graph-shape-over-flat-shape.md                                                  │\n│ content-research-wiki/wiki/themes/graph-shape-over-flat-shape.md#theme-graph-shape-beats-flat-shape-for-llm-cont… │\n│ content-research-wiki/wiki/themes/graph-shape-over-flat-shape.md#…-for-llm-context/instances                      │\n│ content-research-wiki/wiki/entities/aip.md                                                                        │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\nThe bridging document is `graph-shape-over-flat-shape.md`\n\n— a theme note neither endpoint mentions by name. That is the answer to a question no amount of similarity search would have produced, and it took one query.\n\n**themes** — surfacing patterns *(roadmap)*\n\nThe above concerns navigation and search, but in the spirit of the higher-level synthesis Karpathy’s recipe calls for, this next command focuses on finding themes across the corpus that no one has named yet. GDS community detection over the wikilink graph is used to surface these unlabeled themes. It is not in a released ki yet — it lives on a branch with the PR still open — but it is a real command rather than a sketch, and the run below is a live one against the same 91-doc vault:\n\n``` bash\n$ ki theme --top-k 3\n\nTHEMES  content-research-wiki   88 docs · 70 grouped into 5 themes by wikilinks · 18 ungrouped · 3 excluded (showing top 3 — 2 smaller themes cover 17 more docs)\n\nT1  22 docs (25%) · tightly interlinked\n    top wikilink targets   [[aip-paper]] in 10 docs · [[aip]] in 9 · [[aip-launch-blog]] in 8\n    most-linked docs       aip-conference-talk.md ........................ D   content-research-wiki/wiki/concepts/aip-conference-talk.md\n                           aip-launch-blog.md ............................ D   content-research-wiki/wiki/concepts/aip-launch-blog.md\n                           (+20 more docs)\n    links into T2 via      honest-roadmap-disclaim.md .................... D   content-research-wiki/wiki/themes/honest-roadmap-disclaim.md\n    links into T3 via      compile-dont-rederive.md ...................... D   content-research-wiki/wiki/themes/compile-dont-rederive.md\n\nT2  17 docs (19%) · tightly interlinked\n    top wikilink targets   [[blog-karpathy]] in 9 docs · [[blog-neo4j-cli]] in 9 · [[semantic-search-without-vectors]] in 9\n    most-linked docs       context-layer-comes-in-shapes-not-vendors.md .. D   content-research-wiki/wiki/concepts/context-layer-comes-in-shapes-not-vendors.md\n                           ki-themes-from-graph.md ....................... D   content-research-wiki/wiki/concepts/ki-themes-from-graph.md\n                           (+15 more docs)\n    links into T1 via      context-layer-comes-in-shapes-not-vendors.md .. D   content-research-wiki/wiki/concepts/context-layer-comes-in-shapes-not-vendors.md\n    links into T3 via      ki-themes-from-graph.md ....................... D   content-research-wiki/wiki/concepts/ki-themes-from-graph.md\n\nT3  14 docs (16%) · tightly interlinked\n    top wikilink targets   [[kf-ki-command-naming]] in 7 docs · [[graph-shape-over-flat-shape]] in 6 · [[kf-themes-from-graph]] in 6\n    most-linked docs       kf-ki-concepts.md ............................. D   content-research-wiki/wiki/ki-feedback/kf-ki-concepts.md\n                           kf-ki-connections.md .......................... D   content-research-wiki/wiki/ki-feedback/kf-ki-connections.md\n                           (+12 more docs)\n    links into T1 via      graph-shape-over-flat-shape.md ................ D   content-research-wiki/wiki/themes/graph-shape-over-flat-shape.md\n    links into T2 via      kf-themes-from-graph.md ....................... D   content-research-wiki/wiki/ki-feedback/kf-themes-from-graph.md\n```\n\nReading it: the header reconciles every document — grouped, ungrouped, excluded — so nothing quietly disappears. Each theme then carries its own evidence rather than a label an LLM guessed at. **Top wikilink targets** are the notes the theme’s documents keep citing, which is usually the closest thing to a name the corpus has: T1 converges on `[[aip-paper]]`\n\n, `[[aip]]`\n\n, `[[aip-launch-blog]]`\n\n, so T1 *is* the AIP (Agent Instruction Protocol) project without anyone having said so. **Most-linked docs** ranks members by links *within* the theme, giving the agent an entry point. **Links into Tvia** names the single document that bridges two themes — the drill handle for “how are these connected.”\n\nThe cohesion word comes from each community’s conductance, the fraction of its link mass that crosses the boundary: at or below 0.35 it reads *tightly interlinked*, at or above 0.60 *loosely*, otherwise *moderately*. It is a hedge against over-reading a weak cluster.\n\nThe value here is the thing search cannot do. Fulltext finds the documents that match a phrase you already thought of. This finds the groups nobody labeled — including the ones the author would deny having, which is exactly when it earns its keep.\n\nThe themes result is derived in multiple underlying graph steps. First a projection of the doc-level wikilink graph is created (sections collapse to their owning document; `LOCAL_STUB`\n\n/ `WIKILINK_UNRESOLVED`\n\nnodes ride along as co-citation glue, so two docs that both cite `[[GraphRAG]]`\n\ncluster together even if they never link each other):\n\n``` php\nMATCH (src)-[l:LINKS_TO]->(tgt)\nWHERE src.uri STARTS WITH $vaultPrefix AND tgt.uri STARTS WITH $vaultPrefix\nMATCH (s:Document {uri: split(src.uri, '#')[0]})\nMATCH (t:Document {uri: split(tgt.uri, '#')[0]})\nWHERE s.sourceType = 'LOCAL_FILE'\n  AND t.sourceType IN ['LOCAL_FILE', 'LOCAL_STUB', 'WIKILINK_UNRESOLVED']\n  AND s <> t\nWITH s, t, count(*) AS weight\nRETURN gds.graph.project(\n  $graphName, s, t,\n  { relationshipProperties: { weight: weight } },\n  { undirectedRelationshipTypes: ['*'] }\n) AS g\n```\n\nThe Leiden community detection is run, followed by per-community conductance for the cohesion word.\n\n```\nCALL gds.leiden.mutate($graphName, {\n  mutateProperty: 'themeId',\n  relationshipWeightProperty: 'weight',\n  gamma: $gamma,\n  randomSeed: 42,\n  concurrency: 1\n}) YIELD communityCount, modularity\nRETURN communityCount, modularity\nCALL gds.conductance.stream($graphName, {\n  communityProperty: 'themeId',\n  relationshipWeightProperty: 'weight'\n}) YIELD community, conductance\nRETURN community, conductance\n```\n\n`mutate`\n\nrather than write: conductance reads `themeId`\n\noff the in-memory graph, which never sees database writes, so persistence waits until after the metric. `randomSeed`\n\nplus `concurrency: 1`\n\nmake the assignment deterministic for a given projection.\n\nWrite the assignment back — clear stale ids, persist, then fold sub-floor themes (< 3 member docs) into ungrouped:\n\n```\n// clear stale themeIds from a prior run\nMATCH (n:Document)\nWHERE n.uri STARTS WITH $vaultPrefix AND n.themeId IS NOT NULL\nREMOVE n.themeId\nCALL gds.graph.nodeProperties.write($graphName, ['themeId'])\nYIELD propertiesWritten\nRETURN propertiesWritten\n// fold themes with fewer than $minThemeDocCount member docs into ungrouped\nMATCH (m:Document {sourceType: 'LOCAL_FILE'})\nWHERE m.uri STARTS WITH $vaultPrefix AND m.themeId IS NOT NULL\nWITH m.themeId AS theme, count(m) AS docCount\nWHERE docCount < $minThemeDocCount\nWITH collect(theme) AS smallThemes\nMATCH (n:Document)\nWHERE n.uri STARTS WITH $vaultPrefix AND n.themeId IN smallThemes\nREMOVE n.themeId\n```\n\nThen read the rendered output — members (with within-theme link counts), top wikilink targets, and crossover docs:\n\n```\n// members + within-theme link counts (drives \"most-linked docs\")\nMATCH (src)-[l:LINKS_TO]->(tgt)\nWHERE src.uri STARTS WITH $vaultPrefix AND tgt.uri STARTS WITH $vaultPrefix\nMATCH (s:Document {uri: split(src.uri, '#')[0]})\nMATCH (t:Document {uri: split(tgt.uri, '#')[0]})\nWHERE s.sourceType = 'LOCAL_FILE' AND t.sourceType = 'LOCAL_FILE' AND s <> t\n  AND s.themeId IS NOT NULL AND s.themeId = t.themeId\nUNWIND [s, t] AS d\nWITH d.themeId AS theme, d, count(*) AS withinThemeLinks\nRETURN theme, d.uri AS uri, d.displayName AS displayName, withinThemeLinks\nORDER BY theme, withinThemeLinks DESC, uri\n// top wikilink targets per theme (\"[[…]] in N docs\")\nMATCH (src)-[l:LINKS_TO {wikilink: true}]->(tgt)\nWHERE src.uri STARTS WITH $vaultPrefix\nMATCH (s:Document {uri: split(src.uri, '#')[0]})\nWHERE s.sourceType = 'LOCAL_FILE' AND s.themeId IS NOT NULL\n  AND split(tgt.uri, '#')[0] <> s.uri\nWITH s.themeId AS theme, tgt,\n     coalesce(tgt.displayName, tgt.name, tgt.uri) AS key,\n     count(DISTINCT s) AS linkingDocs\nWHERE linkingDocs > 1\nORDER BY theme, linkingDocs DESC, key\nWITH theme, collect({uri: tgt.uri, displayName: key, docs: linkingDocs})[..5] AS targets\nRETURN theme, targets\n```\n\nThe `linkingDocs > 1 `\n\nfilter is what keeps this honest: a target only one document cites is that document’s private reference, not something the theme converges on.\n\n``` php\n// crossover docs (\"links into T<j> via\")\nMATCH (src)-[l:LINKS_TO]->(tgt)\nWHERE src.uri STARTS WITH $vaultPrefix AND tgt.uri STARTS WITH $vaultPrefix\nMATCH (s:Document {uri: split(src.uri, '#')[0]})\nMATCH (t:Document {uri: split(tgt.uri, '#')[0]})\nWHERE s.sourceType = 'LOCAL_FILE' AND t.sourceType = 'LOCAL_FILE'\n  AND s.themeId IS NOT NULL AND t.themeId IS NOT NULL\n  AND s.themeId <> t.themeId\nWITH s.themeId AS theme, t.themeId AS otherTheme, s, count(*) AS crossLinks\nORDER BY theme, otherTheme, crossLinks DESC, s.uri\nWITH theme, otherTheme, collect({uri: s.uri, displayName: s.displayName})[0] AS via\nRETURN theme, otherTheme, via\n```\n\nNone of that is reachable without the graph: community detection needs the link structure as a first-class object, and there is nothing in a folder of markdown to run it against. But notice how little of it surfaces — six queries, a GDS pipeline, and a conductance threshold collapse into one command with four flags. That is the pattern across all four verbs: the graph is what makes the question answerable, and the CLI is what makes it a single line an agent can type without knowing any of this exists.\n\nThis theme finding is also low cost, relatively deterministic, and hallucination-free. No LLM is called at any point — Leiden reads the link structure the documents already carry, so themes cost zero tokens to compute. A fixed random seed and single-threaded execution make the grouping deterministic, so re-running on an unchanged vault returns the same themes rather than a fresh hallucination. Change the notes and re-run; the answer moves because the corpus moved, not because the model felt different that session.\n\n## From an abstract wiki to a scalable knowledge layer\n\nThe reason to care past a hobby vault: **This is the version that scales**. Historically, the expensive, brittle part of most GraphRAG pipelines has been an LLM extracting entities or writing summaries at build time. Karpathy’s wiki recipe provides that upfront now. What’s left is a deterministic Neo4j graph built from the structure the documents already carry, which changes the economics at every step up:\n\n**A team’s shared docs or wiki.** Projects like ki serve remote, read-only vaults from Neo4j. Index once, let many agents (or an app) navigate the same graph without a copy each, and deterministic sync keeps it current instead of drifting.**An agent’s long-term memory.** The notes an agent writes and re-reads are markdown with links; a Neo4j graph over them gives the agent navigation and backlinks over its own memory, no other extraction or embedding pipeline to maintain.**A knowledge layer over structured data.** The same “give the agent a navigable representation” idea extends past documents to schemas, lineage, and business definitions — a graph the agent walks instead of guessing JOINs. That’s a bigger post and a[hands-on workshop](https://graphacademy.neo4j.com/courses/workshop-lakehouse/), but it’s the same thesis on the same engine: agents retrieve better when they navigate an explicit structure. Neo4j is where that structure lives, from a personal vault to a warehouse.\n\nAt production scale, “Zero tokens to build, idempotent ingest, reads that stay fast as the corpus grows” stops being a nicety and becomes the reason it’s viable. It’s the whole case that structure-and-link GraphRAG on Neo4j should be the *default*, not a specialist tool.\n\n## Why now\n\nThe Karpathy post made the shape of this problem visible to a much bigger audience. Google’s OKF turned that shape into a proposed standard, but pointedly left retrieval out of scope. The NICD research made the case for navigating a graph over sampling a vector store, with numbers. The format is settling, retrieval is the open question, and the numbers say a graph is the answer.\n\nIf you’re running any version of this stack — personal, team, or production — **retrieval is where it feels least serious**, and it’s the one part with a clean, cheap upgrade. And if you’re building your own agent tools over a corpus of markdown, the point stands regardless of what you build it with: **Give the agent a structure it can navigate, not just a box it can search.** The cheapest place to put that structure is a graph, and the natural home for the graph is Neo4j.\n\n```\ncurl -sSfL https://knowledge-index.ai/install.sh | bash\n```\n\nSame recipe as before, but now the agent can see the whole thing, and walk it.", "url": "https://wpnews.pro/news/scaling-karpathys-llm-wiki-why-your-knowledge-base-needs-a-graph", "canonical_source": "https://neo4j.com/blog/agentic-ai/scaling-karpathy-llm-wiki-graph/", "published_at": "2026-08-31 21:27:49+00:00", "updated_at": "2026-08-31 22:52:36.683978+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-infrastructure", "ai-tools"], "entities": ["Neo4j", "Andrej Karpathy", "Google Cloud", "Open Knowledge Format", "Claude Code", "Cursor", "Obsidian"], "alternates": {"html": "https://wpnews.pro/news/scaling-karpathys-llm-wiki-why-your-knowledge-base-needs-a-graph", "markdown": "https://wpnews.pro/news/scaling-karpathys-llm-wiki-why-your-knowledge-base-needs-a-graph.md", "text": "https://wpnews.pro/news/scaling-karpathys-llm-wiki-why-your-knowledge-base-needs-a-graph.txt", "jsonld": "https://wpnews.pro/news/scaling-karpathys-llm-wiki-why-your-knowledge-base-needs-a-graph.jsonld"}}