A self-evaluating, hallucination-resistant RAG system featuring a 3-stage retrieval pipeline (Hybrid Search β Cross-Encoder Reranking β LLM Generation), dual-LLM judge architecture, multi-modal ingestion, and session-scoped conversational memory.
Architecture Β· 3-Stage Pipeline Β· Judge Architecture Β· Roadmap
Neural KB is a production-grade personal knowledge engine that transforms unstructured documents β PDFs, YouTube videos, and Notion exports β into a queryable, conversational intelligence layer. The system goes far beyond a basic RAG implementation by stacking three architectural layers that most portfolio projects lack entirely.
Layer 1 β Retrieval Quality: A custom Hybrid Retriever fuses BM25 sparse keyword search with Pinecone dense vector search via Reciprocal Rank Fusion (RRF). This ensures both semantic meaning and exact technical terms (e.g., Ξ²β=0.98, BLEU 27.3) are reliably retrieved.
Layer 2 β Precision Filter: A Cross-Encoder reranker (BAAI/bge-reranker-base) re-scores the top 12 candidates by joint query-document relevance before any text reaches the LLM, eliminating topically-adjacent noise that embedding similarity alone cannot distinguish.
Layer 3 β Answer Verification: Every generated answer is independently evaluated by a second LLM pass β the Judge β which detects hallucinations, measures contextual grounding, and returns a calibrated confidence score rendered as a live UI card.
"Don't just retrieve and generate β retrieve precisely, rerank aggressively, generate, and verify."
| Benchmark | Result |
|---|---|
| Direct factual retrieval (e.g., "How many attention heads?") | 100% Confidence β |
| Multi-step numerical retrieval (e.g., BLEU score = 27.3) | 90% Confidence β |
| Anaphora resolution ("What were its hyperparameters?" β Adam) | 100% β Memory resolved β |
| Out-of-context hallucination guard ("FIFA World Cup 2026") | 0% β Correctly refused β |
| Ingestion throughput (15-page technical PDF) | 79 chunks, ~12s β |
| Reranking stage latency (12 candidates, CPU) | ~0.8s (cached model) β |
| Layer | Technology | Purpose |
|---|---|---|
| Orchestration | LangChain 0.3+ (LCEL) | Chain composition, memory, retrieval |
| LLM | Groq / Llama 3.1 8B Instant | Generation + evaluation judge (250+ tok/s) |
| Embeddings | HuggingFace all-MiniLM-L6-v2 |
384-dim semantic vectors (bi-encoder) |
| Reranker | BAAI/bge-reranker-base |
Cross-encoder precision filter (stage 2) |
| Sparse Retrieval | BM25Retriever (rank-bm25) |
Keyword-exact in-memory search |
| Dense Retrieval | Pinecone Serverless (AWS us-east-1) | Scalable ANN search + metadata filtering |
| RRF Fusion | Custom HybridRetriever(BaseRetriever) |
Zero-dependency RRF implementation |
| PDF Ingestion | LangChain PyPDFLoader | Page-level document extraction |
| YouTube Ingestion | youtube-transcript-api + yt-dlp | Dual-plan transcript pipeline |
| Speech-to-Text | Groq Whisper Large v3 | Audio transcription fallback |
| Memory | LangChain RunnableWithMessageHistory |
Session-scoped conversational state |
| UI | Streamlit | Premium dark-mode chat dashboard |
flowchart TD
subgraph INGEST["π₯ Ingestion Layer"]
PDF["π PDF\n(PyPDFLoader)"]
YT["π₯ YouTube\n(Dual-Plan Pipeline)"]
NOTION["π Notion\n(Markdown Export)"]
end
subgraph PROCESS["βοΈ Processing Layer"]
SPLIT["RecursiveCharacterTextSplitter\nchunk_size=1000 | overlap=200"]
EMBED["HuggingFace Embeddings\nall-MiniLM-L6-v2 | 384 dims"]
BM25CORP["BM25 Corpus Cache\n_session_chunks: List[Document]"]
end
subgraph STORE["ποΈ Storage Layer"]
PINE["Pinecone Serverless\nAWS us-east-1 | cosine\nMetadata: source, method, page, title"]
BM25IDX["BM25 In-Memory Index\nrank-bm25 | term frequency"]
end
subgraph RETRIEVAL["π 3-Stage Retrieval Pipeline"]
HYBRID["Stage 1: HybridRetriever\nRRF Fusion β 12 candidates"]
RERANK["Stage 2: Cross-Encoder\nBAAI/bge-reranker-base β top 4"]
GEN["Stage 3: Llama 3 Generation\n+ Conversation Memory"]
end
subgraph EVAL["βοΈ Judge Layer"]
JUDGE["LLM-as-Judge\nConfidence Score 0-100\nHallucination Detection"]
end
PDF --> SPLIT
YT --> SPLIT
NOTION --> SPLIT
SPLIT --> EMBED
SPLIT --> BM25CORP
EMBED --> PINE
BM25CORP --> BM25IDX
PINE --> HYBRID
BM25IDX --> HYBRID
HYBRID --> RERANK
RERANK --> GEN
GEN --> JUDGE
style INGEST fill:#1a1a2e,stroke:#7c6fff,color:#f0f0ff
style PROCESS fill:#1a1a2e,stroke:#ff6f9c,color:#f0f0ff
style STORE fill:#1a1a2e,stroke:#4dffa6,color:#f0f0ff
style RETRIEVAL fill:#1a1a2e,stroke:#ffb347,color:#f0f0ff
style EVAL fill:#1a1a2e,stroke:#ff5f7e,color:#f0f0ff
This is the core architectural differentiator of Neural KB. Most RAG implementations stop at Stage 1. Production systems require all three.
A custom HybridRetriever class (subclassing BaseRetriever) fuses two retrieval signals without any external ensemble dependency:
flowchart LR
Q["π User Query"] --> D["Dense Retriever\nPinecone cosine similarity\nβ ranked list A"]
Q --> S["Sparse Retriever\nBM25 term frequency\nβ ranked list B"]
D --> RRF["β‘ RRF Fusion\nscore = Ξ£ weight / rank + 60\ndense_w=0.6 | bm25_w=0.4"]
S --> RRF
RRF --> OUT["12 Candidates\n(sorted by RRF score)"]
style RRF fill:#1a1a2e,stroke:#ffb347,color:#f0f0ff
style OUT fill:#1a1a2e,stroke:#4dffa6,color:#f0f0ff
RRF Formula:
score(doc) = Ξ£α΅’ weightα΅’ / (rankα΅’(doc) + 60)
The constant 60 is a smoothing factor preventing top-ranked documents from dominating. Documents appearing in both lists receive additive scores β consensus between retrieval methods naturally surfaces the best candidates.
Why hybrid matters: Dense embeddings miss exact technical terms. Ξ²β=0.98, BLEU 27.3, h=8 β these short numeric values have unpredictable embedding geometry. BM25 treats them as high-IDF tokens and ranks chunks containing them at the top regardless of semantic similarity.
flowchart TD
CANDS["12 Candidates\nfrom Stage 1"] --> CE
subgraph CE["π― Cross-Encoder Scoring"]
direction LR
P1["query + chunk_1 β score: 7.23 β
"]
P2["query + chunk_2 β score: 4.89 β
"]
P3["query + chunk_3 β score: 1.20 β
"]
P4["query + chunk_4 β score: -0.44 β
"]
P5["query + chunk_5 β score: -3.81 β"]
P6["query + chunk_6..12 β score: < -4 β"]
end
CE --> TOP4["Top 4 Chunks\n(noise eliminated)"]
TOP4 --> LLM["Llama 3 Context\n~2,000 tokens | high precision"]
style CE fill:#1a1a2e,stroke:#7c6fff,color:#f0f0ff
style TOP4 fill:#1a1a2e,stroke:#4dffa6,color:#f0f0ff
Why cross-encoder beats bi-encoder for reranking:
Bi-encoders (like all-MiniLM-L6-v2) encode query and document independently, then compare via cosine similarity. The encoder never sees both together β semantic proximity in embedding space does not guarantee answer relevance.
A cross-encoder encodes [CLS] query [SEP] document [SEP] as a single input, producing one scalar relevance score. It captures exact term overlap, co-references, and direct answer relevance that cosine similarity structurally cannot.
Noise reduction example:
| Chunk | Bi-encoder score | Cross-encoder score | Passed to LLM? |
|---|---|---|---|
| "Ξ²β=0.9, Ξ²β=0.98, Ξ΅=10β»βΉ (Adam optimizer)" | 0.71 | +7.23 | β |
| "multi-head attention mechanism overview" | 0.74 | +1.20 | β |
| "beta testing software release cycles" | 0.68 | -3.81 | β |
| "abstract: dominant sequence transduction..." | 0.65 | -4.60 | β |
The bi-encoder ranks "beta testing" higher than the optimizer chunk (0.68 vs 0.71) because "beta" shares semantic space. The cross-encoder drops it 11 points β clean context, zero noise.
The top 4 reranked chunks are formatted with source provenance labels (including rerank scores for transparency) and passed to Llama 3 via a RunnableWithMessageHistory-wrapped LCEL chain that injects conversation history automatically.
Every answer triggers a second, independent LLM call β the Judge β which receives the original question, retrieved context, and generated answer, then returns a structured evaluation.
sequenceDiagram
participant U as π€ User
participant H as HybridRetriever
participant CR as CrossEncoder
participant G as Generator LLM
participant J as Judge LLM
participant UI as Streamlit UI
U->>H: Submit query
H->>H: Dense search (k=12) + BM25 (k=12)
H->>H: RRF fusion β 12 candidates
H->>CR: Pass 12 candidates
CR->>CR: Score all 12 jointly with query
CR-->>G: Top 4 high-precision chunks
G->>G: Generate answer (SYSTEM_PROMPT grounding)
G-->>J: question + context + answer
J->>J: Score contextual support (0-100)
J->>J: Detect hallucination markers
J-->>UI: JSON evaluation payload
UI-->>U: Answer + Confidence Card + Mode Badge + Sources
{
"context_sufficient": true,
"confidence_score": 92,
"hallucination_detected": false,
"hallucination_reason": "",
"evaluation_summary": "Answer fully supported β all claims traceable to retrieved context."
}| Score | Interpretation | UI Card |
|---|---|---|
90β100 |
Every claim directly traceable to context | π’ Context Verified |
70β89 |
Mostly supported, minor inferential gaps | π’ Context Verified |
40β69 |
Partial support, some unverifiable claims | π‘ Moderate Confidence |
0β39 |
Weak or no contextual grounding | π΄ Low Confidence |
Layer 1 β Generator Self-Flagging: The SYSTEM_PROMPT instructs the LLM to prefix any outside-knowledge statement with [OUTSIDE KNOWLEDGE]. This creates an explicit, machine-readable signal in the answer text.
Layer 2 β Judge Independent Verification: The Judge LLM separately evaluates whether the answer contains claims absent from the retrieved context β regardless of any self-flagging. Two independent detection mechanisms means a hallucination must fool both the generator and judge simultaneously to pass through undetected.
Validated live result:
Query: "Who won FIFA World Cup 2026?"
Answer: "[OUTSIDE KNOWLEDGE] I couldn't find this in your knowledge base."
Judge: hallucination_detected: true | confidence_score: 0% π΄
flowchart TD
URL["π YouTube URL"] --> EXTRACT["Extract Video ID\n(v=, youtu.be, /shorts/, /embed/)"]
EXTRACT --> PLANA
subgraph PLANA["π
°οΈ Plan A β Direct Transcript API"]
API["youtube-transcript-api\nlist_transcripts()"]
FETCH["find_transcript(en)\nfetch() β Document list"]
API --> FETCH
end
subgraph PLANB["π
±οΈ Plan B β Whisper Transcription"]
DL["yt-dlp audio download\nworstaudio | 32kbps MP3"]
SIZE{"filesize\n> 24MB?"}
FFMPEG["FFmpeg re-encode\n16kHz mono | 16kbps"]
WHISPER["Groq Whisper Large v3\nTranscription API"]
CLEAN["finally block\ndelete temp files"]
DL --> SIZE
SIZE -->|"Yes"| FFMPEG --> WHISPER
SIZE -->|"No"| WHISPER
WHISPER --> CLEAN
end
PLANA -->|"β
Success"| CHUNK
PLANA -->|"β Bot detection / disabled captions"| PLANB
PLANB --> CHUNK["RecursiveCharacterTextSplitter\nchunk_size=1000 | overlap=200"]
CHUNK --> META["Attach metadata\nmethod: groq_whisper | title | source"]
META --> PINE2["Upload to Pinecone\n+ register_chunks() for BM25"]
style PLANA fill:#1a1a2e,stroke:#4dffa6,color:#f0f0ff
style PLANB fill:#1a1a2e,stroke:#ffb347,color:#f0f0ff
Key engineering decisions:
- Plan A fails silently on ~30% of videos (bot detection, disabled captions, region locks). Fallback is automatic and transparent.
- The
finallyblock in Plan B guarantees temp file deletion even if Whisper throws an exception β no disk leaks in long-running deployments. - FFmpeg re-encoding to 16kHz mono WAV keeps all audio under Groq's 24MB Whisper file size limit regardless of video length.
- After ingestion,
register_chunks()populates the in-memory BM25 corpus β both retrieval paths (dense + sparse) are fed simultaneously.
flowchart TD
START(["π΅ User Query"]) --> CHECK{"session_id\nin store{}?"}
CHECK -->|"No"| CREATE["ChatMessageHistory()\nstore[session_id] = history"]
CHECK -->|"Yes"| LOAD["Load existing history"]
LOAD --> TRIM{"len(messages)\n> 20?"}
TRIM -->|"Yes"| EVICT["Evict oldest\nkeep messages[-20:]"]
TRIM -->|"No"| INJECT
EVICT --> INJECT
CREATE --> INJECT["Build prompt\nSystemPrompt\n+ ChatHistory\n+ HumanMessage"]
INJECT --> PIPELINE["3-Stage Retrieval Pipeline\nHybrid β Rerank β Generate"]
PIPELINE --> SAVE["RunnableWithMessageHistory\nauto-saves HumanMessage + AIMessage"]
SAVE --> NEXT{"Another\nquery?"}
NEXT -->|"Yes"| CHECK
NEXT -->|"Clear"| RESET["store[session_id] = ChatMessageHistory()"]
RESET --> START
style START fill:#7c6fff,color:#fff
style RESET fill:#ff5f7e,color:#fff
style PIPELINE fill:#1a1a2e,stroke:#ffb347,color:#f0f0ff
MessagesPlaceholder(variable_name="chat_history") injects the full conversation window into every prompt, enabling the LLM to resolve pronoun references across turns.
Validated 2-turn memory test:
| Turn | Query | Resolved Reference | Confidence |
|---|---|---|---|
| 1 | "What optimizer did the authors use?" | β | 100% β |
| 2 | "What were its hyperparameters?" | its β Adam optimizer |
100% β |
| Result | Ξ²β=0.9, Ξ²β=0.98, Ξ΅=10β»βΉ returned correctly | Anaphora fully resolved | β |
Memory architecture notes:
- Window: 20 messages (10 exchanges). Older turns evicted to cap token spend.
- Store: Global
dictkeyed bysession_id. Multi-user isolated in dev. Production upgrade: Redis (RedisChatMessageHistory, drop-in). - Auto-persistence:
RunnableWithMessageHistoryhandles read-before and write-after. No manual.add_user_message()calls.
rag-knowledge-base/
βββ app.py # Streamlit UI β chat, sidebar, eval cards, mode badge
βββ ingestor.py # Multi-source loader β PDF, YouTube (Plan A/B), Notion
βββ vector_store.py # HybridRetriever, BM25, Pinecone upload/load, RRF
βββ retriever.py # 3-stage pipeline β hybrid β rerank β generate β evaluate
βββ reranker.py # CrossEncoder module β BAAI/bge-reranker-base, score caching
βββ .env # API keys (gitignored)
βββ requirements.txt # Frozen dependencies
βββ data/ # Local document storage
# 1. Clone and create virtual environment
git clone https://github.com/yourusername/neural-kb.git
cd neural-kb
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure environment
cp .env.example .env
# Add: GROQ_API_KEY, PINECONE_API_KEY, PINECONE_INDEX_NAME
# 4. Launch
streamlit run app.pyNote: On first query after ingestion,
BAAI/bge-reranker-base(~278MB) downloads automatically and caches. Subsequent queries use the cached model with ~0.8s reranking latency on CPU.
| Variable | Required | Description |
|---|---|---|
GROQ_API_KEY |
β | Groq API β LLM generation + Whisper transcription |
PINECONE_API_KEY |
β | Pinecone serverless vector store |
PINECONE_INDEX_NAME |
β | Index name (e.g. rag-knowledge-base) |
HUGGINGFACEHUB_API_TOKEN |
Optional | Only for private HF model access |
langchain>=0.3
langchain-groq
langchain-pinecone
langchain-huggingface
langchain-community
pinecone-client
sentence-transformers # CrossEncoder reranker
rank-bm25 # BM25 sparse retrieval
streamlit
python-dotenv
youtube-transcript-api
yt-dlp
LangChain's EnsembleRetriever lives in langchain.retrievers β a package not always present in constrained environments. The custom HybridRetriever(BaseRetriever) implements identical RRF fusion logic natively with zero additional dependencies. It also exposes rrf_score directly in document metadata, enabling full retrieval transparency in the UI.
cross-encoder/ms-marco-MiniLM-L-6-v2 is 67MB and fast, but trained on web search passages. BAAI/bge-reranker-base (278MB) is trained on a broader multilingual corpus including technical and scientific text β substantially better score separation on documents like research papers. Score delta between relevant and irrelevant chunks averages ~11 points vs ~6 points for ms-marco.
384-dimensional vectors β half of text-embedding-ada-002 (1536 dims) and all-mpnet-base-v2 (768 dims). At scale this is a 4x reduction in Pinecone storage and query cost with negligible quality degradation on factual retrieval tasks where the reranking stage compensates for any embedding imprecision.
Groq's LPU hardware runs Llama 3 at 250+ tokens/second vs ~40 tok/s for GPT-4o. Neural KB makes two LLM calls per query (generation + judge). At 10 queries/minute this difference is architecturally significant β the full pipeline completes in under 3 seconds end-to-end on Groq's free tier.
Factual retrieval requires deterministic, reproducible outputs. temperature=0 forces the LLM to select the maximum-likelihood token at every decoding step β eliminating stochastic variation that would produce inconsistent answers to identical queries. The evaluation judge maintains temperature=0 for the same reason: confidence scores must be stable across re-evaluation.
| Feature | Status |
|---|---|
| Hybrid BM25 + Dense Retrieval (RRF) | β Live |
| Cross-Encoder Reranking (bge-reranker-base) | β Live |
| Dual-LLM Judge + Hallucination Detection | β Live |
| Multi-Modal Ingestion (PDF, YouTube, Notion) | β Live |
| Session-Scoped Conversational Memory | β Live |
| Metadata Filtering (PDF / YouTube scoped) | β Live |
| Retrieval Mode Badge in UI | β Live |
| Enhancement | Description | Impact |
|---|---|---|
| Redis Session Store | Replace in-memory store{} with RedisChatMessageHistory |
Persistent memory across server restarts, horizontal scaling |
| Streaming Responses | chain.astream() + st.write_stream() |
Eliminate perceived latency β tokens appear in real-time |
| Async Ingestion | Celery/RQ job queue for PDF processing | Non-blocking UI β large documents process in background |
| Namespace Isolation | Pinecone namespace-per-user | True multi-tenant isolation without multiple indexes |
| Document Versioning | Differential re-ingestion via content hash | Re-embed only changed pages, not entire documents |
| Score Threshold Tuning | Expose score_threshold in UI slider |
User-controlled precision vs recall tradeoff |
Validated against "Attention Is All You Need" (Vaswani et al., 2017) β 15 pages, 79 chunks:
Pipeline: π Hybrid (BM25 + Dense) β π― Reranked (top 4) β Llama 3
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Query: "How many attention heads does the base transformer use?"
Answer: "The base model employs h = 8 parallel attention heads."
Mode: π Hybrid + π― Reranked
Score: 100% β
Context Verified
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Query: "What is the BLEU score reported for the base model?"
Answer: "The base model achieves 27.3 BLEU on WMT 2014 EN-DE."
Score: 90% β
Context Verified
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Query: "What optimizer did the authors use?"
Answer: "The authors used the Adam optimizer."
Score: 100% β
Context Verified
Query: "What were its hyperparameters?" β anaphora: its = Adam
Answer: "Ξ²β=0.9, Ξ²β=0.98, Ξ΅=10β»βΉ"
Score: 100% β
Memory resolved anaphora correctly
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Query: "Who won FIFA World Cup 2026?"
Answer: "[OUTSIDE KNOWLEDGE] I couldn't find this in your KB."
Judge: hallucination_detected: true | 0% π΄ Correctly refused
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
MIT License β see LICENSE for details.
Built with LangChain Β· Pinecone Β· Groq Β· Streamlit Β· HuggingFace Β· rank-bm25 Β· sentence-transformers
Showcasing production RAG patterns: Hybrid Search Β· Cross-Encoder Reranking Β· LLM-as-Judge Β· Conversational Memory
β Star this repo if it helped you build something better.