DocSummarizer Part 5 - lucidRAGM SK2 Multi-Document RAG Web Application
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