deerflow-code/offline-backend-20260512/backend/docs/LLMWIKI_DEERFLOW_LOCAL_VECTOR_INDEX_ZH.md
2026-09-07 18:24:55 +08:00

70 KiB
Raw Blame History

WeKnora Wiki 数据同步至 DeerFlow 并建立本地向量索引——实现级设计方案

文档状态:首版已实现(Implemented,2026-08-23;2026-08-24 增补整库未向量化时的 Wiki-only 检索回退、知识库所有者手动向量化及全量 Excel 导出) 编写日期:2026-08-21
适用代码库:当前 DeerFlow 工作区
WeKnora 适配基线:v0.7.2
关联文档:docs/LLMWIKI_WEKNORA_INTEGRATION_ZH.md
核心边界:不修改 WeKnora 后端;WeKnora 仅作为 Wiki 生成源,DeerFlow 保存完整 Wiki 镜像、章节向量和检索索引,并承接全部内部与外部检索。 现行产品补充决策:内部检索只有在单个知识库整库完成向量化后才使用本地向量;未完成时只调用 WeKnora Wiki 页面检索接口并读取 Wiki 正文,绝不调用原始文档/切片检索。外部 API 继续严格使用本地向量。Wiki 管理页支持所有者手动触发整库向量化,并将全部 Wiki 导出为包含当前目录、完整父级目录和逐级目录列的 Excel。


1. 文档目标

本文是后续编码的默认实施蓝图,目标不是简单描述“可以做向量检索”,而是把以下工程问题一次性定义清楚:

  1. 如何从 WeKnora 拉取已经加工完成的 Wiki 页面,而不是继续检索原始文档切片。
  2. 如何把 Wiki 页面全文、元数据和向量持久化到 DeerFlow 自己的数据库中。
  3. 如何在不引入第二个重型向量服务的前提下,先用 DeerFlow 当前的 SQLite/PostgreSQL/MySQL 数据库与 NumPy 完成可用的精确向量检索。
  4. 如何保证 Wiki 新建、重新生成、人工编辑、回滚、归档和删除后,DeerFlow 本地镜像及向量不会长期失真。
  5. 如何让普通问答、智能体工具、页面检索、右侧参考文献和公共外部接口复用同一套检索结果。
  6. 如何保证个人库、公共库、智能体绑定、对话选择、Wiki 页面状态和外部发布开关在检索时被严格执行。
  7. 如何在外部开放公共 Wiki 检索时,避免把私有库、草稿页、系统“对话沉淀”、WeKnora 原始 ID 或内部凭据泄露出去。
  8. 如何迁移、灰度、回滚、监控和验收,而不是一次性替换后无法定位质量问题。

本文提到的表名、字段、模块、接口和默认行为均作为实现时的默认方案。如编码阶段出现必要调整,应同步更新本文,避免文档与实现长期漂移。


2. 结论摘要

2.1 可行性结论

方案可行,整体难度为中等。DeerFlow 已经具备三项可直接复用的基础能力:

  • OpenAI 兼容的异步 Embedding 客户端:deerflow/knowledge/embeddings.py;
  • 按 Markdown 标题与段落切分的逻辑:deerflow/knowledge/chunking.py;
  • 余弦相似度、关键词评分和混合排序基础:deerflow/knowledge/search.py。

本项目真正需要新增的核心能力是:

  1. Wiki 页面本地镜像数据模型;
  2. WeKnora → DeerFlow 的全量/增量同步与租约;
  3. Wiki 页面章节化、Embedding 和原子换代;
  4. DeerFlow 本地向量快照与权限过滤检索;
  5. 内部问答、引用面板和外部接口的统一结果协议;
  6. 索引状态、失败重试、重建、监控和安全边界。

2.2 最终数据边界

本方案完成后,数据职责如下:

数据/能力 WeKnora DeerFlow
原始文件、原始切片 保留并管理 不复制(除现有引用兼容所需元数据)
Wiki 自动生成 负责 不负责
Wiki 页面主生成源 是 否,保存可检索镜像
Wiki 页面全文镜像 有 有
Wiki 页面章节切分 可有但不依赖 负责
Wiki 页面 Embedding 不要求 负责
Wiki 向量持久化 不要求 负责
内部问答检索 不再作为主查询路径 负责
右侧参考文献详情 不再实时依赖 负责
公共外部检索接口 不对外暴露 负责
用户/公共/智能体权限 不作为最终边界 DeerFlow 为最终边界

这里的“DeerFlow 保存数据”指:DeerFlow 保存一份完整 Wiki 页面镜像和用于检索的章节向量。WeKnora 仍然会保存它生成 Wiki 所需的数据;如果未来要求 WeKnora 完全不保存 Wiki,则必须把 Wiki 生成管道也迁入 DeerFlow,那是另一项更大的工程,不在本文范围内。

2.3 推荐首版技术路线

首版建议:

  • 数据库继续沿用 DeerFlow 的统一 SQLAlchemy 数据库;
  • Wiki 页面全文存 PortableLongText;
  • 向量使用归一化后的 float32 二进制 BLOB 持久化;
  • 检索时按知识库懒加载 NumPy 矩阵,并进行精确余弦相似度计算;
  • 每个 Gateway worker 保持自己的只读内存快照,通过数据库索引修订号判断是否需要重载;
  • 默认使用纯向量检索,Embedding/索引不可用时明确报错或返回“索引未就绪”,不偷偷回退原文 chunk;
  • 可配置开启轻量关键词加权,但它不能改变“结果来源必须是 Wiki 页面”的边界;
  • 当规模或多实例内存成本超过可接受范围时,再将向量执行层切换为 pgvector;页面镜像、权限、API 和引用协议保持不变。

3. 背景与现状诊断

3.1 当前检索的是原始切片

当前 WeKnoraClient.search() 调用:

POST /api/v1/knowledge-search

返回的核心字段是:

chunk_id
content
knowledge_id
knowledge_base_id
title / filename
score
chunk_index
metadata

因此当前检索实体是 WeKnora 原始文档解析后的 chunk,而不是已经归纳、合并、编辑后的 Wiki 页面。

该方法目前被三个主要生产入口复用:

  1. app/gateway/llmwiki_rag.py:普通问答发送给模型前的自动预检索;
  2. deerflow/tools/builtins/llmwiki_search_tool.py:智能体主动调用的 llmwiki_search 工具;
  3. app/gateway/routers/llmwiki.py:POST /api/llmwiki/search 页面检索。

只修改其中一个入口会导致不同页面、智能体和问答路径继续返回两套来源,因此实现时必须统一切换。

3.2 当前参考文献以 chunk/document 为中心

当前引用 URL 主要为:

/weknora-source-preview?knowledge_base_id=<DeerFlow映射ID>&chunk_id=<chunk-id>

部分工具路径还会附带 knowledge_id / document_id。前端 WeKnoraSourceDrawer 在已有 documentId 时会直接加载整份原文件或解析后全文,导致用户看到的是整个文档,而不是对应 Wiki 页面。

现有 /api/llmwiki/sources/context 虽然支持从 chunk metadata 中读取 wiki_page_slug,但它属于尽力映射:

  • 很多历史 chunk 并没有 wiki_page_slug;
  • Wiki 页可能综合多个文档和多个 chunk;
  • 现有映射函数主要依赖 chunk metadata 中直接出现 slug,并没有可靠建立 chunk_refs → wiki page 的本地反向索引;
  • 即使找到 Wiki,当前抽屉也仍可能先展示整份原文,再在下方追加 Wiki 内容。

新方案不再以“先命中 chunk,再猜对应 Wiki”为主路径,而是直接检索 DeerFlow 本地 Wiki 章节向量,结果天然携带 wiki_page_id + wiki_slug。

3.3 WeKnora 已提供 Wiki 页面读取能力

当前适配器已经具备:

  • list_wiki_pages();
  • get_wiki_page();
  • Wiki 页面创建、更新、删除;
  • Wiki 图谱读取。

因此不需要修改 WeKnora 后端即可拉取页面。需要新增的是 DeerFlow 侧同步器、本地存储和向量化流程。

3.4 DeerFlow 现有向量实现的可复用点与限制

当前产品知识库的向量功能使用:

  • KnowledgeEmbeddingRow.vector:JSON 浮点数组;
  • KnowledgeRepository.all_embeddings():一次加载全部向量;
  • Python 循环逐条调用 cosine_similarity()。

这套实现适合小规模功能验证,但不适合作为公共 Wiki 的正式索引,因为:

  1. JSON 浮点向量体积大、解析慢;
  2. 每次查询加载全部向量会放大数据库 IO;
  3. Python 标量循环不能充分利用 NumPy/BLAS;
  4. 没有按知识库权限预分片;
  5. 没有多 worker 索引修订和缓存失效机制。

本方案复用 Embedding 客户端和 Markdown 切分思想,但为 LLMWiki Wiki 新建独立表与检索快照,不直接塞入 knowledge_notes/knowledge_embeddings。


4. 目标、非目标与默认决策

