1. add `/api/v1/documents/{doc_id}/file` endpoint for downloading original document files
2. add file field to document detail API response based on metadata
3. add comprehensive tests for file download API and metadata integration
4. update API documentation in CLAUDE.md
12 KiB
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 融合)。
- query 解析路由: 用 Ollama 小模型将 query 解析为结构化结果(rewrite、关键词、命中类目+置信度);高置信且类目数不超上限时按类目过滤,低置信/解析失败/类目过多则全库兜底(不丢召回)
- L1 - 文档定位层: 在文档 L1 总结集合中检索,产出候选文档集合;无命中时直接全库 chunk 兜底(fallback=True)
- L2 - 章节大纲层: 在候选文档内检索 L2 章节大纲,按 section 剪枝定位;未命中的文档(含 2.5 级文档)落入 L3 b 路(仅按 doc 过滤)
- L3 - 小节内容层: 两路查询(a:L2 命中文档按 section_path 过滤;b:其余文档仅按 doc_id 过滤)后 RRF 融合;无命中时 chunk 层回退为 L1 候选文档级检索
- 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} |
文档详情 | 免登录 |
| GET | /api/v1/documents/{doc_id}/file |
下载关联的原始文件(免登录) | 免登录 |
| 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} 查询。
- 文本提取: 从文件中提取纯文本内容
- 三级总结: 调用 Ollama 依次生成 L1/L2/L3 总结;其中 L2 大纲优先使用文档原生标题树(不调 LLM),无结构文本(标题数 < 2)回退 LLM 生成
- 分类判定: 根据 L1 总结将文档分配到 taxonomy 类目,输出主类目 + 附加标签 + 置信度;LLM 输出解析失败或置信度低于阈值时归 uncategorized 兜底(低置信时候选类目名保留进 tags 供软召回)
- 向量化: 对原文 chunk 和各级总结分别生成 embedding(dense + sparse)
- 写入 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
开发流程
- 修改代码后本地测试:
uv run pytest - 构建镜像:
docker compose build - 启动服务:
docker compose up -d - 查看日志:
docker compose logs -f app