Skip to main content
Glama
README.md
# PDF-RAG

本地元器件手册检索:对 PDF/Markdown 手册库做**语义向量 + 全文(FTS) + 子串**三路混合检索
(RRF 融合 → 同文档去重 → 可选重排),支持表格识别与扫描页 OCR 兜底,通过 **Web 界面**、
**MCP 工具**、**REST API** 三种方式对外提供服务。

```
                     ┌──────────────┐
   浏览器   ──────▶  │  Web UI      │ Vue 3 + naive-ui(打进镜像,由后端托管)
                     ├──────────────┤
  MCP 客户端 ─────▶  │  /mcp        │ 8 个 pdf_rag_* 工具(默认内嵌进后端进程)
  pi 等 REST ─────▶  │  /api/*      │ 与 MCP 共用 backend/library.py 的实体逻辑
                     ├──────────────┤
                     │ LanceDB      │ 向量 + FTS 全文索引
                     └──────────────┘
```

## 快速上手

```bash
cp .env.example .env        # 至少填 EMBED_API_KEY
uv sync                     # 装依赖(Python 3.13)
uv run run_backend.py       # http://127.0.0.1:8100(含 Web UI 与 MCP)
```

手册放进 `pdf/`(或 `.env` 的 `PDF_ROOT`),然后在 Web「索引状态」页点**增量索引**。

| 文档 | 内容 |
|---|---|
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | **架构设计**:分层与依赖方向、检索流水线、索引构建、配置边界、关键不变量——改代码前先读 |
| [DEPLOY.md](DEPLOY.md) | 部署指南、REST API 全表、索引与数据包运维(另附内网形态文档 `DEPLOY-NAS.md`,不入库) |
| [BENCHMARKS.md](BENCHMARKS.md) | 检索质量基线、历次参数实验(含否定性结果)、测试方法学——**调参/换模型前先读** |
| [THIRD-PARTY.md](THIRD-PARTY.md) | 依赖许可清单与第三方声明 |
| [docs/VECTOR-PARTITION.md](docs/VECTOR-PARTITION.md) | 设计稿:按模型分区的向量存储(多模型混用、换模型只做增量) |

## 目录结构

```
backend/
  main.py         FastAPI 应用与全部 HTTP 接口
  config.py       配置中心:环境类常量 + 可调参数注册表(Spec,含中文 label)
  library.py      读页、精确检索、元数据缓存的共享实现(MCP 与 HTTP 共用)
  retrieval.py    检索核心:三路召回 + RRF 融合 + 去重 + 重排 + 片段定位
  ingest.py       索引构建(多进程提取 + 批量嵌入)
  doc_utils.py    PDF/Markdown 文本提取、表格 markdown 化
  ocr.py          OCR 后端抽象(local Paddle / remote VLM)+ 调用状态记录
  bundle.py       数据包(.tar.zst)导出/导入
  vecstore.py     向量分区:uid、分区表、覆盖率(见 docs/VECTOR-PARTITION.md)
server.py         MCP 服务(默认内嵌;MCP_INPROC=0 时独立进程)
ocr_service.py    本机 OCR 服务(独立进程,依赖 ocr-env,可选)
run_backend.py    后端守护:崩溃自愈 + 单例锁
eval_retrieval.py 检索质量评测
eval_queries.json 评测集(55 条)
BENCHMARKS.md     评测结果与历次实验记录
DEPLOY.md         部署指南(裸机 / 交付包 / API 参考)
tools/            开发期诊断与维护脚本
web/              前端源码(Vue 3 + Vite + naive-ui)
index.lance/      LanceDB 索引(运行时生成)
data/             运行时配置覆盖值与导出包(运行时生成)
```

各模块**为什么这样分层、彼此依赖方向、哪些约束不能破**,见
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。

## 环境要求

- **Python 3.13** + [uv](https://docs.astral.sh/uv/)
- **Node.js**(仅前端开发/构建;镜像里已有构建好的 `web/dist`)
- **嵌入 / 重排服务**:OpenAI 兼容的 `/v1/embeddings` 与 `/v1/rerank`
- **OCR(可选)**:本机 Paddle 需独立 `ocr-env`(约 4.9GB);也可用远程视觉模型,无需本地环境

## 配置

配置分两层,边界是**密钥只在环境层**。

**① 环境层**(`.env`,改完需重启):

```ini
PDF_ROOT=/path/to/manuals                     # 留空则用 <项目>/pdf
DB_PATH=/path/to/index.lance                  # 留空则用 <项目>/index.lance
EMBED_BASE_URL=https://api.example.com/v1     # OpenAI 兼容 /v1
EMBED_API_KEY=                                # 只从环境读,绝不入库/入包/进前端
EMBED_MODEL=qwen3-embedding-8b                # 与索引强绑定,必须与建库时一致
EMBED_DIMENSIONS=768                          # 与索引维度一致,建库/查询两侧都显式发送
RERANKER_MODEL=rerank-v4.0-pro
OCR_API_KEY=                                  # remote OCR 密钥(同上,只在环境层)
ADMIN_TOKEN=                                  # 留空则 /api/admin/* 一律 403
```

**② 可调参数层**(54 项 / 10 组):Web「检索配置」页运行时修改、**即时生效**,覆盖值持久化到
`data/settings.json`。OCR 地址/模型/提示词在此层(按 `ocr_backend` 联动),但 `OCR_API_KEY`
永远只在环境层;**向量维度必须与索引一致**(当前 768,随数据包导出),不符时向量通道明确
降级并记日志。

> 设计约束:系统**不以任何方式定时或主动探测**嵌入/重排/OCR 服务的可用性;OCR 状态展示
> 最近一次真实调用的结果。要确认上游可用,跑一次真实检索即可。这类"刻意不做"的决定
> 见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) §7。

## 使用

**Web**(6 个页面):索引状态(触发索引/实时进度)、资料库(统计与历史)、文件库(筛选与
提取质量体检)、检索测试(三模式/重排开关/命中页预览)、检索配置(54 项)、数据管理
(配置与数据包导入导出)。

**MCP**:8 个工具,统一 `pdf_rag_` 前缀(与客户端自带的 `grep`/`read` 区分——后者搜工作区
文件,这里搜手册语料)。

| 工具 | 用途 |
|---|---|
| `pdf_rag_search(query, category, top_k, file, max_per_doc)` | 混合检索,返回已定位到答案的片段窗口 |
| `pdf_rag_grep(pattern, category, file, context, flex_separator)` | 字面/正则精确检索,不走向量,不受嵌入服务影响 |
| `pdf_rag_read_page(file, page)` / `pdf_rag_read_pages(file, pages)` | 读整页原文(扫描页自动 OCR)/ 一次读多页 |
| `pdf_rag_get_outline(file)` | 书签/标题目录,用于大手册导航 |
| `pdf_rag_list_categories()` / `pdf_rag_list_files(category)` | 类别 / 已索引文件与元数据 |
| `pdf_rag_status()` | 索引与依赖状态(含 OCR 最近一次真实调用)。**不探测上游** |

**REST**:与 MCP 能力等价,完整端点表见 [DEPLOY.md](DEPLOY.md)。`clients/pi/pdf-rag-pi/`
附带一个 **pi 编码 agent 的原生扩展示例**(同名 `pdf_rag_*` 工具 + 用法 SKILL),底层走这套
REST 接口,可作为其它非 MCP 客户端的接入参考。

## 索引

```bash
uv run python -m backend.ingest            # 增量(按 MD5 跳过未变化文件)
uv run python -m backend.ingest --rebuild  # 全量重建(换嵌入模型后必须)
```

也可在 Web「索引状态」触发,或在「文件库」勾选指定文件重跑(含 OCR)。长期增量会产生
索引碎片,定期用 `tools/compact_index.py` 整理(见 [DEPLOY.md](DEPLOY.md))。

## 数据包(导出 / 导入)

把**索引 + PDF + 配置**打成一个 `.tar.zst`,可在另一台机器完整复原;导入预检会比对包内
与本地的服务配置,模型不一致时明确拒绝,不让新旧向量静默混用。命令与细节见
[DEPLOY.md](DEPLOY.md);**密钥不入包**。

## 评测

```bash
uv run python eval_retrieval.py              # 55 条评测集(hand 10 / topic 4 / mined 41)
```

两级指标:`recall@k`(期望文档进没进 top-k)与 `snippet@1/@k`(片段是否含答案字面值——
判别指标)。当前基线:**recall@5 100% / snippet@1 62% / snippet@5 91%**。重排器有固有
随机性(±2pp),改动前后各跑两遍;评测方法、匹配规则与历次实验见
[BENCHMARKS.md](BENCHMARKS.md)。

## 开发

```bash
cd web && pnpm install && pnpm dev    # 前端开发(Vite 代理 /api 到 8100)
cd web && pnpm build                  # 构建 → web/dist,Dockerfile 会 COPY 它
```

**测试**:关键路径单测在 `tests/`(纯逻辑,不访问网络与索引,113 个用例):

```bash
uv run python -m unittest discover -s tests
```

覆盖:路径边界防护、分块、版本合并去重、融合排序与平局裁决、缓存失效、file 过滤字面
语义、片段窗口、向量分区(uid 唯一性 / slug 契约区分 / 覆盖率口径 / 索引保真)、
数据包导入判断。改动检索/索引逻辑后跑一遍;端到端质量另用 `eval_retrieval.py` 对比
`snippet@1`;改动前端用 `pnpm build` 的类型检查兜底。

## 鸣谢

- 感谢 [futureppo](https://api.futureppo.top/) 提供了几乎无限的免费向量化/重排序模型,
  帮助本项目完成测试与调优。

## 许可证

本项目以 **GNU AGPL v3.0 或更高版本**(`AGPL-3.0-or-later`)发布,见 [LICENSE](LICENSE)。
这不是偏好而是**依赖约束**:直接依赖的 PyMuPDF(MuPDF)为 AGPL-3.0 / 商业双许可,传染性
使整体必须按 AGPL 分发。

| 场景 | 是否可以 |
|---|---|
| 自用 / 内部部署 | ✅ 可以 |
| 修改后对外提供网络服务 | ✅ 可以,但须按 AGPL §13 向网络用户提供对应源码 |
| 分发修改版 | ✅ 可以,但须同样以 AGPL 开放源码 |
| 闭源 SaaS / 闭源分发 | ❌ 需要 Artifex 商业许可,或替换掉 PyMuPDF |

依赖许可核对与第三方声明见 [THIRD-PARTY.md](THIRD-PARTY.md)。