4.1 功能目标

  1. DeerFlow 能完整保存每个已映射 WeKnora 知识库的 Wiki 页面镜像。
  2. DeerFlow 能按 Wiki 页面 Markdown 结构生成章节向量。
  3. 普通问答、智能体工具和页面检索都能按向量搜索 Wiki 页面。
  4. 大模型收到的是命中 Wiki 页的相关章节,而不是原始文档 chunk。
  5. 右侧参考文献主视图只展示 Wiki 页面。
  6. 公共 Wiki 可通过受控外部 API 进行向量检索。
  7. WeKnora 不可用时,已经同步成功的 Wiki 仍可检索和展示。
  8. Wiki 更新后可自动增量重建对应页面向量,不全库重算。
  9. Embedding 模型变化时可明确触发全量重建。
  10. 用户取消公共发布或关闭外部开放后,外部查询立即失去访问权,不依赖异步清理完成。

4.2 明确非目标

  • 不修改或 fork WeKnora 后端。
  • 不把 WeKnora 原始文件和全部原始 chunks 复制进 DeerFlow。
  • 不让 DeerFlow 首版承担 Wiki 自动生成。
  • 不使用“把 Wiki 再上传成隐藏文档库”的循环导入方案。
  • 不在首版实现分布式 HNSW/Milvus/Qdrant 集群。
  • 不在首版提供原始 embedding 向量下载接口。
  • 不保证历史 chunk 引用全部能够转换为 Wiki 引用。
  • 不允许外部调用者通过 API 传入 WeKnora 原始知识库 ID。
  • 不允许向量服务失败后静默改查原始文档。

4.3 默认产品决策

如实现前未另行变更,采用以下默认值:

  1. 同步范围:所有 DeerFlow 已映射、远端存在且启用 Wiki 的知识库,包括个人库和公共库。
  2. 页面范围:同步 draft/published/archived 全部状态,检索时再按调用者权限和页面状态过滤。
  3. 内部检索:默认只检索 published 页面;所有者/管理员可以通过明确参数包含 draft,普通用户不能。
  4. 外部检索:只检索 published 页面。
  5. 外部开放:新增 external_search_enabled,默认 false,不能仅凭现有 publication_status=published 自动向互联网开放。
  6. 系统“对话沉淀”知识库:允许内部按现有权限使用,但外部接口永远排除。
  7. 检索模式:严格向量;索引未就绪时返回明确状态,不回退原文。
  8. 引用单位:Wiki 页面;检索单位:Wiki 页面内的章节 chunk。
  9. 首版向量执行器:按知识库懒加载的 NumPy 精确检索。
  10. 外部搜索默认不返回完整正文,只返回摘要和命中片段;完整页面通过详情接口读取。

5. 总体架构

5.1 写入与同步链路

flowchart LR
    WK["WeKnora Wiki 生成/编辑"] --> API["WeKnora Wiki REST API"]
    API --> SYNC["DeerFlow Wiki Sync Service"]
    SYNC --> PAGE["llmwiki_wiki_pages"]
    PAGE --> CHUNK["Markdown 章节切分"]
    CHUNK --> EMB["DeerFlow Embedding Client"]
    EMB --> VECTOR["llmwiki_wiki_vectors"]
    VECTOR --> REV["知识库索引修订号 +1"]
    REV --> CACHE["各 Gateway worker 懒重载 NumPy 快照"]

5.2 内部检索链路

flowchart LR
    Q["用户问题"] --> AUTH["解析对话选择/智能体绑定/实时权限"]
    AUTH --> QE["生成一次查询向量"]
    QE --> IDX["按允许的知识库快照检索 TopK 章节"]
    IDX --> GROUP["按 Wiki 页面聚合/去重"]
    GROUP --> PROMPT["相关章节注入模型"]
    GROUP --> REF["结构化 Wiki 引用"]
    REF --> DRAWER["右侧展示 DeerFlow 本地 Wiki 页面"]

5.3 外部检索链路

flowchart LR
    EXT["外部系统 + API Key"] --> GATE["限流/鉴权/输入校验"]
    GATE --> PUB["published + external_search_enabled"]
    PUB --> LOCAL["DeerFlow 本地向量快照"]
    LOCAL --> RESULT["公共 Wiki 摘要/片段/页面标识"]
    RESULT --> DETAIL["可选读取完整公共 Wiki 页面"]

5.4 查询期间不依赖 WeKnora

同步完成后,以下请求不得实时访问 WeKnora:

  • 内部向量搜索;
  • 普通问答预检索;
  • llmwiki_search 工具;
  • 右侧 Wiki 参考文献详情;
  • 外部公共 Wiki 搜索;
  • 外部公共 Wiki 页面详情。

WeKnora 只出现在同步与管理链路中。这样可以降低延迟、避免跨库扇出,并确保外部流量不会直接放大到 WeKnora。


6. 配置设计

在 deerflow/config/llmwiki_config.py 的 LlmWikiConfig 下增加独立的 local_wiki_index 配置,避免和产品“我的知识”功能强耦合。

建议配置:

llmwiki:
  weknora:
    api_base_url: "http://weknora-app:8080"
    web_base_url: "http://weknora-app:8080"
    admin_email: "$WEKNORA_ADMIN_EMAIL"
    admin_password: "$WEKNORA_ADMIN_PASSWORD"

  local_wiki_index:
    enabled: true
    # 默认只允许手动向量化;需要定时增量同步时再开启。
    auto_sync: false
    # 每条 SQL 最多插入的向量行数;整页替换仍在同一事务内完成。
    database_batch_size: 50
    embedding:
      model: bge-m3
      base_url: "$WIKI_EMBEDDING_BASE_URL"
      api_key: "$WIKI_EMBEDDING_API_KEY"

日常部署只需配置上面三个 Embedding 值并将 enabled 改为 true。 auto_sync=false 时仅由页面“手动向量化”触发;即使设为 true,定时器也会先等待一个完整同步周期,不会在服务刚启动时立即重建索引。其余切分、检索、超时与重试参数继续使用代码默认值,只有确有调优需要时才覆盖。

6.1 配置校验规则

启动时应执行:

  1. enabled=true 时,Embedding base_url 和 model 必须非空;
  2. 不向上游发送 dimensions 参数;第一次成功 Embedding 后从响应自动发现维度,并固定到索引 profile;
  3. chunk_overlap_chars < chunk_max_chars;
  4. top_k_pages <= top_k_sections;
  5. external_api.enabled=true 时必须配置非空 API Key;
  6. API Key 不得出现在日志、状态接口或前端运行时配置;
  7. strict_vector=true 时,Embedding 不可用必须反映为索引失败/搜索不可用,不能静默关键词回退;
  8. 更换 model/base_url/dimensions 后计算出的 embedding_fingerprint 变化,系统应标记全量索引过期。

6.2 Embedding 指纹

建议指纹:

sha256(provider + "\0" + base_url_normalized + "\0" + model + "\0" + dimensions)

不把 API Key放入指纹。指纹用于:

  • 判断现有向量是否可继续使用;
  • 避免不同维度向量进入同一矩阵;
  • 触发管理员重建;
  • 在状态接口中显示短指纹,便于排障。

7. 数据模型

7.1 为什么使用独立表

不建议复用 knowledge_notes 和 knowledge_embeddings,原因是:

  • 产品知识库当前是全局共享语义,LLMWiki 有个人/公共/智能体绑定权限;
  • Wiki 页面需要保存 WeKnora mapping、slug、版本、状态和同步信息;
  • Wiki 页面可能频繁由生成管道重写,生命周期与人工知识笔记不同;
  • 避免出现在“我的知识”列表、目录、导出和图谱中;
  • 后续可单独迁移到 pgvector,不影响现有知识功能。

7.2 扩展 llmwiki_knowledge_bases

建议新增字段:

字段 类型 默认 说明
wiki_index_enabled bool true 是否同步并建立本地 Wiki 索引
external_search_enabled bool false 是否允许外部 API 检索
external_search_updated_at datetime nullable null 外部开关最后变化时间

注意:

  • publication_status=published 继续表示 DeerFlow 内部公共;
  • external_search_enabled=true 才表示允许对外;
  • 外部检索必须同时满足两者;
  • 系统“对话沉淀”即使字段误设为 true,也应在代码层强制排除。

7.3 llmwiki_wiki_pages

建议 ORM 模型路径:

deerflow/persistence/llmwiki_index/model.py

字段设计:

字段 类型 约束/索引 说明
id String(64) PK DeerFlow 本地稳定页面 ID
knowledge_base_mapping_id String(64) FK + index DeerFlow 映射 ID,不是 WeKnora ID
remote_page_id String(128) nullable index WeKnora 页面 ID,仅内部使用
slug String(1024) 普通字段 Wiki 页面路径
slug_hash String(64) unique(mapping, hash) 避免 MySQL 长索引限制
title String(512) index/prefix index 页面标题
summary PortableLongText 页面摘要
content_md PortableLongText 完整 Wiki Markdown 镜像
page_type String(32) index summary/entity/concept/...
status String(32) index draft/published/archived
aliases PortableJSON 别名数组
category_path PortableJSON Wiki 目录路径
parent_slug String(1024) nullable 父页面
wiki_path String(1024) nullable 上游规范化路径
source_refs PortableJSON 原文文档级溯源,仅内部使用
chunk_refs PortableJSON 原始 chunk 级溯源,仅内部使用
page_version int index 上游 Wiki 版本
content_hash String(64) index 参与向量化内容哈希
remote_created_at datetime nullable 上游创建时间
remote_updated_at datetime nullable index 上游更新时间
index_status String(24) index pending/indexing/ready/failed/stale/deleted
indexed_fingerprint String(64) nullable 已生成向量的 Embedding 指纹
indexed_at datetime nullable 最近成功索引时间
last_error Text nullable 截断、脱敏后的错误
is_remote_deleted bool index 完整同步确认上游不存在
last_seen_sync_id String(64) nullable index 本轮完整扫描标记
created_at datetime 本地创建时间
updated_at datetime index 本地更新时间

