WeKnora 技术实现:从文档入库、Chunk 数据模型到混合检索
从源码解释 WeKnora 如何把异构文档转换为可检索、可重建、可被 RAG、Agent 与 Wiki 共用的知识资产。
很多 RAG 示例都可以用一条很短的链路解释:
上传文档 → 切分 Chunk → 写入向量数据库 → 检索 Top-K → 交给大模型回答。
这条链路适合验证想法,却解释不了生产系统里真正困难的部分:原文件和索引谁才是事实来源?文档什么时候算可用?解析到一半失败如何恢复?用户编辑 Chunk 后,旧向量如何避免继续参与检索?多个知识库使用不同 Embedding 模型时能否一起查询?Redis 丢失以后,后台工作还能不能恢复?
WeKnora 值得研究,不是因为它接入了很多模型和向量库,而是因为它把这些控制面问题放进了同一套知识系统:文档生命周期、Parser 路由、Chunk 数据模型、检索融合、异步任务、Agent、Wiki、权限和审计。
研究基线:本文以 WeKnora
v0.7.1为版本基线,源码快照固定到main分支 commit51b88bb,时间为 2026-08-05 UTC。项目更新较快,后续字段与流程可能继续变化。本文优先依据源码、迁移和官方文档,而不是只依据 README 的功能描述。
摘要
WeKnora 不只是带聊天界面的向量知识库。它把原始文件、文档状态、Chunk、检索索引和异步增强结果分层管理,并以此支持 Quick Q&A、ReAct Agent、知识图谱与 Wiki。本文从源码拆解这套数据模型、入库流程、混合检索和生产风险。
读者将学到什么
读完本文,你应该能够回答以下问题:
- 一份 PDF 上传后,系统内部究竟生成了哪些数据;
KnowledgeBase、Knowledge、Chunk和检索索引分别负责什么;- 为什么 Chunk 应保存在业务数据库中,而不是只写进向量数据库;
- 父子分块如何同时兼顾匹配精度和上下文完整性;
- BM25 与向量召回如何通过 RRF 融合;
- 文档可检索与全部后台任务完成为什么是两个状态;
- 多模型、多向量库和多租户检索会引入哪些额外问题;
- 一个生产级 RAG 系统应如何处理失败、重试、删除和重建。
问题背景:上传文档为什么不是 RAG 的终点
真实文档不是一组干净字符串。一份企业文档可能同时包含:
- 标题层级和目录;
- 普通段落、脚注和跨页引用;
- 表格、图片、图注和公式;
- 扫描页和错误文本层;
- 页眉、页脚与多栏排版;
- 用户后续修订的内容。
系统还要回答一些和向量相似度无关的问题:
- 原始文件存在哪里;
- 哪个 Parser 解析了它;
- 当前使用哪套 Chunking 配置;
- Chunk 已经写入,但索引是否完成;
- 文档可以检索了,摘要和图谱是否仍在运行;
- 用户修改正文后,索引是否追上当前 revision;
- 删除文档时,正在运行的后台任务怎么办。
如果这些问题没有明确的数据模型,系统很容易进入一种危险状态:页面显示文档存在,数据库里也有记录,但实际参与检索的内容已经过期、残缺或重复。
上传成功不等于可以检索
一次文档导入至少包含以下动作:
保存原文件
→ 创建文档元数据
→ 解析正文、图片和表格
→ 切分 Chunk
→ 保存 Chunk
→ 调用 Embedding
→ 写向量或关键词索引
→ 生成摘要、问题、图谱或 Wiki这些动作跨越关系数据库、对象存储、向量数据库、Redis 队列和外部模型 API,通常不处在同一个事务中。任何一步都可能失败。
假设一份 PDF 已经解析出 300 个 Chunk,但 Embedding Provider 在批量写入时触发限流。此时不能只返回一个“上传失败”:原文件已经存在,Chunk 可能已经写入,部分向量可能进入索引,后台还可能残留摘要任务。
生产系统因此需要:
- 明确的状态机;
- 幂等任务和补偿操作;
- 可取消、可重试、可巡检的后台执行;
- 可以从事实数据重新构建的派生索引。
核心心智模型:事实数据与检索投影
理解 WeKnora 最重要的一个视角,是把数据分成两类:
- 事实数据:应用运行时继续加工和恢复系统所依赖的基础数据;
- 派生投影:为了检索、摘要、图谱或 Wiki 等用途,从基础数据生成的表示。
最关键的关系是:
Chunk → Index而不是:
Index = Chunk向量索引服务于查询,但不应该反过来承担业务事实存储。
| 对象 | 回答的问题 | 丢失后的处理 |
|---|---|---|
| 原始文件 | 用户最初上传了什么 | 通常不可接受,应从备份恢复 |
| Knowledge | 这份文档是什么,当前处于什么状态 | 从关系库备份恢复 |
| Chunk | 系统将文档理解成哪些内容单元 | 可从原文件重建,但应持久化 |
| 向量/BM25 索引 | 如何快速找到相关 Chunk | 从 Chunk 重建 |
| 摘要、图谱、Wiki | 如何以其他结构消费内容 | 通常可以重新生成 |
| Redis 运行态 | 哪个 Worker 正在执行什么 | 根据持久状态补发任务 |
这个模型带来一个直接结论:
向量索引应被视为可重建的查询投影,而不是知识系统唯一的数据源。
整体架构:Go 负责控制,Parser 与模型负责计算
WeKnora 的默认部署至少包含:
- Web 前端与 Nginx;
- Go 编写的主应用;
- 独立的 DocReader 文档解析服务;
- PostgreSQL / ParadeDB;
- Redis 与 Asynq 后台任务;
- 本地文件系统或对象存储;
- Chat、Embedding、Rerank、VLM 与 ASR 模型;
- 可选的外部向量数据库和 Neo4j 知识图谱。
v0.7.1 移除了旧的 Neo4j 对话记忆依赖,但知识图谱能力仍保留。两者不应混为一谈。
为什么 DocReader 要独立部署
文档解析和普通 Web API 的资源模型不同。解析 PDF 可能涉及文本层识别、页面布局、扫描页渲染、图片抽取、OCR 和表格重建,CPU、内存和执行时间都明显更高。
把 DocReader 拆成独立服务,可以:
- 单独限制 CPU、内存和 Worker;
- 独立升级 Python 解析依赖;
- 避免 Parser 崩溃拖垮普通 API;
- 为 gRPC 增加超时、TLS 和认证;
- 根据文档压力独立扩缩容。
Parser 不是单一实现
当前 Parser Registry 包括 builtin、simple、weknoracloud、MinerU 和 PaddleOCR-VL 等实现。不同文件类型可以通过 ParserEngineRules 绑定不同引擎。
Parser 的目标不是直接写向量库,而是收敛成统一中间结构,让后续 Chunking、Embedding 和索引逻辑不依赖某个具体解析器。
核心数据模型
WeKnora 的知识数据不是一张 documents 表加一张 vectors 表,而是四层结构:
| 层级 | 主要对象 | 作用 |
|---|---|---|
| 空间层 | Tenant / Workspace | 用户、权限、配额、模型和基础设施隔离 |
| 知识库层 | KnowledgeBase | 保存处理策略与资源绑定 |
| 文档层 | Knowledge | 保存来源文件和处理生命周期 |
| 内容层 | Chunk / ChunkRevision | 保存正文、关系和编辑历史 |
KnowledgeBase:处理策略和基础设施边界
KnowledgeBase 不只是文档目录。它保存:
- 知识库类型:
document、faq、wiki; - Chunk 大小、Overlap、父子分块和 Parser 路由;
- Embedding、Summary、VLM 与 ASR 配置;
- StorageBackend 和 VectorStore 绑定;
- FAQ、问题生成、Wiki 与 Graph 配置;
- Vector、Keyword、Wiki、Graph 四条索引管线开关。
当前 VectorStoreID 只允许创建时设置。原因是底层存储切换不是普通配置更新,而是数据迁移:目标索引需要先全量重建和验证,再切换绑定,最后清理旧索引。
Knowledge:一份文档的生命周期
Knowledge 保存来源、渠道、文件信息、对象存储路径、解析状态、错误和处理时间。它更接近“文档任务实体”,回答的是:
这是什么文档?
从哪里来?
属于哪个知识库?
文件保存在哪里?
当前处理到哪个阶段?
上次失败的原因是什么?它还把内部 Metadata 与用户撰写的 CustomMetadata 分开,避免内部路径、任务字段和控制信息无意进入 LLM 上下文。
Chunk:检索、编辑和引用的共同单元
Chunk 不只是切分后的一段文本,还保存:
SourceContent:Parser 输出的原始正文;ContentRevision:每次编辑或回滚递增;IndexStatus:当前正文是否已经同步到检索后端;StartAt/EndAt:原始文档位置;PreChunkID/NextChunkID:邻接关系;ParentChunkID:父子分块关系;ContextHeader:Markdown 标题面包屑;RelationChunks:关联内容;ChunkType:文本、图片 OCR、表格摘要、FAQ、Wiki 页面等。
当用户修改 Chunk 时,数据库正文可能先更新,而索引仍是旧版本。WeKnora 在查询结果回查阶段会跳过 IndexStatus=processing 或 failed 的 Chunk,宁可暂时不返回,也不把旧向量和新正文拼成一个无法解释的结果。
工作原理:文档入库状态机
HTTP 上传请求不应同步等待所有步骤完成。更合理的过程是:接收文件、创建 Knowledge、保存原文件并投递任务,然后由 Worker 推进状态。
finalizing 为什么重要
WeKnora 区分:
processing
核心解析或索引仍在执行
finalizing
主索引已经可用,增强任务仍在执行
completed
本次配置要求的任务均已进入终态因此:
可检索,不等于所有后台处理已经结束。
如果向量写入成功后立刻标记 completed,后处理任务可能因为状态检查而跳过摘要、问题或图谱生成。finalizing 让用户可以先查询文档,同时系统继续完成非阻塞增强。
幂等重建与失败窗口
重新解析前,WeKnora 会清理旧 Chunk、旧检索数据和旧图谱,然后写入新结果。这能避免同一文档的多个版本同时参与检索。
但“先删旧数据,再写新数据”存在失败窗口:旧索引已经删除,新 Embedding 尚未完成,文档会暂时不可查。
高可用系统可以增加 generation:
后台构建 generation=8
→ 校验 Chunk 和索引数量
→ 抽样检索验证
→ 原子切换 active_generation: 7 → 8
→ 延迟删除 generation=7这是基于当前流程给出的工程增强建议,不代表 WeKnora 所有后端已经统一采用这种切换方式。
自适应 Chunking 与父子分块
固定长度切分只解决“输入不能无限长”,没有解决“什么是有意义的内容边界”。
WeKnora 当前支持:
legacy/recursive:传统递归分隔符切分;heading:按标题结构切分;heuristic:根据文档特征进行启发式切分;auto:分析文档 Profile 后选择策略链。
自动模式不会盲目信任某一层输出。每个 Tier 生成 Chunk 后都要经过完整性检查;失败则回退到下一层,最终回到 Legacy Splitter。系统还会根据 Token Limit 估算字符预算,并限制过大的 Overlap,避免相邻 Chunk 接近重复。
Parent-Child 的目标不是“把 Chunk 做大”
父子分块分离了两个互相冲突的目标:
小 Child Chunk:提高匹配精度
大 Parent Chunk:提高生成上下文完整性默认配置中,Parent 约为 4096 字符,Child 约为 384 字符,Child Overlap 约为 20%。Parent 存在数据库中但不写向量索引;真正参与匹配的是 Child。命中 Child 后,查询链路再回查 Parent。
ContextHeader 则把标题路径拼到 Embedding 输入中。数据库正文可能只有:
默认超时时间为 30 秒,可以通过配置文件覆盖。索引文本可以是:
WeKnora 部署配置
## 网络与超时
### DocReader RPC
默认超时时间为 30 秒,可以通过配置文件覆盖。索引文本为了匹配,Chunk 正文为了展示和生成,两者可以相关,但职责不同。
索引层:默认 PostgreSQL 如何同时承载 BM25 与向量检索
默认 PostgreSQL Retriever 使用 embeddings 表保存:
source_id、chunk_id;knowledge_id、knowledge_base_id;- 用于检索的文本;
- Embedding 维度;
halfvec向量;- 是否启用和 Tag。
表上分别建立:
- 基于
pg_search的 BM25 索引; - 基于
pgvector的 HNSW 向量索引。
“都放在 PostgreSQL”不代表只执行一次查询。应用层仍然会分别发起向量召回和关键词召回,再在上层融合。
正确但很慢:1024 维 HNSW 缺失
早期迁移只为部分向量维度创建了 HNSW 索引。当本地部署使用常见的 BGE-M3 1024 维向量时,查询仍然能得到正确结果,但可能退化为顺序扫描。后续迁移 000059_embeddings_hnsw_1024 专门补上 1024 维部分索引。
这类问题说明,生产监控不能只检查“结果是否正确”,还要检查:
查询计划是否命中预期索引
Embedding 维度是否有对应 HNSW
迁移是否覆盖当前模型
索引构建是否完成混合检索:先保证可比较,再谈融合
WeKnora 的 Hybrid Search 不是简单并发调用向量检索和 BM25。它先解决四个更基础的问题:
- 本次查询包含哪些 KB、Knowledge、Tag 和 @Mention 范围;
- 调用者是否有权访问每一个目标 KB;
- 多个 KB 是否处于兼容的 Embedding 空间;
- 每个 KB 绑定到哪个 VectorStore,以及该 Store 属于哪个 Workspace。
源码用“模型名 + Endpoint”识别底层 Embedding 模型,而不只比较不同租户中的 Model ID。多个 KB 共享同一模型时,Query Embedding 只计算一次。随后按 (VectorStoreID, OwnerTenantID) 分组扇出,防止共享 KB 错用调用方自己的存储配置。
为什么使用 RRF
BM25、向量相似度和不同后端的绝对分数通常没有共同量纲。WeKnora 在两路结果同时存在时使用加权 Reciprocal Rank Fusion,倒数排名融合:
RRF(chunk)
= vectorWeight / (k + vectorRank)
+ keywordWeight / (k + keywordRank)RRF 主要依赖排名位置,降低不同评分尺度之间的冲突。只有单路召回时,系统才保留原始分数并按 Chunk ID 去重。
检索结果不是最终上下文
检索后端先返回候选 Chunk ID,应用层再回查数据库并补充:
- Parent Chunk;
- 前后相邻 Chunk;
- Relation Chunk;
- 图片 OCR 或 Caption 对应的正文;
- Knowledge 来源和自定义元数据。
更准确的链路是:
召回候选 ID
→ 回查当前 Chunk
→ 验证 enabled / index_status / 权限
→ 加载父级和邻近内容
→ 组装引用
→ 可选 Rerank
→ 交给 LLMQuick Q&A 与 ReAct Agent 是两条不同链路
两者共用知识检索,但控制权不同。
| 维度 | Quick Q&A | ReAct Agent |
|---|---|---|
| 流程 | 理解 → 搜索 → 排序 → 生成 | LLM 在循环中决定是否调用工具 |
| 知识检索 | 回答前固定阶段 | knowledge_search 是工具 |
| 工具范围 | 主要是 KB 与 Web | KB、Web、MCP、Skills、Sandbox |
| 输出 | 流式答案与引用 | Thinking、Tool Call、Result、Complete |
| 上下文 | Top-K 和提示拼装 | Token Estimator 与 Memory Consolidator |
Agent Engine 跨轮无状态。每轮开始前,调用方从数据库重建会话历史并作为 llmContext 传入;引擎不依赖进程本地的跨轮缓存。
这个边界使服务重启和水平扩容更容易,但要求消息层保存足够的执行事实:历史内容、知识引用、工具步骤、模型信息和当时真正发送给 LLM 的上下文。
最小实现示例
下面的代码不是 WeKnora 源码复制,而是把它的核心设计压缩成一个最小 Go 示例:先保存事实数据,再构建可重建索引;查询时并行召回、RRF 融合,最后根据 ID 回查正文。
package knowledge
import (
"context"
"errors"
"sort"
)
type Knowledge struct {
ID string
KBID string
Title string
Status string // pending, processing, finalizing, completed, failed
Generation int
}
type Chunk struct {
ID string
KnowledgeID string
KBID string
Content string
Revision int
IndexStatus string // ready, processing, failed
ParentID string
}
type IndexRecord struct {
ChunkID string
KnowledgeID string
KBID string
Revision int
Content string
Embedding []float32
}
type FactStore interface {
ReplaceChunks(ctx context.Context, knowledgeID string, chunks []Chunk) error
LoadChunks(ctx context.Context, ids []string) (map[string]Chunk, error)
UpdateKnowledgeStatus(ctx context.Context, id, status string) error
}
type SearchIndex interface {
ReplaceKnowledge(ctx context.Context, knowledgeID string, records []IndexRecord) error
VectorSearch(ctx context.Context, kbIDs []string, query []float32, topK int) ([]string, error)
KeywordSearch(ctx context.Context, kbIDs []string, query string, topK int) ([]string, error)
}
type Embedder interface {
Embed(ctx context.Context, texts []string) ([][]float32, error)
}
type Service struct {
facts FactStore
index SearchIndex
embed Embedder
}
func (s *Service) ProcessDocument(
ctx context.Context,
k Knowledge,
chunks []Chunk,
) error {
if err := s.facts.UpdateKnowledgeStatus(ctx, k.ID, "processing"); err != nil {
return err
}
// Chunk 是事实数据,先持久化。
if err := s.facts.ReplaceChunks(ctx, k.ID, chunks); err != nil {
_ = s.facts.UpdateKnowledgeStatus(ctx, k.ID, "failed")
return err
}
texts := make([]string, len(chunks))
for i, c := range chunks {
texts[i] = k.Title + "\n\n" + c.Content
}
vectors, err := s.embed.Embed(ctx, texts)
if err != nil {
_ = s.facts.UpdateKnowledgeStatus(ctx, k.ID, "failed")
return err
}
if len(vectors) != len(chunks) {
return errors.New("embedding result count mismatch")
}
records := make([]IndexRecord, len(chunks))
for i, c := range chunks {
records[i] = IndexRecord{
ChunkID: c.ID, KnowledgeID: c.KnowledgeID, KBID: c.KBID,
Revision: c.Revision, Content: texts[i], Embedding: vectors[i],
}
}
// 索引是派生投影,应支持按 Knowledge 整体替换。
if err := s.index.ReplaceKnowledge(ctx, k.ID, records); err != nil {
_ = s.facts.UpdateKnowledgeStatus(ctx, k.ID, "failed")
return err
}
// 主索引可用,增强任务可以继续异步执行。
return s.facts.UpdateKnowledgeStatus(ctx, k.ID, "finalizing")
}
type RankedID struct {
ID string
Score float64
}
func weightedRRF(vectorIDs, keywordIDs []string, k int, vw, kw float64) []RankedID {
scores := map[string]float64{}
for rank, id := range vectorIDs {
scores[id] += vw / float64(k+rank+1)
}
for rank, id := range keywordIDs {
scores[id] += kw / float64(k+rank+1)
}
out := make([]RankedID, 0, len(scores))
for id, score := range scores {
out = append(out, RankedID{ID: id, Score: score})
}
sort.Slice(out, func(i, j int) bool { return out[i].Score > out[j].Score })
return out
}这个最小实现仍省略了很多生产细节:事务 Outbox、Generation、权限 Scope、批量限流、模型分组、索引补偿、Parent 回查、重试和 Trace。但它保留了最重要的边界:
Fact Store 保存当前事实
Search Index 保存可重建投影生产环境中的失败模式
1. 表格合成表头会破坏原始 Offset
2026-08-05 的 commit 51b88bb 修复了一个很典型的问题。
为了让每个表格 Chunk 能独立理解,分块器可能为后续 Chunk 重复添加表头。但合成表头并不存在于原始文件的 StartAt / EndAt 中。如果摘要重建仍完全依赖原始 Offset,可能覆盖或丢失表格行。
修复改为根据真实文本重叠合并表格 Chunk,而不是只按 Offset 切片。
这个案例说明:
一旦 Chunk 被补标题、补表头、OCR、人工编辑或上下文增强,原始字符位置就不再足以描述当前内容。
2. Embedding 维度不同,却被放进同一次查询
不同模型、Endpoint、归一化方式和距离度量产生的向量不能直接比较。即使维度相同,也不代表处于同一语义空间。
多 KB 查询必须先验证模型身份,再决定是否共享 Query Embedding。不能只把多个向量库结果拼起来按原始 Score 排序。
3. Chunk 已修改,旧索引仍在返回
人工编辑通常先写数据库,再异步更新检索索引。这个窗口里,向量库返回的是旧 revision。
应至少保存:
chunk.content_revision
index.revision
chunk.index_status查询回查时验证版本;Repair Job 定期扫描漏写、旧写和孤儿索引。
4. 增加 Worker 后,队列反而更堵
Worker 并发只是任务准入预算,不会扩大模型 RPM/TPM、DocReader CPU、数据库连接池或向量库写入能力。
如果模型限流器等待时间持续上升,增加 Worker 只会制造更多等待者。应同时观察队列最老任务年龄、Worker 利用率、模型并发、Provider 限流、DocReader 资源和数据库连接。
5. Redis 丢失导致文档永久停在 processing
Redis 适合回答“当前由谁执行”,不适合作为“最终还有什么必须完成”的唯一来源。
长期任务应在数据库中保留 Outbox / Pending Ops、Attempt、Lease、Retry Count 和 Dead Letter。服务启动或巡检时,根据 Knowledge 状态补发丢失任务。
6. URL 导入扩大 SSRF 攻击面
URL 导入、数据源同步、Web Fetch、远程 Parser、模型 Endpoint 和对象存储下载都会触发服务端出站请求。
生产系统需要统一的安全 HTTP Transport:
- 拒绝内网和云 Metadata 地址;
- 重定向后重新校验目标;
- 限制协议、端口和响应大小;
- 设置 DNS、连接和读取超时;
- 对必要内部地址使用明确 Allowlist;
- 日志中脱敏 Token 与密钥。
WeKnora v0.7.0 和 v0.7.1 都包含大量 SSRF 与凭据泄露加固,这说明它不是外围问题,而是知识摄取系统的核心安全边界。
7. 删除和后台任务发生竞态
用户删除 Knowledge 时,Parser、Embedding 或 Graph 任务可能仍在运行。只删数据库记录不够:旧 Worker 可能随后把 Chunk 或索引重新写回来。
常见策略是:
先将 Knowledge 标记为 deleting
→ 后续阶段入口反复检查状态
→ 取消或忽略排队任务
→ 清理 Chunk、索引、图和原文件
→ 最后完成删除方案比较与取舍
| 维度 | WeKnora 当前思路 | 更简单的实现 | 取舍 |
|---|---|---|---|
| 事实数据 | 关系库保存 Knowledge/Chunk | 向量库直接保存正文 | 前者更易重建和治理,组件更多 |
| 原始资产 | 独立对象存储 | 只保留解析文本 | 前者可重新解析,成本更高 |
| 关键词检索 | BM25 独立召回 | 只做向量检索 | 混合召回更稳,但需要融合和评测 |
| 分块 | 自适应 + Parent-Child | 固定长度 | 质量更好,调试复杂度更高 |
| 文档状态 | 状态机 + 异步队列 | 请求内同步完成 | 可扩展且可恢复,状态协调更复杂 |
| 存储绑定 | 每 KB 绑定实例 | 全局单一存储 | 灵活隔离,但迁移和路由更复杂 |
| 多 KB 查询 | 模型校验 + Store Fan-out | 合并所有结果 | 更正确,但实现和观测成本更高 |
| Agent | 作为上层工具消费者 | 检索逻辑写死在 Agent | 边界清晰,但工具协议需要治理 |
| 索引升级 | 清理重建,可增强为 Generation | 原地覆盖 | 前者可恢复,重建期间占用双份资源 |
不要为了“企业级”复制全部组件
对于单人使用、文档量较小、更新频率低的知识库,PostgreSQL + pgvector + 本地存储可能已经足够。只有当你确实遇到以下问题时,才需要引入更复杂的多 Store、Wiki、Graph 和独立 Worker 治理:
- 不同业务要求物理隔离;
- 索引量或并发超过单库能力;
- 文档持续同步和频繁更新;
- 需要可审计的多人协作;
- 必须支持复杂的 Agent 工具执行;
- 需要在解析失败后自动恢复。
实践检查清单
数据与所有权
- 原文件是否有不可替代的持久存储和备份;
- Knowledge、Chunk 和索引是否有稳定 ID;
- 是否明确了关系库与向量库谁是 Source of Truth;
- Chunk 是否保留 SourceContent、Revision 和 IndexStatus;
- 用户元数据是否与内部 ingestion metadata 分开。
Parser 与 Chunking
- 是否按文件类型选择 Parser,而不是全局单一引擎;
- 是否保留标题路径、表格上下文和图片来源;
- Parent 是否只用于上下文,Child 是否用于召回;
- Overlap 是否过大,导致候选高度重复;
- Parser 评估是否延伸到最终 Retrieval,而不只看文本输出。
索引与检索
- Embedding 模型、维度、归一化和距离度量是否记录;
- 每个实际向量维度是否存在匹配的 ANN 索引;
- 多 KB 查询是否先校验 Embedding 空间;
- 是否复用同模型 Query Embedding;
- BM25 和 Vector 是否使用 RRF 等稳定方式融合;
- Rerank 失败是否能回退;
- 查询结果是否回查当前 Chunk、权限和 revision。
任务与一致性
- 每个任务是否有幂等键、Attempt 和 Stage;
- Redis 丢失后是否能从数据库恢复任务;
- 删除、取消和重新解析是否会阻止旧 Worker 回写;
- 是否有 Dead Letter 和人工重放入口;
- 是否区分“可检索”和“所有增强任务完成”;
- 是否有 Repair / Reindex Job 检查孤儿与过期索引。
安全与可观测
- 所有服务端出站请求是否经过统一 SSRF 防护;
- 模型、存储和向量库密钥是否加密并在 API 中脱敏;
- 向量查询 Scope 是否由服务端身份生成;
- 是否记录 Parser、Chunking、Embedding、Retrieve、Rerank 和 Agent Span;
- 是否能看到队列最老任务年龄,而不只看任务数量;
- 是否对模型并发、RPM、TPM 和成本进行独立治理。
总结
WeKnora 最值得研究的部分不是模型接入数量,而是它承认知识系统会不断失败、更新和迁移,并为这些变化保留明确状态:
- 原文件属于哪个存储实例;
- 文档是否已经可检索;
- 后台还有多少子任务;
- Chunk 当前是哪个 revision;
- 索引是否追上当前正文;
- 多个 KB 是否处于同一个 Embedding 空间;
- 某条检索或 Rerank 链路失败后如何降级;
- Worker 崩溃后哪些任务仍必须执行;
- 向量索引损坏后从哪里重建。
一句话总结:
保存好关系数据库中的业务事实和对象存储中的原始资产,把向量、BM25、图谱和 Wiki 设计成可重建投影;再用状态机、Revision、持久任务和分阶段评测管理它们之间的不一致。
参考资料
- Tencent/WeKnora 源码快照:51b88bb
- WeKnora v0.7.1 Changelog
- KnowledgeBase 与 ChunkingConfig
- Knowledge 生命周期
- Chunk 与 ChunkRevision
- Adaptive Chunking Strategy
- Parser Engine Registry
- 文档处理主链路
- Hybrid Search
- RRF 融合
- 检索结果回查与上下文补全
- Composite Retrieve Engine
- PostgreSQL Embedding 结构
- 1024 维 HNSW 索引迁移
- Worker Pool Governance
- ReAct Agent Engine
- 表格 Chunk 摘要重建修复
图片清单
weknora-document-to-knowledge-cover.webp
- 用途:文章原创封面;
- 尺寸:16;
- Alt 文本:异构文档经过解析、分块和索引,转换为可供检索、Agent 与 Wiki 使用的知识网络;
- 生成提示词:
16:9 editorial technology illustration, heterogeneous documents flowing through parsing layers into modular knowledge blocks, vector nodes and structured connections, subtle database and retrieval motifs, calm blue-gray and muted green palette, precise technical editorial style, generous negative space, no text, no logo, no brand marks; - 状态:待生成并加入博客静态资源。
weknora-knowledge-pipeline.mmd
- 用途:事实数据与派生投影心智模型;
- Alt 文本:原始文件转换为 Knowledge 和 Chunk,再派生向量、BM25、摘要、图谱与 Wiki。
weknora-system-architecture.mmd
- 用途:系统运行架构;
- Alt 文本:客户端、Go App、DocReader、任务队列、数据库、对象存储、模型和检索后端之间的关系。
weknora-data-model.mmd
- 用途:核心数据模型;
- Alt 文本:Workspace、KnowledgeBase、Knowledge、Chunk、Revision 与 Retrieval Index 的关系。
weknora-ingestion-sequence.mmd
- 用途:文档入库时序;
- Alt 文本:文档从上传、解析、分块、索引到异步增强和完成的调用过程。
weknora-processing-state.mmd
- 用途:Knowledge 状态机;
- Alt 文本:pending、processing、finalizing、completed、failed、cancelled 和 deleting 之间的转换。
weknora-hybrid-search.mmd
- 用途:混合检索流程;
- Alt 文本:查询经过权限 Scope、Embedding 复用、多 Store 扇出、Vector/BM25、RRF 和上下文补全。
讨论
继续讨论这篇笔记
有问题、补充案例或不同观点,可以通过 GitHub Discussions 继续交流。