# KnowHub 决策记录

记录设计过程中的关键决策及推演依据。

---

## 0. Resource存储系统：分离公开/敏感内容

**日期**：2026-03-06

**背景**：用户需要存储有价值的工具资源，包括：
- 复杂代码片段
- 特定网站的账号密码
- 登录状态的Cookie（含获取时间）

**问题**：
- 原有resources表只有body字段，明文存储不安全
- 如果整体加密，会失去搜索能力
- 缺少元数据（获取时间、过期时间、内容类型）

**决策**：扩展resources表，分离公开/敏感内容
- `body`: 公开内容（明文，可搜索）
- `secure_body`: 敏感内容（AES-256-GCM加密）
- `content_type`: 内容类型（text|code|credential|cookie）
- `metadata`: JSON元数据（language, acquired_at, expires_at）

**加密机制**：
- 使用AES-256-GCM加密
- 密钥从环境变量ORG_KEYS读取（格式：org1:key1_base64,org2:key2_base64）
- resource_id前缀决定使用哪个组织密钥（如test/tools/...使用test的密钥）
- 访问时需提供X-Org-Key头，验证通过才解密

**实现位置**：`knowhub/server.py`
- `encrypt_resource()`: 加密函数
- `decrypt_resource()`: 解密函数
- `POST /api/resource`: 提交资源
- `GET /api/resource/{id}`: 获取资源（支持X-Org-Key头）
- `PATCH /api/resource/{id}`: 更新资源
- `GET /api/resource`: 列出资源

**文档**：`knowhub/docs/resource-storage.md`

---

## 1. 定位：经验层而非工具目录

**背景**：调研发现现有生态（详见 `docs/knowledge/`）已充分覆盖工具发现：

| 已解决的问题 | 谁在做 |
|-------------|--------|
| 工具有哪些 | Glama（17k+ MCP）、Smithery（125k+ Skills）、awesome-mcp-servers（81k Stars） |
| 哪个热门 | Smithery 安装量排行、Glama 下载量 |
| 静态质量指标 | Glama 质量评分、GitHub Stars、License |
| 安装方式 | Smithery CLI、npx、pip/uvx |
| 在线测试 | Glama MCP Inspector |

**缺失**：Agent 在特定任务中使用工具后的真实体验。

**决策**：KnowHub 不做工具目录，只做经验层。发现交给现有平台，KnowHub 收集"用了之后怎么样"。

---

## 2. 核心实体：experience 而非 tool

**曾考虑的方案**：三张表（tools + reports + reviews），以 tool 为中心。

**问题**：
- tools 表复制 Glama/Smithery 已有的目录
- reports（调研发现）的价值有限——Agent 的调研结论不比 Glama 的质量评分更可靠
- 只有 reviews（实际使用评价）是独有价值

**关键洞察**：Agent 的问题不是"pymupdf 怎么样"（工具中心），而是"我要从 PDF 提取表格，谁做过"（任务中心）。

**决策**：单一实体 `experience`（任务 + 工具 + 结果 + 建议）。工具信息从 experiences 中按 tool_name 聚合得出，不单独维护。取消 reports——没用过不评价。

---

## 3. 搜索：任务驱动

**决策**：LIKE 拆词搜索 task + tips + outcome + name 字段，结果按 name 分组，每组返回相关 experiences 和聚合分数。

Server 只做检索 + GROUP BY，不做智能处理。Agent 拿到 JSON 后自行判断。体现"端侧算力"原则。

**搜索范围**：仅命中 experience。Content 是 experience 的深入层，通过 resource_id 关联获取，不参与搜索。如果后续发现 content 中有大量有价值信息未被 experience 覆盖，再扩展搜索范围。

---

## 4. 接口：curl 直调 API（MVP）

**曾考虑的方案**：

| 方案 | 优势 | 劣势 |
|------|------|------|
| CLI（mpk 命令） | 封装 HTTP 调用，管理 agent_id | 需要 pip install，多一个组件 |
| MCP Server | Agent 原生工具调用 | 需 MCP SDK + 本地进程，多一跳 |
| curl 直调 API | 零安装，Agent 和人都能用 | 命令略长，无本地状态管理 |

