# DocSummarizer 部件 : @ 5 @ @ MPK1 @ 清晰的RAG}% MS K2 多-Document RAG Web 应用程序

<!--category-- AI, LLM, RAG, C#, HTMX, GraphRAG, Semantic Search, DuckDB -->
<datetime class="hidden">2026-01-01T18:00</datetime>

这是 **部分#5** 和它也是世界顶峰 [图图RAG系列](/blog/graphrag-minimum-viable-implementation) 和 [语义搜索系列](/blog/semantic-search-with-onnx-and-qdrant)@._We'}将所有内容合并成可部署的网络应用程序@.#

> “.”将会在LawryraGQ.com .I''在这里添加截图,

> **建立RAG基础设施的目的就是要用它来做一些真实的东西**

在过去几周内,我们建造了:

- **DocSummamer 缩写器** -文档解析@,语义填充 @,ONNX嵌入
- **图图RAG** “-实体提取”“,知识图 ,社区探测”
- **语义搜索** -BMZK1+BERT混合搜索与RRF聚变

现在我们把他们联合起来 **更清晰** - @ a 独立的网络应用程序, 用于多@ MS K1_ document problem question 回答知识图解可视化的答题@.}

**网站#:** [清晰度@ . @ com](https://lucidrag.com) | **资料来源::** [吉特胡布](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid.RagDocuments)

[TOC]

## 何为清晰的RAG

上传文档@.问问题 @.以引用获得答案,并有一个显示概念如何连接\.的知识图表。

**密钥特性 @:**

- **多@ - @ document 上传** 拖动@ - @ 和 @ MS K1}% 跳
- **RAG 代理RAG** -查询分解和自定义=-校正=@(,,在结构上的确定性{MSK4}单要求生命周期_)
- **知识图图可视化** - 见实体关系
- **证据观点** @-_ 句子-}来源引用
- **独立部署** - 單一執行檔或 docker

**设计限制 :**

- 索引编制不依赖云
- 磁性预处理@(_chunking @,}嵌入,}实体提取=)}
- 可重建的矢量状态
- 使用的LLM *仅* 供回答合成检索过的证据

> 在任何关卡中都从未使用 LLMs 来填块@, @ 嵌入\ ,} 实体提取, @ 或存储 *-{ 仅用于对已取回的回答进行合成=%,# 引用_ MS K5 @ 支持的证据=.}

## 为何合并矢量搜索@+知识图表 @?#

某些查询类型的矢量单搜索分类@: @%

|查询类型 @|矢量搜索问题@|图表解答 *|
|------------|----------------------|----------------|
@"#X 和Y?"}{|_BARBAR_Cross@-}文件=|Q{MSK3} X有什么关系?
“|”实体“-”核心“MS K2” ,“"” 那么“Docker @?"” 来自实体“|”的“I|”图表翻转情况如何?
全球摘要 @|#_"}主題 @MS K3> @|社区探测@|#

清晰的RAG 使用两个“:”矢量来精确@,}图表用于上下文 @.}图表查询是深度\-}<(>max {2}{Hops)},并用于检索文档的范围,以防止大型子公司无限制地穿行*.}

## 建筑结构概览

应用三层三项目 we'_#已经建立 ,}{通过 **[斯泰罗花](/blog/styloflow-signal-driven-workflows)** - 信号- 驱动的工作流程引擎:

```
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
```

**为什么斯泰洛花 ?** 而不是硬编码的输油管,}每个处理阶段都是一个Q"}波浪•"}当触发条件与允许平行执行的“,”相匹配时,发出信号{.〞波跑 [StyloFlow :信号-Driven工作流管弦化](/blog/styloflow-signal-driven-workflows) 执行细节@. @%

## 处理管道

当您上传文档时 ,}它会流过三个阶段\ OMK1

### 阶段 @ 1: 上传和排队

上传端点验证文件@ , @ 计算一个内容散列, 用于解码\ MS K1} @ 并排队用于背景处理@ MPK2

```csharp
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
```

关键洞察@: @ we hash first @ , @ save later\ .} 这防止在重复上传中浪费处理时间 @ MPK3 @

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

### 阶段2:踢和嵌入

背景处理器会通过 DocSummarizer {:}来抓取排队文档并运行它们。

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

