DocSummarizer Part 5 - lucidRAG: MultiM SK3Document RAG Web Application (Italiano (Italian))

DocSummarizer Part 5 - lucidRAG: MultiM SK3Document RAG Web Application

Thursday, 01 January 2026

//

10 minute read

Questo Γ¨ Parte 5 della serie DocSummarizer, ed Γ¨ anche la culminazione del Serie GraphRAG e Serie di ricerche semantiche. Noi' stiamo combinando tutto in un'applicazione web deployabile.

🚨🚨 ARTICLE DI PREVIEW 🚨🚨 Stiamo ancora lavorando su alcuni kinks e aggiungendo delle caratteristicheM SK2 Ma il cuore è finito e funziona bene. Ci aspettiamo delle nuove versioni nei prossimi settimaneMSC4 Sarà al lucidRAGMST5comMst6 IoMSt7 Aggiungerò screenshot qui una volta che ho tirato fuori il designMSST8

L'intero obiettivo di costruire infrastrutture RAG Γ¨ usarle per qualcosa di reale.

Nelle ultime settimane abbiamo costruito

  • DocSummarizer - Percezione del documento, blocco semanticoM SK2 Inserzioni ONNX
  • GraphRAG - Extrazione delle entitΓ M SK1 grafici di conoscenza, rilevamento della comunitΓ 
  • La ricerca semantica - BMM SK1 + Ricerca ibrida BERT con fusione RRF

Ora li mettiamo insieme. lucidRAG - un'applicazione web standalone per la risposta a domande multi-documenti con il grafico della conoscenza.

Il sito web: lucidrag.com | Source: GitHub

Cosa fa lucidRAG?

Lavorare i documenti. Chiedere domande. Ricevere risposte con citazioni e un grafico della conoscenza che mostra come i concetti si connettono.

Principali aspetti:

  • La carica di un documento multi- con il drag-and-drop
  • Agentic RAG - Decomposizione della domanda e auto-determinazioneM SK1Corrigione dipendente, determinista nella strutturaMSC4 singolo ciclo di vita della richiestaMST5
  • Visualizzazione del grafico della conoscenza - Vedete le relazioni delle entitΓ 
  • Il punto di vista delle prove - Sentenza-citatione di sorgenti a livello
  • Dispiegamento autonomo - Single executable or Docker

