DocSummarizer Part 5 - lucidRAGM SK2 Multi-Document RAG Web Application (Français (French))

DocSummarizer Part 5 - lucidRAGM SK2 Multi-Document RAG Web Application

Thursday, 01 January 2026

//

10 minute read

Il s'agit de Partie 5 de la série DocSummarizer, et elleM SK1 est également le sommet du GraphRAG et Série de recherche sémantique. NousM SK1combinons tout en une application Web déployable.

🚨🚨 ARTICLE DE PREVIEW 🚨🚨 Je travaille encore sur quelques kinks et j’ajouterai des fonctionnalitésM SK2 Mais le noyau est bien fait et fonctionne bien. J’attends les mises à jour au cours des prochaines semainesMSC4 Il sera à lucidRAGMST5comMst6 J'ajouterais ici des screenshots une fois que je terminerai la conceptionM st8

Le but de la construction d’infrastructures RAG est de les utiliser pour quelque chose réel.

Au cours des dernières semaines, nous avons construit

  • DocSummarizer - Parsage de documents, fragmentation sémantique , Emplacements ONNX
  • GraphRAG - Extraction d'entitésM SK1 Graphiques de connaissances, Détection communautaire
  • Recherche sémantique Recherche hybride BERT avec fusion RRF

Maintenant, nous les connectons à lucidRAG - une application Web autonome pour répondre à des questions de documents multiples avec la visualisation d'un graphique de connaissances .

Site Web: lucidrag.com | Source: GitHub

Que fait lucidRAG

Uploader les documents. poser des questionsM SK1 Obtenir des réponses avec des citations et un graphique de connaissance montrant comment les concepts se connectent.

Principales caractéristiques:

  • Multi- téléchargement de documents avec le drag-etM SK1droppe
  • RAG Agentique - Décomposition de la demande et autodestruction de celle-ciM SK1correction déterministe dans la structure, cycle de vie d’une seule demandeMSC5
  • Visualisation du graphique des connaissances - Voir les relations des entités
  • Vue des preuves - Sentence-sources citatives de niveau
  • Déploiement autonome - Unité exécutable ou Docker

Constraints de conception:

  • Aucune dépendance cloud pour l'indexation
  • Prétraitement déterministe (chunking, emplacementM SK2 extraction d'entités )
  • État de vecteur reconstruitable à partir des documents source
  • LLMs utilisés seulement pour la synthèse des réponses sur les preuves récupérées

À aucun moment, les LLM ne sont utilisés pour la mise en bloc, embedding, extraction de l’entitéM SK2 ou stockage - uniquement pour synthétiser des réponses sur celles qui ont été retiréesMska4 citationM Ska5 preuves corroborées Mska6

Pourquoi combiner la recherche par vecteur + Graphiques de connaissances?

La recherche par vecteur ne fonctionne que pour certains types de demandes:

Type de demande Problème de recherche par vecteur Résolution des graphiques M
CrossM SK1document "Comment X se rapporte-t-il à Y?
EntitéM SK1centrique "Qu'en est-il de Docker?" | Traversement d'un graphe à partir d'une entité ?
Sommaires globaux MSC2Thèmes principauxMSC3 4 Détection communautaire

lucidRAG utilise à la fois les vecteurs: pour l'exactitudeM SK1 les graphiques pour le contexte . Les demandes de graphe sont fondées sur la profondeur-limitée MNK4max MNK5 hopsMMK6 et portées aux documents récupérés afin d'empêcher une traversation sans limites sur des grandes corporationsMRK7

Aperçu de l'architecture

L’application couche trois projets que nous avons déjà construits, ' et ,. StyloFlow - un signal - moteur de travail dirigé:

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

Pourquoi StyloFlow? Au lieu de pipelines hardcoded, chaque étape de traitement est un "onde" qui émet des signauxM SK3 Les ondes fonctionnent lorsque leurs conditions de déclenchement correspondent à celles de l’exécution parallèle. StyloFlow:Signal-Orchestrement du flux de travail dirigé pour plus de détails sur la mise en œuvre.

Le pipeline de traitement

Lorsque vous téléchargez un document, il passe par trois étapes :

Étape 1: Déchargement et enchaînement

Le point final du téléchargement valide le fichier, calcule un hash de contenu pour la déduplication, et l'échelonne pour le traitement arrière-plan :

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

La clé de l’incertitude: on achemine d’abordM SK1 on sauvegarde plus tard. Cela évite le gaspillage du temps de traitement lors des téléchargements répétés .

    // 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;
}

Étape 2: Configuration et emplacement