本地 id 建议是 UUID,不直接使用 remote ID。唯一性使用:

UNIQUE(knowledge_base_mapping_id, slug_hash)
slug_hash = sha256(normalized_slug)

写入时如果 hash 相同但 slug 不同,应视为极低概率冲突并拒绝覆盖,记录显式错误。

7.4 llmwiki_wiki_vectors

字段 类型 约束/索引 说明
id String(64) PK 向量记录 ID
page_id String(64) FK cascade + index 本地 Wiki 页面
knowledge_base_mapping_id String(64) index 冗余字段,便于分片加载
section_index int unique(page,index) 页面内章节序号
heading String(1024) nullable 命中章节标题
section_content PortableLongText 送给模型和展示的片段
content_hash String(64) index 章节文本哈希
embedding_fingerprint String(64) index 模型指纹
embedding_dimensions int 维度
vector_blob LargeBinary 归一化 float32 little-endian
created_at datetime 创建时间

约束:

UNIQUE(page_id, section_index, embedding_fingerprint)

向量写入前必须:

  1. 校验长度等于 embedding_dimensions;
  2. 拒绝 NaN/Inf;
  3. L2 归一化;
  4. 转为 little-endian float32;
  5. 保存 BLOB;
  6. 读取时再次验证 BLOB 字节数为 dimensions * 4。

7.5 llmwiki_wiki_sync_states

每个知识库一行:

字段 类型 说明
knowledge_base_mapping_id String(64) PK 本地映射 ID
state String(24) idle/running/partial/failed/disabled
active_sync_id String(64) nullable 当前扫描 ID
lease_owner String(128) nullable worker 标识
lease_expires_at datetime nullable 多 worker 租约
last_started_at datetime nullable 最近启动
last_completed_at datetime nullable 最近完整成功
last_success_at datetime nullable 最近任意成功
last_error Text nullable 最近错误
remote_page_count int 上游扫描数量
local_page_count int 本地有效页面数量
ready_page_count int 已就绪页面数量
vector_count int 向量数量
failed_page_count int 失败页面数量
index_revision bigint 该知识库索引修订号
embedding_fingerprint String(64) nullable 当前期望指纹
updated_at datetime 状态更新时间

index_revision 只在可见检索数据真正变化后递增,例如:

  • 页面首次索引成功;
  • 页面向量被替换;
  • 页面被删除/归档;
  • 页面状态变化影响检索;
  • Embedding 指纹重建完成。

单纯更新进度、错误文字或同步时间不应递增。

7.6 可选任务明细表

首版可仅使用 sync state + 页面状态。如果需要管理页展示逐页面进度、失败重试和历史记录,可增加:

llmwiki_wiki_index_jobs

记录任务类型(sync/rebuild/page_reindex)、状态、总数、完成数、失败数、发起人和错误摘要。该表不是检索正确性的必要条件,可以第二阶段增加。


8. Wiki 同步设计

8.1 同步触发方式

必须同时支持:

  1. 周期完整扫描:捕获 WeKnora 异步 Wiki 生成、后台修订和不经过 DeerFlow BFF 的编辑;
  2. 管理操作后即时同步:通过 DeerFlow 创建、更新、删除、回滚 Wiki 页面后,立即入队同步该页;
  3. 管理员手动同步:用于排障和首次导入;
  4. 管理员全量重建:用于切换 Embedding 模型/维度或修复索引。

8.2 知识库发现

每轮调度:

  1. 从 llmwiki_knowledge_bases 获取 wiki_index_enabled=true 的映射;
  2. 排除已删除映射;
  3. 向 WeKnora 查询远端知识库存在性与 Wiki 能力;
  4. 远端不存在时不立即删除本地数据,而是标记同步失败;
  5. 只有显式删除映射或连续确认上游知识库已删除时,才进入本地清理流程;
  6. 每个知识库独立获得租约,避免多 Gateway worker 重复同步。

8.3 完整扫描算法

伪代码:

async def sync_knowledge_base(mapping_id):
    state = await repo.try_acquire_lease(mapping_id, owner, ttl=600)
    if not state.acquired:
        return "already_running"

    sync_id = uuid4()
    complete_scan = False
    try:
        remote_kb = await weknora.get_knowledge_base(remote_id)
        assert wiki_enabled(remote_kb)

        page = 1
        while True:
            batch = await weknora.list_wiki_pages(remote_id, page=page, page_size=100)
            for remote_page in batch.pages:
                await mirror_and_maybe_index(mapping_id, remote_page, sync_id)
            await repo.renew_lease(mapping_id, owner)
            if page >= batch.total_pages:
                break
            page += 1

        complete_scan = True
        await repo.mark_missing_pages_deleted(mapping_id, last_seen_sync_id_not=sync_id)
        await repo.finish_sync_success(mapping_id, sync_id)
    except Exception as exc:
        await repo.finish_sync_failure(mapping_id, sync_id, safe_error(exc))
        raise
    finally:
        await repo.release_lease(mapping_id, owner)

关键安全规则:只有本轮所有分页都成功完成时,才能把“未出现页面”标为删除。 中途超时或某一页失败时,绝不能根据不完整集合删除本地页面。

8.4 页面变化判断

用于 content_hash 的规范化内容建议为:

title
aliases(稳定排序)
summary
content_md(统一换行,保留 Markdown)
page_type
status

不把更新时间本身放入 hash,避免上游只刷新时间就重复调用 Embedding。

判断:

场景 动作
本地没有页面 保存镜像,状态 pending,建立向量
hash 和 embedding fingerprint 都相同 仅更新 last_seen_sync_id 和必要元数据
hash 变化 保存新镜像,重建该页全部章节向量
fingerprint 变化 内容不变也要重建向量
状态变 archived 保存镜像,删除/停用活动向量,revision +1
状态从 archived 变 published 重新建立或恢复向量,revision +1
完整扫描确认页面消失 标记 remote_deleted,删除向量,revision +1

8.5 页面镜像与向量的原子可见性

不能先删除旧向量,再调用远程 Embedding,否则 Embedding 失败期间页面完全不可检索。

推荐流程:

  1. 在事务外准备规范化文本、chunks 和新 embedding;
  2. Embedding 全部成功并校验维度;
  3. 开启数据库事务;
  4. upsert 页面镜像;
  5. 删除该页旧活动向量;
  6. 批量插入新向量;
  7. 设置 index_status=ready、更新 fingerprint;
  8. 对知识库 index_revision + 1;
  9. 提交事务。

如果内容镜像已经变化但向量生成失败,有两种选择:

  • 保留旧页面+旧向量,直到新向量成功;
  • 保存新页面但继续用旧向量。

本文默认采用更一致的第一种:页面检索镜像与向量同事务换代。可另存 pending_content_hash/last_error 记录待更新状态,但查询始终看到同一版本的页面与向量。

8.6 删除和归档

  • 上游页面归档:本地保留页面镜像用于审计,但搜索快照排除;
  • 上游页面删除:本地标记 is_remote_deleted=true,删除向量;
  • DeerFlow 映射删除:级联清理本地页面和向量;
  • 知识库取消公共发布:不删除本地向量,因为所有者仍可能内部检索;只通过权限过滤立即禁止公共/外部访问;
  • 关闭 wiki_index_enabled:停止同步并从搜索快照排除,是否物理清理由管理员另行执行。

8.7 多 worker 租约

租约获取应使用数据库原子条件更新:

lease_owner IS NULL OR lease_expires_at < now OR lease_owner = current_owner

同步过程中每完成一个分页或一批 Embedding 续租。进程崩溃后,租约到期由其它 worker 接管。不要只使用本地文件锁,因为 PostgreSQL/MySQL 多实例部署下文件锁不可见。

SQLite 单机仍可使用同一数据库租约逻辑,不需要分支实现。


9. Wiki 页面切分与 Embedding

9.1 为什么检索章节、引用页面

完整 Wiki 页面可能很长。如果整页只生成一个向量:

  • 多主题页面语义被平均;
  • 局部事实召回变差;
  • 送给模型时正文过长;
  • 页面更新任一小段都会改变整页表达。

因此:

  • 向量实体:Wiki 页面章节;
  • 引用实体:Wiki 页面;
  • 展示实体:Wiki 页面;
  • 给模型的内容:命中的 1~2 个章节。

9.2 文本预处理

处理顺序:

  1. 统一 \r\n 为 \n;
  2. 去除 YAML frontmatter,但将有价值的 aliases/category 显式加入标题前缀;
  3. 保留 Markdown 标题文字;
  4. 保留表格文本,但可把过长表格按行切分;
  5. 移除纯 HTML script/style;
  6. 不执行 Markdown 内嵌 HTML;
  7. 图片仅保留 alt 文本和标题,不下载图片生成 embedding;
  8. 规范化连续空行;
  9. 对“对话沉淀”继续沿用已有品牌清理规则;
  10. 不对 Wiki 正文做展示层敏感词替换后再向量化,避免管理配置变化导致全量重建;敏感信息是否允许进入索引应在同步前使用专门安全策略决定。