此单行可做很多工作 @ (_ See [DocSummarizer 部件 *\ 1 {](/blog/building-a-document-summarizer-with-rag)):

- 分析文档结构 *{(_PDF,} DOCX} , Markdown@)
- 按标题划分成语义块
- 生成每个块块的ONNX嵌入
- 使用 HNSW 索引化将矢量存储在 DuckDB 中

### 实体采掘阶段

在块积后, 我们使用 GraphraG' 来提取实体@ : @

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

这使用了以色列国防军的评分和结构信号,而不是使用任何-chunk LLM电话@-见。 [图表( G) 部分 @ 2 @%](/blog/graphrag-minimum-viable-implementation) 详细描述@. @%

## 后压带宽通道

一个天真的执行将会使用无约束的队列@ , @ 冒着在上传洪涝期间发生崩溃的风险\ MS K1_ @ - @ momory crash

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

当队列填满时 @ , @ {} `Wait` 新区块模式新写到打开空间@. @ we 添加一个超时, 这样用户就有一个清晰的错误, 而不是挂上@ :} @

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

## 佩尔-文件超时

大文档处理@ . @ but a stuck 文档应该'}%t 屏蔽整个队列\ MS K2每份文件都有其自身的超时=:}

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

链接的标记确保了我们仍然尊重应用程序关闭, 同时添加 per-\ document 限制@ . {}

## 进度频道清理

每个处理文档都有一个 SSE 更新的进度通道@.}但是如果用户关闭浏览器中"-\upload @,", 这个频道就会成为孤儿{.} 我们跟踪创建时间并定期清理:}

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

AA `PeriodicTimer` 在背景处理器中每分钟 15 @ minutes

## 存储@: @ duckDB @ + @ postgreSQL @ /_ SQLite

我们为不同目的使用两个数据库 :

**PostgreSQL/SQ莱特 @(EF 核心@)** 文档存储元数据 @ -}\ MS K1\ 处理状态@ ,\ 关系 @ I. 此数据是耐久和可查询的@ MPK4}

**鸭子数据库** 存储矢量和实体图形@. @ it'_%s epheperal *-}您可以从源文档中重建它 < . > 此分隔表示矢量仓库腐败}{'不会丢失您的文件清册>.}

```csharp
// 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)
```

## 聊天 API

问题通过毒气搜索管道流出

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

搜索服务在需要时处理查询分解@,}然后合成一个回答\:

```csharp
    // 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
    });
}
```

## 阿尔卑斯山 .日

用户界面是一个单页,左侧为文档 *,}右侧的聊天时间*:}

```
┌──────────────────┬─────────────────────────────────────┐
│  📁 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 管理 stateMS K1HTMX 处理文档列表更新@:

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

## 演示模式

对于诸如 Llearrag.com,示范模式等公共部署, 禁用上传并使用 pre@ -loaded 内容@.Demo 模式使公共部署安全@ MPK4 确定性 @,, 没有特殊@ I-case 代码路径@MS K7\}

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

AA `DemoContentSeeder` 背景服务 监视内容目录并处理任何已丢弃的文件@: @%

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

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

这样您就可以仅通过复制文件 {-} 来更新演示内容。 没有需要的重新启动@.}fn微软雅黑

## 运行清晰

### 无依赖性 **

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

使用 SQLite @ + @ duckDB 本地端@ MS K1 打开 `http://localhost:5080`.

### 嵌入器

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

## 实际运行的

| 目的 @|
|-----------|--------|---------|
| 文档解析 @| DocSummarizer @MS K2 PDF, DOCX}, 标记@|
|ONNX 嵌入@| DocSummarizer @| 本地},没有 API 键 *|
I|实体提取|G MS K2IFD+结构信号MSKO4
“| 混合搜索”“|”都“MS K2B25++BERT与RF|”
|Async 处理 @|新的#|Bounded croute+,超时@|
@| @WebUI#| @New |_HTMX@+}Alpine @.js @MS K5 @

### 费用成本

**零APPI费用** 您在查询时间只支付LLM合成费@,}和本地Ollama. 的工作单位。

## 相关条款

- [DocSummarizer 部件 @ 1 @ @ MPK1 @ 建筑](/blog/building-a-document-summarizer-with-rag)
- [DocSummarizer 部件 *% 1 4 * _ MS K1 * RAG 管道](/blog/docsummarizer-rag-pipeline)
- [图图RAG 部分 @ 2 @ @ MPK1 @ 执行](/blog/graphrag-minimum-viable-implementation)
- [用 ONNX 搜索语义](/blog/semantic-search-with-onnx-and-qdrant)