Contrattivi di design:

  • Nessuna dipendenza dalla nuvola per l'indexazione.
  • Preprocesso deterministico (chunking
  • Lo stato del vettore ricostruibile dai documenti sorgenti
  • LLM usati Solo per la sintesi delle risposte sulle prove rilevate.

In nessun momento i LLM sono usati per un blocco, embedding, extrazione di entitΓ M SK2 o immagazzinamento - solo per sintetizzare le risposte su quelle che vengono ricavateMSC4 citazioniMST5 prove supportateMst6

PerchΓ© combinare la ricerca dei vettori + Grafi di conoscenza?

La ricerca a vector solo si rompe per alcuni tipi di query:

Tipo di domanda Problema di ricerca dei vectori Risolto grafico
CrossM SK1documento SSK3Come X si riferisce a Y?"
EntitΓ M SK1centrica "Che ne dite del Docker?" | Tracce grafiche dall'entitΓ  SSK6
Sommari globali "Tema principaleM SK3 Detezione nella comunitΓ  β™«

lucidRAG usa entrambi i vektori: per la precisione, grafici per il contestoM SK2 Le domande di grafico sono profonditΓ MSC3limitata MST4max MST5 hopsM ST6 e scopata per i documenti recuperati per prevenire il traversamento senza limiti su grandi corporaMSST7

Architecture Overview

L'app stratifica tre progetti che abbiamo giΓ  costruito, ' , orchestrati tramite StyloFlow - un segnale - motore di flusso di lavoro guidato:

lucidRAG
β”œβ”€β”€ Controllers/Api/    # REST endpoints
β”œβ”€β”€ Services/           # Business logic
β”‚   β”œβ”€β”€ DocumentProcessingService   # Wraps DocSummarizer
β”‚   β”œβ”€β”€ EntityGraphService          # Wraps GraphRAG
β”‚   └── Background/                 # Async queue processing (StyloFlow waves)
└── Views/              # HTMX + Alpine.js UI

PerchΓ© StyloFlow? Invece di pipelines codificati, ogni fase di processo Γ¨ una M SK1onda" che emette segnaliMSC3 Le onde funzionano quando le condizioni di attenuazione corrispondono a quelle di un'altra , permettendo l'esecuzione in parallelo β™«( inserendo β™« + estrazione delle entitΓ  β™« StyloFlow: Signal-Orchestrazione del flusso di lavoro guidato per i dettagli di applicazione.

Il tubo di trattamento

Quando si carica un documento, si passa attraverso tre fasi:

Livello 1: La carica e la fila

Il punto finale dell'upload valida il file, calcola un hash del contenuto per la deduplicazione, e lo rinchiusi per il processo di fondoM SK2

public async Task<Guid> QueueDocumentAsync(Stream fileStream, string fileName)
{
    // Compute hash to detect duplicates
    var contentHash = ComputeHash(fileStream);

    var existing = await _db.Documents
        .FirstOrDefaultAsync(d => d.ContentHash == contentHash);
    if (existing != null)
        return existing.Id; // Already processed

L'intuizione chiave: lo scartamo prima, lo risparmiamo piΓΉ tardiM SK2 Questo impedisce di sprecare il tempo di processamento per i ricavi duplicatiMSC3

    // Save to disk, create DB record
    var docId = Guid.NewGuid();
    await SaveFileToDiskAsync(fileStream, docId, fileName);

    // Queue for background processing
    await _queue.EnqueueAsync(new DocumentProcessingJob(docId, filePath));

    return docId;
}

Livello 2: Cambiamento e inserzione

Il processore di fondo raccoglie i documenti in fila e li fa passare attraverso DocSummarizer:

var result = await _summarizer.SummarizeFileAsync(job.FilePath, progressChannel);

Questa singola linea fa molto lavoro (see DocSummarizer Part 1):

  • Parlare la struttura del documento (PDF, DOCXM SK2 Markdown )
  • Dividere in blocchi semantici rispetto alle sedi.
  • Generano inserzioni ONNX per ogni frammento.
  • Gestire i vettori in DuckDB con l'indexazione HNSW

Stage 3: Extrazione delle entitΓ 

Dopo la frammentazione, estragiamo le entitΓ  usando l'approccio heuristico di GraphRAG'

var segments = await _vectorStore.GetDocumentSegmentsAsync(documentId);
var entityResult = await _entityGraph.ExtractAndStoreEntitiesAsync(documentId, segments);

Si usa il punteggio IDF e i segnali strutturali piuttosto che per le chiamate -chunk LLM - vedete. GraphRAG Part 2 per i dettagli.

I canali confinati per la pressione posteriore

Un'implementazione naiva avrebbe usato quei filamenti senza limiti, rischiando di uscire-diM SK2cassamenti della memoria durante le inondazioni di caricamento . Usiamo canali confinati con limiti di capacitΓ  esplicitiMSC4

private readonly Channel<DocumentProcessingJob> _queue =
    Channel.CreateBounded<DocumentProcessingJob>(new BoundedChannelOptions(100)
    {
        FullMode = BoundedChannelFullMode.Wait
    });

Quando la fila si riempie, Wait il modo blocca i nuovi scritti finché non si apre lo spazio. aggiungiamo un'interruzione di tempo così che gli utenti ricevono un errore chiaro invece di appendere:

using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(TimeSpan.FromMinutes(5));

try {
    await _queue.Writer.WriteAsync(job, timeoutCts.Token);
} catch (OperationCanceledException) when (!ct.IsCancellationRequested) {
    throw new InvalidOperationException("Queue full. Try again later.");
}

Per-Temperature di Documentazione

I grandi documenti possono impiegare pochi minuti per essere processati. Ma un documento bloccato non dovrebbe' bloccare l'intera filaM SK2 Ogni documento ha la propria interruzione di tempo.

while (!stoppingToken.IsCancellationRequested)
{
    var job = await _queue.DequeueAsync(stoppingToken);

    // 30-minute timeout per document
    using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
    timeoutCts.CancelAfter(TimeSpan.FromMinutes(30));

    try {
        await ProcessDocumentAsync(job, timeoutCts.Token);
    } catch (OperationCanceledException) when (!stoppingToken.IsCancellationRequested) {
        await MarkDocumentFailedAsync(job.DocumentId, "Processing timed out");
    }
}

Il token collegato ci assicura di continuare a rispettare la fine dell'applicazione aggiungendo il limite per-document.

Il ripulimento del canale Progress

Ogni documento di processazione riceve un canale di progresso per le SSE aggiornazioni. Ma se l'utente chiude il suo browser a metΓ M SK1upload, quel canale diventa orfano . Seguiamo i tempi della creazione e puliamo periodicamenteMSC4

private readonly ConcurrentDictionary<Guid, ProgressChannelEntry> _progressChannels = new();

public int CleanupAbandonedChannels()
{
    var cutoff = DateTimeOffset.UtcNow - TimeSpan.FromHours(1);
    var cleaned = 0;

    foreach (var kvp in _progressChannels.Where(x => x.Value.CreatedAt < cutoff))
    {
        if (_progressChannels.TryRemove(kvp.Key, out var entry))
        {
            entry.Channel.Writer.TryComplete();
            cleaned++;
        }
    }
    return cleaned;
}
  1. PeriodicTimer lo chiama ogni 15 minuto nel processore di fondo

Storage: DuckDB + PostgreSQLM SK2SQLite

Usiamo due database per scopi diversi:

PostgreSQL/SQLite (EF CoreM SK2 archivia i metadati del documento - ciΓ² che esiste, lo stato di processoM SK2 le relazioni . Questi dati sono durabili e rintracciabiliMSC4

DuckDB conserva i vettori e il grafico dell'entitΓ . E' l'efemera M SK2 lo si puΓ² ricostruire dai documenti di sorgente . Questa separazione significa che la corruzione del magazzino dei vettori non uccide l'inventario dei documentiMSC5

// Metadata in PostgreSQL
public class DocumentEntity
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string ContentHash { get; set; }
    public DocumentStatus Status { get; set; }
}

// Vectors in DuckDB (managed by DocSummarizer)
// Entities in DuckDB (managed by GraphRAG)

L'API del Chat

Le domande passano attraverso la catena di ricerca agentica:

[HttpPost]
public async Task<IActionResult> ChatAsync([FromBody] ChatRequest request)
{
    // 1. Get or create conversation for memory
    var conversation = await GetOrCreateConversationAsync(request.ConversationId);

    // 2. Search with hybrid retrieval
    var searchResult = await _search.SearchAsync(request.Query, new SearchOptions
    {
        TopK = 10,
        IncludeGraphData = request.IncludeGraphData
    });

Il servizio di ricerca gestisce la decomposizione della domanda se necessario, poi sintetizza una risposta:

    // 3. Generate answer with LLM
    var answer = await _summarizer.SummarizeAsync(
        request.Query,
        searchResult.Segments,
        new SummarizeOptions { IncludeCitations = true });

    // 4. Save to conversation history
    await SaveToConversationAsync(conversation.Id, request.Query, answer);

    return Ok(new ChatResponse
    {
        Answer = answer.Text,
        Sources = answer.Citations,
        GraphData = searchResult.GraphData
    });
}

L'UI: HTMX + AlpineM SK2js

L'interfaccia Γ¨ una singola pagina con i documenti sulla sinistra, chat sulla destra:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  πŸ“ Documents    β”‚  πŸ’¬ Chat                            β”‚
β”‚  ─────────────   β”‚  [Answer] [Evidence] [Graph]       β”‚
β”‚  [+ Upload]      β”‚                                     β”‚
β”‚  πŸ“„ api-docs.pdf β”‚  Q: How does auth work?            β”‚
β”‚  πŸ“ readme.md    β”‚  A: JWT tokens stored... [1][2]    β”‚
β”‚  ─────────────   β”‚                                     β”‚
β”‚  πŸ•ΈοΈ Graph: 168   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚                  β”‚  β”‚ Ask about your documents... β”‚   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”˜

Alpine.js gestisce lo stato; HTMX gestisce l'update della lista dei documentiM SK2

function ragApp() {
    return {
        messages: [],
        isTyping: false,

        async sendMessage() {
            const query = this.currentMessage.trim();
            this.messages.push({ role: 'user', content: query });
            this.isTyping = true;

            const result = await fetch('/api/chat', {
                method: 'POST',
                body: JSON.stringify({ query })
            }).then(r => r.json());

            this.messages.push({
                role: 'assistant',
                content: result.answer,
                sources: result.sources
            });
            this.isTyping = false;
        }
    };
}

Il modo di dimostrazione

Per i deploimenti pubblici come lucidrag.com, il modo di dimostrazione disabilita l'upload e usa preM SK2 contenuti caricatiMSC3 Il modo di demo esiste per rendere le deploiezioni pubbliche sicure , deterministiceMST5 ed economiche senza codici speciali

public class DemoModeConfig
{
    public bool Enabled { get; set; } = false;
    public string ContentPath { get; set; } = "./demo-content";
    public string BannerMessage { get; set; } = "Demo Mode: Pre-loaded RAG articles";
}
  1. DemoContentSeeder Il servizio di sfondo guarda il directorio del contenuto e processa tutti i file abbandonati:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    if (!_config.DemoMode.Enabled) return;

    await SeedExistingContentAsync();
    StartFileWatcher(_config.DemoMode.ContentPath);
}

