Skip to main content
Glama
Mily-Lv
by Mily-Lv
README.md
# RAG-MCP-SERVER

一个**可插拔、全链路可观测**的模块化 RAG 检索服务。以 MCP(Model Context Protocol)工具的形式对外暴露检索能力,可被 Claude Desktop、GitHub Copilot 等 MCP Client 直接调用。

核心设计目标是解决 RAG 工程中两个具体痛点:

1. **链路难定位** —— 检索结果不对,问题出在召回、融合还是重排?索引与查询两条链路共 10 个阶段逐阶段记录耗时、候选数、分数以及**排名变化**,Dashboard 可视化回溯。
2. **调优靠感觉** —— 换个 Embedding 模型到底变好还是变坏?Hit Rate@K / MRR 与 Ragas Faithfulness / Context Precision 联合评估,基于固定测试集做回归,用指标而非主观判断校准。

---

## 目录

- [架构总览](#架构总览)
- [核心能力](#核心能力)
- [快速开始](#快速开始)
- [配置说明](#配置说明)
- [可观测性](#可观测性)
- [评估体系](#评估体系)
- [测试](#测试)
- [项目结构](#项目结构)
- [License](#license)

---

## 架构总览

```
                    ┌──────────────────────────────────────────┐
  文档 (PDF/DOCX/    │           Ingestion Pipeline             │
  MD/TXT)      ───▶ │  load → split → transform → embed →      │
                    │  upsert                                  │
                    └────────────────┬─────────────────────────┘
                                     │  SHA256 指纹 + SQLite 摄取历史
                                     │  (文档级增量索引 / 幂等)
                                     ▼
                    ┌──────────────────────────────────────────┐
                    │   ChromaDB (Dense)  +  BM25 (Sparse)     │
                    └────────────────┬─────────────────────────┘
                                     ▼
                    ┌──────────────────────────────────────────┐
  查询          ───▶│            Query Engine                  │
                    │  query_processing → dense ┐              │
                    │                            ├→ RRF fusion │
                    │                    sparse ┘      │       │
                    │                                  ▼       │
                    │                              rerank      │
                    │                    (失败回退至 RRF 顺序) │
                    └────────────────┬─────────────────────────┘
                                     ▼
             ┌───────────────┬───────────────┬──────────────────┐
             │  MCP Server   │  CLI Scripts  │  Dashboard       │
             │  (3 tools)    │  (5 scripts)  │  (Streamlit 6页) │
             └───────────────┴───────────────┴──────────────────┘

  贯穿全程:TraceContext(trace → stage)写入 logs/traces.jsonl
```

### 可插拔底座

每个核心环节都定义了统一 Base 接口,通过 Factory + YAML 配置切换,替换组件零代码修改:

| 环节 | 接口 | 已实现的 Provider |
|---|---|---|
| LLM | `BaseLLM` | openai / azure / deepseek / kimi / ollama |
| Vision LLM | `BaseVisionLLM` | openai / azure / kimi |
| Embedding | `BaseEmbedding` | openai / azure / siliconflow / bge / ollama |
| Vector Store | `BaseVectorStore` | chroma |
| Splitter | `BaseSplitter` | recursive |
| Reranker | `BaseReranker` | llm / cross_encoder(BGE) |
| Evaluator | `BaseEvaluator` | custom / ragas / composite |
| Loader | `BaseLoader` | pdf / docx / markdown / text |

> 任何 OpenAI 兼容端点都可以走 `provider: "openai"` + 自定义 `base_url` 接入,无需新增代码。

---

## 核心能力

**混合检索**:BM25 稀疏检索负责专有名词精确匹配,Dense 向量检索负责语义匹配,双路召回后 RRF 融合,再由 Reranker 精排。重排后端失败时自动回退到 RRF 融合顺序,不会让一次超时打断整条链路。

**增量索引与幂等**:SHA256 内容指纹 + SQLite `ingestion_history` 表实现文档级增量。重复摄取直接跳过,内容变更才重建,重复摄取不产生脏数据。

**多模态**:PyMuPDF 提取 PDF 内嵌图片并保留原始位置,Vision LLM 生成图片描述缝合进 Chunk,从而复用纯文本 RAG 链路实现"搜文字出图"。MCP 响应以 `ImageContent` 返回图片。

**MCP 工具**:

| Tool | 用途 |
|---|---|
| `query_knowledge_hub` | 混合检索 + 重排,返回带引用的结果(含图片) |
| `list_collections` | 列出所有集合及文档/分块统计 |
| `get_document_summary` | 返回指定文档的摘要与分块概览 |

**Dashboard**(Streamlit 六页):系统总览 / 数据浏览 / 摄取管理 / 摄取追踪 / 查询追踪 / 评估面板。

---

## 快速开始

### 环境要求

Python ≥ 3.10。

### 安装

```bash
git clone <your-repo-url>
cd RAG-MCP-SERVER

python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux / macOS
source .venv/bin/activate

pip install -e ".[dev]"
```

> 依赖全部带版本上界。`mcp` 锁在 `<2.0`(2.x 重命名了 `CallToolResult.isError` 等字段),
> `langchain-community` 锁在 `<0.4`(0.4 移除了 `chat_models.vertexai`,会导致 ragas 导入失败)。

### 配置

```bash
cp config/settings.yaml.example config/settings.yaml
```

编辑 `config/settings.yaml` 填入自己的 API Key。**该文件已被 `.gitignore` 忽略,不要提交。**

### 摄取文档

```bash
python scripts/ingest.py --path ./your_docs --collection my_kb
python scripts/ingest.py --path ./your_docs --collection my_kb --force   # 强制重建
python scripts/ingest.py --path ./your_docs --dry-run                    # 只看会处理哪些文件
```

### 查询

```bash
python scripts/query.py -q "你的问题" -c my_kb --top-k 5 --verbose
```

`--verbose` 会打印 dense / sparse / fusion / rerank 每一步的中间结果。

### 启动 Dashboard

```bash
python scripts/start_dashboard.py
```

### 接入 MCP Client

以 Claude Desktop 为例,在 `claude_desktop_config.json` 中加入:

```json
{
  "mcpServers": {
    "rag-mcp-server": {
      "command": "<绝对路径>/.venv/Scripts/python.exe",
      "args": ["<绝对路径>/main.py"]
    }
  }
}
```

---

## 配置说明

关键配置段(完整注释见 `config/settings.yaml.example`):

```yaml
retrieval:
  dense_top_k: 20
  sparse_top_k: 20
  fusion_top_k: 10
  rrf_k: 60
  # 路由开关,用于 A/B 基线:只测 dense 则 enable_sparse: false,反之亦然
  enable_dense: true
  enable_sparse: true

rerank:
  enabled: true
  provider: "llm"           # 走已配置的 LLM,零额外依赖
  # provider: "cross_encoder"  # 本地 BGE cross-encoder,需 pip install sentence-transformers
  top_k: 5

evaluation:
  enabled: true
  provider: "composite"     # 同时跑检索指标与生成指标
  backends: ["custom", "ragas"]
  metrics: ["hit_rate", "mrr", "faithfulness", "context_precision"]
```

> `embedding.dimensions` 一旦首次摄取完成就不能再改 —— 已存在的 Chroma collection 与向量维度绑定。

---

## 可观测性

每次摄取和查询都会生成一条 trace 写入 `logs/traces.jsonl`,结构为 `trace → stages[]`,每个 stage 记录 `elapsed_ms` 与该阶段的 `data`。

| 链路 | 阶段 |
|---|---|
| Ingestion | `load` → `split` → `transform` → `embed` → `upsert` |
| Query | `query_processing` → `dense_retrieval` → `sparse_retrieval` → `fusion` → `rerank` |

### 排名变化追踪

只记录每个阶段结束后的分数列表,无法回答"这一阶段到底改善了排序吗、改善了哪个分块"。因此 `fusion` 与 `rerank` 两个阶段额外记录排名变化(`src/core/query_engine/rank_tracking.py`):

- 约定 1-based,`rank_delta = rank_before - rank_after`,**正值表示排名上升**
- `fusion` 的 `rank_before` 取该分块在双路中的**最优**排名,回答"RRF 是否把它提升到了单路召回之上";同时记录 `dense_rank` / `sparse_rank`,显示它由哪条路召回
- `rerank` 的 `rank_before` 是交给重排器的融合列表位置,精确显示重排器提升/打压了谁
- 新进入的分块上报 `None` 而非伪造的排名提升
- 阶段级汇总:`moved_up` / `moved_down` / `unchanged` / `new` / `max_gain` / `max_drop` / `dropped`

实际 trace 片段:

```
stage=fusion   elapsed=0.2ms
  rank_changes: {moved_up: 3, moved_down: 1, unchanged: 1, max_gain: 2, dropped: 18}
  rank=2  before=4  delta=+2   dense_rank=4  sparse_rank=4

stage=rerank   elapsed=12231ms
  rank_changes: {moved_up: 1, moved_down: 1, unchanged: 3, max_gain: 1}
  rank=1  before=2  delta=+1
```

Dashboard 的「查询追踪」页会把这些渲染成阶段瀑布图 + 排名变化表。

---

## 评估体系

```bash
python scripts/evaluate.py --collection my_kb
python scripts/experiment.py --variants dense,sparse,hybrid,hybrid_rerank
```

- **检索指标**(`CustomEvaluator`):Hit Rate@K、MRR —— 需要测试集提供 `expected_chunk_ids` 作为 ground truth
- **生成指标**(`RagasEvaluator`):Faithfulness、Answer Relevancy、Context Precision
- **`CompositeEvaluator`** 同时跑两类后端并合并结果;每个后端各自从共享的 `metrics` 列表中挑出属于自己的指标,单个后端失败不影响其余

`scripts/experiment.py` 用于 A/B 对比不同检索变体,输出各变体的指标与延迟,用来回答"加上 rerank 到底值不值这 12 秒"。

---

## 测试

分层测试,共 **1456** 个用例:

```bash
pytest tests/unit                      # 1298 passed, 1 skipped
pytest tests/integration -m "not llm"  #   94 passed, 10 skipped
pytest tests/e2e -m "not llm"          #   30 passed,  2 skipped
```

`-m "not llm"` 排除需要真实 LLM API 调用的用例。缺少某个 Provider 的凭证时,相关用例会 **skip 并给出原因**,而不是失败。

关键分支都有针对性覆盖:

| 关注点 | 测试 |
|---|---|
| RRF 融合 | `test_fusion_rrf.py` |
| 重排降级路径 | `test_reranker_fallback.py` |
| 幂等写入 | `test_vector_upserter_idempotency.py` |
| 排名变化追踪 | `test_rank_tracking.py` |
| 分词器索引/查询一致性 | `test_sparse_encoder.py` / `test_query_processor.py` |
| Chroma 客户端并发构建 | `test_chroma_client.py` |
| 向量存储契约 | `test_vector_store_contract.py` |

---

## 项目结构

```
src/
├── core/
│   ├── query_engine/       # 混合检索:dense / sparse / RRF fusion / rerank
│   │   └── rank_tracking.py  # 排名变化计算(融合与重排共用)
│   ├── response/           # 响应组装、引用生成、多模态拼装
│   ├── trace/              # TraceContext:trace → stage
│   ├── tokenization.py     # BM25 分词器(索引端与查询端唯一实现)
│   └── settings.py         # YAML 配置加载与校验
├── ingestion/
│   ├── chunking/ embedding/ storage/ transform/
│   ├── pipeline.py         # 五阶段摄取流水线
│   └── document_manager.py # 文档删除(跨 Chroma / BM25 / 图片 / 摄取历史)
├── libs/                   # 可插拔底座:base_*.py + *_factory.py
│   ├── llm/ embedding/ loader/ reranker/ splitter/ vector_store/ evaluator/
├── mcp_server/             # MCP 协议与 3 个 Tool
└── observability/
    ├── dashboard/          # Streamlit 六页
    └── evaluation/         # ragas / composite / eval_runner

scripts/   ingest / query / evaluate / experiment / start_dashboard
config/    settings.yaml.example + prompts/
tests/     unit / integration / e2e
```

> `data/`(Chroma、BM25 索引、抽取出的图片、摄取历史)与 `logs/`(trace)都是运行时
> 生成的本地产物,已被 `.gitignore` 忽略,不随仓库分发;首次运行时会自动创建。

---

## License

[MIT](LICENSE)