knowledge-hub

Knowledge Hub · 多源知识库与 RAG 助理

ci Python 3.13 FastAPI PostgreSQL+pgvector Qdrant React 19 License: MIT

多租户知识库服务:上传文档 / 照片 → 解析 → 切分 → 嵌入 → 四路索引(向量 + BM25 + 子串 + 视觉) → 带引用溯源的 RAG 问答与检索,并对外提供 MCP(给 AI 客户端)与 A2A(给别的 Agent)。 单机自部署、局域网可用;默认生成层是抽取式,答案句子全部摘自原文 ⇒ 引用 100% 可核验、零外传。

运行时数据在 data/(不进 git)。

   
契约 82 个端点声明 / 81 实现 / 1 明确不做;eval/contract_check.py 守路径·方法·参数面
测试 pytest 1335 · 前端 vitest 316 · 三套验收 1b 138 / 1c 218 / 1d 199
检索 四条腿 RRF 融合:dense(bge-small-zh)· BM25(自实现)· trgm · 视觉(colSmol late-interaction 多向量 MaxSim)
前端 React 19 + Vite 7 + Tailwind v4;零 CDN / 零 Web Font(内网离线可用)
规模 services ≈ 25k 行(90 个模块)· 测试 ≈ 23k 行(80 个文件)· 前端 ≈ 16k 行

下图为合成演示数据(自造的产品 / 运维 / 接口文档),不含任何真实资料。

对话(角标可点开核对原文) 引用抽屉(逐条命中片段 + 原文出处)
对话页 引用抽屉
知识库 · 文档(拖拽上传 + 状态 + 进度) 检索调试(四路召回 + RRF 过程)
文档 调试
知识库列表 设置 · 外观(深色)
列表 外观

这是什么 / 解决什么问题

个人或小团队手上有一堆散落的多源资料(办公文档、聊天记录、照片), 想”问一句话就能定位到原文那一页那一句”。这类需求有三条不能让步:

  1. 可核验 —— 每个结论都要能点回原文(所以引用做成了 L3 级别:页图 + 命中框)
  2. 不外传 —— 默认不把私人原文发给任何外部服务(抽取式生成层;接外部 LLM 必须显式配置)
  3. 可运维 —— 单机自部署,有任务队列、进度、索引对账、备份与恢复演练

架构(30 秒)

        前端 React 19 / Vite(对话 · 知识库 5 个 tab · 设置 7 分区)
                              │ REST + SSE(Bearer API Key)
┌──────────────┐   ┌──────────▼──────────────────────────────┐
│ worker       │   │ API(FastAPI)                           │
│ 摄取 / 清理   │◀─▶│ · 租户过滤**唯一来源** ctx.readable_kb_ids │
│ (任务队列)   │jobs│ · 检索只有一套实现 services/retrieval/    │
└──────┬───────┘   └───────┬──────────────────┬──────────────┘
       │ 视觉编码(可远端)   │                  │
┌──────▼────────┐   ┌───────▼────────┐  ┌──────▼─────────┐
│ visual_service│   │ PostgreSQL     │  │ Qdrant         │
│ colSmol 常驻  │   │ + pgvector     │  │ dense/sparse/  │
│ 只做编码      │   │ (真相来源)     │  │ visual 三集合   │
└───────────────┘   └────────────────┘  └────────────────┘
   MCP(4 工具)/ A2A(2 skill)复用同一套检索内核

亮点(都能在仓库里找到实现与守卫)

快速开始

bash deploy/dev.sh start      # 起 API(8010) + worker + 视觉查询服务(8020)
bash deploy/dev.sh stop
bash deploy/dev.sh restart    # 改了后端代码之后用这个(见下方 ⚠️)
PKH_WORKERS=4 bash deploy/dev.sh start   # 多 worker(需 VISUAL_WRITE_VIA_SERVICE=true)

# 视觉服务也可**单独**管理(2.65GB 的那一个;脚本化,不用 systemd):
bash deploy/visual.sh {start|stop|restart|status|logs}

# ⚠️ `dev.sh` **只有** start / restart / stop 三个子命令(没有 status/logs)。看状态:
curl -s --noproxy '*' http://127.0.0.1:8010/health   # 队列/索引状态(公开版)
tail -20 /tmp/pkh_api.log /tmp/pkh_worker.log /tmp/pkh_visual.log   # 三个进程三个日志

⚠️ 视觉查询服务首次启动要 13s(加载 colSmol,约 2.65GB),dev.sh 等不到它也会正常退出 —— 那段时间里视觉腿降级成 skipped,检索照常。不想要它:VISUAL_SERVICE_URL=(留空)关掉读侧,写入侧不受影响。

⚠️ 改了后端代码要 bash deploy/dev.sh restart;只 kill 进程组 leader 杀不干净子进程, 旧 worker 会继续用旧代码处理任务(症状:”改了没生效”)。

验收:本机全量 vs CI 门禁

  跑什么 为什么这么分
