Files
kplam a1b80566bb feat: 升级 Ollama 本地模型 qwen2.5:1.5b → qwen3:1.7b
- config.py / .env.example / docker-compose.yml:默认模型改为 qwen3:1.7b(备选 qwen3:4b),compose 同步拉取
- 经 runtime_settings 注入 summarize/query/classify 三用途,改默认值即全局生效
- README / CLAUDE.md / 前端 Settings.vue 占位符同步更新
2026-08-01 00:28:50 +08:00

12 KiB
Raw Permalink Blame History

QMDSearch - AI Agent 分层信息检索服务

项目概述

QMDSearch 是面向 AI Agent 的分层信息检索服务,支持多层级知识库检索、向量语义搜索和结构化数据查询。基于 Docker 部署在 NAS 上,为 AI Agent 提供高效、精准的信息检索能力。

技术栈

  • 语言: Python 3.12+
  • Web 框架: FastAPI
  • 向量数据库: Qdrant (Docker)
  • 缓存: Redis (Docker)
  • 关系数据库: PostgreSQL (Docker, 可选)
  • 嵌入模型: OpenAI / 本地模型 (通过配置切换)
  • 本地推理模型: Ollama (Docker) — 用于文档三级总结,默认 qwen3:1.7b
  • 部署: 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 + sparseRRF 融合)。

  1. query 解析路由: 用 Ollama 小模型将 query 解析为结构化结果(rewrite、关键词、命中类目+置信度);高置信且类目数不超上限时按类目过滤,低置信/解析失败/类目过多则全库兜底(不丢召回)
  2. L1 - 文档定位层: 在文档 L1 总结集合中检索,产出候选文档集合;无命中时直接全库 chunk 兜底(fallback=True
  3. L2 - 章节大纲层: 在候选文档内检索 L2 章节大纲,按 section 剪枝定位;未命中的文档(含 2.5 级文档)落入 L3 b 路(仅按 doc 过滤)
  4. L3 - 小节内容层: 两路查询(aL2 命中文档按 section_path 过滤;b:其余文档仅按 doc_id 过滤)后 RRF 融合;无命中时 chunk 层回退为 L1 候选文档级检索
  5. 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 附 resultfailed 附 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 tokenTTL 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
PATCH /api/v1/auth/users/{username} 更新用户角色/启用状态(仅 admin) 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 tokenadmin/user 角色均可),查询类端点免登录。

入库任务状态持久化在 Redis(key: ingest_task:{task_id}):进行中与 done 保留 24hfailed 保留 7 天;Redis 不可用时降级为纯内存。

管理页面

浏览器访问 /admin,页面带登录门禁(未登录/token 失效自动回登录卡片;must_change_password 用户先强制改密后方可进入)。单页面含八个区块(admin 视角;user 角色无用户管理区块):概览、个人中心(user 角色默认进入且可见,含账号信息/改密/退出)、文档管理(列表/详情/删除)、文档入库(文本 + 文件上传)、检索测试台、类目列表、API 指南(端点清单 + 在线测试台)、用户管理(仅 admin 角色挂载,含搜索筛选/行内编辑角色与启用状态/弹窗式重置密码与删除)。

认证与用户

会话制认证:登录签发 session tokenRedis 持久化,TTL 12h;Redis 不可用时降级为进程内存,重启失效),请求经 Authorization: Bearer <token> 携带,鉴权依赖见 app/api/deps.py

  • 角色与权限边界: admin 拥有全部权限(含 /auth/users* 用户管理);user 可登录并调用变更类文档端点(入库/上传/删除),访问用户管理端点返回 1006
  • 启用状态与 PATCH 端点: 用户记录含 enabled 字段(默认 true);admin 可经 PATCH /auth/users/{username} 更新角色或启用状态(请求体至少一项字段,空 body 1001;role 非法 1001;用户不存在 1004)。禁用用户时清除其全部 session(旧 token 立即失效 1005),被禁用用户登录拒绝 1005("账号已禁用");最后一个 admin 禁止降级 role 或被禁用(1001)。角色变更不清 session,同一 token 即时反映新角色
  • 初始 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 和各级总结分别生成 embeddingdense + sparse
  5. 写入 Qdrant: 将文档元数据、各级总结、向量写入对应四层集合(L1/L2/L3/chunks

本地模型 (Ollama)

  • 服务: Ollama 容器,默认模型 qwen3:1.7b(约 1.3GB,适合 NAS 低资源环境)
  • 备选模型: qwen3:4b(更好的总结质量,约 2.5GB
  • 模型通过环境变量 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