9.3 切分格式

建议每个 embedding 输入都带页面上下文:

页面:{title}
别名:{aliases}
类型:{page_type}
章节:{heading_path}

{section_content}

这样即使命中片段正文没有重复实体名,向量仍能感知页面主题。

首版参数:

  • max_chars=1000;
  • overlap=120;
  • min_chars=80;
  • 优先按 H1~H6 切分;
  • 过长章节按段落打包;
  • 单段仍超长时硬切;
  • 空章节跳过;
  • summary 可作为第 0 段,格式为“页面摘要”。

9.4 内容去重

同一页面内如果两个 chunk 规范化后 hash 相同,只保留一个。不同页面内容相同也不全局去重,因为它们可能属于不同权限知识库和不同引用页面。

9.5 Embedding 调用要求

现有 EmbeddingClient 是 best-effort 设计,失败时返回空数组。Wiki 正式索引应封装一个严格模式:

  • 支持批量请求;
  • 指数退避重试;
  • 区分超时、鉴权、限流、响应格式和维度错误;
  • 返回数量必须与输入数量相同;
  • 任一向量为空、维度不同、含 NaN/Inf,则本批失败;
  • 错误写入页面 last_error,但不覆盖旧索引;
  • 日志不输出正文、API Key 或完整响应体;
  • 记录模型名、批大小、耗时、状态码和安全错误分类。

9.6 模型切换

Embedding 指纹变化后:

  1. 新查询不得把旧模型向量和新查询向量比较;
  2. 状态接口显示 rebuild_required=true;
  3. 管理员触发全量重建;
  4. 重建可逐知识库进行;
  5. 某个知识库全部新向量完成后,再切换其 active fingerprint;
  6. 未完成的知识库继续使用旧 fingerprint 与旧查询模型是不现实的,因此建议全量重建期间暂停严格向量查询,或同时保留旧、新两个 Embedding 客户端配置。首版选择“显示维护中并暂停该库搜索”,降低复杂度。

10. DeerFlow 本地向量执行器

10.1 首版选择:NumPy 精确检索

当前依赖中已有 NumPy。首版无需引入 FAISS/HNSW/Qdrant,采用:

  1. 从数据库读取某知识库当前 fingerprint 的 ready vectors;
  2. 将 BLOB 解码为 numpy.ndarray(dtype=float32);
  3. 堆叠为形状 [N, D] 的归一化矩阵;
  4. 查询向量归一化为 [D];
  5. 计算 scores = matrix @ query_vector;
  6. 使用 numpy.argpartition 取 TopK,而不是完整排序;
  7. 根据 metadata 返回页面和章节。

这是精确余弦检索,不是近似检索。

10.2 为什么按知识库懒加载

如果一次加载全部 Wiki:

  • 多 worker 会重复占用大量内存;
  • 权限过滤需要在全矩阵上构造 mask;
  • 热门小库会被冷门大库拖累;
  • 单库更新导致全局矩阵重建。

因此每个 worker 保存 LRU:

mapping_id -> WikiVectorSnapshot

快照包含:

class WikiVectorSnapshot:
    mapping_id: str
    index_revision: int
    embedding_fingerprint: str
    dimensions: int
    matrix: np.ndarray
    rows: list[VectorMetadata]
    loaded_at: datetime

查询多个知识库时,对各知识库快照分别取 TopK,再做全局合并。这样无需把不同库物理拼成一个永久矩阵。

10.3 缓存失效

每次查询或最多每隔 cache_revision_check_seconds 检查数据库中的 index_revision:

  • 修订号相同:复用快照;
  • 修订号变化:为该知识库加载新快照并原子替换;
  • 加载失败:继续保留旧快照,但若权限/发布状态变化,权限层仍会立即阻断;
  • 映射删除/索引禁用:立即从 LRU 移除;
  • LRU 超过 cache_max_knowledge_bases:驱逐最久未使用快照。

同一 worker 内多个请求同时发现修订变化时,用 per-KB async lock 合并重载,避免击穿数据库。

10.4 内存估算

归一化 float32 矩阵仅向量部分约占:

向量数 × 维度 × 4 字节

示例(不含 Python metadata 开销):

章节向量数 维度 纯矩阵内存
10,000 1,024 约 39 MiB
50,000 1,024 约 195 MiB
100,000 1,024 约 391 MiB
100,000 768 约 293 MiB

因此:

  • 首版上线前必须用真实 Wiki 数量做容量评估;
  • 多 worker 内存要乘以 worker 数;
  • LRU 应限制同时驻留的知识库数量;
  • 大库可进一步按固定 shard 切分;
  • 若单库达到数十万章节向量或内存不可接受,应切换 pgvector,而不是继续扩大 Python 进程内存。

上述数量仅用于容量估算,不作为硬性能承诺,最终阈值由目标机器基准测试确定。

10.5 查询算法

伪代码:

async def vector_search(query, allowed_mapping_ids, top_k_sections=30):
    q = await embedding.embed_query(query)
    q = validate_and_normalize(q)

    candidates = []
    for mapping_id in allowed_mapping_ids:
        snapshot = await cache.get(mapping_id)
        if snapshot.fingerprint != active_fingerprint:
            continue
        scores = snapshot.matrix @ q
        local_top = argpartition_top(scores, min(top_k_sections, len(scores)))
        candidates.extend(snapshot.materialize(local_top, scores))

    candidates.sort(key=lambda item: item.score, reverse=True)
    candidates = [c for c in candidates if c.score >= min_similarity]
    return group_by_page(candidates)

10.6 页面聚合

同一页可能命中多个章节。聚合规则:

  1. 页面分数默认取最高章节分数;
  2. 可选加入第二章节小幅加权,但不能简单相加导致长页面占优;
  3. 每页最多保留 max_sections_per_page=2;
  4. 页面按最终分数排序;
  5. 同一 slug 在不同知识库中视为不同页面;
  6. 返回 matched_sections,模型可接收多个片段,右侧只列一张页面卡片。

建议页面分数:

page_score = max_section_score + 0.05 * second_section_score(若存在)

最终裁剪到 1.0 仅用于展示;内部排序保留原始值。

10.7 可选关键词加权

用户已明确要求向量检索,因此关键词不得成为隐式回退。可选 hybrid 仅用于重排:

final_score = (1 - w) * vector_score + w * keyword_score

默认 w=0。若后续启用,关键词评分只针对已由向量召回的候选,不额外从原始文档召回。


11. 权限模型

11.1 内部检索权限

检索前必须先得到 DeerFlow 本地 mapping ID 列表,并沿用现有规则:

  1. 当前对话明确选择的 llmwiki_knowledge_base_ids;
  2. 当前智能体 config.yaml 允许绑定的范围;
  3. 当前用户对映射的实时读取权限;
  4. 对话明确空列表表示不检索;
  5. 用户权限撤销后,旧线程下一轮查询也必须失效;
  6. 不接受前端或模型传入 WeKnora remote ID。

完成上述交集后,才允许读取相应本地向量快照。

11.2 页面状态

默认:

  • 普通用户:仅 published;
  • 公共库访问者:仅 published;
  • 所有者/管理员:默认仍仅 published,明确 include_drafts=true 才包含 draft;
  • archived/deleted:任何搜索都排除;
  • 详情接口对所有者/管理员可按管理场景读取 archived,但引用详情不读取。

11.3 外部接口权限

每个返回结果必须同时满足:

mapping.publication_status == "published"
AND mapping.external_search_enabled == true
AND mapping.wiki_index_enabled == true
AND page.status == "published"
AND page.is_remote_deleted == false
AND page.index_status == "ready"
AND mapping 不是系统“对话沉淀”

即使 NumPy 快照仍在内存中,只要数据库映射已取消发布或关闭外部开关,权限检查就应在查询入口立即阻断。因此缓存失效延迟不会造成权限泄漏。

11.4 外部开放开关为什么独立

当前“公共库”语义是 DeerFlow 登录用户可读,不等于互联网匿名/服务对服务可读。如果直接把所有 published 库放进外部 API,会扩大历史数据的受众范围。

默认策略:

  • 数据库迁移后,所有现有公共库 external_search_enabled=false;
  • 所有者或管理员明确开启;
  • UI 显示“内部公共”和“外部 API 开放”是两个开关;
  • 开启外部开放前提示内容将可由持有 API Key 的系统检索;
  • 关闭后立即生效。

11.5 系统“对话沉淀”

该知识库在现有逻辑中始终 public,但可能含用户对话内容。因此:

  • 外部搜索强制排除,不能仅依赖可编辑字段;
  • 外部详情接口同样排除;
  • 状态接口不得把其 remote ID 暴露出去;
  • 内部检索继续按当前产品策略使用;
  • 未来若要外部开放,必须另做数据脱敏和用户授权设计,不属于本方案。

12. 统一检索结果协议

12.1 内部结构

建议统一结果:

{
  "kind": "wiki_page",
  "id": "local-page-id",
  "knowledge_base_id": "deerflow-mapping-id",
  "knowledge_base_name": "公共知识库",
  "wiki_page_id": "local-page-id",
  "wiki_slug": "entity/alice",
  "title": "Alice",
  "summary": "页面摘要",
  "page_type": "entity",
  "page_version": 3,
  "updated_at": "2026-08-21T10:00:00+08:00",
  "score": 0.8621,
  "matched_sections": [
    {
      "section_index": 2,
      "heading": "主要经历",
      "content": "命中的 Wiki 章节正文",
      "score": 0.8621
    }
  ],
  "url": "/weknora-source-preview?knowledge_base_id=deerflow-mapping-id&wiki_slug=entity%2Falice",
  "source": "公共知识库",
  "type": "llmwiki_wiki",
  "mode": "citation"
}