CLI 和 MCP 都在 HTTP API 外面包了一层。MVP 阶段这层封装提供的价值（agent_id 管理、输出格式化、错误处理）Agent 自身都能做。

**决策**：MVP 不做客户端，skill.md 直接引导 Agent 用 curl 调 API。CLI 和 MCP Server 作为后续接入方式。

---

## 5. skill.md：两条路径而非线性流程

**曾考虑的方案**：四步线性流程（查→找→评→报），发现渠道与查经验并列。

**问题**：有经验时不需要去外部平台找工具，经验中已包含工具名、URL 和使用建议。

**决策**：分叉流程——有结果直接用，无结果才 fallback 到外部平台。发现渠道从主流程降级为 fallback。

---

## 6. 冷启动：有机增长而非种子数据

**曾考虑的方案**：从调研成果导入精选工具目录作为种子数据。

**问题**：导入没有 experience 的工具条目和空数据库无区别。空壳结果反而降低信任感。

**决策**：数据库从零开始。冷启动路径：
1. Agent 搜索 → 空结果
2. skill.md 引导去 Smithery/PyPI 找工具
3. Agent 使用后 `POST /api/experience` 上报
4. 下一个 Agent 搜索时看到前人经验

空结果不是问题，假数据才是。加速冷启动通过运营（自己先用热门工具并提交真实经验），不通过技术手段。

---

## 7. 身份标识：可选 submitted_by

**曾考虑的方案**：
- 自动生成 agent_id 持久化到本地文件 → 需要客户端组件
- 要求注册/登录 → 摩擦太大
- 要求邮箱 → 如何零成本获取？

**决策**：可选字段 `submitted_by`，自由格式（邮箱、用户名、团队名均可）。skill.md 建议用 `git config user.email` 自动填充——几乎所有开发者已配置，零额外设置。

内部项目设置后方便 debug 追溯；外部用户可不填，零摩擦。

---

## 8. 算力分布

| 工作 | 执行方 | 理由 |
|------|--------|------|
| 搜索工具 | Agent（Smithery/PyPI/GitHub） | 利用 Agent 搜索和浏览器能力 |
| 评估工具质量 | Agent | Agent 有判断力 |
| 决定用哪个 | Agent | 结合经验 + 自身评估 |
| 总结使用经验 | Agent | LLM 能力 |
| 存储/检索/聚合 | Server | 简单 SQL 操作 |

Server 零 LLM 成本。

---

## 9. 资源范围：从工具扩展到一般性知识

**背景**：最初定位仅覆盖工具（mcp/skill/library/api）。但 Agent 的集体记忆不应限于工具——论文、博客、电子书、课程等知识资源同样有"用了之后怎么样"的经验价值。

**决策**：`category` 从工具类型扩展为资源类型（`mcp | skill | library | api | paper | blog | book | course` 等）。数据模型不变，只是语义范围扩大。字段从 `tool_*` 重命名为通用名（`name` / `url` / `category`）。

---

## 10. 触发条件：从"需要工具"到更广的场景

**背景**：skill.md 原始触发条件是"需要外部工具但不确定选哪个时"，将 KnowHub 限定为工具推荐器。

**决策**：扩展为更广的触发条件——遇到复杂任务、可能超出自身能力、多次失败、可能需要外部资源时。KnowHub 是 Agent 面对不确定性时的第一反应，不只是找工具时才想起来。

---

## 11. 内容层：按需积累而非预设结构

**背景**：Experience（tips/outcome）能承载一条经验的关键信息，但有些资源需要更深入的内容（论文章节、API 文档、书籍目录）。

**曾考虑的方案**：
- 固定三层结构（experience → resource detail → sections）：预设层级，用 section_key 约定
- 通用内容图：每个内容有 ID 和 body，可以引用其他内容

**决策**：通用的 contents 表，路径式 ID（`pymupdf` 或 `pymupdf/find-tables`），扁平二级结构（根 + 叶），body 为 Markdown。内容由 Agent 按需提交——没人需要的资源停留在 experience 层，高频使用的资源自然积累出完整内容。

