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
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
Lavorare i documenti. Chiedere domande. Ricevere risposte con citazioni e un grafico della conoscenza che mostra come i concetti si connettono.
Principali aspetti:
Contrattivi di design:
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
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
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.
Quando si carica un documento, si passa attraverso tre fasi:
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;
}
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):
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.
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.");
}
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.
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;
}
PeriodicTimer lo chiama ogni 15 minuto nel processore di fondoUsiamo 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)
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'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;
}
};
}
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";
}
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
dotnet run --project Mostlylucid.RagDocuments -- --standalone
Usa SQLite + DuckDB locally. Open http://localhost:5080.
services:
lucidrag:
build: .
ports: ["5080:8080"]
depends_on: [postgres, ollama]
| 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 |
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
© 2026 Scott Galloway β Unlicense β All content and source code on this site is free to use, copy, modify, and sell.