注意:URL 名称可继续保持 /weknora-source-preview 以兼容现有解析器,但它实际上读取 DeerFlow 本地镜像。也可以新增更准确的 /llmwiki-wiki-preview,并同时兼容旧链接。实现时推荐新增链接格式并保留旧格式解析。

12.2 不对外返回的字段

外部 API 默认不返回:

  • remote_page_id;
  • WeKnora remote knowledge base ID;
  • source_refs;
  • chunk_refs;
  • 向量 BLOB;
  • Embedding 模型内部地址或 API Key;
  • owner user ID;
  • 同步错误原文;
  • 草稿/归档页面存在性。

12.3 给大模型的上下文

建议格式:

<llmwiki_wiki_reference_batch source_count="N">
[1] 页面标题
知识库:xxx
页面:entity/alice
命中章节:主要经历
相关度:0.8621
正文:...

[2] ...
</llmwiki_wiki_reference_batch>

要求模型:

  • 正文引用只能使用存在的 [N];
  • 同一 Wiki 页面多个章节仍只对应一个编号;
  • 不把 source_refs/chunk_refs 暴露给模型;
  • 资料不足时明确说明;
  • 不自行把 Wiki 内容描述成原始一手文献。

13. 后端接口设计

13.1 内部搜索接口

建议新增:

POST /api/llmwiki/wiki-vector-search
Authorization: Bearer <DeerFlow session token>
Content-Type: application/json

请求:

{
  "query": "任正非早期创业经历",
  "knowledge_base_ids": ["deerflow-mapping-id"],
  "top_k": 8,
  "include_drafts": false,
  "include_content": true
}

约束:

  • query 1~4000 字符;
  • mapping IDs 最多 100;
  • top_k 1~50;
  • 服务端重新鉴权,不能相信前端选择;
  • include_drafts 仅所有者/管理员生效;
  • strict vector 下索引未就绪返回 409/503,不走 chunk search。

响应:

{
  "query": "任正非早期创业经历",
  "mode": "vector",
  "results": [],
  "knowledge_base_count": 1,
  "index_fingerprint": "abc123...",
  "partial": false,
  "warnings": []
}

13.2 本地 Wiki 页面详情

GET /api/llmwiki/knowledge-bases/{mapping_id}/local-wiki/pages/{slug:path}

返回本地镜像,不访问 WeKnora。权限与现有 get_authorized(write=false) 一致。

响应包含:

  • title/summary/content/page_type/status;
  • aliases/category/version/updated_at;
  • 索引状态和最近同步时间(管理者可见);
  • 普通引用视图不返回 source_refs/chunk_refs;
  • 管理详情可增加 include_provenance=true,仅所有者/管理员读取溯源。

13.3 索引管理接口

管理员接口:

GET  /api/llmwiki/wiki-index/status
POST /api/llmwiki/wiki-index/sync
POST /api/llmwiki/wiki-index/rebuild
POST /api/llmwiki/wiki-index/knowledge-bases/{mapping_id}/sync
POST /api/llmwiki/wiki-index/knowledge-bases/{mapping_id}/rebuild

sync 请求示例:

{
  "knowledge_base_ids": ["..."],
  "force": false
}

rebuild 请求示例:

{
  "knowledge_base_ids": ["..."],
  "reason": "embedding_model_changed"
}

长任务应返回 202 Accepted + job_id,不要在 HTTP 请求中等待整个知识库完成。

13.4 外部向量搜索接口

推荐路径:

POST /api/external/llmwiki/wiki/vector-search
X-API-Key: <external key>
Content-Type: application/json

请求:

{
  "query": "任正非早期创业经历",
  "knowledge_base_ids": [],
  "limit": 10,
  "include_content": false
}

语义:

  • 空 knowledge_base_ids 表示所有允许外部搜索的公共 Wiki;
  • 非空列表必须是 DeerFlow mapping ID;
  • 请求私有/未开放 ID 时建议统一返回 404 或空结果,避免枚举资源;
  • 查询只使用本地向量;
  • 默认返回摘要和命中片段;
  • include_content=true 也要受单页和总响应字符上限约束。

响应:

{
  "query": "任正非早期创业经历",
  "mode": "vector",
  "items": [
    {
      "knowledge_base_id": "public-mapping-id",
      "knowledge_base_name": "人物知识库",
      "wiki_slug": "entity/ren-zhengfei",
      "title": "任正非",
      "summary": "...",
      "matched_heading": "早期创业",
      "snippet": "...",
      "page_type": "entity",
      "page_version": 5,
      "updated_at": "...",
      "score": 0.89
    }
  ],
  "count": 1,
  "partial": false
}

13.4.1 免鉴权浏览器向量搜索

本部署另行提供一个与上述 API-Key 命名空间隔离的新接口:

POST /api/knowledge/vector-search
Content-Type: application/json

最小请求只需检索词:

{"query": "任正非早期创业经历"}

可选参数只有 top_k(默认 10,最大 100)和 include_page_content(默认 false)。接口不需要登录 Token 或 X-API-Key,不做应用内限流,并针对这一条路由返回 Access-Control-Allow-Origin: *。检索范围是全部 published 知识库, 不检查 external_search_enabled,但在检索前后都排除系统“对话沉淀”库。

响应按向量分段展开:同一 Wiki 页命中多个分段时会返回多条结果。每条结果 包含命中文本和分数、DeerFlow/WeKnora 知识库与 Wiki 页 ID、页面元数据、 原始 source_refs/chunk_refs、可提取到的文档/分块 ID,以及可直接打开的 wiki_url。当 include_page_content=true 时额外返回完整 page_content。 wiki_url 的前缀配置为 llmwiki.local_wiki_index.frontend_base_url,开发默认值是 http://127.0.0.1:5174。

13.5 外部页面详情

GET /api/external/llmwiki/wiki/pages/{mapping_id}/{slug:path}
X-API-Key: <external key>

再次实时检查公共/外部开关和页面 published 状态,不能因为搜索结果曾经返回过就绕过当前权限。

13.6 外部 API 鉴权接入

当前 /api/public/ 前缀会绕过全局登录鉴权,不适合直接承载未加保护的重型向量接口。推荐:

  1. 使用 /api/external/;
  2. 在 AuthMiddleware 中增加仅对该前缀生效的外部 API Key 分支;
  3. 使用 hmac.compare_digest() 常量时间比较;
  4. API Key 从环境变量解析,不存数据库明文;
  5. 缺失/错误统一返回 401,不透露配置状态;
  6. 日志只记 key 指纹前 8 位或完全不记;
  7. CORS 不作为服务到服务鉴权手段;
  8. 生产环境同时在 Nginx/API Gateway 做 IP、速率和请求体大小限制。

如果为了少改 AuthMiddleware 而把接口挂到 /api/public/,路由自身也必须强制校验 API Key;但长期推荐独立 /api/external/ 语义。

上面的原则仍适用于通用服务间接口;13.4.1 是经过明确产品确认的例外,并通过 精确路径白名单和路由级 CORS 隔离,不能据此放宽其他 API。

13.7 状态码约定

状态码 场景
200 搜索成功,包括 0 结果
202 同步/重建任务已接受
400 参数格式错误
401 外部 API Key 或登录凭据无效
403 已认证但无操作权限
404 映射/页面不存在或不可见
409 索引未就绪、正在重建或模型指纹不一致
413 查询/响应内容请求过大
429 限流
502 同步阶段 WeKnora 异常(搜索阶段不应返回)
503 Embedding 服务或本地向量执行器不可用

14. 内部问答与工具接入

14.1 普通问答预检索

修改 app/gateway/llmwiki_rag.py:

  • 原 build_weknora_client(...).search() 替换为本地 WikiVectorSearchService;
  • 仍沿用对话选择和用户权限;
  • _source_from_result() 改为 Wiki 结构;
  • prompt 只包含命中章节;
  • _MAX_SOURCES 仍可保留 8;
  • _MAX_SOURCE_CHARS 应对每页所有命中章节的合计做限制;
  • 索引未就绪时给模型明确提示,不能声称“知识库没有内容”。

14.2 智能体工具

修改 deerflow/tools/builtins/llmwiki_search_tool.py:

  • 工具名可继续为 llmwiki_search,避免技能提示词迁移;
  • 实现改为查询 DeerFlow 本地 Wiki 索引;
  • 实时检查智能体绑定、对话选择和用户权限;
  • 返回 JSON 中 results 全部为 kind=wiki_page;
  • URL 只使用 mapping ID + wiki slug;
  • 不再返回原始 chunk_id/knowledge_id/document_id 作为主引用;
  • 工具描述改成“检索已处理的 Wiki 页面”。

由于 harness 包不能 import app.*,本地索引 repository/search service 必须放在 deerflow.* 下,并通过 runtime setter 或应用初始化注入,不能让工具直接引用 Gateway router。

14.3 页面检索接口

现有 POST /api/llmwiki/search 可做兼容升级:

  • 默认 source_kind=wiki;
  • 新增 mode=vector;
  • Wiki 库走本地向量;
  • 标准非 Wiki 库是否保留原始检索需产品明确。本文默认:该接口如果明确选择标准库,可保留 raw_chunk,但问答中选中 Wiki 库时严格只查 Wiki;
  • 更清晰的做法是保留旧接口并新增 /wiki-vector-search,前端逐步迁移后再废弃旧模式。

14.4 深度研究和 AI 写作

它们通过 llmwiki_search 或共享参考文献归一化间接使用 WeKnora 时,应自然收到新的 Wiki result。需要回归:

  • deep_research.adapters.material_provider;
  • deep_research.collection.thread_harvester;
  • AI 写作 _skill_agent.py 参考文献归一化;
  • deerflow.runtime.references 对 raw 字段的保留。

必须确保 wiki_slug/page_id/matched_sections 不在归一化过程中被丢弃。


15. 前端改造

15.1 引用目标解析

修改:

frontend-web/src/core/llmwiki/source-links.ts

当前 preview URL 解析要求 knowledge_base_id && chunk_id。新逻辑应允许:

knowledge_base_id && (wiki_slug || chunk_id || document_id)

优先级:

  1. wiki_slug;
  2. chunk_id(历史兼容);
  3. document_id(历史兼容)。

15.2 右侧参考文献抽屉

修改 WeKnoraSourceDrawer.tsx 或拆出 LlmWikiWikiSourceDrawer:

当 target 有 wikiSlug:

  • 调用 DeerFlow 本地页面详情接口;
  • 不调用 WeKnora 文档 preview;
  • 不加载 document chunks;
  • 不展示“下载原文”作为主按钮;
  • 展示标题、摘要、页面类型、更新时间、版本、完整 Markdown;
  • 可高亮 matched_heading;
  • 可以有默认折叠的“支撑来源”入口,但仅登录且有权限时可用;
  • 外部公共页面不能借此读取 source_refs/chunk_refs。

当 target 只有旧 chunkId/documentId:

  • 保留当前历史引用预览;
  • 可尝试本地反向映射到 Wiki;
  • 找不到则继续显示原文,不阻断历史聊天。

15.3 参考文献卡片

卡片显示:

  • Wiki 页面标题;
  • 知识库名称;
  • 页面类型;
  • 命中章节标题;
  • Wiki 摘要或命中片段;
  • 向量相关度;
  • 更新时间;
  • “查看 Wiki”而非“查看原文”。

不要在卡片上显示:

  • 原文件名作为主标题;
  • chunk index;
  • WeKnora remote ID;
  • 向量维度/模型;
  • source_refs 原始 ID。

15.4 管理页面建议

在知识库详情或系统设置增加“本地向量索引”状态:

  • 已同步页面数;
  • 已索引页面数;
  • 向量章节数;
  • 最近同步时间;
  • 当前状态/错误;
  • Embedding 模型与短指纹;
  • “立即同步”;
  • “重建向量”;
  • “允许外部 API 检索”开关;
  • 重建进度。

首版如果不做完整管理页,至少保证管理员 API 和日志可用。


16. 后端模块划分

建议新增:

packages/harness/deerflow/persistence/llmwiki_index/
  __init__.py
  base.py
  model.py
  memory.py
  sql.py

packages/harness/deerflow/integrations/weknora/local_index/
  __init__.py
  schemas.py
  normalize.py
  chunking.py
  embedding.py
  repository.py          # 可直接使用 persistence store 时省略
  vector_codec.py
  vector_cache.py
  search.py
  sync.py
  runtime.py

app/gateway/routers/
  llmwiki_index.py
  external_llmwiki.py

app/gateway/
  llmwiki_index_scheduler.py

也可以将 local_index 放到 deerflow/llmwiki_index/,关键要求是:

  • harness 不 import app;
  • Gateway 负责 HTTP、用户解析、调度启动;
  • harness 负责数据模型、同步核心、向量编码、缓存和搜索;
  • WeKnora client 仍只负责官方 REST 适配;
  • 路由不直接写 SQL。

16.1 Store 接口建议

class LlmWikiIndexStore(ABC):
    async def get_page(...): ...
    async def list_pages(...): ...
    async def stage_or_replace_page_vectors(...): ...
    async def mark_page_failed(...): ...
    async def mark_missing_pages_deleted(...): ...
    async def load_vector_snapshot(mapping_id, fingerprint): ...
    async def get_index_revision(mapping_id): ...
    async def try_acquire_sync_lease(...): ...
    async def renew_sync_lease(...): ...
    async def finish_sync(...): ...
    async def list_index_status(...): ...

提供 SQL 和 memory 实现,便于单元测试及 auth-disabled 开发模式。

16.2 应用初始化

在 Gateway lifespan 中:

  1. 创建/取得 index store;
  2. 创建严格 Embedding client;
  3. 创建 WikiVectorCache;
  4. 创建 WikiVectorSearchService;
  5. 创建 WikiSyncService;
  6. 挂到 app.state;
  7. 给 harness runtime setter 注入只读 search/store;
  8. 若 sync enabled,启动调度 loop;
  9. shutdown 时停止调度并清理缓存/HTTP client。

调度器需要和现有 lifespan 后台任务保持相同的取消与超时风格。


17. 数据库迁移与兼容性

17.1 Alembic 迁移

新增迁移:

deerflow/persistence/migrations/versions/<date>_01_llmwiki_local_wiki_index.py

迁移内容:

  1. 扩展 llmwiki_knowledge_bases;
  2. 创建 llmwiki_wiki_pages;
  3. 创建 llmwiki_wiki_vectors;
  4. 创建 llmwiki_wiki_sync_states;
  5. 建立 FK、唯一约束和索引;
  6. 所有现有 mapping 的 external_search_enabled=false;
  7. wiki_index_enabled=true 或按配置决定;
  8. 不在迁移过程中调用 WeKnora 或 Embedding。

迁移必须兼容 SQLite、PostgreSQL 和 MySQL。注意 MySQL utf8mb4 长索引限制,slug 使用 hash 唯一约束。

17.2 向量 BLOB 可移植性

  • SQLite:BLOB;
  • PostgreSQL:BYTEA;
  • MySQL:LONGBLOB/BLOB,按最大维度选择;
  • SQLAlchemy 使用 LargeBinary;
  • 字节序固定 little-endian;
  • 模型维度显式保存;
  • 不依赖数据库特定 vector 类型,便于首版跨后端运行。

17.3 首次回填

迁移完成后不自动阻塞启动进行全量回填。建议:

  1. 服务正常启动;
  2. 管理员查看状态为 not_synced;
  3. 手动或后台分库同步;
  4. 同步完成一个库就允许该库向量检索;
  5. 全部稳定后再将问答默认切换为 local wiki vector;
  6. 外部 API 最后开启。

17.4 历史引用

历史消息中的引用仍可能只有 chunk/document。必须保留:

  • 旧 URL 解析;
  • 旧 source drawer;
  • /api/llmwiki/sources/context;
  • 原文预览权限。

新消息只生成 Wiki 引用。不要批量重写历史消息,除非后续建立可靠 chunk_ref → local wiki page 反向表并单独实施迁移。


18. 外部 API 安全设计

18.1 威胁模型

需要防范:

  • 枚举私有 mapping ID;
  • 通过 query 大小或 top_k 放大 CPU/Embedding 成本;
  • 高频调用耗尽 Embedding 服务配额;
  • 请求全部公共库导致加载大量快照;
  • 返回完整 Wiki 内容造成带宽放大;
  • Markdown 中的 HTML/script 被下游不安全渲染;
  • API Key 泄漏;
  • 关闭发布后缓存继续返回;
  • “对话沉淀”误开放;
  • 错误日志包含正文或敏感标识。

18.2 控制措施

  1. 外部 API Key 必填;
  2. query 字符数、mapping 数、top_k、响应字符数硬上限;
  3. 请求体大小由应用和反向代理共同限制;
  4. 限流按 API Key + IP;
  5. 查询向量短 TTL 缓存可减少重复 embedding,但缓存 key 不保存原文日志;
  6. 外部查询只加载明确开放的 KB;
  7. 权限在检索前和返回前各检查一次;
  8. 页面详情接口再次检查;
  9. 返回 Markdown 时标注 content type,前端渲染必须禁用危险 HTML;
  10. 不返回 remote ID、source refs、chunk refs、owner 信息;
  11. 日志对 query 做长度裁剪或 hash;
  12. “对话沉淀”代码级 deny;
  13. API Key 支持轮换:可配置 current + previous,短时间双 key 过渡;
  14. 对外接口默认关闭。

18.3 Embedding 成本控制

外部每次查询需要一次 embedding。建议:

  • 对规范化 query 做 1~5 分钟进程内 LRU 缓存;
  • 缓存 key 使用 embedding_fingerprint + sha256(query);
  • 缓存值是查询向量,不是结果权限;
  • 权限和结果仍每次实时计算;
  • 不跨 fingerprint 复用;
  • 缓存大小有上限;
  • API Key 限流在调用 Embedding 前执行。

19. 失败处理与一致性