**内容格式**：Markdown。Agent 原生理解 markdown，图片用 URL 引用，不存文件。Resonote 的 HTML + span_id 体系是为人类阅读设计的，KnowHub 不需要。

**观测性**：Server 记录搜索日志。Agent 对不存在内容的 GET 请求频率本身就是"需要更深内容"的信号。

---

## 12. KnowHub 与 Resonote：独立演进

**背景**：两者在内容处理上有潜在协同（内容去重、分析管线、向量检索）。曾讨论 Resonote 依赖 KnowHub 内容层。

**分析**：
- Library 的 HTML + span_id + 虚拟切分管线对人类阅读有价值，对 Agent 也有潜在价值（精确引用、结构化内容）
- 但 KnowHub 在 MVP 阶段，数据模型会频繁变动；Library 已稳定，不应因 KnowHub 迭代而受影响
- 合并内容层的工程收益小于耦合成本

**决策**：两个项目独立推进。唯一预留的接口：两边使用相同的 content hash 方案（sha256），未来如果整合，内容去重天然可行。等 KnowHub 验证"集体记忆"方向后再讨论整合。

---

## 13. 向量检索：Milvus Lite 单一存储架构

**日期**：2026-03-09

**背景**：现有检索方案使用 LLM 语义路由（gemini-2.0-flash-001），从所有知识中挑选候选。存在以下问题：
- 每次检索都需要调用 LLM，成本和延迟较高
- 无法利用向量相似度进行精确的语义匹配
- 难以支持大规模知识库（需要将所有知识元数据传给 LLM）

**方案对比**：

| 方案 | 部署复杂度 | 性能 | 功能完整性 | 迁移成本 |
|------|-----------|------|-----------|---------|
| sqlite-vec | 低（单文件） | 中 | 基础向量检索 | 低 |
| **Milvus Lite** | **低（pip install）** | **高** | **完整（标量过滤+向量检索）** | **中** |
| Qdrant | 中（需 Docker） | 高 | 完整 | 低 |
| 完整 Milvus | 高（多组件） | 极高 | 完整 | 高 |

**决策**：采用 Milvus Lite 单一存储架构

**为什么不用 SQLite + Milvus Lite 双存储？**
- Milvus Lite 支持标量字段存储，可以存储所有知识数据
- 维护两个数据库增加同步复杂度和一致性风险
- Milvus Lite 数据也是本地文件存储，备份和迁移同样简单
- 单一存储简化架构，降低维护成本

**架构设计**：

```
┌─────────────────────────────────────────────────────┐
│                   KnowHub Server                     │
├─────────────────────────────────────────────────────┤
│                                                      │
│              ┌─────────────────────┐                │
│              │   Milvus Lite       │                │
│              │   (单一存储)         │                │
│              ├─────────────────────┤                │
│              │ knowledge 集合       │                │
│              │ ├─ id (PK)          │                │
│              │ ├─ embedding (向量)  │                │
│              │ ├─ task             │                │
│              │ ├─ content          │                │
│              │ ├─ types (JSON)     │                │
│              │ ├─ tags (JSON)      │                │
│              │ ├─ scopes (JSON)    │                │
│              │ ├─ owner            │                │
│              │ ├─ resource_ids     │                │
│              │ ├─ source (JSON)    │                │
│              │ ├─ eval (JSON)      │                │
│              │ ├─ created_at       │                │
│              │ └─ updated_at       │                │
│              └─────────────────────┘                │
│                                                      │
│              向量索引：HNSW (COSINE)                  │
│              参数：M=16, efConstruction=200          │
│                                                      │
│  ┌──────────────────────────────────────────────┐  │
│  │           检索流程                            │  │
│  ├──────────────────────────────────────────────┤  │
│  │ 1. 向量召回：Milvus 检索 top 3*k 候选        │  │
│  │ 2. LLM 精排：Gemini 对候选重新排序           │  │
│  │ 3. Fallback：LLM 失败时直接返回向量 top k    │  │
│  └──────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘
```