Le processeur arrière-plan prend les documents en queue et les lance par DocSummarizer:

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

Cette ligne unique fait beaucoup de travail (voir DocSummarizer Part 1):

  • Parsez la structure du document (PDF, DOCXM SK2 Markdown )
  • Split into semantic chunks respecting headings
  • Générer des embeddings ONNX pour chaque tronçon
  • Entreposer les vecteurs dans DuckDB avec indexation HNSW

Étape 3: Extraction des entités

Après la fragmentation,, nous extraisons des entités en utilisant l’approche héuristique de GraphRAG'

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

Cette méthode utilise les cotes IDF et les signaux structurels plutôt que par les appels -chunk LLM - voir GraphRAG Partie 2 pour plus de détails.

Voies limitées pour la pression arrière

Une mise en oeuvre naïve utiliserait des queues non limitées, risqueant d’être détournéesM SK1de -craques de mémoire pendant les inondations d’uploads. Nous utilisons des canaux limités avec des limites explicites de capacitéMSC4

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

Quand la liste est remplie, Wait mode bloque les nouveaux écritures jusqu’à ce que l’espace soit ouvert. Nous ajoutons un délai pour que les utilisateurs obtiennent une erreur claire au lieu d’en suspendre

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-Excès de temps des documents

Les documents de grande taille peuvent prendre quelques minutes à traiter. Mais un document bloqué ne devrait pas bloquer l’ensemble de la queue . Chaque document a sa propre période d’interruption

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");
    }
}

Le symbole relié nous garantit que nous respectons toujours la fermeture de l'application tout en ajoutant la limite du document per-.

Épuration des canaux de progrès

Chaque document de traitement reçoit un canal d’avancement pour les mises à jour SSE. Mais si l’utilisateur ferme son navigateur à la mi-parcoursMSC1upload, ce canal devient orphelinM SK3 Nous suivons le temps de création et nous nettoyons périodiquement :

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;
}

A PeriodicTimer appele cela tous les 15 minutes dans le processeur de fond

Entreposage: DuckDB + PostgreSQLM SK2SQLite

Nous utilisons deux bases de données à des fins différentes:

PostgreSQL/SQLite (EF CoreM SK2 stocke les métadonnées du document - ce qui existeM SK1 le statut de traitement , les relations. Ces données sont durables et vérifiables

DuckDB stores les vecteurs et le graphique de l'entité. Il ' est éphémère M SK2 vous pouvez le reconstruire à partir des documents source. Cette séparation signifie que la corruption du stockage des vecteurs ne se produit pas

// 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 de chat

Les questions s’écoulent dans le pipeline de recherche agentique:

[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
    });

Le service de recherche traite la décomposition des demandes si nécessaire, puis synthétise une réponse:

    // 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'interface est une seule page avec des documents à gauche, chat à droite:

┌──────────────────┬─────────────────────────────────────┐
│  📁 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 gère l'étatM SK1 HTMX gère les mises à jour de la liste des documents:

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;
        }
    };
}

Mode de démonstration

Pour des déploiements publics comme lucidrag.com, mode de démonstration désactive les téléchargements et utilise le contenu pré-achevéM SK2 . Le mode de démonstration existe pour rendre les déploiements publics sécuritaires

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";
}

A DemoContentSeeder service arrière-plan surveille le répertoire de contenu et traite les fichiers sortants:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    if (!_config.DemoMode.Enabled) return;

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

Cela vous permet de mettre à jour le contenu de la démonstration en copient simplement les fichiers - aucun restart n'est nécessaire.

Activation de lucidRAG

Standalone (Sans dépendance)

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

Utilise SQLite + DuckDB localement. Ouvrir http://localhost:5080.

Docker

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

Ce qui fonctionne réellement

Composante Source MSC2 But MSC3
Parsage des documents DocSummarizer PDFM SK3 DOCX, Marquage ≥
Intégrations ONNX DocSummarizer
Extraction d'entités GraphRAG IDF M+ Signaux structurels P
Recherche hybride Les deux MSC2 BMMSC3 SMC4 BERT avec RRF S
Traitement asynchrone Nouveaux CNS2 Canaux limitésCns3 Interruptions de temps DNS4
L'interface Web Nouveau HTMX + AlpineMSC4js S

Coût

Aucun coût de l'API pour l'indexation - les embeddings sont ONNX, les entités sont héuristesM SK2 vous payez seulement la synthèse de LLM au moment de la demande , et cela fonctionne avec Ollama localMSC4

Articles connexes

Finding related posts...
logo

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