故障 预期行为 是否回退原文
WeKnora 同步超时 保留旧镜像/向量,状态 partial/failed 否
某页读取失败 旧页继续可用,记录页面错误 否
Embedding 超时 不覆盖旧向量,重试 否
Embedding 401/403 标记配置错误,停止无意义重试 否
返回向量数量不一致 本批失败,不落库 否
维度变化 标记 rebuild_required 否
BLOB 损坏 跳过损坏库/页并告警,触发重建 否
NumPy 快照加载失败 保留旧快照;无旧快照则索引不可用 否
单个 KB 搜索失败 内部可返回 partial + warning;外部默认 partial 否
所有 KB 不可用 返回 409/503 否
取消公共发布 权限层立即过滤 不适用
外部开关关闭 权限层立即过滤 不适用
Gateway 重启 从 DB 懒加载,无需重做 embedding 不适用

19.1 “没有命中”和“索引不可用”必须区分

  • 正常 0 结果:HTTP 200,results=[];
  • 未同步:409,错误码 WIKI_INDEX_NOT_READY;
  • 正在重建:409,WIKI_INDEX_REBUILDING;
  • Embedding 服务失败:503,WIKI_EMBEDDING_UNAVAILABLE;
  • 指纹不匹配:409,WIKI_INDEX_FINGERPRINT_MISMATCH;
  • 无权限/不存在:404 或权限语义对应状态。

模型提示也必须区分,避免把系统故障回答成“知识库没有相关资料”。


20. 可观测性

20.1 结构化日志

同步日志字段:

mapping_id
sync_id
page_number
remote_page_count
changed_pages
unchanged_pages
deleted_pages
embedded_chunks
failed_pages
duration_ms
error_category

检索日志字段:

request_id
caller_type (internal/external/tool/pre_rag)
authorized_kb_count
loaded_snapshot_count
candidate_section_count
result_page_count
embedding_ms
search_ms
total_ms
partial

不得记录:API Key、完整 query、Wiki 全文、向量。

20.2 指标

建议指标:

  • llmwiki_sync_runs_total{status};
  • llmwiki_sync_duration_seconds;
  • llmwiki_pages_total{status,index_status};
  • llmwiki_vectors_total;
  • llmwiki_embedding_requests_total{status};
  • llmwiki_embedding_duration_seconds;
  • llmwiki_search_requests_total{caller,status};
  • llmwiki_search_duration_seconds{stage};
  • llmwiki_search_results_count;
  • llmwiki_vector_cache_hits_total;
  • llmwiki_vector_cache_reloads_total;
  • llmwiki_vector_cache_bytes;
  • llmwiki_external_rate_limited_total。

若当前没有 Prometheus,可先接现有日志/工具指标体系,并保留后续扩展点。

20.3 状态接口

管理员状态页至少能回答:

  • 哪些库尚未同步;
  • 哪些库正在同步;
  • 哪些页面失败;
  • 当前模型指纹;
  • 是否需要重建;
  • 最近一次完整成功时间;
  • 本地页数和向量数;
  • 缓存是否加载;
  • 外部开放是否开启。

21. 性能与扩展路线

21.1 首版基准测试要求

用真实或合成数据覆盖:

  • 1,000 / 10,000 / 50,000 / 100,000 章节;
  • 768 / 1,024 / 1,536 维;
  • 单库与 5/20 库并发查询;
  • 1/4 Gateway workers;
  • 冷加载与热缓存;
  • 同步和搜索并行;
  • SQLite 与生产数据库后端。

记录:

  • 冷加载时间;
  • 单次向量计算 P50/P95/P99;
  • 进程 RSS;
  • DB BLOB 读取吞吐;
  • Embedding 延迟;
  • 多并发下事件循环阻塞情况。

NumPy 矩阵计算和大 BLOB 解码应放入 asyncio.to_thread(),避免阻塞事件循环。

21.2 何时迁移 pgvector

出现以下任一情况时评估 pgvector:

  • 单库快照内存不可接受;
  • 多 worker 重复内存过高;
  • 冷启动/重载时间过长;
  • 向量数量达到数十万以上且查询并发提高;
  • 需要数据库端按 mapping/status 过滤并近似索引;
  • 需要多节点共享统一向量执行层。

迁移时保持:

  • 页面表不变;
  • 统一结果协议不变;
  • 权限服务不变;
  • API 不变;
  • 只替换 WikiVectorEngine 实现。

建议抽象:

class WikiVectorEngine(ABC):
    async def search(self, query_vector, mapping_ids, top_k, fingerprint): ...
    async def invalidate(self, mapping_id): ...

首版 NumpyExactWikiVectorEngine,未来 PgVectorWikiVectorEngine。


22. 测试方案

22.1 单元测试

  1. Markdown 标题切分、长段落、表格、空页、frontmatter;
  2. title/alias/heading 前缀构造;
  3. chunk hash 与页面 hash 稳定性;
  4. float32 BLOB 编解码;
  5. NaN/Inf/维度错误拒绝;
  6. 向量归一化;
  7. NumPy TopK 与手工余弦结果一致;
  8. 页面聚合和每页章节上限;
  9. min similarity;
  10. LRU 和 revision 失效;
  11. API Key 常量时间校验;
  12. 系统“对话沉淀”排除;
  13. embedding fingerprint;
  14. 错误脱敏。

22.2 Repository 测试

使用 SQLite 测:

  • 创建页面/向量;
  • 原子替换;
  • FK cascade;
  • unique mapping+slug_hash;
  • 完整扫描后删除缺失页;
  • 不完整扫描不删除;
  • lease 获取/续租/过期接管;
  • revision 只在检索数据变化时递增;
  • public/external 开关默认值;
  • BLOB round trip。

生产若使用 MySQL/PostgreSQL,补容器集成测试验证字段和索引兼容性。

22.3 同步集成测试

使用假的 WeKnora client:

  1. 首次 3 页同步并建立向量;
  2. 第二次无变化不调用 embedding;
  3. 一页 version/content 变化只重建该页;
  4. 一页归档后不再检索;
  5. 一页删除后清理向量;
  6. 分页第 2 页失败不误删本地页;
  7. Embedding 失败保留旧索引;
  8. 多 worker 只有一个获得租约;
  9. fingerprint 改变标记重建;
  10. 服务重启后从 DB 恢复快照。

22.4 检索与权限测试

  1. 查询仅存在于 Wiki、原始文档无该措辞,仍能命中;
  2. 查询只存在于原始 chunk、Wiki 不包含,严格模式不命中;
  3. 用户 A 不能检索用户 B 私有库;
  4. 公共库对登录用户可检索;
  5. 智能体未绑定库不能越权;
  6. 对话明确空选择不检索;
  7. 撤销公共发布后下一请求立即无结果;
  8. draft 默认不命中;
  9. 所有者明确 include_drafts 可以命中;
  10. archived/deleted 永不命中;
  11. 同一页多个章节只产生一个引用编号;
  12. 多库同 slug 不互相覆盖。

22.5 外部 API 安全测试

  1. 无 key/错 key → 401;
  2. 私有库/未外部开放 → 不可枚举;
  3. public 但 external_search_enabled=false → 不返回;
  4. draft/archived → 不返回;
  5. “对话沉淀”永不返回;
  6. 关闭外部开关立即失效;
  7. 超长 query → 400/413;
  8. 超大 top_k → 拒绝或钳制;
  9. 高频请求 → 429;
  10. 响应没有 remote ID/source_refs/chunk_refs/vector;
  11. 日志没有 API Key 和正文;
  12. 详情接口重新鉴权,不依赖旧搜索结果。

22.6 前端测试

  1. knowledge_base_id + wiki_slug 可解析为有效 target;
  2. Wiki target 不请求文档 preview/chunks;
  3. 抽屉展示本地 Wiki Markdown;
  4. 卡片显示 Wiki 标题和命中章节;
  5. 历史 chunk 引用仍能打开;
  6. 权限撤销后详情返回 404,前端正确提示;
  7. Wiki 内容 Markdown/HTML 安全渲染;
  8. loading/error/empty 状态。

22.7 性能测试

至少验证:

  • 50k × 1024 维单库热查询 P95;
  • 5 个库合并查询;
  • 冷加载内存和耗时;
  • 10/50 并发查询;
  • 同步时搜索延迟;
  • 多 worker 总内存;
  • 查询 embedding 缓存命中效果。

验收阈值应在目标部署机器基准后写入配置/运维文档,不在未测量前拍脑袋承诺具体毫秒数。


23. 分阶段实施计划

阶段 0:契约与配置

  • 增加配置模型;
  • 定义统一 result schema;
  • 定义错误码;
  • 增加 feature flag,默认关闭;
  • 确认 Embedding endpoint/model/dimensions。

完成标准:应用在功能关闭时行为完全不变。

阶段 1:本地镜像与同步

  • 新增迁移和三张表;
  • 实现 SQL/memory store;
  • WeKnora 页面规范化;
  • 完整扫描、页面 upsert、缺失删除;
  • 租约、状态、手动同步 API;
  • 暂不切换问答。

完成标准:DeerFlow DB 中 Wiki 页面和 WeKnora 一致,可独立读取详情。

阶段 2:Embedding 与本地向量

  • 严格 Embedding client;
  • Markdown chunks;
  • BLOB codec;
  • 页面原子换代;
  • rebuild;
  • 状态与失败重试。

完成标准:所有 ready 页面有正确 fingerprint 和向量;无变化同步不重复 embedding。

阶段 3:NumPy 检索服务

  • per-KB snapshot;
  • LRU/revision;
  • TopK/阈值/聚合;
  • 权限过滤;
  • 内部搜索 API;
  • 性能基准。