**检索流程**（向量召回 + LLM 精排）：

```python
async def knowledge_search(query: str, filters: dict, top_k: int = 5):
    # 1. 生成查询向量
    query_embedding = await get_embedding(query)

    # 2. 向量召回（快速、便宜）
    candidates = await milvus_store.search(
        embedding=query_embedding,
        filters=filters,  # types, owner, scopes
        limit=top_k * 3   # 召回 3*k 个候选
    )

    # 3. LLM 精排（准确、贵）
    try:
        ranked = await llm_rerank(
            query=query,
            candidates=candidates,
            top_k=top_k
        )
        return ranked
    except Exception as e:
        # 4. Fallback：LLM 失败时直接返回向量 top k
        logger.warning(f"LLM rerank failed: {e}, fallback to vector top-k")
        return candidates[:top_k]

async def llm_rerank(query: str, candidates: List[dict], top_k: int):
    """使用 LLM 对候选进行精排"""
    # 构造 prompt
    candidates_text = "\n".join([
        f"[{i+1}] ID: {c['id']}\nTask: {c['task']}\nContent: {c['content'][:200]}..."
        for i, c in enumerate(candidates)
    ])

    prompt = f"""你是知识检索专家。根据用户查询，从候选知识中选出最相关的 {top_k} 条。

用户查询："{query}"

候选知识：
{candidates_text}

请输出最相关的 {top_k} 个知识 ID，按相关性从高到低排序，用逗号分隔。
只输出 ID，不要其他内容。"""

    response = await openrouter_llm_call(
        messages=[{"role": "user", "content": prompt}],
        model="google/gemini-2.5-flash-lite"
    )

    # 解析 LLM 输出
    selected_ids = parse_ids(response["content"])

    # 按 LLM 排序返回
    id_to_candidate = {c["id"]: c for c in candidates}
    return [id_to_candidate[id] for id in selected_ids if id in id_to_candidate]
```

**Embedding 模型选择**：

优先级：
1. **OpenAI text-embedding-3-small**（推荐）
   - 1536 维，性能好，成本低（$0.02/1M tokens）
   - 支持中英文
2. **本地模型**（备选）
   - paraphrase-multilingual-MiniLM-L12-v2
   - 零成本，但需要本地计算资源

**成本分析**：

假设 1000 条知识，每条平均 200 tokens：

| 操作 | 旧方案（纯 LLM 路由） | 新方案（向量召回 + LLM 精排） |
|------|---------------------|---------------------------|
| 每次检索 | 200k tokens → $0.04 | 召回：0 成本<br>精排：3k tokens → $0.0006 |
| 1000 次检索 | $40 | $0.60 |
| 节省 | - | **98.5%** |

**迁移路径**：

阶段 1：实现 Milvus Lite 存储（2-3 周）
- 从 SQLite 迁移数据到 Milvus Lite
- 实现向量召回 + LLM 精排
- 保留旧 API 兼容性

阶段 2：效果评估（1-2 个月）
- 对比新旧方案的准确率和成本
- 收集用户反馈
- 调优召回倍数（3*k）和精排策略

阶段 3：可能的演进方向
- 方向 A：优化精排 prompt，提升准确率
- 方向 B：引入混合检索（向量 + 关键词）
- 方向 C：升级到完整 Milvus（如果数据量暴增）

**实现位置**：
- Milvus 封装：`knowhub/vector_store.py`
- Embedding 生成：`knowhub/embeddings.py`
- 检索逻辑：`knowhub/server.py:knowledge_search`
- LLM 精排：`knowhub/server.py:llm_rerank`

**优势**：
1. 单一存储，架构简单
2. 向量召回快速且便宜
3. LLM 精排保证准确性
4. Fallback 机制保证可用性
5. 成本降低 98.5%

**权衡**：
1. 从 SQLite 迁移需要一次性工作
2. Milvus Lite 的标量查询不如 SQL 灵活（但够用）
3. 存储空间增加（向量数据）
