rag-mcp-server
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)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues