# MyRag **Repository Path**: debiaoANDdashuai/my-rag ## Basic Information - **Project Name**: MyRag - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-26 - **Last Updated**: 2026-09-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # MyRag > **可度量、可调优、可私有化的企业知识引擎。** > > 别人给你一个黑盒,我给你一条**每一环都能度量、能调优、能用数据证明有效**的检索链路。 MyRag 是知识库/flow 产品线的第四代落地:以**手写 RAG 流水线**(阶段一 AgentFlow)的透明链路为骨架, 吸收 **LlamaIndex 模块化管线**(阶段二 LIGEFLOW)的工程规范,用 FastAPI + PostgreSQL + Milvus 重新落地。 --- ## 1. 与 Dify / 扣子 的分野 它们擅长让不懂技术的人快速搭应用——这一点我做不过它们。 它们把检索链路封装成黑盒(通常只暴露 `top_k` 和 `score 阈值` 两个旋钮); **MyRag 把每一环都打开了**:文档怎么清洗、BM25 和向量怎么融合、要不要 rerank、 每环的开关与参数全可配,并且能用分层评测 + 消融实验证明每一次改动是正收益。 --- ## 1.1 数据边界(两套话术,口径必须一致) **当前口径:存储不出内网,模型调用走合规云 API。** | 边界 | 现状 | |---|---| | 原文 / 切片 / 向量 / 会话 | ✅ 全部落在自有 PostgreSQL + Milvus,不出内网 | | 解析 / Embedding / Rerank / LLM | 调用合规云 API(MinerU 云端 / DashScope) | **面试话术(诚实版)**: > "我的边界是**存储不出内网**——所有原文、切片、向量、会话记录都在自建的 PG + Milvus 里, > 没有任何第三方托管。模型调用我明确承认走云 API,因为单机演示环境不具备自建推理的算力, > 但我把 embedding / LLM / rerank / 解析**全部做成了可配置的后端**—— > 换成内网自建模型只改配置,不动业务代码。" **入职后话术(资源到位、全面转内网)**: > "进公司后我把模型后端整体切到内网自建:embedding 走 BGE-M3,LLM 走 vLLM 部署的开源模型, > 解析换成内网 MinerU 服务。因为一开始就是接口抽象 + 连接可配置,切换只改配置不改链路。" > ⚠️ **不要把这两套混着讲。** 面试官最容易抓的就是"你说数据不出内网,但代码把正文发给了云 API"—— > 这是可以避免的翻车点。说清"存储"与"推理"的边界差异,反而显得你分得清。 --- ## 2. 技术栈 | 层 | 选型 | 说明 | |---|---|---| | 网关 | **Nginx** | 限流、SSE 免缓冲、统一入口 | | 应用 | **FastAPI**(async) | 异步原生,Pydantic 校验,自动 OpenAPI | | ORM | **SQLAlchemy 2.0 + asyncpg** | 全异步,连接串唯一事实源在 `.env` | | 关系库 | **PostgreSQL** | JSONB 存灵活结构;邻接表/自引用处理父子块 | | 向量库 | **Milvus**(standalone) | 单 collection + `kb_id` filter,改连接串即可升级集群 | | 缓存 | **Redis** | 占位(会话/限流),P1 未强依赖 | | 模型 | **DashScope**(OpenAI 兼容) | embedding / LLM 走兼容端点,rerank 走原生端点 | | 解析 | **MinerU 云端 REST + 轻量本地库** | 见下方「依赖治理」 | | 依赖管理 | **uv** | `pyproject.toml` + `uv.lock` | **贯穿性约束**:每个中间件都通过配置切换形态,不硬编码连接方式。 本地跑轻量形态,讲扩展时能说清换成什么(见《系统设计说明书》第 10 章)。 ### 2.1 依赖治理:为什么不用官方 mineru 包 解析这块踩过一个真实的坑,值得单独讲: **问题**:`mineru` 4.x 把 `gradio` / `docvortex` / `modelscope` / `opencv-python` / `onnxruntime` / `mineru-llama-cpp` 全部声明为**强制依赖**(不是可选 extra), 装上就是 **3.26 GB 镜像**、lock 要 9 分钟、传输和部署都很痛。 **而我只用它的云端 HTTP 接口**——为了调四个 REST 端点,背了一整套本地 OCR 推理框架。 **做法**:自己写一个约 200 行的云端客户端(`services/mineru_cloud.py`,只用 httpx): 上传 → 建解析任务 → 轮询 → 取产物。Office 格式改用 `python-docx` / `python-pptx` / `openpyxl` 直读,PDF 兜底用 `pypdf`。 **效果**: | 指标 | 改前 | 改后 | |---|---|---| | 锁文件包数 | 202 | **135** | | 虚拟环境体积 | 693 MB | **242 MB** | | 镜像体积 | 3.26 GB | **1.38 GB**(-58%) | | 依赖安装耗时 | 42 s | **22 s** | > **面试话术**:"我做过一次依赖治理。官方解析包把 gradio/modelscope/opencv 作为强制依赖, > 但我只需要它的云 API,所以我按 REST 契约自己实现了轻量客户端, > 把镜像从 3.26GB 压下来,同时保留本地兜底路径——**依赖是可以按需裁剪的,前提是你知道自己在用什么。**" --- ## 3. 目录结构 ``` MyRag/ ├── app/ │ ├── main.py # 应用入口(含 /health 与 lifespan 建表) │ ├── core/ # 配置 / 数据库 / 安全 / 依赖注入 │ │ ├── config.py # pydantic-settings,.env 唯一事实源 │ │ ├── database.py # async engine + Base + get_db │ │ ├── security.py # bcrypt 哈希 + JWT │ │ └── deps.py # get_current_user(Bearer 鉴权) │ ├── models/ # 12 张表(对齐设计说明书 4.1) │ ├── schemas/ # 对外请求/响应契约 │ ├── services/ # ★ 核心 RAG 引擎 │ │ ├── parser.py # 三路解析:MinerU云端 / Office轻量直读 / 直读 │ │ ├── mineru_cloud.py # MinerU 云端 REST 客户端(仅 httpx,~200 行) │ │ ├── mineru_clean.py # 深度清洗:噪声 + 跨页重复 + bbox 位置带 │ │ ├── chunking.py # 自研递归切块(零外部依赖) │ │ ├── embedding.py # 批量向量化 + 维度自检 │ │ ├── milvus_client.py # 向量读写(filter 强制限定 kb_id) │ │ ├── bm25_retriever.py # BM25 召回 + RRF 融合 │ │ ├── rerank.py # DashScope 精排 │ │ ├── llm.py # 强制引用生成 + 无据拒答 │ │ └── retrieval.py # ★ 可插拔检索链路编排 + 分层耗时 │ └── routers/ # auth / kb / docs / import / retrieval ├── alembic/ # 数据库迁移 ├── deploy/nginx.conf # 网关配置 ├── docker-compose.yml └── pyproject.toml ``` --- ## 4. 快速开始 ### 4.1 准备配置 ```bash cp .env.example .env # 编辑 .env,至少填 LLM_API_KEY;要解析 PDF 再填 MINERU_TOKEN ``` **配置加载规则(重要)**: 优先级 **环境变量 > `.env` 文件 > 代码默认值**。 `.env` 的路径用 `__file__` 反推项目根后**锚定为绝对路径**, 所以**不管从哪个目录启动进程,读到的都是同一个 `MyRag/.env`**。 > 这里踩过一个坑:原本写的是 `env_file=".env"`,它相对【当前工作目录】解析。 > 后果是从别的目录(如 systemd / IDE 默认 cwd)启动时,会**静默**找不到 `.env` > 并回退到代码默认值——包括弱密钥 `JWT_SECRET_KEY="CHANGE_ME_IN_PRODUCTION"`。 > 也就是说:**带着"任何人都能伪造 JWT"的配置跑起来,却一行日志都不报。** **生产环境防呆**:设置 `APP_ENV=production` 后,如果 `JWT_SECRET_KEY` 仍是占位值、 或 `DEBUG=True`,进程会**直接启动失败**(fail fast),而不是带病运行: ```bash # 生产部署示例 export APP_ENV=production export JWT_SECRET_KEY="$(openssl rand -hex 32)" # 强密钥 export DEBUG=False ``` ### 4.2 一键启动全栈 ```bash docker compose up -d --build ``` 服务与端口(**刻意与同机 K12 项目错开**): | 服务 | 宿主机端口 | 容器端口 | |---|---|---| | Nginx 网关 | **8091** | 80 | | FastAPI | 8001 | 8000 | | PostgreSQL | **5434** | 5432 | | Redis | **6380** | 6379 | | Milvus | **19532** | 19530 | 访问 查看 OpenAPI。 ### 4.3 本地开发(不用容器) ```bash uv sync # 安装依赖(含 dev 组) docker compose up -d postgres redis milvus # 只起中间件 uv run alembic upgrade head # 建表(或直接启动 app,lifespan 会 create_all) uv run uvicorn app.main:app --reload --port 8000 ``` > 跑评测时再装重依赖:`uv sync --extra eval` --- ## 5. API 一览 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/auth/register` | 注册,返回 JWT | | POST | `/api/auth/login` | 登录,返回 JWT | | GET | `/api/auth/me` | 当前用户 | | GET | `/api/kb` | 知识库列表(含 docCount / segmentCount) | | POST | `/api/kb` | 新建知识库 | | GET/PUT/DELETE | `/api/kb/{kb_id}` | 详情 / 更新 / 删除(删除会级联清理向量) | | GET | `/api/kb/stats` | 全局统计 | | POST | `/api/kb/{kb_id}/import/upload` | 上传登记 | | POST | `/api/kb/{kb_id}/import/preview` | 切块预览(不落库) | | POST | `/api/kb/{kb_id}/import/execute` | 执行入库(解析→切块→向量化) | | GET | `/api/docs/kb/{kb_id}` | 文档列表(分页/搜索/状态过滤) | | GET | `/api/docs/{doc_id}/chunks` | 查看某文档的切块 | | DELETE | `/api/docs/{doc_id}` | 删除文档(级联清理向量) | | POST | `/api/kb/{kb_id}/retrieval` | **检索 + 生成**(JSON 一次性返回) | | POST | `/api/stream/kb/{kb_id}/retrieval` | **SSE 流式检索** | | GET | `/health` | 健康检查 | 除 `/health` 外,业务接口都需要 `Authorization: Bearer `。 > **为什么流式单独挂在 `/api/stream/` 下**:nginx 对 `/api/` 开的是默认缓冲, > SSE 会被缓冲成「假死不流式」;只有 `/api/stream/` 这个 location 配了 `proxy_buffering off`。 > 这是阶段一踩过的坑,网关配置已写死,后端路径必须跟着走。 ### 5.1 检索请求示例 ```bash curl -X POST http://localhost:8091/api/kb/1/retrieval \ -H "Authorization: Bearer $TOKEN" \ -F "query=年假有多少天?" \ -F "mode=hybrid" \ -F "top_k=5" \ -F "rerank=true" \ -F "generate=true" ``` 响应包含 `answer`(带 `[n]` 引用编号)、`sources`(文档 / 页码 / 分段三级溯源)、 `refused`(是否触发无据拒答)。 --- ## 6. 核心设计要点(面试可讲) ### 6.1 可插拔检索链路 ``` Query → 向量召回 ∥ BM25 召回 → RRF 融合 → rerank 精排 → LLM 生成(带引用) ``` 每一环都记录输入/输出数量与耗时(`RetrievalContext.trace()`), 所以能单独回答「去掉 rerank 掉多少分」。这是评测台的底座。 ### 6.2 检索层强制权限过滤(RAG 特有安全风险) 向量检索会**静默穿越权限边界**:只在接口层校验「你能不能访问这个知识库」是不够的, 如果多库混搜、召回后再过滤,别人的文档片段可能出现在你的答案里, 而且日志上看不出来(检索本身合法、鉴权也通过)。 MyRag 的做法是**检索时限定范围**——Milvus 查询强制带 `filter="kb_id == N"`, 无权限的文档根本不进候选集。 ### 6.3 三级溯源 `chunks.page_idx`(页码)+ `chunks.section_path`(标题层级路径)+ `documents.name`, 支撑「文档 → 页码 → 分段」定位;LLM 被强制要求输出 `[n]` 编号, 生成后再做一次**越界编号剔除**,杜绝指向不存在来源的引用。 ### 6.4 页眉页脚判定不靠标记 `mineru_clean.py` 用**跨页重复频次 + bbox 位置带**判定噪声—— 很多页眉被 MinerU 标成 `text`,只靠 `type` 过滤是过滤不掉的。 ### 6.5 阶段一致命 bug 的防御性设计 阶段一曾在「两组件间契约」上翻车:向量召回写 `text` 键、BM25 写 `content` 键, 而 `rrf_fuse` 用 `setdefault` 取先到者,导致**最相关结果的 content 为空**。 MyRag 的对策是把契约显式化:所有召回源的条目都必须过 `_normalize_hit()`, RRF 融合时**逐字段补全**(谁有值用谁的值),不再有「先到者锁定」。 --- ## 7. 当前进度与边界(如实说) | 能力 | 状态 | |---|---| | 解析 → 切块 → 向量化 → 入库 | ✅ 已实现 | | 向量 + BM25 混合召回 → RRF → rerank → 生成 | ✅ 已实现 | | 三级溯源 + 强制引用 + 无据拒答 | ✅ 已实现(claim 级逐句对齐在 P3) | | JWT 鉴权、知识库/文档 CRUD、级联清理 | ✅ 已实现 | | SSE 流式检索 | ✅ 已实现 | | 父子块(`chunks.parent_id` 已建表) | ⏳ P2 | | 评测台 / 分层指标 / 消融实验 / CI 门禁 | ⏳ P3(表结构已就位) | | 可观测性(追踪 / 成本看板 / 告警) | ⏳ P4 | | 异步任务(`async_tasks` 已建表) | ⏳ 当前导入在请求内同步完成 | > **诚实边界**:当前是单机验证版。embedding / LLM 调用已是异步(不阻塞事件循环), > 但导入流程仍在请求内完成;数据规模是功能验证规模,不是压测规模。 > 架构上按 1000 万 chunk / 50 QPS 设计,扩容路径与瓶颈点见《系统设计说明书》。 --- ## 8. 相关文档 - 《系统设计说明书》—— 总体架构、数据模型、选型表、扩展路径、面试话术速查卡 - 《系统设计决策日志》—— 16 轮设计决策的追溯记录 --- ## 9. 开发约定 ```bash uv run ruff check . # 静态检查 uv run ruff format . # 格式化 uv run pytest # 测试 uv run alembic revision --autogenerate -m "描述" # 生成增量迁移 ``` - 密钥一律走环境变量,**禁止硬编码**(`.env` 已在 `.gitignore` 中)。 - 模块间通过 service 契约通信,不互相 import 内部实现(逻辑微服务化)。