Zum Hauptinhalt springen

How v10r organizes the knowledge its chatbot answers from, and how it retrieves it. The integrating blueprint above layered-rag.md, graph-rag.md, and surfaces.md — read those for the per-layer detail; read this for the whole-system mental model and the honest map of what is wired versus scaffold.

This is a reference project. Wired-vs-scaffold honesty is the point. Several pieces below are designed and schema-backed but not running in production (the llmwiki pointer layer is empty; tier-3 graph is dormant for docs; some telemetry is fabricated). Marking that truthfully is the showcase — a RAG pattern library that lies about what it ships teaches nothing. The Phased roadmap at the end is designed, not built, past Phase C.


Terminology

Three names, one of which is not code.

Name What it is Where
rawrag The retrieval engine: chunk → embed → tiers 1/2/3 → RRF fusion → drill-by-id. The ground truth. src/lib/server/rawrag/
llmwiki The pointer/connector layer over rawrag: curated pages (title + tldr + body + tags) whose pointers: are raw chunk ids. A summary surface, not a separate corpus. src/lib/server/llmwiki/
"nRAG" Umbrella term in docs/memory only — not a code identifier. Means "the whole retrieval subsystem."

The user's mental model is exact and worth stating plainly:

nRAG = a connector/pointer over raw-RAG. That is the llmwiki design — one curated TLDR per topic, each carrying chunk-id pointers down into the immutable rawrag layer. It is a recognized pattern (multi-vector / doc-summary / parent-doc retrieval), not an invention.

The model answers from llmwiki TLDRs by default and drills into rawrag chunks only for exact wording or claim verification. llmwiki is the answer surface; rawrag is the audit trail. See layered-rag.md.


Wired vs scaffold (the honest map)

What actually runs today, what is schema-backed scaffolding, and what is designed only.

Piece Status Reality
rawrag tier-1 (contextual hybrid vector + BM25) BUILT The live grounding path — and the only tier the chatbot reaches (it requests tiers:[1]).
rawrag tier-2 (parent-child) BUILT (engine) / not reached by chatbot Engine works. The unified ingest door (Phase C step 1) now writes hierarchical parents+children, so tier-2 is eligible for converted docs — but the chatbot requests tiers:[1] only, so tier-2 stays unexercised until a tier-2 surface (rag-demo / reranker) lands.
Unified ingest door (hierarchical docs chunks) BUILT 2026-06-25 / partial corpus ingest-docs.ts reuses the app's pure planChunks() → section-parents + paragraph-children. Groundwork: does not change chatbot answers today (the chatbot is tier-1-only; flat and hierarchical paragraph-children read identically through tier-1). Conversion partial — 36 of 93 docs as of 2026-06-25 (quota-gated, multi-day; see corpus model below).
rawrag tier-3 (Neo4j graph) BUILT (engine) / DORMANT for docs Engine works; the docs corpus seeds no entities, so tier-3 returns []. The catalog :Resource graph exists but is never queried by retrieval.
RRF fusion (k=60) + drill-by-id BUILT Live, but cross-tier RRF rarely fires for docs because tiers 2/3 are empty.
llmwiki search / pointer hydration BUILT (engine) / EMPTY in prod searchLlmwiki works, but no pages exist — compile/ + lint/ are scaffolds (COMPILE_SCAFFOLD/LINT_SCAFFOLD). Only a per-user hand-seed script creates pages. So the elegant pointer layer never runs for normal users.
Relevance-gated system-docs prefetch BUILT The chatbot's actual safety net today — a parallel tier-1 retrieve() over the system-docs corpus. See layered-rag.md.
Citation verification (verify.ts) BUILT Runs on onFinish; assigns quote/paraphrase/drifted per chunk.
Catalog grounding (search_catalog, <catalog-map>) BUILT Live. Postgres-only — does not touch the Neo4j catalog graph.
System overview anchor BUILT · browser-verified 2026-06-25 getOverview/loadOverview are owner-aware (inArray(userId, ownerIds)); the orchestrator injects a <project-overview> block from loadOverview([SYSTEM_DOCS_USER_ID], PROJECT_DOCS_COLLECTION_ID), written by ingest-docs.ts. The load-bearing fix for "how do I use v10r?" — a plain DB-row read, embed-independent, so it lands even when the embed quota is exhausted. A deterministic in-intro Stack: line now rescues the stack answer from read-truncation — built + validated 2026-06-25, reaching the live anchor on the next post-quota-reset re-ingest; see corpus model below.
Embed retry / loud failure tracing BUILT 2026-06-25 embed.ts retries Gemini 429/quota with asymmetric backoff; the orchestrator now emits {step:'embed', status:'error'} when system-docs retrieval rejects — the silent ungrounding (Promise.allSettled masking) is gone.
rag-demo showcase surface BUILT (thin) / DISHONEST Requests tiers:[1], maxChunks:3 only — RRF-across-tiers never fires; no rerank/transform/eval/drill. The teaching surface can't teach the pipeline.
Step-back transform · reranker · eval harness · graph :DEPENDS_ON DESIGNED, NOT BUILT See Phased roadmap.
Telemetry: vectorHits / bm25Hits split FABRICATED The llmwiki branch emits the same number three times. LlmwikiSearchDetail promises a split that doesn't exist.