完成标准:可稳定从本地向量返回 Wiki 页与命中章节。

阶段 4:问答和参考文献切换

  • 普通问答预检索;
  • llmwiki_search 工具;
  • 页面检索;
  • runtime references;
  • 前端 source links/drawer/card;
  • 历史兼容。

完成标准:新问答不再产生原始文档 chunk 引用,右侧只显示 Wiki 主视图。

阶段 5:外部公共 API

  • 外部开关;
  • /api/external 鉴权;
  • vector-search/page detail;
  • 限流/响应上限;
  • “对话沉淀”强制排除;
  • 安全测试。

完成标准:仅明确外部开放的公共 published Wiki 可被持 key 系统检索。

阶段 6:运维与灰度

  • 管理状态;
  • 指标/告警;
  • 分库灰度;
  • 容量评估;
  • 故障演练;
  • 回滚验证。

24. 灰度与回滚

24.1 Feature flags

建议至少有:

local_wiki_index.enabled
local_wiki_index.auto_sync
local_wiki_index.external_api.enabled

可以逐步:

  1. 只同步;
  2. 只提供管理员搜索对比;
  3. 指定测试用户/知识库使用向量;
  4. 全量内部问答;
  5. 引用 UI;
  6. 外部 API。

24.2 双跑对比

灰度期可在不影响回答的情况下后台双跑:

  • 旧 chunk search;
  • 新 local wiki vector search。

只记录安全的对比指标:

  • 命中数;
  • 标题集合 hash;
  • TopK 重合率;
  • 查询耗时;
  • 是否为空;
  • 人工抽样质量。

不要把两套内容同时注入模型,以免引用编号混乱。

24.3 回滚

出现问题时:

  • 关闭 internal search flag,可恢复旧 chunk 检索;
  • 关闭 reference UI flag,可恢复旧抽屉;
  • 本地页面/向量表保留,便于排查;
  • 关闭 sync 不删除数据;
  • 外部 API 可独立关闭;
  • 数据库迁移不需要立即 downgrade;
  • 新消息引用格式解析器应向后兼容,避免回滚后打不开已生成的新 Wiki 引用。

注意:产品最终目标是严格 Wiki 检索。旧 chunk 回滚仅用于上线故障处置,不应成为长期隐式 fallback。


25. 验收标准

25.1 数据同步

  • 所有目标 Wiki 页面在 DeerFlow 有镜像;
  • title/summary/content/slug/version/status 与上游一致;
  • 无变化不重复 embedding;
  • 单页更新只重建该页;
  • 完整扫描失败不误删;
  • 删除/归档后搜索立即排除;
  • Gateway 重启不丢数据。

25.2 向量检索

  • 查询向量只生成一次;
  • 检索只使用 DeerFlow 本地 Wiki vectors;
  • 不访问 WeKnora search;
  • TopK 返回相关 Wiki 章节;
  • 同页多章节只占一个引用;
  • 索引未就绪不会误报“无资料”;
  • Embedding 失败不回退原始文档。

25.3 问答与参考文献

  • 普通问答、智能体工具、页面搜索结果一致;
  • 新引用携带 wiki_page_id/wiki_slug;
  • 右侧展示完整 Wiki 页面;
  • 不自动展示整个原文文档;
  • 历史 chunk 引用仍可用;
  • 撤权后引用详情不可访问。

25.4 外部接口

  • 只返回 published + external enabled;
  • 私有/草稿/归档/对话沉淀永不泄漏;
  • 不暴露 remote IDs、vectors、source refs;
  • key、限流、请求上限生效;
  • 取消发布或关闭外部开关立即生效;
  • WeKnora 不可用时已有本地索引仍可查询。

26. 风险清单与处理

风险 影响 处理
WeKnora 没有增量变更 feed 需要完整分页扫描 周期完整扫描 + BFF 操作即时同步;后续如上游支持 cursor 再优化
Wiki 生成有延迟 原文已上传但本地暂无 Wiki 显示 not ready,不回退原文
Embedding 服务不稳定 新页面无法入索引 保留旧索引、严格错误、后台重试
模型维度变化 新旧向量不可比较 fingerprint + 全量重建
大库内存高 多 worker OOM per-KB LRU、容量测试、后续 pgvector
SQLite 写锁 同步与业务写竞争 小事务、批量写、WAL、低并发同步;生产大规模用 Postgres/MySQL
页面和向量版本不一致 引用内容与召回语义错位 原子换代
公共库语义扩大 历史内部公共内容外泄 独立 external flag,默认关闭
对话沉淀误开放 用户对话泄漏 代码级 deny
历史引用无 slug 不能自动展示 Wiki 保留旧引用兼容,不强制迁移
Markdown 恶意 HTML 下游 XSS API 无执行;前端安全渲染/禁危险 HTML
多 worker 重复同步 重复 Embedding/写冲突 DB 租约
缓存旧权限 取消发布后仍可查 权限不缓存或短缓存;返回前复核

27. 文件级实施清单

后端配置与模型

  • deerflow/config/llmwiki_config.py:新增 local index/embedding/external 配置;
  • deerflow/persistence/llmwiki/model.py:mapping 外部开关;
  • deerflow/persistence/llmwiki/{base,memory,sql}.py:读写新开关;
  • deerflow/persistence/llmwiki_index/*:新增页面、向量、同步状态 store;
  • Alembic migration:新增表与字段;
  • deerflow/persistence/models/__init__.py:注册模型。

同步与向量

  • deerflow/integrations/weknora/client.py:确保 Wiki list/get 字段完整,保留 chunk_refs;
  • deerflow/integrations/weknora/local_index/normalize.py:规范化;
  • .../chunking.py:Wiki 专用 chunks;
  • .../embedding.py:严格 Embedding;
  • .../vector_codec.py:BLOB;
  • .../sync.py:同步、租约、原子换代;
  • .../vector_cache.py:NumPy 快照;
  • .../search.py:权限后的本地向量检索;
  • app/gateway/llmwiki_index_scheduler.py:周期任务;
  • app/gateway/app.py:初始化/关闭。

路由与业务接入

  • app/gateway/routers/llmwiki_index.py:内部搜索/状态/同步/重建;
  • app/gateway/routers/external_llmwiki.py:外部搜索/详情;
  • app/gateway/auth_middleware.py:外部 API Key 分支;
  • app/gateway/llmwiki_rag.py:预检索切换;
  • deerflow/tools/builtins/llmwiki_search_tool.py:工具切换;
  • app/gateway/routers/llmwiki.py:页面搜索、详情和外部开关接入;
  • deerflow/runtime/references.py:保留 Wiki 结构字段;
  • deep research / AI writing:回归兼容。

前端

  • frontend-web/src/core/llmwiki/source-links.ts;
  • frontend-web/src/components/workspace/skill-display-sources.tsx;
  • frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer.tsx;
  • frontend-web/src/strategy-components/api/llmwiki.ts;
  • 知识库详情/系统设置中的索引状态和外部开关(如纳入首版 UI)。

测试

  • 新增 tests/test_llmwiki_local_index_repository.py;
  • 新增 tests/test_llmwiki_wiki_sync.py;
  • 新增 tests/test_llmwiki_vector_search.py;
  • 新增 tests/test_external_llmwiki_search.py;
  • 扩展 tests/test_llmwiki_weknora.py;
  • 前端增加 source target / drawer 测试;
  • 增加性能基准脚本,不放入默认每次单测。

28. 实现前必须确认的部署输入

编码本身可以先做,但上线前必须明确:

  1. DeerFlow 可调用的 OpenAI-compatible Embedding 地址;
  2. 模型名称;
  3. 向量维度;
  4. 最大 Wiki 页面数、平均页面长度、预计章节数;
  5. Gateway worker 数;
  6. 生产数据库是 SQLite、PostgreSQL 还是 MySQL;
  7. 外部接口是否只给内网系统,是否有统一 API Gateway;
  8. 外部 API Key 的生成、分发和轮换责任;
  9. 哪些现有公共库允许外部开放;
  10. 同步允许的最大延迟,例如 1 分钟或 5 分钟。

若没有额外输入,开发默认使用:

  • 本地/测试 SQLite;
  • bge-m3 类 1024 维模型(实际以配置为准);
  • 120 秒轮询;
  • NumPy exact;
  • 外部 API 默认关闭;
  • strict vector;
  • 每页最多 2 个命中章节;
  • Top 8 页面。

29. 最终推荐

本需求不应被实现成“把 WeKnora 的搜索接口换个 URL”,也不应通过隐藏文档库重复导入来绕过同步问题。推荐把它建设为 DeerFlow 内部的正式 Wiki 检索子系统:

  1. WeKnora 负责生成;
  2. DeerFlow 完整镜像;
  3. DeerFlow 按章节向量化;
  4. DeerFlow 本地持久化;
  5. DeerFlow 统一检索和权限;
  6. 引用始终落到 Wiki 页面;
  7. 外部接口只查询明确开放的公共 Wiki;
  8. 未来只替换向量执行器即可扩容,不推翻页面、权限、API 和引用设计。

该方案能同时满足:不修改 WeKnora 后端、检索处理后的 Wiki、右侧展示 Wiki、向量语义召回、公共 Wiki 外部检索、WeKnora 查询故障隔离,以及后续向 pgvector/分布式向量引擎平滑演进。