内容
<p align="center">
<img src="logo.png" alt="A-RAG-OG" width="220">
</p>
<h1 align="center">A-RAG-OG</h1>
<p align="center">
<b>自主RAG,离网</b> — 一个用于OpenWebUI的自主托管混合RAG <b>MCP服务器</b>:<br>
结构感知索引、混合检索和交叉编码器重新排序,作为LLM代理驱动的工具暴露。
</p>
---
## ✨ 它是什么
A-RAG-OG将文档索引到一个单一的混合(稠密+稀疏)Qdrant存储中,并将**检索作为MCP工具**暴露。代理位于**OpenWebUI**中 - 它的模型决定调用哪个工具,分多轮搜索,读取有希望的块并确定其答案。该服务保持一个薄的,无状态的检索层。
| 部分 | 描述 |
|------|-------------|
| 🧩 **混合分块器** | 文档标记和结构感知的分块,标题路径前置(上下文) |
| 🏷️ **LLM丰富** | 每个块:上下文,关键词,假设问题,实体,日期 |
| 🔄 **混合检索** | 稠密(bge-m3)+ 稀疏(BM25),一个Qdrant存储 |
| 📑 **重新排序** | bge-reranker-v2-m3交叉编码器,`top_k`调用前/后 |
| 🔌 **MCP服务器** | FastMCP(可流式HTTP)+ OpenWebUI JWT身份验证 |
| 🤖 **代理** | OpenWebUI的模型协调工具(A-RAG风格) |
---
## 🧰 MCP工具
| 工具 | 目的 |
|------|---------|
| `keyword_and_semantic_search(query, top_k_before, top_k_after)` | **默认** — 稠密+稀疏,由重新排序器融合 |
| `semantic_search(query, top_k_before, top_k_after)` | 稠密检索(按含义)+ 重新排序 |
| `keyword_search(query, top_k_before, top_k_after)` | 稀疏/BM25检索(精确术语)+ 重新排序 |
| `filtered_search(query, keywords, entities, content_types, date_from, date_to, …)` | 混合(稠密+稀疏)+ 元数据过滤 + 重新排序 |
| `find_related(chunk_ids, query, …)` | 关联多跳 — 更多块共享命中实体的块 |
| `read_chunk(chunk_ids)` | 按ID读取块的完整内容 |
| `read_neighbors(chunk_ids, window)` | 读取命中周围的块的完整内容(文档顺序) |
每个搜索返回块ID + 片段;代理使用`read_chunk`读取完整块或使用`read_neighbors`拉取周围上下文。
---
## 🚀 快速开始
```bash
cp .env.example .env
# 后续服务(Qdrant,MinIO,Docling转换器)
docker compose --profile docling up -d
# 索引文档(独立:上传到MinIO + 构建存储)
uv run python index.py path/to/doc1.pdf path/to/doc2.pdf
# 运行MCP服务器
uv run python server.py
```
MCP服务器监听`http://HOST:PORT`(默认`0.0.0.0:8000`,可流式HTTP)。将OpenWebUI的MCP集成指向它;七个工具可供代理使用。
> **首次运行:** 分块器标记器(HF)和稀疏BM25模型(FastEmbed)下载到`./data`中 - 设置`HF_HUB_OFFLINE=0`,然后切换回`1`以进行离线启动。密集嵌入和重新排序由外部OpenAI-/`/rerank`-兼容端点提供(在`.env`中配置它们的URL)。
---
## 🏗️ 流程
```
index.py → MinIO + Docling 混合分块器 → 丰富 → 稠密+稀疏嵌入 → Qdrant
server.py → FastMCP 工具 → (稠密|稀疏)检索 → 交叉编码器重新排序 → 片段
OpenWebUI → 代理:搜索 → 读取 → 推理 → 答案(引用块ID)
```
---
## ⚙️ 配置
所有设置都位于`.env`中(参见`.env.example`)。
### 📦 模型缓存
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `HF_HUB_OFFLINE` | 0 | 设置为`1`以进行离线启动 |
| `HF_HOME` | ./data/huggingface | HuggingFace缓存(分块器标记器) |
| `FASTEMBED_CACHE_PATH` | ./data/fastembed | FastEmbed缓存(稀疏BM25模型) |
### 🖥️ 服务器
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `HOST` | 0.0.0.0 | MCP绑定主机 |
| `PORT` | 8000 | MCP端口(可流式HTTP) |
### 🚦 限制和弹性
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `RATE_LIMIT_RPS` | 5 | **每个用户**的持续MCP请求/秒(令牌桶按JWT身份键入) |
| `RATE_LIMIT_BURST` | 15 | 令牌桶的每个用户突发容量 |
| `SEARCH_MAX_CONCURRENCY` | 16 | 同时运行的搜索管道(保护嵌入器/重新排序器;vLLM在内部批处理) |
| `SEARCH_TIMEOUT` | 30 | 每个搜索调用的端到端截止时间(秒),包括队列等待;如果失败,则告诉调用代理重试 |
### 🔐 认证(OpenWebUI JWT)
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `JWT_SECRET` | – | OpenWebUI JWT验证密钥 |
| `JWT_ALGORITHM` | HS256 | JWT算法 |
### 🗂️ S3存储(索引)
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `MINIO_ENDPOINT` | localhost:9000 | S3端点 |
| `MINIO_USER` / `MINIO_PASSWORD` | – | S3凭证(也用于compose `minio`服务) |
| `MINIO_BUCKET` | default | 索引文件的存储桶 |
| `MINIO_URL_EXPIRE` | 3600 | 搜索结果中预签名源URL的生命周期(秒) |
### 🗄️ 文档存储
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `QDRANT_URL` | http://localhost:6333 | Qdrant URL |
| `QDRANT_TOKEN` | – | Qdrant API密钥(也用于compose `qdrant`服务) |
| `QDRANT_TIMEOUT` | 30 | Qdrant操作的超时(秒)(搜索+读取) |
| `QDRANT_EMBEDDING_DIM` | 1024 | 稠密嵌入维度 |
| `QDRANT_COLLECTION` | aragog | Qdrant集合 |
### 📑 转换器
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `DOCLING_URL` | http://localhost:5001 | Docling-serve端点 |
| `DOCLING_TIMEOUT` | 600 | 每个请求和作业超时(秒) |
### 🧩 文档分块器
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `CHUNKER_TOKENIZER` | BAAI/bge-m3 | HF标记器用于计算块标记(匹配嵌入模型) |
| `CHUNKER_MAX_TOKENS` | 1000 | 混合分块器标记预算 |
### 🏷️ 丰富器
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `ENRICHER_MAX_WORKERS` | 3 | 并行LLM元数据提取 |
| `ENRICHER_MODEL` | gemma4:31b | 丰富模型 |
| `ENRICHER_URL` | http://localhost:11434/v1 | OpenAI兼容端点(丰富) |
| `ENRICHER_TOKEN` | – | 丰富端点的API密钥 |
| `ENRICHER_TIMEOUT` | 300 | 丰富请求超时(秒) |
| `ENRICHER_LANGUAGE` | english | 提取的元数据的语言 — 必须与`SPARSE_EMBEDDING_LANGUAGE`和BM25工作的语料库匹配 |
### 🔮 嵌入器(稠密+稀疏)
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `DENSE_EMBEDDING_MODEL` | BAAI/bge-m3 | 嵌入服务器上的稠密模型ID(多语言,1024维) |
| `DENSE_EMBEDDING_URL` | http://localhost:8001/v1 | OpenAI兼容嵌入端点(compose `embedder`配置文件;也适用于Ollama/TEI/Infinity) |
| `DENSE_EMBEDDING_TOKEN` | – | 嵌入端点的API密钥 |
| `DENSE_EMBEDDING_TIMEOUT` | 15 | 嵌入请求超时(秒) — 保持低于`SEARCH_TIMEOUT`,以便SDK的瞬时重试仍然符合截止日期 |
| `SPARSE_EMBEDDING_MODEL` | Qdrant/bm25 | 稀疏模型(FastEmbed,在本地运行) |
| `SPARSE_EMBEDDING_LANGUAGE` | english | BM25停止词/词干提取语言 — 必须与语料库匹配,并在索引和查询之间保持相同 |
| `SPARSE_EMBEDDING_DEVICE` | cpu | 稀疏模型的设备(`cpu`或`cuda`) |
| `EMBEDDED_META_FIELDS` | context,keywords,headings,hypothetical_questions | 嵌入内容(稠密+稀疏)的元字段 |
### 🔁 重新排序器
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `RERANKER_MODEL` | BAAI/bge-reranker-v2-m3 | 重新排序服务器上的重新排序模型ID |
| `RERANKER_URL` | http://localhost:8002/v1 | vLLM基准URL(VLLMRanker调用其`/rerank`;compose `reranker`配置文件) |
| `RERANKER_TOKEN` | – | 重新排序端点的API密钥 |
| `RERANKER_TIMEOUT` | 15 | 重新排序请求超时(秒) — 保持低于`SEARCH_TIMEOUT`,以便挂起的重新排序器在截止日期前显示为特定错误 |
| `RERANKER_SCORE_THRESHOLD` | 0.2 | 命中的最低重新排序分数(0-1) — 较低分数的命中将被丢弃 |
---
## 🐳 后续服务
```bash
docker compose up -d # MinIO + Qdrant
docker compose --profile docling up -d # + Docling转换器
```
| 服务 | 端口 | 配置文件 | 描述 |
|---------|------|---------|-------------|
| Qdrant | 6333 | – | 向量数据库(REST + 仪表盘) |
| MinIO | 9000 / 9001 | – | S3 API / 控制台 |
| Docling | 5001 | `docling` | 文档转换器 |
| 嵌入器 | 8001 | `embedder` | vLLM稠密嵌入服务器(GPU) |
| 重新排序器 | 8002 | `reranker` | vLLM重新排序服务器(GPU) |
| 服务器 | 8000 | `server` | MCP服务器本身,容器化 |
---
## 📄 许可证
[MIT](LICENSE)