The asymmetry is deliberate: the engine is mature; the food (corpus + curated pointer pages) is thin. That is the inverse of where the leverage is — see The recall safety net.


The corpus & chunking model (the "food")

Retrieval quality is bounded by chunk quality. Markdown becomes a two-level hierarchy:

  • Parents (level = 'section') — heading-bounded sections. Stored with embedding = NULL so tier-1 never surfaces them; they exist only as tier-2 parent context.
  • Children (level = 'paragraph') — embedded leaves, each linked to its parent via parent_id.

Every child carries a deterministic heading-breadcrumb context prefix${doc.title} › ${deepest heading} — prepended before embedding so a chunk pulled in isolation still knows where it sits. This is a cheap stand-in for Anthropic-style LLM contextual prep, which is deferred: per-chunk LLM calls are infeasible on a ~20-req/day free-tier ceiling. The breadcrumb is derived by scanning ATX heading lines, no model call.

Two corpora, one kernel:

Corpus Owner Fed by Status
SYSTEM_DOCS (the chatbot's real KB) SYSTEM_DOCS_USER_ID scripts/db/ingest-docs.ts Live — hierarchical conversion in progress (36/93 as of 2026-06-25)
Per-user desk files the real user syncDeskFileToRag (shared kernel) Live, hierarchical

The unified ingest door (the two-chunker divergence, resolved 2026-06-25). The docs corpus was historically fed by a Bun script that re-implemented chunking via the dependency-free markdown-split.ts — flat paragraph chunks, bare-title prefix, no parents. As of 2026-06-25, ingest-docs.ts reuses the app's pure planChunks() (rawrag/plan.ts), so both runtimes share one door: it writes section-parents (embedding = NULL) + paragraph-children (parent_id set, deterministic "<docTitle> › <deepestHeading>" breadcrumb prefix). Entities + Neo4j remain app-path-only.

Conversion is partial and quota-gated — not a defect. Live state 2026-06-25: 36 of 93 docs hierarchical, 57 still flat. The Gemini free tier caps embeddings at 1000 embed_content requests/day; the corpus is ~1586 chunks, so a full flat→hierarchical re-ingest spans multiple days. The re-ingest is resume-safe — INGEST_FORCE=1 / --force skips any doc that already has a level='section' chunk. This partial state does not degrade chatbot answers: the chatbot reaches tier-1 only, and tier-1 reads flat and hierarchical paragraph-children identically. Proven 2026-06-25 — a question answered from a still-flat doc (multi-client-core, 0 sections / 33 embedded paragraphs) returned a correct, cited answer. Hierarchical chunking is groundwork for future tier-2 surfaces (rag-demo / reranker), not the change that corrected the chatbot.

The system overview anchor (BUILT, browser-verified 2026-06-25). A single high-level kind='overview' llmwiki row (system-owned, deterministic 94-doc knowledge-map TOC) injected into every chatbot turn as a <project-overview> block. ingest-docs.ts writes it; getOverview/loadOverview are owner-aware (inArray(userId, ownerIds)); the orchestrator loads loadOverview([SYSTEM_DOCS_USER_ID], PROJECT_DOCS_COLLECTION_ID). Verified live: exactly one overview row (system-docs/project-docs, source_count = 94, embedded); grounding fires end-to-end (llmwiki:overview → search → context → generate). (source_count = 94 is the full manifest doc count; the 36/57/93 corpus figures above count docs with live chunks — one doc transiently sits in a soft-deleted gap when the daily embed cap is hit, self-healing on the next resume run.) This is the load-bearing fix for the original broad-question bug — and because it is a plain DB-row read with no embedding, it lands even when the embed quota is exhausted. Read-truncation hazard, fixed 2026-06-25: the stored body is 5000 chars but loadOverview truncates at read to OVERVIEW_MAX_TOKENS × CHARS_PER_TOKEN = 500 × 4 = 2000 chars. The TOC is section-ordered foundation › blueprint › stack, so the large blueprint section pushed the ## stack list past 2000 chars — "which stack does v10r use?" was truncated off before the model saw it (compounded by design: every clean stack enumerationdocs/stack/README.md, docs/stack/vendors.md, CLAUDE.md — is corpus-excluded, so retrieval alone can't surface it). Fix: buildOverviewBody(), extracted to the Vite-free, unit-testable src/lib/server/docs/overview-body.ts (imported back by ingest-docs.ts, same pattern as doc-filter.ts), now inserts a deterministic one-line Stack: summary right after the intro and before the TOC (~char 320, well inside the ceiling), names derived from the core stack subsection doc titles — no hardcoding. OVERVIEW_MAX_TOKENS is deliberately unchanged. The rest of the TOC still truncates; only the stack answer is rescued. Built + validated 2026-06-25; reaches the live anchor on the next post-quota-reset db:ingest-docs — the stored row is not yet rewritten (today's embed quota is exhausted).

Why README nav-hubs are excluded. Every docs directory has a README.md that is a navigation index — topic tables mapping files to topics (docs/README.md). Those are pointers, not prose; embedding them pollutes retrieval with link-lists that answer nothing. isBlocked drops every */README.md from the corpus.


The recall safety net

The pointer (llmwiki) layer must always run IN PARALLEL with flat retrieval. A summary can never gate recall.

This is the load-bearing principle of the whole design. The research consensus is blunt about the failure mode: a summary/pointer layer is the lowest-leverage lever and the highest-risk one, because "the summary hid the chunk" silently drops recall — the answer was in the corpus, but the curated page didn't point at it.

Source Finding
ARAGOG (retrieval ablation survey) Summary-first retrieval underperforms hybrid flat retrieval on recall.
Anthropic Contextual Retrieval Contextual chunk prefixes cut retrieval failures by −67% — fix the chunk, not the summary.

So flat retrieve() runs unconditionally underneath the pointer layer; results are deduped by chunkId and reranked (no second RRF pass). The flat net is the recall guarantee. llmwiki must not be promoted to the default answer surface until the lint job is scheduled and green — until then, the parallel flat path is what keeps recall honest. This is why the system-docs prefetch (live) matters more than the empty llmwiki layer.


Retrieval-strategy seams

The target query path, with each seam's build status. "A blank stage is a lie; a skipped stage is the lesson" — the showcase must emit every stage, marking unrun ones status:'skipped' rather than hiding them.

query
  → step-back query-transform   (PLANNED · gated on broad-query intent, quota-budgeted, identity-fallback)
  → per-tier retrieval          (BUILT · tier-1 live & chatbot-only; tiers 2/3 eligible for converted docs, unexercised by chatbot)
  → RRF fusion (k=60)           (BUILT · rarely fires multi-tier for docs today)
  → rerank                      (PLANNED · slots after fusion, deterministic/local fallback)
  → drill-by-id                 (BUILT · verbatim-ids rule, see layered-rag.md)
  → citation-verify             (BUILT · onFinish, quote/paraphrase/drifted)
  → eval                        (PLANNED · golden-set harness, RAGAS-style metrics)

Sequencing matters: step-back runs BEFORE embed (it rewrites the query); rerank runs AFTER fusion (it reorders candidates). Both are gated and fall back to identity so a quota-exhausted turn still answers. The orchestrator must be split along these seams before eval and rerank have a slot to live in — extracting the retrieval-strategy seam is the unblocking move. See Phased roadmap.


Data-model & contract notes

Summaries only — the detail lives in code and in the roadmap.

Area Decision
Embedding dimension 1536 is DDL-bound (vector(N) literal) → stays a TS constant (SSOT in rag-shared/embed-config.ts, re-exported by config.ts). The rag.embedding_model row becomes the live model-identity registry (provider/task/active) with a CHECK guarding agreement. Dimension and identity are split: one is structural, one is data.
Owner-aware overview getOverview(ownerIds: string[]) via inArray (the precedent graph/rag/queries.ts already uses) so a logged-in user loads both their own overview and the system overview.
Eval store New eval pgSchema cluster: golden_item, golden_expected_source, eval_run, eval_result — one numeric column per RAGAS-style metric.
Graph :DEPENDS_ON The catalog graph has only PART_OF (containment) → can't answer "what depends on Drizzle?". Earns its keep only with a :DEPENDS_ON edge + an :Entity-[:ABOUT]->:Resource bridge. :Entity and :Resource stay separate.
Per-surface request schemas Split ChatRequestSchema per surface so rag-demo knobs (tiers, rerank toggles) can't leak into the product chatbot. Low-risk: clients send a superset, ignore extras.
/admin/ai/rag/health New endpoint = the wired-vs-scaffold board (above) made live + per-tier contribution telemetry. Surfaces the *_SCAFFOLD sentinels the UI and these docs both cite.
Per-locale tsvector The BM25 tsvector is English-only across a tri-locale (en/de/ru) app → a CASE-on-locale generated column keyed off document.locale / chunk.locale.

Tooling rationale

Choice Verdict
Hand-rolled (no LangChain / LlamaIndex) Right call. The TS RAG framework ecosystem is 18–24 months behind Python and a framework graveyard at this scale. Hand-rolled keeps every seam inspectable — which is the whole point of a showcase.
pgvector / Neon Fine at this scale (nowhere near the 5–10M-vector range where dedicated stores matter). Caveat: disable Neon auto-suspend or the HNSW index pays a cold-start penalty on the first query.
Neo4j (graph tier) ~Zero value for single-hop docs Q&A (GraphRAG-Bench: vanilla 60.92 vs MS-graph 49.29). Earns its keep only for multi-hop dependency queries ("what depends on Drizzle?") — hence :DEPENDS_ON is the one graph investment worth making.

The research line throughout: fix the food, not the framework. Self-contained chunks, contextual prefixes, an overview anchor, README exclusion, hybrid BM25, step-back, a reranker, and a golden-set eval all outrank any amount of orchestration cleverness. The summary/pointer layer is the last thing to invest in, and only behind a lint net.


Phased roadmap

Everything past Phase C is DESIGNED, not built. Phase C landed and was browser-verified on 2026-06-25; it is migration-free (no db:push — every column already exists) and its only deploy action is re-running db:ingest-docs. The rest is captured here so the decisions don't evaporate.

Phase C — Foundation (BUILT + browser-verified 2026-06-25, migration-free)

The three quick-wins, independently the right foundation. Steps 2 + 3 share one orchestrator edit hotspot (the Promise.allSettled grounding block) and landed in one pass. All three are live — 5/5 functional probes green on 2026-06-25, with honest "I don't have that in the provided documentation" on out-of-corpus questions (no fabricated paths or numbers).

# Step Effect Status
1 Unify the ingest dooringest-docs.ts reuses pure Vite-free planChunks + rag-shared/embed-config.ts. Writes hierarchical section-parents + paragraph-children. Groundwork only — makes tier-2 eligible; does not change chatbot answers (chatbot is tier-1-only). Zero query-path change. BUILT · corpus 36/93 (partial, quota-gated, resume-safe)
2 Embed-retry + loud tracing — bounded retry/backoff in embed.ts; add the missing rejected branch in the orchestrator. Kills silent ungrounding; a 429 becomes an observable {step:'embed', status:'error'}. BUILT
3 Owner-aware overview + write the system anchorgetOverview(ownerIds); write the deterministic system-overview row; inject the <project-overview> block. The load-bearing fix for "how do I use v10r?". Embed-independent — lands even when embed quota is exhausted. BUILT · verified

Beyond Phase C (designed, not built)

Order Item Status
4 Retrieval-strategy orchestrator split — extract rawrag/strategy/chatbot-context.ts FIRST; it unblocks the eval harness and the reranker slot. DESIGNED
5 Step-back query-transform + reranker — both gated on broad-query intent + quota, both with deterministic/identity fallback. DESIGNED
6 llmwiki compile + recompile job — auto-generate page TLDR/body/pointers from rawrag chunks; drift→recompile via the existing source_hash_at_compile detector. DESIGNED
7 lint nightly — must be scheduled and green before llmwiki is promoted to the default answer surface (flat net is the guarantee until then). DESIGNED
8 eval schema + golden-set harnesseval pgSchema cluster; 202+poll admin endpoints (reuse the /admin/db run pattern, Idempotency-Key). DESIGNED
9 Graph :DEPENDS_ON + docs entity tier — earns tier-3 its keep for multi-hop dependency queries; heavier (needs a Neo4j driver in the script). DESIGNED
10 Per-locale tsvectorCASE-on-locale generated column. DESIGNED
11 Fabricated-telemetry fix — stop emitting the phantom vectorHits/bm25Hits split; emit real per-tier contribution. DESIGNED

Open product decisions

Decision Recommendation Why
Recompile trigger (eager / nightly-batched / lazy-on-read) Nightly-batched All three amplify LLM calls against a 20-req/day ceiling; batching is the only one that survives the quota.
Per-surface request schema split Proceed Low-risk — clients send a superset and ignore extras; the win (knobs can't leak to the product chatbot) is worth it.
Admin endpoint versioning Unversioned Matches existing admin precedent (/admin/db, /admin/ai/*).

  • layered-rag.md — the two-layer split, read path, tool contracts, catalog grounding, graph tenancy. The primary RAG doc.
  • surfaces.md — chatbot vs deskbot; the one-kernel/two-profiles model.
  • graph-rag.md — rawrag internals; the catalog :Resource seed; the hybrid pipeline.
  • README.md — AI blueprint nav hub.
← Back to Blueprint

Geht dieses Pattern noch besser? Sag uns, wie.

Feedback geben