92b062c048
- 新增 app/api/deps.py、app/core/users.py、app/core/sessions.py:会话鉴权依赖、 用户存储(PBKDF2-HMAC-SHA256 + 随机 salt,Redis/内存降级)、会话签发与校验(TTL 12h) - auth.py 新增用户管理端点(列表/创建/重置密码/删除)与 admin/user 角色权限边界, user 访问用户管理返回 1006,禁删自己与最后一个 admin - admin.html 新增用户管理面板(仅 admin 挂载)与 API 指南在线测试台 - Dockerfile 将 uv 放入 PATH;docker-compose 调整 qdrant 依赖为 service_started 并移除依赖 curl 的 healthcheck(官方镜像不含 curl) - 新增用户管理测试(users/sessions/auth_api/auth_integration),全量 461 项测试通过 Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
187 lines
11 KiB
Markdown
187 lines
11 KiB
Markdown
# QMDSearch - AI Agent 分层信息检索服务
|
||
|
||
## 项目概述
|
||
|
||
QMDSearch 是面向 AI Agent 的分层信息检索服务,支持多层级知识库检索、向量语义搜索和结构化数据查询。基于 Docker 部署在 NAS 上,为 AI Agent 提供高效、精准的信息检索能力。
|
||
|
||
## 技术栈
|
||
|
||
- **语言**: Python 3.12+
|
||
- **Web 框架**: FastAPI
|
||
- **向量数据库**: Qdrant (Docker)
|
||
- **缓存**: Redis (Docker)
|
||
- **关系数据库**: PostgreSQL (Docker, 可选)
|
||
- **嵌入模型**: OpenAI / 本地模型 (通过配置切换)
|
||
- **本地推理模型**: Ollama (Docker) — 用于文档三级总结,默认 qwen2.5:1.5b
|
||
- **部署**: Docker Compose on NAS
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
QMDSearch/
|
||
├── app/ # 应用主目录
|
||
│ ├── main.py # FastAPI 入口(含 /admin 管理页面挂载)
|
||
│ ├── config.py # 配置管理
|
||
│ ├── api/ # API 路由层
|
||
│ │ ├── response.py # 统一响应格式 (ok/error/ApiError)
|
||
│ │ ├── deps.py # 会话鉴权依赖 (Bearer → session → 用户记录)
|
||
│ │ └── v1/ # API v1 版本
|
||
│ │ ├── auth.py # 认证与用户管理接口
|
||
│ │ ├── search.py # 检索接口
|
||
│ │ ├── document.py # 文档入库/管理接口
|
||
│ │ └── knowledge.py # 知识库接口
|
||
│ ├── core/ # 核心业务逻辑
|
||
│ │ ├── retriever.py # 分层检索引擎 (L1→L2→L3→chunk)
|
||
│ │ ├── query_parser.py # query 解析与分类路由
|
||
│ │ ├── embeddings.py # 向量嵌入
|
||
│ │ ├── sparse.py # 稀疏向量编码 (BM25 近似)
|
||
│ │ ├── ranker.py # RRF 融合与结果截断
|
||
│ │ ├── summarizer.py # 文档三级总结 (Ollama)
|
||
│ │ ├── headings.py # 原生标题树解析
|
||
│ │ ├── classifier.py # 文档分类 (主类+标签+置信度)
|
||
│ │ ├── chunker.py # 标题树感知 chunk 切分
|
||
│ │ ├── users.py # 用户存储与密码哈希 (PBKDF2,Redis/内存降级)
|
||
│ │ ├── sessions.py # 会话签发与校验 (Redis TTL 12h/内存降级)
|
||
│ │ └── ingestion.py # 文档入库 (总结→分类→写入)
|
||
│ ├── models/ # 数据模型
|
||
│ │ ├── search.py # 检索请求/响应模型
|
||
│ │ ├── knowledge.py # 知识库/taxonomy 模型
|
||
│ │ └── document.py # 文档/总结模型
|
||
│ ├── services/ # 服务层 (外部交互)
|
||
│ │ ├── qdrant.py # Qdrant 客户端 (四层集合)
|
||
│ │ ├── redis.py # Redis 缓存客户端
|
||
│ │ └── ollama.py # Ollama 客户端
|
||
│ ├── static/ # 静态资源
|
||
│ │ └── admin.html # 管理页面 (单文件)
|
||
│ └── utils/ # 工具函数
|
||
├── scripts/ # 运维与评测脚本
|
||
│ ├── eval/ # 回归评测 (run_eval.py / metrics.py / judge.py / regression_set.json)
|
||
│ └── smoke_live.py # 在线冒烟脚本
|
||
├── tests/ # 测试
|
||
├── docker-compose.yml # Docker 编排
|
||
├── Dockerfile # 应用镜像
|
||
├── .env.example # 环境变量模板
|
||
├── pyproject.toml # 项目配置
|
||
└── CLAUDE.md # 本文件
|
||
```
|
||
|
||
## 分层检索架构
|
||
|
||
检索链路:query 解析路由 → L1→L2→L3 摘要树逐层剪枝 → chunk 层 hybrid 检索 (dense + sparse,RRF 融合)。
|
||
|
||
0. **query 解析路由**: 用 Ollama 小模型将 query 解析为结构化结果(rewrite、关键词、命中类目+置信度);高置信且类目数不超上限时按类目过滤,低置信/解析失败/类目过多则全库兜底(不丢召回)
|
||
1. **L1 - 文档定位层**: 在文档 L1 总结集合中检索,产出候选文档集合;无命中时直接全库 chunk 兜底(fallback=True)
|
||
2. **L2 - 章节大纲层**: 在候选文档内检索 L2 章节大纲,按 section 剪枝定位;未命中的文档(含 2.5 级文档)落入 L3 b 路(仅按 doc 过滤)
|
||
3. **L3 - 小节内容层**: 两路查询(a:L2 命中文档按 section_path 过滤;b:其余文档仅按 doc_id 过滤)后 RRF 融合;无命中时 chunk 层回退为 L1 候选文档级检索
|
||
4. **chunk 层**: 在 L3 收窄的范围内对原文 chunk 做 hybrid 检索(dense 向量 + sparse 稀疏向量 RRF 融合),截断后返回最终 Top-K
|
||
|
||
## API 设计规范
|
||
|
||
- RESTful 风格,版本化路径: `/api/v1/`
|
||
- 请求/响应使用 Pydantic 模型校验
|
||
- 统一响应格式: `{"code": 0, "data": {...}, "message": "ok"}`
|
||
- 错误码: 0=成功, 1xxx=客户端错误, 2xxx=服务端错误
|
||
|
||
## API 清单
|
||
|
||
| 方法 | 路径 | 说明 | 鉴权 |
|
||
|------|------|------|------|
|
||
| GET | `/api/v1/health` | 健康检查 | 免登录 |
|
||
| POST | `/api/v1/search` | 分层检索 | 免登录 |
|
||
| POST | `/api/v1/documents` | 文档入库(202 异步入库,返回 task_id) | Bearer |
|
||
| POST | `/api/v1/documents/upload` | multipart 文件上传入库(202 异步,支持 .txt/.md/.html/.htm/.pdf/.docx) | Bearer |
|
||
| GET | `/api/v1/documents/tasks/{task_id}` | 入库任务状态查询(done 附 result,failed 附 error) | 免登录 |
|
||
| GET | `/api/v1/knowledge/categories` | 知识分类类目集 | 免登录 |
|
||
| GET | `/api/v1/knowledge/stats` | 统计(四层点数 + 类目分布 + uncategorized 数) | 免登录 |
|
||
| GET | `/api/v1/documents` | 文档列表(limit/offset 分页) | 免登录 |
|
||
| GET | `/api/v1/documents/{doc_id}` | 文档详情 | 免登录 |
|
||
| DELETE | `/api/v1/documents/{doc_id}` | 删除文档(幂等) | Bearer |
|
||
| POST | `/api/v1/auth/login` | 用户名密码登录,签发 session token(TTL 12h) | 免登录 |
|
||
| POST | `/api/v1/auth/logout` | 退出登录(删除当前 session) | Bearer |
|
||
| POST | `/api/v1/auth/password` | 修改自己的密码(must_change_password 用户唯一可用接口) | Bearer |
|
||
| GET | `/api/v1/auth/me` | 当前登录用户信息(脱敏) | Bearer |
|
||
| GET | `/api/v1/auth/users` | 用户列表(脱敏) | Bearer + admin |
|
||
| POST | `/api/v1/auth/users` | 创建用户(重名/非法用户名/弱密码 1001) | Bearer + admin |
|
||
| POST | `/api/v1/auth/users/{username}/password` | 重置指定用户密码(成功后清除其全部 session) | Bearer + admin |
|
||
| DELETE | `/api/v1/auth/users/{username}` | 删除用户(清除其 session;禁删自己/最后一个 admin) | Bearer + admin |
|
||
| GET | `/admin` | 管理页面 | 页面登录门禁 |
|
||
|
||
「Bearer」指请求头 `Authorization: Bearer <token>`,token 经 `/api/v1/auth/login` 获取;变更类文档端点(POST /documents、POST /documents/upload、DELETE /documents/{id})需 Bearer token(admin/user 角色均可),查询类端点免登录。
|
||
|
||
入库任务状态持久化在 Redis(key: `ingest_task:{task_id}`):进行中与 done 保留 24h,failed 保留 7 天;Redis 不可用时降级为纯内存。
|
||
|
||
## 管理页面
|
||
|
||
浏览器访问 `/admin`,页面带登录门禁(未登录/token 失效自动回登录卡片;must_change_password 用户先强制改密后方可进入)。单页面含七个区块:概览、文档管理(列表/详情/删除)、文档入库(文本 + 文件上传)、检索测试台、类目列表、API 指南(端点清单 + 在线测试台)、用户管理(仅 admin 角色挂载,含创建/重置密码/删除)。
|
||
|
||
## 认证与用户
|
||
|
||
会话制认证:登录签发 session token(Redis 持久化,TTL 12h;Redis 不可用时降级为进程内存,重启失效),请求经 `Authorization: Bearer <token>` 携带,鉴权依赖见 `app/api/deps.py`。
|
||
|
||
- **角色与权限边界**: `admin` 拥有全部权限(含 /auth/users* 用户管理);`user` 可登录并调用变更类文档端点(入库/上传/删除),访问用户管理端点返回 1006
|
||
- **初始 admin 引导**: 空库启动时 `bootstrap_admin` 自动创建 admin 账号,随机明文密码仅在启动日志中打印一次(must_change_password=true),首次登录后须先经 POST /auth/password 改密,改密前访问其他端点返回 1006
|
||
- **免登录端点**: 查询类端点(POST /search、GET /documents*、GET /knowledge/*、GET /health)不需要 token
|
||
- **相关错误码**: 1005 未认证或凭证无效,1006 权限不足/首次登录须先改密
|
||
|
||
## 编码规范
|
||
|
||
- 使用 uv 管理依赖
|
||
- 类型注解必须 (Python 3.12+ 语法)
|
||
- 异步优先 (async/await)
|
||
- 配置通过环境变量注入,使用 pydantic-settings
|
||
- 日志使用 structlog 结构化日志
|
||
|
||
## 文档入库与三级总结
|
||
|
||
文档入库时通过 Ollama 本地小模型对文档内容进行三级总结,然后根据总结进行分类入库。
|
||
|
||
### 总结层级
|
||
|
||
| 层级 | 名称 | 说明 | 存储用途 |
|
||
|------|------|------|----------|
|
||
| L1 | 总结 | 对整篇文档的一句话高度概括 | 快速分类、知识库路由 |
|
||
| L2 | 大纲 | 提取文档主要章节和关键主题(优先使用文档原生标题树,无结构文本回退 LLM 生成) | 检索召回、上下文概览 |
|
||
| L3 | 内容大纲 | 每个章节的详细内容摘要 | 精确匹配、深度检索 |
|
||
|
||
**2.5 级回退**: 当文档内容不足以支撑三级总结时(如短文本、简单说明),自动降级为二级总结:
|
||
- L1: 总结(同上)
|
||
- L2.5: 内容大纲(跳过大纲层,直接输出详细摘要)
|
||
|
||
### 入库流程
|
||
|
||
```
|
||
文档输入 → 文本提取 → 三级总结(Ollama) → 分类判定(L1总结) → 向量化 → 写入Qdrant
|
||
↓
|
||
不足三级 → 2.5级回退
|
||
```
|
||
|
||
**文件上传通道**: 除 JSON 文本入库外,`POST /api/v1/documents/upload` 提供 multipart 文件上传入口,支持 .txt/.md/.html/.htm/.pdf/.docx;服务端调用 `app/core/file_parser.py` 按扩展名提取纯文本后,复用上述同一入库流水线;原始文件落盘到 `settings.upload_dir`(落盘失败仅告警不阻塞入库),任务状态同样经 `GET /api/v1/documents/tasks/{task_id}` 查询。
|
||
|
||
1. **文本提取**: 从文件中提取纯文本内容
|
||
2. **三级总结**: 调用 Ollama 依次生成 L1/L2/L3 总结;其中 L2 大纲优先使用文档原生标题树(不调 LLM),无结构文本(标题数 < 2)回退 LLM 生成
|
||
3. **分类判定**: 根据 L1 总结将文档分配到 taxonomy 类目,输出主类目 + 附加标签 + 置信度;LLM 输出解析失败或置信度低于阈值时归 uncategorized 兜底(低置信时候选类目名保留进 tags 供软召回)
|
||
4. **向量化**: 对原文 chunk 和各级总结分别生成 embedding(dense + sparse)
|
||
5. **写入 Qdrant**: 将文档元数据、各级总结、向量写入对应四层集合(L1/L2/L3/chunks)
|
||
|
||
### 本地模型 (Ollama)
|
||
|
||
- 服务: Ollama 容器,默认模型 `qwen2.5:1.5b`(约 1GB,适合 NAS 低资源环境)
|
||
- 备选模型: `qwen2.5:3b`(更好的总结质量,约 2GB)
|
||
- 模型通过环境变量 `OLLAMA_MODEL` 配置
|
||
- 首次启动时自动拉取模型,需 NAS 可访问外网
|
||
|
||
## Docker 部署说明
|
||
|
||
- 目标环境: NAS (ARM64/AMD64)
|
||
- 镜像: python:3.12-slim
|
||
- 持久化: NAS 本地目录挂载
|
||
- 端口: 默认 8000 (API), 6333 (Qdrant), 6379 (Redis), 11434 (Ollama)
|
||
- 健康检查: `/api/v1/health`
|
||
|
||
## 开发流程
|
||
|
||
1. 修改代码后本地测试: `uv run pytest`
|
||
2. 构建镜像: `docker compose build`
|
||
3. 启动服务: `docker compose up -d`
|
||
4. 查看日志: `docker compose logs -f app`
|