# RAG
**Repository Path**: wingame/rag
## Basic Information
- **Project Name**: RAG
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-14
- **Last Updated**: 2026-09-21
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
PCB-RAG
面向 PCB 知识库的检索增强生成(RAG)问答系统
文档入库 · 向量检索 · BM25 词法召回 · 多路融合 · Rerank 精排 · Dify 集成
博客文章:https://blog.eecs.top/index.php/archives/3/
---
PCB-RAG 面向 PCB 设计规范、工艺资料与工程经验文档,覆盖从文档预处理、结构切块入库,到混合检索、重排序、答案合成与对外服务的完整链路,可用于构建 PCB 领域的智能问答助手。
## 目录
- [项目亮点](#项目亮点)
- [系统架构](#系统架构)
- [技术栈](#技术栈)
- [目录结构](#目录结构)
- [快速开始](#快速开始)
- [检索链路](#检索链路)
- [Dify 外部知识库 API](#dify-外部知识库-api)
- [配置说明](#配置说明)
- [评测](#评测)
- [常见问题](#常见问题)
- [开发意义](#开发意义)
- [公开仓库说明](#公开仓库说明)
- [License](#license)
## 项目亮点
| 亮点 | 说明 |
| --- | --- |
| **本地 / API 双后端** | LLM、Embedding、Rerank 均支持「本地服务」与「OpenAI 兼容 API」两种后端,通过 `*_BACKEND` 环境变量切换,不锁定厂商 |
| **一键环境补全** | `scripts/setup.sh` 自动创建虚拟环境、安装依赖、生成 `.env`、启动 Milvus,并按后端检查模型可用性 |
| **领域化文档处理** | 面向 PCB 规范、EDA 工具文档与工艺参数,内置编码修复、OCR 乱码清理、结构感知切块与元数据抽取 |
| **混合检索架构** | Milvus 向量检索 + BM25 词法检索 + HyDE 查询扩展 + 加权 RRF 多路融合,提升专业问题召回率 |
| **查询理解** | 意图识别与动态权重、复合问题自动分解、过于具体的问题自动 Step-back 补充背景知识 |
| **Agentic RAG** | 可选的迭代检索:检索 → 充分性判断 → 信息不足则改写查询重检,直到满足或达到迭代上限 |
| **Contextual Retrieval** | 入库时为每个 chunk 注入「文档 · 章节 · 标准号」语境前缀,缓解切块导致的上下文丢失 |
| **可切换精排** | 支持 API 精排(Jina / 硅基流动等)与本地精排(Qwen3-Reranker / cross-encoder / SBERT) |
| **上下文扩展** | 命中的 chunk 自动并入相邻 chunk(`prev_id`/`next_id`)与父块锚点内容,缓解长文档上下文割裂 |
| **Dify 集成** | 提供符合规范的 FastAPI 外部知识库接口,可直接接入 Dify 工作流或对话应用 |
| **GraphRAG 图检索** | 入库时抽取「实体—关系—实体」三元组构成可遍历图谱,检索时做实体链接与邻域扩展,补足多跳与聚合类问题 |
| **上下文压缩** | 近似去重 + 抽取式压缩 + 字符预算裁剪,在长召回链路上显著降低 token 消耗 |
| **多租户权限** | 按租户 / 角色 / 组做检索级过滤(向量库表达式 + 内存兜底双层),并支持访问审计 |
| **可观测性** | 结构化 tracing(JSONL)、计数器与延迟分位,`/metrics` 直接暴露 |
| **多模态** | PDF 图片经视觉模型转成可检索描述,表格结构化为 Markdown,列值对应关系不再丢失 |
| **标准工程结构** | `src/pcb_rag` 包结构 + `pyproject.toml`,便于安装、导入与维护 |
## 系统架构
### 模型层:本地 / API 双后端
```mermaid
flowchart TB
subgraph F["api_clients.py — 统一模型工厂"]
direction LR
LLM["LLM"]
EMB["Embedding"]
RRK["Rerank"]
end
LLM --> LLM_L["local:Ollama"]
LLM --> LLM_A["api:OpenAI 兼容"]
EMB --> EMB_L["local:Ollama"]
EMB --> EMB_A["api:OpenAI 兼容"]
RRK --> RRK_A["api:HTTP /rerank"]
RRK --> RRK_H["local:HF / Qwen3-Reranker / SBERT"]
RRK --> RRK_N["none:关闭精排"]
```
### 检索链路
```mermaid
flowchart LR
Q["用户问题"] --> ACL["权限过滤 + 元数据解析"]
ACL --> EXP["查询扩展 + HyDE"]
EXP --> DENSE["向量召回"]
EXP --> SPARSE["BM25 词法召回"]
EXP --> GRAPH["图检索:实体链接 + 邻域"]
DENSE --> FUSE["加权 RRF 融合"]
SPARSE --> FUSE
GRAPH -.-> FUSE
FUSE --> RERANK["Rerank 精排"]
RERANK --> CTX["相邻 / 父级 Chunk 扩展"]
CTX --> COMP["去重 + 上下文压缩"]
COMP --> ANS["LLM 合成答案 + 引用"]
```
## 技术栈
| 层次 | 组件 |
| --- | --- |
| 语言 | Python 3.10+ |
| RAG 框架 | LlamaIndex |
| 向量数据库 | Milvus 2.6(Docker Compose 一键启动) |
| 模型服务 | OpenAI 兼容 API / Ollama / HuggingFace Transformers |
| 服务框架 | FastAPI + Uvicorn |
| 外部集成 | Dify External Knowledge API |
| 分词与检索 | jieba 领域词典 + 本地 BM25+ 词法索引 |
## 目录结构
```text
.
├── .env.example # 环境变量模板(本地 / API 双后端)
├── .gitlab-ci.yml # CI:lint + pytest
├── .github/workflows/ci.yml # GitHub Actions:ruff 静态检查 + 构建 + pytest
├── requirements.txt # Python 依赖
├── pyproject.toml # Python 包配置
├── src/pcb_rag/ # 核心源码包
│ ├── api_clients.py # LLM / Embedding / Rerank 双后端工厂
│ ├── cache.py # 语义缓存(精确匹配 + 向量相似)
│ ├── ingest.py # 文档预处理、切块与入库
│ ├── query.py # 检索链路与交互式问答
│ ├── preprocess_docs.py # 编码修复与乱码清理
│ └── dify_external_api.py # Dify 外部知识库 API 服务
├── tests/ # 单元测试(缓存 / 评测指标 / 文档预处理)
├── eval/ # 评测体系(黄金集 + 四指标)
│ ├── build_golden_dataset.py # 数据集构建
│ ├── metrics.py # 忠实度 / 相关性 / 精确率 / 召回率
│ └── evaluate.py # 评测主脚本
├── scripts/ # 一键安装与运行脚本
│ ├── setup.sh # 环境一键补全
│ ├── check_env.py # 环境自检
│ ├── run_ingest.sh # 文档入库
│ ├── run_query.sh # 交互式问答
│ ├── serve_api.sh # 启动 API 服务
│ └── run_optimized.sh # 交互式参数配置脚本
├── docker/milvus/docker-compose.yml
├── data/README.md # 数据目录说明
└── docs/ # 配置、集成与优化文档
```
## 快速开始
### 前置要求
- Python 3.10+
- Docker 与 Docker Compose(用于启动 Milvus)
- 模型服务(二选一):
- 本地:安装并启动 [Ollama](https://ollama.com/)
- API:准备任意 OpenAI 兼容服务的 `BASE_URL` 与 `API_KEY`
### 1. 一键补全环境
```bash
bash scripts/setup.sh
```
脚本执行内容:
| 步骤 | 内容 |
| --- | --- |
| 1 | 创建 `.venv` 虚拟环境 |
| 2 | 安装 `requirements.txt`,并执行 `pip install -e .` |
| 3 | 从 `.env.example` 生成 `.env` |
| 4 | 创建 `data/clear_docs/`、`data/raw_docs/`、`logs/` |
| 5 | 启动 `docker/milvus/docker-compose.yml` |
| 6 | 按 `LLM_BACKEND` / `EMBED_BACKEND` 检查模型后端 |
| 7 | 运行 `scripts/check_env.py` 完成环境自检 |
### 2. 配置模型后端
编辑 `.env` 选择后端,三者可自由混搭。
**全本地(Ollama)**
```bash
LLM_BACKEND=local
EMBED_BACKEND=local
OLLAMA_LLM_MODEL=qwen3.5:35b-a3b-q4_K_M
OLLAMA_EMBED_MODEL=qwen3-embedding:8b-q8_0
```
**全 API(OpenAI 兼容,以硅基流动为例)**
```bash
LLM_BACKEND=api
EMBED_BACKEND=api
LLM_BASE_URL=https://api.siliconflow.cn/v1
LLM_API_KEY=sk-xxxx
LLM_MODEL=Qwen/Qwen3-8B
EMBED_MODEL=BAAI/bge-m3
EMBED_DIM=1024
```
**Rerank 后端**
```bash
# API 精排(推荐,无需本地显存)
RERANK_BACKEND=api
RERANK_API_URL=https://api.jina.ai/v1/rerank
RERANK_API_KEY=xxxx
RERANK_API_MODEL=jina-reranker-v2-base-multilingual
# 本地精排(可选 hf / qwen3reranker / sbert),或 none 关闭
RERANK_BACKEND=qwen3reranker
```
> 混搭示例:LLM 走 API、Embedding 走本地,只需分别设置 `LLM_BACKEND` 与 `EMBED_BACKEND`。
### 3. 准备数据
公开仓库不包含任何原始语料。请将你有权使用的 PCB 文档放入:
```text
data/clear_docs/
```
### 4. 文档入库
```bash
bash scripts/run_ingest.sh # 等价:python -m pcb_rag.ingest
```
> 默认增量写入(`INGEST_OVERWRITE=0`);如需清空重建集合,设置 `INGEST_OVERWRITE=1`。
### 5. 运行问答
```bash
bash scripts/run_query.sh # 等价:python -m pcb_rag.query
```
示例问题:
```text
4 层 PCB 的阻抗控制需要关注哪些参数?
Altium Designer 中如何处理高速差分线等长?
```
## 检索链路
| 阶段 | 说明 |
| --- | --- |
| 权限过滤 | 按请求头解析出的租户 / 角色生成 ACL 条件,与业务过滤以 AND 合并,在召回阶段就挡掉无权数据 |
| 元数据过滤解析 | 从问题中提取 `vendor` / `eda` / `layer_count` / `copper_oz` 等条件,缩小检索范围 |
| 查询理解 | 规则 + LLM 混合的意图识别,输出查询类型与动态检索权重 |
| 查询分解 | 复合问题拆成多个子问题分别召回,缓解多跳问题漏召 |
| 查询扩展 | PCB 同义词与缩写扩展,提升专业术语召回 |
| HyDE 增强 | 生成假设文档参与向量检索,缓解短查询语义稀疏问题 |
| Step-back | 过于具体的问题抽象为上位问题,单独一路召回补充背景知识 |
| 多路召回 | 向量检索与 BM25 词法检索并行执行 |
| 加权 RRF 融合 | 按查询类型动态调整各路线权重并融合排序 |
| Rerank 精排 | 使用 API 或本地 cross-encoder 对候选重排序 |
| 上下文扩展 | 命中 chunk 并入相邻 / 父级 chunk,保持上下文完整 |
| GraphRAG 图检索 | 实体链接命中图谱后取邻域关系与原文依据,作为独立一路(有独立权重与 rank)参与 RRF 融合,并顺带补全多跳事实 |
| 权限复核 | 对召回结果逐条复核,向量库过滤被忽略时也能兜底拦截 |
| 上下文压缩 | 近似去重 → 抽取式压缩(优先保留含数值 / 标准号的句子)→ 字符预算裁剪 |
| 答案合成 | 基于 RAG Prompt 生成答案并标注引用来源 |
## Dify 外部知识库 API
```bash
bash scripts/serve_api.sh
# 等价:uvicorn pcb_rag.dify_external_api:app --host 0.0.0.0 --port 8000
```
在 Dify 外部知识库中配置:
| 配置项 | 值 |
| --- | --- |
| URL | `http://:8000/retrieval` |
| API Key | `Bearer ` |
主要端点:
| 端点 | 说明 |
| --- | --- |
| `POST /retrieval` | Dify 外部知识库规范接口,返回命中的 records |
| `POST /api/ask` | 检索 + LLM 合成答案,支持传入对话历史 |
| `POST /api/ask/stream` | SSE 流式问答:先推检索来源,再逐 token 推答案 |
| `POST /api/chat` | 服务端会话记忆问答,返回 `session_id` |
| `GET /health` | 轻量健康检查(LB / 探针用),只返回状态与依赖就绪标记,不含敏感配置 |
| `GET /health/detail` | 完整配置摘要(索引 / 后端 / 检索参数 / 缓存 / 图谱 / 权限 / 压缩),需鉴权 |
| `GET /metrics` | 运行指标:计数器与延迟分位(p50 / p95 / p99) |
更多步骤见 `docs/DIFY_INTEGRATION_GUIDE.md`。
## 配置说明
### 模型后端
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `LLM_BACKEND` | `local` | `local`(Ollama)/ `api`(OpenAI 兼容) |
| `EMBED_BACKEND` | `local` | `local`(Ollama)/ `api`(OpenAI 兼容) |
| `RERANK_BACKEND` | `qwen3reranker` | `api` / `hf` / `qwen3reranker` / `sbert` / `none` |
| `RERANK_ENABLED` | `1` | 是否启用精排 |
| `OLLAMA_BASE` | `http://127.0.0.1:11434` | 本地 Ollama 服务地址 |
| `OLLAMA_LLM_MODEL` | `qwen3.5:35b-a3b-q4_K_M` | 本地 LLM 模型 |
| `OLLAMA_EMBED_MODEL` | `qwen3-embedding:8b-q8_0` | 本地 Embedding 模型 |
| `LLM_BASE_URL` | 空 | API 后端地址(如 `https://api.deepseek.com/v1`) |
| `LLM_API_KEY` | 空 | API 密钥 |
| `LLM_MODEL` | 空 | API 模型名 |
| `EMBED_BASE_URL` | 空 | Embedding API 地址(留空复用 `LLM_BASE_URL`) |
| `EMBED_MODEL` | 空 | Embedding 模型名(api 后端必填) |
| `EMBED_DIM` | 空 | 向量维度(留空自动探测) |
| `RERANK_API_URL` | 空 | API 精排地址 |
| `RERANK_API_MODEL` | 空 | API 精排模型名 |
### 存储与入库
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `DATA_DIR` | `./data/clear_docs` | 待入库文档目录 |
| `MILVUS_URI` | `http://127.0.0.1:19530` | Milvus 服务地址 |
| `COLLECTION` | `pcb_kb` | 向量集合名称 |
| `INGEST_OVERWRITE` | `0` | 入库时是否清空重建集合(重建时**忽略**入库清单,本次全量处理并重写清单) |
| `INGEST_INCREMENTAL` | `1` | 增量入库:按内容哈希指纹只处理新增 / 变更文档 |
| `LEXICAL_CACHE_TTL_HOURS` | `24` | 词法索引缓存有效期(小时) |
| `LEXICAL_RELOAD_STAMP` | `<词法缓存>.stamp` | 入库结束后写入的重载标记;长驻 API 按它轮询并热重载 BM25 |
### 检索与生成
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `RECALL_TOP_K` | `200`(API 进程 `setdefault` 亦为 `200`) | 初始召回数量 |
| `RERANK_TOP_N` | `200`(**API 进程强制 `setdefault` 为 `10`**) | 送入精排并保留的候选数量 |
| `CHUNK_EXPAND_MAX_EXTRA` | `5` | 上下文扩展可额外返回的条数 |
| `HYDE_ENABLED` | `1` | 是否启用 HyDE 查询增强 |
| `QUERY_UNDERSTANDING_MODE` | `hybrid` | 查询理解模式:`rule` / `llm` / `hybrid` |
| `QUERY_DECOMPOSE_ENABLED` | `1` | 是否对复合问题自动分解 |
| `QUERY_STEP_BACK_ENABLED` | `1` | 是否对具体问题自动 Step-back |
| `STEPBACK_ROUTE_WEIGHT` | `0.7` | Step-back 召回路线在 RRF 中的权重系数 |
| `CONTEXTUAL_RETRIEVAL_MODE` | `rule` | chunk 语境前缀:`off` / `rule` / `llm` |
| `CITATION_ENABLED` | `1` | 是否在答案中标注引用来源 |
### 缓存
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `SEMANTIC_CACHE_ENABLED` | `1` | 是否启用语义缓存 |
| `SEMANTIC_CACHE_MAX_SIZE` | `512` | 缓存条目上限 |
| `SEMANTIC_CACHE_TTL_SECONDS` | `3600` | 缓存有效期(秒) |
| `SEMANTIC_CACHE_THRESHOLD` | `0.95` | 语义命中的相似度阈值 |
### 服务
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `DIFY_API_TOKEN` | 无(**未设置时服务拒绝启动**) | Dify 外部知识库鉴权 Token;占位值 `change-me` 同样会被拒绝 |
| `CORS_ALLOW_ORIGINS` | 空(不下发跨域头) | 允许跨域的来源白名单,逗号分隔;需要浏览器直连时才配置 |
| `API_PORT` | `8000` | API 服务端口 |
| `SESSION_EXPIRE_MINUTES` | `60` | 会话过期时间(分钟) |
| `SESSION_MAX_COUNT` | `1000` | 最大会话数 |
| `SESSION_STORE` | `memory` | 会话存储:`memory` / `sqlite`(重启后保留) |
| `SESSION_DB_PATH` | `./data/sessions.db` | SQLite 会话库路径 |
| `SELF_RAG_ENABLED` | `0` | 是否启用 Agentic RAG 迭代检索 |
| `SELF_RAG_MAX_ITERATIONS` | `3` | 迭代检索的最大轮数 |
### GraphRAG(P1-2)
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `GRAPH_RAG_ENABLED` | `0` | 是否启用图检索(图谱缺失时自动降级为无图模式) |
| `GRAPH_PATH` | `./data/graph/kg.json` | 图谱文件路径 |
| `GRAPH_EXTRACT_BACKEND` | `rule` | 抽取方式:`rule`(零 LLM 成本)/ `llm` / `hybrid` |
| `GRAPH_HOP` | `1` | 实体邻域扩展跳数 |
| `GRAPH_TOP_K` | `5` | 图路径返回的候选数 |
| `GRAPH_WEIGHT` | `0.35` | 图路在 RRF 融合中的权重(独立一路,有独立 rank) |
| `GRAPH_RRF_K` | `60` | 图路 RRF 平滑参数 k |
| `GRAPH_EVIDENCE_MAX` | `2` | 图证据在最终结果中的独立配额(不占主结果 top-k 名额) |
| `GRAPH_EXPAND_MAX` | `2` | 邻域扩展追加的额外事实条数 |
| `GRAPH_COMMUNITY_ENABLED` | `0` | 是否生成社区摘要(面向综述类问题) |
开启图检索前先跑一次入库:入库流程会自动抽取三元组并维护 `GRAPH_PATH`。
### 上下文压缩(P2)
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `CONTEXT_COMPRESSION_ENABLED` | `0` | 是否启用压缩 |
| `COMPRESSION_MODE` | `extractive` | `extractive` / `llm` / `hybrid` |
| `COMPRESSION_TARGET_RATIO` | `0.6` | 目标保留比例 |
| `COMPRESSION_BUDGET_CHARS` | `12000` | 进入上下文的字符预算 |
| `CONTEXT_DEDUPE_ENABLED` | `1` | 是否做近似去重 |
| `CONTEXT_DEDUPE_THRESHOLD` | `0.85` | 去重相似度阈值 |
### 访问控制(P2)
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `ACL_ENABLED` | `0` | 是否启用多租户过滤 |
| `ACL_DEFAULT_VISIBILITY` | `private` | 入库默认可视性 |
| `ACL_PUBLIC_VALUE` | `public` | 该 visibility 的文档对所有租户可见 |
| `ACL_ADMIN_ROLES` | `admin,root` | 可跨租户访问的角色 |
| `ACL_TRUST_HEADERS` | `0` | 是否信任客户端自报的身份头。默认关闭:打开后任何人都能自报 `X-User-Roles: admin` 读全库 |
| `ACL_TRUST_HEADERS_ACK` | `0` | 显式确认「网关已剥离客户端同名头」。`ACL_ENABLED=1` 且 `ACL_TRUST_HEADERS=1` 而未设此项时,服务拒绝启动 |
| `ACL_STRICT_LEGACY` | `0` | 未标注权限字段的历史数据是否拒绝访问 |
| `ACL_HEADER_TENANT` | `X-Tenant-Id` | 租户请求头,另有 `X-User-Id` / `X-User-Roles` / `X-User-Groups` |
### 可观测性与多模态(P2)
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `OBSERVABILITY_ENABLED` | `1` | 观测总开关 |
| `TRACE_ENABLED` | `1` | 是否记录 tracing |
| `TRACE_LOG_PATH` | `./data/traces.jsonl` | trace 落盘路径 |
| `TRACE_SAMPLE_RATE` | `1.0` | 采样率(高 QPS 可降到 `0.1`) |
| `MULTIMODAL_ENABLED` | `0` | 是否启用 PDF 图片描述(需配置视觉模型) |
| `MULTIMODAL_VLM_MODEL` | 空 | 视觉模型名(留空复用 `LLM_MODEL`) |
| `TABLE_MARKDOWN_ENABLED` | `1` | 表格结构化为 Markdown(零成本) |
完整配置见 `.env.example` 与 `docs/CONFIGURATION_GUIDE.md`。
## 评测
项目自带评测体系,用四个指标量化检索与生成质量:
```bash
# 1. 构建黄金数据集(需已入库)
python eval/build_golden_dataset.py --limit 100
# 2. 人工抽检 20~30 条后执行评测
python eval/evaluate.py --dataset eval/datasets/golden.jsonl
```
| 指标 | 度量对象 | 参考阈值 |
| --- | --- | --- |
| Faithfulness | 答案与检索上下文的事实一致性 | ≥ 0.75 |
| Answer Relevancy | 是否真正回应了问题 | ≥ 0.80 |
| Context Precision | 相关上下文是否排在前面 | ≥ 0.70 |
| Context Recall | 所需信息是否都被召回 | ≥ 0.80 |
支持 `--no-rerank` 对比重排收益、`--retrieval-only` 只评检索(更快更省)。详见 `eval/README.md`。
## 常见问题
Milvus 连接失败怎么办?
```bash
# 查看容器状态
docker compose -f docker/milvus/docker-compose.yml ps
# 重启 Milvus
docker compose -f docker/milvus/docker-compose.yml restart
```
Milvus 首次启动约需 1-2 分钟,可用 `python scripts/check_env.py` 确认连通性。
API 后端启动时报缺少配置?
- `LLM_BACKEND=api`:需要 `LLM_BASE_URL` 与 `LLM_MODEL`
- `EMBED_BACKEND=api`:需要 `EMBED_MODEL`(`EMBED_BASE_URL` 留空时自动复用 `LLM_BASE_URL`)
- `RERANK_BACKEND=api`:需要 `RERANK_API_URL`
运行 `python scripts/check_env.py` 可直接看到缺失项。
Rerank 不可用或显存不足?
- `RERANK_BACKEND=api` 必须配置 `RERANK_API_URL`,否则精排初始化失败并回退;
- 本地精排(`hf` / `qwen3reranker`)需要足够显存,不足时会自动回退为「仅召回」;
- 不需要精排时设置 `RERANK_BACKEND=none`,或直接改用 API 精排。
重新入库后 BM25 仍在检索旧内容?
词法索引缓存在 `LEXICAL_CACHE_PATH`,入库完成后会自动清理,同时写一个 `LEXICAL_RELOAD_STAMP` 标记文件:
- **长驻 API 进程**会按该标记的 mtime 做惰性热重载(后台线程重建,本次检索仍用旧索引),无需重启;
- **CLI / 一次性进程**下次启动时自然重建;`LEXICAL_CACHE_TTL_HOURS`(默认 24 小时)超期也会重建。
如何清空并重建向量库?
设置 `INGEST_OVERWRITE=1` 后重新执行入库脚本即可清空重建集合;保持默认 `0` 时为增量 upsert。
注意:`INGEST_OVERWRITE=1` 会**忽略并重写**入库清单(`INGEST_MANIFEST_PATH`)。这一步是必须的 —— 集合被清空后清单里的指纹全部失效,若仍按指纹比对会得到「无需处理的文档」,重建后的库将是空的,而清单还声称数据都在,后续增量入库会继续空转。
入库后新文档检索不到 / 排得很靠后?
依次检查三件事:
1. **词法索引是否已重载** —— 长驻 API 会按 `LEXICAL_RELOAD_STAMP` 自动热重载,`/metrics` 里的 `lexical.reloaded` 计数可确认;
2. **入库清单是否与实际数据一致** —— 重建库后若没走 `INGEST_OVERWRITE` 全量,清单会与库脱节;
3. **是否只看了 Hit@K** —— 新文档缺一路投票时 Hit@K 可能不变,但 MRR/NDCG 会掉。
## 已知问题(Known Issues)
列在这里的目的是**可证伪**:每条都写清现象、根因、当前状态与彻底修法,便于接手的人复核。
| # | 现象 | 根因 | 当前状态 |
| --- | --- | --- | --- |
| 1 | 评测脚本与评测报告未入库 | `eval/reports/`、`eval/*.json(l)` 被 `.gitignore` 排除(数据集含语料原文,不宜公开);`eval/evaluate_recall.py` 亦未纳入版本控制 | **未修(唯一需要人工决策的一条)**:脱敏后提交脚本 + 报告摘要,或在 README 给出「数据集规模 / 该集指标 / 脚本版本」对照表。在提交之前,README 里的所有 MRR / Hit@K / NDCG 数字都无法被第三方复现 |
| 2 | 指标绝对值不可跨数据集比较 | 仓库内多版数据集互不重合(`eval_dataset.json` / `eval_dataset_146.json`);真值由 chunk 反推问题生成,chunk 按定义相关 | 只用于**同数据集内**的配置对比;换集必须重跑基线。生成侧偏差方向明确:共享词汇使 Hit/Recall 系统性**高估** |
| 3 | LLM 出题 / 判卷与生成同源 | `build_golden_dataset.py`、`evaluate.py`、`metrics.py` 都用 `build_llm()` | 部分修:`evaluate_recall.py` 支持 `SOFT_JUDGE_*` 指向另一厂商模型;生成侧 judge 仍同源,**需人工抽检 20~30 条校准并报告 judge 与人评一致率** |
| 4 | 每请求一个 `ThreadPoolExecutor` | `dify_external_api` 在同步端点里按请求开池(`min(n_workers, 8)`),叠加 anyio 线程池与全局 HyDE 线程池 | 未修(检索是 IO 密集,正解是 `asyncio + httpx.AsyncClient`)。当前并发受 anyio 的 40 线程与 HyDE 的 2 worker 约束,不是无界增长 |
| 5 | SSE 断开后已提交的 HyDE 调用仍会跑完 | 线程池任务无法取消(`Future.cancel()` 只能取消未开始的任务) | 未修;短期可加"同 query 结果缓存"与队列上限,长期改 asyncio 任务 + `wait_for` |
| 6 | 分路贡献无 ablation | 只有整体开关对比,没有关掉单路的对照实验 | `fusion.route_contribution()` 已提供"这条只靠哪一路进来"的归因工具;完整 ablation 表待补 |
| 7 | 父块扩展语义弱于"分层检索" | 默认 `parent_child` 模式下父块文本未单独入库,`parent_id` 指向同父块的首个子块 | 部分修:父级扩展已真实生效(此前是 no-op)。真正的"子块检索 + 父块替换"需要把父块也写入向量库并加 `node_level` 过滤 |
| 8 | O(n²) 的其它潜在热点 | 已修 `_parent_child_parse` 的邻居查找(此前 1e4 chunk 约 30~60s);其它循环未逐一审计 | 未完全修 |
### 本轮已修复(可对照 git log 复核)
| 问题 | 现象 | 修法 |
| --- | --- | --- |
| `INGEST_OVERWRITE` 与 manifest 冲突 | 重建集合后清单未失效 → `to_process` 为空 → **库被清空且什么都不插入**,后续增量继续空转 | `incremental.plan_ingest()`:overwrite 时忽略清单并写入真实指纹 |
| 增量指纹用 mtime | `git clone` / `cp -r` 改写 mtime → 全量误判重嵌入 | 改为 size 快筛 + blake2b 内容哈希,路径不进指纹 |
| CLI HyDE 键错位 | `primed_queries` 以 `expanded_q` 为键,却用 `hyde_q` 发起查询 → 原始查询没进检索、BM25 被喂 LLM 长文 | 改用 `expanded_q` 发起查询(与 eval / API 一致) |
| 图融合量纲 | 图规则分(0.2~1.0)× 0.35 与 RRF 分(~0.02)同池排序 → 图节点无条件霸榜 | 改为独立的 rank-based RRF 路;API 侧再加独立配额 `GRAPH_EVIDENCE_MAX` |
| BM25 不重载 | 长驻 API 只在启动时建一次索引 → 新文档缺一整路投票 | 入库写 `LEXICAL_RELOAD_STAMP`,API 按 mtime 惰性后台重载 |
| BM25 文档侧 tf 被压成 0/1 | `cut` + `cut_for_search` 合并去重 → k1 完全失效,与"调高 k1 增强词频"的注释矛盾 | `cut_words` 保留词频,`search_words` 只补新词 |
| IDF 高频词 postings 截断 | `postings[:5000]` 按入库顺序截断 → 新文档永远进不了高频词候选 | 默认不截断(DF>80% 已在打分阶段过滤) |
| judge 静默降级为 0 分 | judge 失败 / 无可判定陈述都返回 0.0 → 所有生成侧指标被低估且无人察觉 | 三态返回(`None` = 评测失败),汇总分列 evaluated / failed,失败率超阈值标记 `invalid` |
| ACL 合并把 OR 摊平 | ACL 的 `tenant OR public` 被并进外层 AND → 本租户私有文档全部不可见 | `merge_filters` 保留 OR 子树(已提交,含真实编译表达式回归测试) |
| 缓存跨租户复用 | 缓存 key 不含访问主体 → 语义命中会跨租户返回 | scope 前缀 + 同 scope 语义匹配;未传 scope 时告警 |
| `/api/ask` 绕过兜底 | 直调 `_retrieve_nodes` → 少了内存侧权限复核,且 GraphRAG 完全不生效 | 两步都下沉到 `_retrieve_nodes` 末尾,所有端点共享 |
| 默认不安全 | `DIFY_API_TOKEN=change-me` 可启动;`ACL_TRUST_HEADERS` 默认信任自报头(自报 admin 即读全库);CORS 通配 | Token 缺失/占位时拒绝启动;信任头默认关闭 + 需显式 ACK;CORS 改白名单 |
| `/health` 泄露配置 | 无鉴权的 `/health` 返回租户字段名、admin 角色、后端地址 | 拆为 `/health`(探针用)与 `/health/detail`(需鉴权) |
| 纯逻辑不可测 | 融合 / 增量比对与重型 import 绑在一起,测试只能 `importorskip` 静默跳过 | 下沉到 `pcb_rag/fusion.py`、`pcb_rag/incremental.py`,新增 60+ 用例;CI 增加依赖自检让"被跳过"变成硬失败 |
| `FUSION_MODE` 空转 | 三份文档把它当 v1.3 核心变更,代码里只用来挑打印字符串 | 代码与文档同步标注为"不改变行为",CLI 启动横幅显式提示 |
## 评测口径速查
| 口径 | CLI / eval | API 进程 |
| --- | --- | --- |
| `RECALL_TOP_K` | 200(`FUSION_RRF_K`=40) | 200 |
| `RERANK_TOP_N` | 200 | **10**(`dify_external_api` 顶部 `setdefault`) |
| 实际喂给 LLM | `RAG_TOP_DOCS`=5 | 5 |
| `--soft-match` 默认 | `none`(`0.78` 是 **embed 模式**的阈值,非命中判定默认) | — |
| `--rrf-k` / `--recall-k` 默认 | 60 / 40 | 40 / 200 |
> 复现线上行为:`python eval/evaluate_recall.py --rrf-k 40 --recall-k 200 --rerank --rerank-top-n 10`。
> 未显式传参时,评测脚本与线上并不同参,分数不可直接当作线上指标。
## 开发意义
- 设计并实现面向 PCB 领域文档的 RAG 问答系统,支持规范文档入库、向量检索、BM25 召回、多路融合、Rerank 精排与答案生成。
- 基于 LlamaIndex、Milvus、FastAPI 搭建知识库服务,模型层抽象出「本地 / OpenAI 兼容 API」双后端,可按部署环境自由切换。
- 针对 PCB 专业术语、工艺参数和 EDA 工具场景设计元数据抽取、查询过滤和混合检索策略,提升专业问题召回与回答相关性。
- 整理为可复现工程:提供标准 Python 包结构、环境变量模板、Docker Compose 与一键 setup/run 脚本。
## 公开仓库说明
请在本地通过 `.env` 配置真实服务地址和密钥,不要将 `.env`、原始数据或模型缓存提交到 GitHub。
## License
遵循 MIT 协议,任何人都有权使用,但是修改请务必再开源