Questo vi permette di aggiornare il contenuto della dimostrazione semplicemente copiando i file - non Γ¨ necessario ricominciare

Il lucidRAG in funzione

Standalone (No Dependencies)

dotnet run --project Mostlylucid.RagDocuments -- --standalone

Usa SQLite + DuckDB locally. Open http://localhost:5080.

Docker

services:
  lucidrag:
    build: .
    ports: ["5080:8080"]
    depends_on: [postgres, ollama]

Cosa funziona davvero?

Componente
Percezione del documento DocSummarizer PDFM SK3 DOCX, Markdown
Inserzioni ONNX DocSummarizer
Extrazione delle entità GraphRAG IDF МSK3 Signali strutturali
Ricerca ibrida Entrambi SSK2 BMM SK3 + BERT con RRF S
Trattamento asynco Nuovi Canali confinati, Interruzioni temporali
Interfaccia web Nuovo HTMX M+ Alpine.js P

Costo

Zero Costi di API per l'indexazione - gli inserimenti sono ONNXM SK1 le entitΓ  sono heuristice. pagate solo la sintesi LLM al momento della domandaMSC3 e questo funziona con il locale Ollama

Article connessi

Finding related posts...
logo

© 2026 Scott Galloway β€” Unlicense β€” All content and source code on this site is free to use, copy, modify, and sell.