70 KiB
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. 文档目标
本文是后续编码的默认实施蓝图,目标不是简单描述“可以做向量检索”,而是把以下工程问题一次性定义清楚:
- 如何从 WeKnora 拉取已经加工完成的 Wiki 页面,而不是继续检索原始文档切片。
- 如何把 Wiki 页面全文、元数据和向量持久化到 DeerFlow 自己的数据库中。
- 如何在不引入第二个重型向量服务的前提下,先用 DeerFlow 当前的 SQLite/PostgreSQL/MySQL 数据库与 NumPy 完成可用的精确向量检索。
- 如何保证 Wiki 新建、重新生成、人工编辑、回滚、归档和删除后,DeerFlow 本地镜像及向量不会长期失真。
- 如何让普通问答、智能体工具、页面检索、右侧参考文献和公共外部接口复用同一套检索结果。
- 如何保证个人库、公共库、智能体绑定、对话选择、Wiki 页面状态和外部发布开关在检索时被严格执行。
- 如何在外部开放公共 Wiki 检索时,避免把私有库、草稿页、系统“对话沉淀”、WeKnora 原始 ID 或内部凭据泄露出去。
- 如何迁移、灰度、回滚、监控和验收,而不是一次性替换后无法定位质量问题。
本文提到的表名、字段、模块、接口和默认行为均作为实现时的默认方案。如编码阶段出现必要调整,应同步更新本文,避免文档与实现长期漂移。
2. 结论摘要
2.1 可行性结论
方案可行,整体难度为中等。DeerFlow 已经具备三项可直接复用的基础能力:
- OpenAI 兼容的异步 Embedding 客户端:
deerflow/knowledge/embeddings.py; - 按 Markdown 标题与段落切分的逻辑:
deerflow/knowledge/chunking.py; - 余弦相似度、关键词评分和混合排序基础:
deerflow/knowledge/search.py。
本项目真正需要新增的核心能力是:
- Wiki 页面本地镜像数据模型;
- WeKnora → DeerFlow 的全量/增量同步与租约;
- Wiki 页面章节化、Embedding 和原子换代;
- DeerFlow 本地向量快照与权限过滤检索;
- 内部问答、引用面板和外部接口的统一结果协议;
- 索引状态、失败重试、重建、监控和安全边界。
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 页面。
该方法目前被三个主要生产入口复用:
app/gateway/llmwiki_rag.py:普通问答发送给模型前的自动预检索;deerflow/tools/builtins/llmwiki_search_tool.py:智能体主动调用的llmwiki_search工具;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 的正式索引,因为:
- JSON 浮点向量体积大、解析慢;
- 每次查询加载全部向量会放大数据库 IO;
- Python 标量循环不能充分利用 NumPy/BLAS;
- 没有按知识库权限预分片;
- 没有多 worker 索引修订和缓存失效机制。
本方案复用 Embedding 客户端和 Markdown 切分思想,但为 LLMWiki Wiki 新建独立表与检索快照,不直接塞入 knowledge_notes/knowledge_embeddings。
4. 目标、非目标与默认决策
4.1 功能目标
- DeerFlow 能完整保存每个已映射 WeKnora 知识库的 Wiki 页面镜像。
- DeerFlow 能按 Wiki 页面 Markdown 结构生成章节向量。
- 普通问答、智能体工具和页面检索都能按向量搜索 Wiki 页面。
- 大模型收到的是命中 Wiki 页的相关章节,而不是原始文档 chunk。
- 右侧参考文献主视图只展示 Wiki 页面。
- 公共 Wiki 可通过受控外部 API 进行向量检索。
- WeKnora 不可用时,已经同步成功的 Wiki 仍可检索和展示。
- Wiki 更新后可自动增量重建对应页面向量,不全库重算。
- Embedding 模型变化时可明确触发全量重建。
- 用户取消公共发布或关闭外部开放后,外部查询立即失去访问权,不依赖异步清理完成。
4.2 明确非目标
- 不修改或 fork WeKnora 后端。
- 不把 WeKnora 原始文件和全部原始 chunks 复制进 DeerFlow。
- 不让 DeerFlow 首版承担 Wiki 自动生成。
- 不使用“把 Wiki 再上传成隐藏文档库”的循环导入方案。
- 不在首版实现分布式 HNSW/Milvus/Qdrant 集群。
- 不在首版提供原始 embedding 向量下载接口。
- 不保证历史 chunk 引用全部能够转换为 Wiki 引用。
- 不允许外部调用者通过 API 传入 WeKnora 原始知识库 ID。
- 不允许向量服务失败后静默改查原始文档。
4.3 默认产品决策
如实现前未另行变更,采用以下默认值:
- 同步范围:所有 DeerFlow 已映射、远端存在且启用 Wiki 的知识库,包括个人库和公共库。
- 页面范围:同步
draft/published/archived全部状态,检索时再按调用者权限和页面状态过滤。 - 内部检索:默认只检索
published页面;所有者/管理员可以通过明确参数包含draft,普通用户不能。 - 外部检索:只检索
published页面。 - 外部开放:新增
external_search_enabled,默认false,不能仅凭现有publication_status=published自动向互联网开放。 - 系统“对话沉淀”知识库:允许内部按现有权限使用,但外部接口永远排除。
- 检索模式:严格向量;索引未就绪时返回明确状态,不回退原文。
- 引用单位:Wiki 页面;检索单位:Wiki 页面内的章节 chunk。
- 首版向量执行器:按知识库懒加载的 NumPy 精确检索。
- 外部搜索默认不返回完整正文,只返回摘要和命中片段;完整页面通过详情接口读取。
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 配置校验规则
启动时应执行:
enabled=true时,Embeddingbase_url和model必须非空;- 不向上游发送
dimensions参数;第一次成功 Embedding 后从响应自动发现维度,并固定到索引 profile; chunk_overlap_chars < chunk_max_chars;top_k_pages <= top_k_sections;external_api.enabled=true时必须配置非空 API Key;- API Key 不得出现在日志、状态接口或前端运行时配置;
strict_vector=true时,Embedding 不可用必须反映为索引失败/搜索不可用,不能静默关键词回退;- 更换
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)
向量写入前必须:
- 校验长度等于
embedding_dimensions; - 拒绝 NaN/Inf;
- L2 归一化;
- 转为 little-endian float32;
- 保存 BLOB;
- 读取时再次验证 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 同步触发方式
必须同时支持:
- 周期完整扫描:捕获 WeKnora 异步 Wiki 生成、后台修订和不经过 DeerFlow BFF 的编辑;
- 管理操作后即时同步:通过 DeerFlow 创建、更新、删除、回滚 Wiki 页面后,立即入队同步该页;
- 管理员手动同步:用于排障和首次导入;
- 管理员全量重建:用于切换 Embedding 模型/维度或修复索引。
8.2 知识库发现
每轮调度:
- 从
llmwiki_knowledge_bases获取wiki_index_enabled=true的映射; - 排除已删除映射;
- 向 WeKnora 查询远端知识库存在性与 Wiki 能力;
- 远端不存在时不立即删除本地数据,而是标记同步失败;
- 只有显式删除映射或连续确认上游知识库已删除时,才进入本地清理流程;
- 每个知识库独立获得租约,避免多 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 失败期间页面完全不可检索。
推荐流程:
- 在事务外准备规范化文本、chunks 和新 embedding;
- Embedding 全部成功并校验维度;
- 开启数据库事务;
- upsert 页面镜像;
- 删除该页旧活动向量;
- 批量插入新向量;
- 设置
index_status=ready、更新 fingerprint; - 对知识库
index_revision + 1; - 提交事务。
如果内容镜像已经变化但向量生成失败,有两种选择:
- 保留旧页面+旧向量,直到新向量成功;
- 保存新页面但继续用旧向量。
本文默认采用更一致的第一种:页面检索镜像与向量同事务换代。可另存 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 文本预处理
处理顺序:
- 统一
\r\n为\n; - 去除 YAML frontmatter,但将有价值的 aliases/category 显式加入标题前缀;
- 保留 Markdown 标题文字;
- 保留表格文本,但可把过长表格按行切分;
- 移除纯 HTML script/style;
- 不执行 Markdown 内嵌 HTML;
- 图片仅保留 alt 文本和标题,不下载图片生成 embedding;
- 规范化连续空行;
- 对“对话沉淀”继续沿用已有品牌清理规则;
- 不对 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 指纹变化后:
- 新查询不得把旧模型向量和新查询向量比较;
- 状态接口显示
rebuild_required=true; - 管理员触发全量重建;
- 重建可逐知识库进行;
- 某个知识库全部新向量完成后,再切换其 active fingerprint;
- 未完成的知识库继续使用旧 fingerprint 与旧查询模型是不现实的,因此建议全量重建期间暂停严格向量查询,或同时保留旧、新两个 Embedding 客户端配置。首版选择“显示维护中并暂停该库搜索”,降低复杂度。
10. DeerFlow 本地向量执行器
10.1 首版选择:NumPy 精确检索
当前依赖中已有 NumPy。首版无需引入 FAISS/HNSW/Qdrant,采用:
- 从数据库读取某知识库当前 fingerprint 的 ready vectors;
- 将 BLOB 解码为
numpy.ndarray(dtype=float32); - 堆叠为形状
[N, D]的归一化矩阵; - 查询向量归一化为
[D]; - 计算
scores = matrix @ query_vector; - 使用
numpy.argpartition取 TopK,而不是完整排序; - 根据 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 页面聚合
同一页可能命中多个章节。聚合规则:
- 页面分数默认取最高章节分数;
- 可选加入第二章节小幅加权,但不能简单相加导致长页面占优;
- 每页最多保留
max_sections_per_page=2; - 页面按最终分数排序;
- 同一 slug 在不同知识库中视为不同页面;
- 返回
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 列表,并沿用现有规则:
- 当前对话明确选择的
llmwiki_knowledge_base_ids; - 当前智能体
config.yaml允许绑定的范围; - 当前用户对映射的实时读取权限;
- 对话明确空列表表示不检索;
- 用户权限撤销后,旧线程下一轮查询也必须失效;
- 不接受前端或模型传入 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
}
约束:
query1~4000 字符;- mapping IDs 最多 100;
top_k1~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/ 前缀会绕过全局登录鉴权,不适合直接承载未加保护的重型向量接口。推荐:
- 使用
/api/external/; - 在 AuthMiddleware 中增加仅对该前缀生效的外部 API Key 分支;
- 使用
hmac.compare_digest()常量时间比较; - API Key 从环境变量解析,不存数据库明文;
- 缺失/错误统一返回 401,不透露配置状态;
- 日志只记 key 指纹前 8 位或完全不记;
- CORS 不作为服务到服务鉴权手段;
- 生产环境同时在 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)
优先级:
wiki_slug;chunk_id(历史兼容);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 中:
- 创建/取得 index store;
- 创建严格 Embedding client;
- 创建 WikiVectorCache;
- 创建 WikiVectorSearchService;
- 创建 WikiSyncService;
- 挂到
app.state; - 给 harness runtime setter 注入只读 search/store;
- 若 sync enabled,启动调度 loop;
- shutdown 时停止调度并清理缓存/HTTP client。
调度器需要和现有 lifespan 后台任务保持相同的取消与超时风格。
17. 数据库迁移与兼容性
17.1 Alembic 迁移
新增迁移:
deerflow/persistence/migrations/versions/<date>_01_llmwiki_local_wiki_index.py
迁移内容:
- 扩展
llmwiki_knowledge_bases; - 创建
llmwiki_wiki_pages; - 创建
llmwiki_wiki_vectors; - 创建
llmwiki_wiki_sync_states; - 建立 FK、唯一约束和索引;
- 所有现有 mapping 的
external_search_enabled=false; wiki_index_enabled=true或按配置决定;- 不在迁移过程中调用 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 首次回填
迁移完成后不自动阻塞启动进行全量回填。建议:
- 服务正常启动;
- 管理员查看状态为
not_synced; - 手动或后台分库同步;
- 同步完成一个库就允许该库向量检索;
- 全部稳定后再将问答默认切换为 local wiki vector;
- 外部 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 控制措施
- 外部 API Key 必填;
- query 字符数、mapping 数、top_k、响应字符数硬上限;
- 请求体大小由应用和反向代理共同限制;
- 限流按 API Key + IP;
- 查询向量短 TTL 缓存可减少重复 embedding,但缓存 key 不保存原文日志;
- 外部查询只加载明确开放的 KB;
- 权限在检索前和返回前各检查一次;
- 页面详情接口再次检查;
- 返回 Markdown 时标注 content type,前端渲染必须禁用危险 HTML;
- 不返回 remote ID、source refs、chunk refs、owner 信息;
- 日志对 query 做长度裁剪或 hash;
- “对话沉淀”代码级 deny;
- API Key 支持轮换:可配置 current + previous,短时间双 key 过渡;
- 对外接口默认关闭。
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 单元测试
- Markdown 标题切分、长段落、表格、空页、frontmatter;
- title/alias/heading 前缀构造;
- chunk hash 与页面 hash 稳定性;
- float32 BLOB 编解码;
- NaN/Inf/维度错误拒绝;
- 向量归一化;
- NumPy TopK 与手工余弦结果一致;
- 页面聚合和每页章节上限;
- min similarity;
- LRU 和 revision 失效;
- API Key 常量时间校验;
- 系统“对话沉淀”排除;
- embedding fingerprint;
- 错误脱敏。
22.2 Repository 测试
使用 SQLite 测:
- 创建页面/向量;
- 原子替换;
- FK cascade;
- unique mapping+slug_hash;
- 完整扫描后删除缺失页;
- 不完整扫描不删除;
- lease 获取/续租/过期接管;
- revision 只在检索数据变化时递增;
- public/external 开关默认值;
- BLOB round trip。
生产若使用 MySQL/PostgreSQL,补容器集成测试验证字段和索引兼容性。
22.3 同步集成测试
使用假的 WeKnora client:
- 首次 3 页同步并建立向量;
- 第二次无变化不调用 embedding;
- 一页 version/content 变化只重建该页;
- 一页归档后不再检索;
- 一页删除后清理向量;
- 分页第 2 页失败不误删本地页;
- Embedding 失败保留旧索引;
- 多 worker 只有一个获得租约;
- fingerprint 改变标记重建;
- 服务重启后从 DB 恢复快照。
22.4 检索与权限测试
- 查询仅存在于 Wiki、原始文档无该措辞,仍能命中;
- 查询只存在于原始 chunk、Wiki 不包含,严格模式不命中;
- 用户 A 不能检索用户 B 私有库;
- 公共库对登录用户可检索;
- 智能体未绑定库不能越权;
- 对话明确空选择不检索;
- 撤销公共发布后下一请求立即无结果;
- draft 默认不命中;
- 所有者明确 include_drafts 可以命中;
- archived/deleted 永不命中;
- 同一页多个章节只产生一个引用编号;
- 多库同 slug 不互相覆盖。
22.5 外部 API 安全测试
- 无 key/错 key → 401;
- 私有库/未外部开放 → 不可枚举;
- public 但
external_search_enabled=false→ 不返回; - draft/archived → 不返回;
- “对话沉淀”永不返回;
- 关闭外部开关立即失效;
- 超长 query → 400/413;
- 超大 top_k → 拒绝或钳制;
- 高频请求 → 429;
- 响应没有 remote ID/source_refs/chunk_refs/vector;
- 日志没有 API Key 和正文;
- 详情接口重新鉴权,不依赖旧搜索结果。
22.6 前端测试
knowledge_base_id + wiki_slug可解析为有效 target;- Wiki target 不请求文档 preview/chunks;
- 抽屉展示本地 Wiki Markdown;
- 卡片显示 Wiki 标题和命中章节;
- 历史 chunk 引用仍能打开;
- 权限撤销后详情返回 404,前端正确提示;
- Wiki 内容 Markdown/HTML 安全渲染;
- 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
可以逐步:
- 只同步;
- 只提供管理员搜索对比;
- 指定测试用户/知识库使用向量;
- 全量内部问答;
- 引用 UI;
- 外部 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. 实现前必须确认的部署输入
编码本身可以先做,但上线前必须明确:
- DeerFlow 可调用的 OpenAI-compatible Embedding 地址;
- 模型名称;
- 向量维度;
- 最大 Wiki 页面数、平均页面长度、预计章节数;
- Gateway worker 数;
- 生产数据库是 SQLite、PostgreSQL 还是 MySQL;
- 外部接口是否只给内网系统,是否有统一 API Gateway;
- 外部 API Key 的生成、分发和轮换责任;
- 哪些现有公共库允许外部开放;
- 同步允许的最大延迟,例如 1 分钟或 5 分钟。
若没有额外输入,开发默认使用:
- 本地/测试 SQLite;
- bge-m3 类 1024 维模型(实际以配置为准);
- 120 秒轮询;
- NumPy exact;
- 外部 API 默认关闭;
- strict vector;
- 每页最多 2 个命中章节;
- Top 8 页面。
29. 最终推荐
本需求不应被实现成“把 WeKnora 的搜索接口换个 URL”,也不应通过隐藏文档库重复导入来绕过同步问题。推荐把它建设为 DeerFlow 内部的正式 Wiki 检索子系统:
- WeKnora 负责生成;
- DeerFlow 完整镜像;
- DeerFlow 按章节向量化;
- DeerFlow 本地持久化;
- DeerFlow 统一检索和权限;
- 引用始终落到 Wiki 页面;
- 外部接口只查询明确开放的公共 Wiki;
- 未来只替换向量执行器即可扩容,不推翻页面、权限、API 和引用设计。
该方案能同时满足:不修改 WeKnora 后端、检索处理后的 Wiki、右侧展示 Wiki、向量语义召回、公共 Wiki 外部检索、WeKnora 查询故障隔离,以及后续向 pgvector/分布式向量引擎平滑演进。