Files
amb_rag/docs/technical-review.md
T
2026-09-01 11:53:59 +08:00

24 KiB
Raw Blame History

AI Knowledge Link — 技术审查报告(MVP v1)

审查范围:需求文档全文(57 节)。 核心原则:MVP 保持简单,6 个扩展接口预留但不实现,SQLite + 本地文件 + 同步解析。 结论:架构成立,核心需求可实现。发现 1 个必须修改的设计、若干建议补充。


1. 最终架构

1.1 架构总览

开发环境(Windows                    生产环境(Ubuntu Docker)
─────────────────                    ─────────────────────
Vue3 dev server  ─┐                  nginx :80
                  ├─► FastAPI :8000       ├── /api/**  → backend:8000
uvicorn --reload ─┘                     ├── /k/**    → backend:8000
                                         └── /        → frontend:80

FastAPI                              Docker Compose:
 ├── /api/**  管理端(需登录)           ├── frontend  (Vue build → nginx serve)
 ├── /k/**    公共AI页(SSR, 零JS      ├── backend   (uvicorn)
 └── SQLite + 本地文件系统               └── nginx

/data/                               /data/ (宿主机 volume)
 ├── app.db                              ├── app.db
 └── users/{uid}/kbs/{kb}/...           └── users/{uid}/kbs/{kb}/...
      ├── original/                        ├── original/
      └── markdown/                        └── markdown/

1.2 分层

api/         → 路由层:参数校验 + 鉴权 + 调 Service + 组装响应
services/    → 业务逻辑:配额、Token、编排
repositories/→ 数据访问:隔离 SQLAlchemy 细节
processors/  → 文档解析:MarkItDown → PyMuPDF/python-docx fallback
storage/     → 存储抽象:LocalStorageService(未来 MinIO/S3
retrieval/   → 检索抽象:KeywordRetriever(未来 Vector/Hybrid
public/      → AI 公共页面渲染:Jinja2 SSR + md/txt/json 生成器

1.3 关键技术决策

决策点 选择 理由
数据库 SQLite + SQLAlchemy + Alembic MVPDATABASE_URL 可切换 PostgreSQL(§7 扩展接口)
文件存储 本地文件系统 + StorageService 抽象 MVP;不引入 MinIO(§8 扩展接口)
文档解析 同步 + DocumentProcessor 抽象 MVP 单用户低并发;未来 Celery(§9 扩展接口)
检索 SQLite FTS5 + Retriever 抽象 中文 FTS5 支持 better than LIKE;未来 Qdrant(§10 扩展接口)
鉴权 服务端 Session(签发 opaque token 存 SQLiteCookie 传递) 同源 SPA;比 JWT 简单且可即时吊销;SQLite 即 session store
密码哈希 Argon2idargon2-cffi 需求指定
限流 内存 TokenBucket(单进程够用) + RateLimiter 抽象 无 Redis 依赖;未来 RedisRateLimiter(§39
公共页渲染 Jinja2 SSR,零 JS 零外链 AI 兼容性第一原则
Token 存储 SHA-256 hashDB+ Fernet 加密原文(可解密,SECRET_KEY 派生) 满足"后台显示完整链接"需求;DB 泄漏 ≠ 链接泄漏(需 + SECRET_KEY
后缀路由 显式注册 /k/{token}.md / .txt / .json 先于 /k/{token} FastAPI path param 默认匹配 [^/]+{token} 会吞掉 .md(§19
Python 版本 3.12+ 编写;本机 3.13.9 运行 FastAPI / SQLAlchemy 2 / Pydantic v2 均已支持
PyMuPDF 许可 MVP 阶段可接受(AGPL-3.0);商品化前必须替换为 pypdfium2 需求 §24 明确指定 PyMuPDFAGPL 对未分发的 SaaS 后端(仅服务端运行)在多数法域下合规,但商品化闭源分发时需替换。MVP 阶段标记 TODO 并在 requirements 中注释

2. 数据库 ER 设计

2.1 实体关系

plans 1 ──── N users 1 ──── N knowledge_bases 1 ──── N documents 1 ──── 1 document_categories
                                    │        │
                                    │        └ 1 ──── N access_logs
                                    └ 1 ──── N (配额归集)

2.2 表定义

plans

类型 说明
id integer PK
code text unique free / basic / pro
name text 套餐名
storage_quota integer 字节;free = 104857600 (100MB)
max_file_size integer 字节;free = 20971520 (20MB)
is_active boolean

users

类型 说明
id text PK (UUID4 hex)
username text unique not null
email text unique not null
password_hash text not null Argon2id
status text active / disabled
plan_id integer FK → plans 默认 free
storage_used integer default 0 字节计数器
created_at / updated_at text (ISO8601)

knowledge_bases

类型 说明
id text PK (UUID4 hex)
user_id text FK → users not null 隔离边界
name text not null
description text
enabled boolean default true 链接开关
token_hash text unique not null SHA-256(secret_token) hex
token_encrypted text Fernet 加密原文(可解密供后台显示)
token_hint text token 末 8 位明文,供后台识别
created_at / updated_at text

SQLite 不支持 citext / timestamptz / uuid 原生类型,统一用 text + 应用层校验。 迁移到 PostgreSQL 时,Alembic 迁移负责类型映射,业务层无感。

document_categories

类型
id text PK, knowledge_base_id text FK, name text not null, sort_order integer, created_at text

documents

类型 说明
id text PK (UUID4 hex)
knowledge_base_id text FK
user_id text FK 冗余存,便于隔离校验 + 配额统计
category_id text FK nullable
original_filename text 仅展示用,不用于物理路径
storage_path text 相对于 data/ 的物理路径,不外泄
markdown_path text 相对于 data/ 的 Markdown 文件路径
file_size integer 字节
mime_type text
file_ext text .docx / .pdf
sha256 text 去重与完整性
doc_token_hash text unique 单文档公共 URL 凭证
doc_token_encrypted text Fernet 加密
doc_token_hint text
title text 用户可改,默认从 Markdown H1 提取
description text 用户可改,默认自动生成摘要
keywords text 逗号分隔,jieba 提取 + 用户可改
content_summary text 抽取式摘要 ~200 字
status text PENDING / PROCESSING / READY / FAILED / DELETED
error_code text 安全错误码(如 SCANNED_PDF_NO_TEXT_LAYER
created_at / updated_at text

access_logs

类型
id integer PK AUTOINCREMENT, knowledge_base_id text FK, document_id text nullable FK, path text, accessed_at text, user_agent text, request_type text

SQLite FTS5 虚拟表(documents_content)单独建,与 documents 通过 id 关联。

2.3 索引

  • knowledge_bases(token_hash) unique
  • documents(doc_token_hash) unique
  • documents(knowledge_base_id, status)
  • documents(user_id, status)
  • users(username) / users(email) unique

3. API 设计

3.1 管理端(/api,需登录)

POST   /api/auth/register                {username, email, password} → 201
POST   /api/auth/login                   {username|email, password} → Set-Cookie + 200
POST   /api/auth/logout                  清除 Cookie
GET    /api/me                           用户信息 + 套餐
PATCH  /api/me                           改密码

GET    /api/knowledge-bases              分页列表
POST   /api/knowledge-bases              {name, description} → 201(返回完整 AI URL,仅此一次)
GET    /api/knowledge-bases/{id}         详情
PUT    /api/knowledge-bases/{id}         改名/改描述
DELETE /api/knowledge-bases/{id}         软删 + 异步清理文件

POST   /api/knowledge-bases/{id}/regenerate-token   旧链立即失效
POST   /api/knowledge-bases/{id}/enable
POST   /api/knowledge-bases/{id}/disable

GET    /api/knowledge-bases/{id}/documents           文档列表
POST   /api/knowledge-bases/{id}/documents/upload    multipart 上传
GET    /api/documents/{id}               详情 + markdown 预览
PUT    /api/documents/{id}               改 title/description/keywords/category
DELETE /api/documents/{id}               软删 + 清理文件
POST   /api/documents/{id}/reprocess     重新解析

GET    /api/knowledge-bases/{id}/categories    分类 CRUD
GET    /api/storage                       {storage_used, storage_quota}

统一错误体:{"code": "STORAGE_QUOTA_EXCEEDED", "message": "存储空间不足", "detail": null}

3.2 公共 AI 端(/k,无登录,限流)

GET  /k/{token}                       HTML 入口页(Jinja2 SSR
GET  /k/{token}.md                    Markdown 入口
GET  /k/{token}.txt                   纯文本入口
GET  /k/{token}.json                  JSON 目录(白名单字段)

GET  /k/{token}/search?q=&page=       HTML(默认)/ .json

GET  /k/{token}/doc/{doc_token}       文档 HTML 页
GET  /k/{token}/doc/{doc_token}.md    原始 Markdown
GET  /k/{token}/doc/{doc_token}.txt   纯文本

公共端硬性规则:

  1. token 不存在 / enabled=false / 软删 → 一律 404
  2. <meta name="robots" content="noindex,nofollow,noarchive">
  3. <meta name="referrer" content="no-referrer">
  4. JSON 输出白名单:name, description, documents[{title, file_type, description, summary, keywords, updated_at, url}]
  5. 限流:内存 TokenBucket,单 IP 30/min,单 token 60/min

4. 项目目录

amb_rag/
├── backend/
│   ├── app/
│   │   ├── main.py                  # app 工厂、路由挂载、异常处理、lifespan
│   │   ├── api/                     # 管理端路由: auth.py, knowledge_bases.py, documents.py, me.py, deps.py
│   │   ├── public/                  # 公共AI路由: routes.py, render.py, serializers.py
│   │   ├── core/                    # config.py, security.py, errors.py, session.py, rate_limit.py, logging.py
│   │   ├── models/                  # SQLAlchemy 2.0 (Mapped/mapped_column)
│   │   ├── schemas/                 # Pydantic v2 request/response
│   │   ├── services/                # auth_service.py, kb_service.py, doc_service.py, kb_public_service.py, storage_service.py
│   │   ├── repositories/            # user_repo.py, kb_repo.py, doc_repo.py, access_log_repo.py
│   │   ├── processors/              # base.py(DocumentProcessor), local_processor.py, parsers/markitdown_parser.py, pdf_parser.py, docx_parser.py
│   │   ├── retrieval/               # base.py(Retriever), keyword_retriever.py
│   │   ├── storage/                 # base.py(StorageService), local_storage.py
│   │   └── templates/               # kb_index.html.j2, doc_page.html.j2
│   ├── alembic/                     # env.py + versions/
│   ├── tests/                       # conftest.py + 各模块测试
│   ├── requirements.txt
│   ├── pyproject.toml               # ruff + mypy + pytest 配置
│   └── Dockerfile
├── frontend/
│   ├── src/
│   │   ├── api/                     # axios 实例 + 各资源 client
│   │   ├── views/                   # Login, Register, Dashboard, KnowledgeBases, KbDetail, Settings
│   │   ├── components/
│   │   ├── layouts/
│   │   ├── stores/                  # Pinia stores
│   │   ├── router/
│   │   └── types/
│   ├── package.json
│   ├── vite.config.ts
│   ├── tsconfig.json
│   └── Dockerfile
├── nginx/
│   └── nginx.conf
├── data/                             # .gitkeep;运行时生成
│   └── .gitkeep
├── docs/
│   └── technical-review.md           # 本文件
├── docker-compose.yml
├── .env.example
├── .gitignore
├── CLAUDE.md
└── README.md

5. 六个未来扩展接口设计

5.1 SQLite → PostgreSQL

影响
core/config.py DATABASE_URLsqlite:///./data/app.db 切为 postgresql+asyncpg://...;引擎换 async
core/db.py 同步引擎 → 异步引擎 + async_sessionmakerget_session 改 async generator
repositories/ 现有同步调用改 awaitSQLAlchemy 2.x 统一风格使改动最小
Alembic 新增迁移处理类型映射(text→varchar/citext/uuid, integer→bigint
业务层 零改动Repository 已隔离)

第一版必须遵守的规则

  • 不用 PRAGMA 做业务逻辑(只用 WAL 模式设置)
  • 不用 SQLite 专属函数(group_concat → SQLAlchemy func 通用)
  • UUID 存 text 而非 blob(跨 DB 兼容)
  • 时间存 ISO8601 text 而非 SQLite datetime 函数
  • 布尔存 integer 0/1SQLAlchemy Boolean 在 SQLite 自动映射)

5.2 本地文件 → MinIO/S3

# storage/base.py
class StorageService(Protocol):
    async def save(self, key: str, data: bytes | IO) -> str: ...
    async def read(self, key: str) -> bytes: ...
    async def delete(self, key: str) -> None: ...
    async def exists(self, key: str) -> bool: ...
    async def get_size(self, key: str) -> int: ...
  • LocalStorageServicekey 即相对路径,拼 data/ 前缀;save → 写文件,read → 读文件。
  • MinioStorageServicekey 即对象 keysave → minio.put_objectread → minio.get_object。
  • 业务层通过 core/config.py 中的 STORAGE_BACKEND 选择实现,注入 Service。
  • 第一版:所有 open() / Path() 操作只在 local_storage.py 中出现。

5.3 同步解析 → 异步任务

# processors/base.py
class DocumentProcessor(Protocol):
    async def process(self, document_id: str) -> None: ...
  • LocalDocumentProcessor:同步调用 parsers,在同一进程中完成。
  • CeleryDocumentProcessor:提交 Celery task,轮询状态。
  • 第一版:上传 endpoint 中调 await processor.process(doc_id)(同步包在 run_in_executor 里避免阻塞事件循环)。
  • 文档状态机(PENDING→PROCESSING→READY/FAILED)在两个实现中完全相同。

5.4 关键词搜索 → RAG

# retrieval/base.py
class Retriever(Protocol):
    async def search(self, query: str, knowledge_base_id: str, limit: int = 10) -> list[SearchResult]: ...
  • KeywordRetrieverSQLite FTS5 MATCH 查询 + snippet() 高亮。
  • VectorRetrieverEmbedding + Qdrant search。
  • HybridRetriever:关键词 + 向量加权融合。
  • 第一版FTS5 在 Alembic 迁移中建虚拟表 documents_fts;写入时同步更新 FTS。

5.5 个人 → 团队/企业

  • 第一版knowledge_bases.user_id 是 owner;所有查询 WHERE user_id = :me
  • 扩展:新增 organizations 表 + org_membersrole);knowledge_bases.owner_type + owner_id(多态外键);Repository 查询加 owner_scope 参数。
  • 关键:第一版不把 user_id 硬编码成无法泛化的东西(如拼接进 URL 路径)。

5.6 网页访问 → API/MCP

  • public/ 路由调用 KnowledgeBasePublicService 获取数据 → 由 RendererHTML/MD/TXT/JSON)格式化输出。
  • 扩展:新增 /api/v1/public/... 路由,鉴权用 API Key复用同一个 Service
  • MCP Server 是独立进程,调 Repository 层。
  • 关键:公共数据获取逻辑只在 Service 中写一次,Renderer 各格式只做序列化。

6. 安全模型

6.1 Secret Token

生成:token = secrets.token_urlsafe(16)   # 22 字符, 128bit 熵
存储:knowledge_bases.token_hash = SHA-256(token).hexdigest()
      knowledge_bases.token_encrypted = Fernet(SECRET_KEY 派生).encrypt(token)
      knowledge_bases.token_hint = token[-8:]
校验:请求 token → 正则校验 → SHA-256 → 单条索引查询 → enabled/软删检查 → 404 或放行
轮换:regenerate → 新 token,旧 hash 失效,旧缓存失效

安全属性:

  • 128bit 熵 + 限流 → 暴力枚举不可行
  • DB 泄漏 ≠ 链接泄漏(需 + SECRET_KEY
  • 统一 404 防存在性探测
  • <meta name="referrer" content="no-referrer"> 防文档页跳出时 Referer 泄漏 token

6.2 IDOR 防护

所有管理端 API 在 Service 层强制校验 resource.user_id == current_user.id;不匹配 → 404(不暴露存在性)。

6.3 文件上传安全

  • 扩展名白名单(.docx, .pdf
  • MIME 嗅探(filetype 库,前 8KB
  • 随机物理文件名(secrets.token_urlsafe(8)_safe_original_name
  • 路径穿越清洗(取 basename,剔除 .. / \ / 控制字符)
  • 文件大小前置校验(Nginx client_max_body_size + 应用层双重保险)

6.4 XSS 防护

  • AI 公共页:Markdown → HTML 走 Jinja2 {{ content | safe }},但 Markdown 本身是服务端生成(非用户直接输入 HTML),风险可控;用户手改 Markdown 时需 sanitizebleach 或 markdown 配置 sanitize=True)。
  • 管理端:Vue SPAAPI 返回 JSONElement Plus 组件默认 escape。

6.5 Session 安全

  • Cookie: HttpOnly, SameSite=Lax, 生产环境 Secure
  • Session token: secrets.token_urlsafe(32), 存 SQLite sessions 表(或内存 dict — MVP 足够)
  • 过期: 7 天不活动自动清除

7. 文档处理流程

[HTTP 上传](同步返回)
  鉴权 → KB 归属 → MIME 嗅探 → 扩展名白名单 → 大小校验
  → 配额原子扣减(UPDATE users SET storage_used = storage_used + :size WHERE id=:uid AND storage_used + :size <= storage_quota
    失败 → STORAGE_QUOTA_EXCEEDED / FILE_TOO_LARGE
  → 流式 SHA256(边读边算,内存峰值 = 一个 chunk
  → StorageService.save() 保存原始文件(随机文件名)
  → Document(PENDING) 入库
  → DocumentProcessor.process(doc_id) 同步解析:
      ┌ .pdf  → markitdown → 失败/空 → PyMuPDF fallback → 仍空 → SCANNED_PDF_NO_TEXT_LAYER
      ├ .docx → markitdown → 失败 → python-docx fallback
      └ 其他  → Phase 2+ 扩展
      → Markdown 清洗(压空行、规整标题层级、截超长行)
      → 提取 H1 → title(未填时)
      → 抽取前 ~200 字纯文本 → content_summary
      → jieba.analyse.extract_tags top-10 → keywords
      → StorageService.save() 保存 Markdown 文件
      → 更新 FTS 索引
      → Document(READY)
    任何异常 → Document(FAILED, error_code),用户只看友好文案
  → 返回 201 + Document 信息

同步解析的注意事项:大 PDF 解析可能耗时数秒,会阻塞当前请求。MVP 阶段可接受(单用户低并发),但必须:

  1. 设置请求级超时(uvicorn --timeout-keep-alive 不够,需应用层 middleware 或 nginx proxy_read_timeout
  2. 前端上传组件显示 loading + 预估时间
  3. 未来切 Celery 时,上传立即返回 202,前端轮询状态

8. AI 访问流程

用户 → 在 AI 对话中粘贴 https://example.com/k/7fA92xKpQ8m... + 描述需求
AI 抓取器 → GET /k/7fA92xKpQ8m...(无 Cookie/JS
  服务端:限流 → SHA256(token) → 查 KB → 未命中 404
  → 渲染入口页:
     · KB 名称/描述
     · 文档列表(每条:标题、类型、描述、一句话摘要、关键词、更新时间、完整文档 URL)
     · 分页(≤50 条/页)
     · 页脚双语机器可读说明
AI → 根据用户描述匹配文档 → GET /k/{token}/doc/{doc_token}.md(优先 .md
  或 .html;需精确定位 → /k/{token}/search?q=
AI → 汇总回答用户

兜底:若 AI 不跟随链接,用户可直接粘贴文档级 URL。README 必须说明。


9. Windows 开发流程

# 后端
cd backend
python -m venv .venv
# Git Bash:
source .venv/Scripts/activate
# 或 CMD:
.venv\Scripts\activate
pip install -r requirements.txt
# 初始化数据库
alembic upgrade head
# 启动
uvicorn app.main:app --reload --port 8000
# 访问
# http://localhost:8000/api/health  → 健康检查
# http://localhost:8000/api/docs    → Swagger(开发期)

# 前端
cd frontend
npm install
npm run dev
# http://localhost:5173
# Vite proxy: /api → localhost:8000, /k → localhost:8000

关键data/ 目录在首次运行时由 backend 自动创建(os.makedirs);.env 仅生产需要(开发期 config 用默认值)。


10. Docker 生产流程

# docker-compose.ymlUbuntu 服务器)
services:
  frontend:    # multi-stage: node build → nginx:alpine serve dist
  backend:     # python:3.12-slim → uvicorn
  nginx:       # 反代 /api + /k → backend:8000/ → frontend:80

volumes:
  - ./data:/app/data    # SQLite + 用户文件,宿主机持久化
# 部署
git clone <repo> /opt/ai-knowledge-link
cd /opt/ai-knowledge-link
cp .env.example .env   # 修改全部密钥
mkdir -p data
docker compose up -d --build

# 更新
git pull
docker compose up -d --build    # 数据在 ./data 不受影响

# 备份
sqlite3 data/app.db ".backup data/backup/app_$(date +%Y%m%d).db"
tar czf data/backup/files_$(date +%Y%m%d).tar.gz data/users/

11. 风险清单

# 风险 等级 缓解
R1 同步解析阻塞请求:大 PDF 解析耗时数秒到十几秒 MVP 可接受;前端显示 loading;设请求超时 60s;未来切 Celery
R2 SQLite 并发写锁:多用户同时上传时 WAL 模式允许并发读但写仍串行 PRAGMA journal_mode=WAL;写操作短事务;未来切 PostgreSQL
R3 PyMuPDF AGPL-3.0:商品化闭源分发时许可风险 MVP 阶段可接受(仅服务端运行,不分发);requirements 中注释标记;商品化前换 pypdfium2
R4 SQLite FTS5 中文分词FTS5 的 unicode61 tokenizer 对中文按字分词(非词),召回率低于 jieba 第一版仍用 FTS5(比 LIKE 好很多);关键词字段用 jieba 分词写入辅助;Phase 9 验收时若不满意可切换到 jieba 预分词 + LIKE 方案
R5 FastAPI 后缀路由被 path param 吞掉 显式后缀路由注册在无后缀路由之前;token 用正则 ^[A-Za-z0-9_-]{22}$
R6 内存限流单进程局限 MVP 单 uvicorn worker 够用;未来多 worker/容器需 Redis 限流
R7 Markdown XSS:用户手改 Markdown 注入 <script> Jinja2 渲染 AI 页时 sanitizebleach 或 Markdown 库 safe 模式);管理端 Vue 默认 escape
R8 配额并发超额:SQLite 写锁串行化已天然防竞态,但应用层"先查后写"仍有窗口 原子 SQL UPDATE ... WHERE storage_used + :n <= quota

12. 建议修改 / 补充

  1. 【建议】Token 加密存储Fernet)以满足"后台显示完整链接"。需求 §16 说"只在生成时提供",但 §33 需要"复制 AI 链接"按钮 → 后台需能取回原文。两处表述矛盾,Fernet 方案同时满足。
  2. 【建议】入口页每篇文档附一句话摘要:显著提高 AI 不点进去也能判断相关性的概率;代价是页面变大,用 50 条/页分页对冲。
  3. 【建议】公共页加 <meta name="referrer" content="no-referrer">:需求未提,防文档页跳出时 Referer 泄漏 token。
  4. 【建议】文档 Token 也用 Fernet 加密存储:与 KB Token 同方案,便于构建文档 URL。
  5. 【建议】Session 用内存 dict + TTL 而非 SQLite 表:MVP 更简单,重启丢失可接受;未来切 Redis 时无缝。
  6. 【明确化】需求 §24 指定 PyMuPDF:MVP 阶段接受,但必须在代码中标记 # TODO: 商品化前替换为 pypdfium2AGPL 风险)
  7. 【明确化】配额扣减必须用原子 SQL,不能"先查 storage_used 再写"(即使 SQLite 串行写也尽量用原子语句,养成习惯以备 PostgreSQL 切换)。

13. 开发阶段确认

按需求 §56,共 17 个 Phase。技术审查通过后从 Phase 1(项目初始化)开始,每 Phase 完成必须验证后再进入下一 Phase。