本机全量 uv run pytest(1335)· 三套 eval(138 / 218 / 199)· monitor.py 十四项 需要真实 Postgres(pgvector) + Qdrant + 本地缓存的模型(bge-small ~100MB;视觉腿 colSmol 2.65GB),而本项目有一条硬约束是模型离线加载
CI 门禁 bash deploy/ci.sh:前端 typecheck + vitest + build · 契约覆盖率 · 443 项免基础设施单测 不需要 DB / 模型 / 一个在跑的服务 / 特定 locale ⇒ 在托管 CI 上必然绿灯;把全量塞进 CI 只会得到”视觉腿静默 skipped”的假绿灯

🔑 CI 与本地跑的是同一份命令(workflow 只是调 deploy/ci.sh):

uv run pytest                  # 1335 项
uv run python eval/contract_check.py
uv run python eval/api_1b_check.py   # 138 项
uv run python eval/api_1c_check.py   # 218 项
uv run python eval/api_1d_check.py   # 199 项
cd web && npm test             # 316 项
uv run python deploy/monitor.py       # 十四项巡检
bash deploy/ci.sh              # CI 那一道门禁(本地同款)

目录结构

services/
  api/          FastAPI:routers/(端点)+ main.py + static_site.py(前端托管)
  common/       models / auth(租户过滤唯一来源)/ deps / jobs / storage / consistency(对账)
  ingest/       parsers.py(解析器派发表)/ strategies(切分策略)/ validate(分层校验)/
                chunker / locations / versions / contextual / purge / privacy(归属判定)
  retrieval/    fusion(RRF 融合)/ dense / sparse / bm25 / visual / rerank
                ask / llm / routing / pool / trgm
  visual_service/ 常驻 colSmol 的**只做编码**的进程:POST /encode · /encode_image + /health
  worker/       handlers/(摄取与清理任务)+ doc_state.py(终态判定)
  mcp/          MCP Server(4 个工具)
web/            React + Vite 前端(架构与规矩见 web/README.md)
docs/           API 契约(DESIGN-API.md)+ 读者文档 + 图源(diagrams/)+ 界面截图(img/)
deploy/         dev.sh / ci.sh / monitor.py / backup.sh / restore.sh / seed_photos.py
                measure_visual_leg.py / verify_*.py(手工验证脚本,不在基线里)
eval/           三套验收脚本 + contract_check.py + _fixtures.py + _reset.py
                检索质量回放(retrieval_eval.py / retrieval_metrics.py)
                ⚠️ 真实数据**不进仓库**:`_qset.py`(问题集)· `_local_data.py`(探针 PDF/问题/标记)·
                   `reports/` 都只留本地;仓库里只有 `*.example.py` 模板。
                守卫:`tests/test_no_real_data.py`(按本地 deny-list 扫已跟踪文件)
tests/          pytest

文档

契约在 docs/DESIGN-API.md:82 个端点、错误码、分页,改端点先改它 —— eval/contract_check.py 会逐条校验。

面向读者的文档在 docs/ 下,按“先懂边界,再看链路”的顺序读:

文档 回答什么
docs/README.md 导读:定位、三条链路、目录、验收命令、已知限制
docs/architecture.md 三个进程与端口、模块依赖、PG/Qdrant/blob 的数据平面
docs/ingestion.md 上传到 ready 的每一步、格式派发、六种切分、失败分类
docs/retrieval.md 四腿召回、RRF、查询路由、精排、降级与实测指标
docs/answering.md SSE 帧序、证据组装、抽取式/外部 LLM、引用与多轮
docs/data-model.md 18 张表、内容寻址、软删与版本、删除链、对账与重建
docs/tenancy.md API Key 推导上下文、三档可见性、两个过滤入口
docs/operations.md 八种任务、文档终态、巡检、备份恢复、蓝绿重建
docs/interfaces.md HTTP/MCP/A2A 如何共用检索与问答内核
docs/testing.md 测试分层、CI 门禁、三套验收、结构守卫与盲区
docs/failure-modes.md 检索降级、摄取失败、状态卡住、索引漂移、越权形状

图源在 docs/diagrams/src/,导出 PNG 2x 在 docs/diagrams/。改图先改 .drawio,再重新导出;正文只引用导出产物。

五条硬约束(违反即错)

  1. 运行时只有一棵依赖树 —— 别引入第二套实现
  2. 租户过滤只有两个入口,都在 services/common/auth.py (build_qdrant_filter / build_kb_scope_sql / visible_kb_clause / writable_kb_clause)
  3. 检索实现唯一:services/retrieval/
  4. 无权限 ≡ 不存在(KB_NOT_FOUND / 403,不区分”不存在”与”没权限”)
  5. API Key 是唯一租户来源

隐私设计(第一卖点)

子目录的 README

文件 什么时候读
web/README.md 前端:目录、样式系统、主题、必须保住的坑
deploy/README.md 部署、巡检、备份、对外访问

已知限制

许可与第三方