WeKnora 深度解读
“三路出击、一站融合”——把检索质量做到极致的企业级知识库引擎
一句话定位
腾讯 IMA 团队出品的 RAG 引擎,核心差异化在于 BM25 + 向量 + 知识图谱三路混合检索,辅以 RRF 融合和复合 Rerank,并独创知识库自维护(自动发现过期/矛盾内容)机制。
调研元信息
| 项目 | 值 |
|---|---|
| 调研日期 | 2026-06-22 |
| 仓库 | Tencent/WeKnora |
| Stars | ~14.3K |
| 主语言 | Go(后端) / React+TypeScript(前端) |
| 代码量 | ~800+ Go 文件(后端核心)+ 前端 |
| 许可证 | Apache-2.0 |
| 犀牛鸟状态 | 候选竞赛项目 |
设计哲学上最精彩的地方
WeKnora 对”什么是好的检索”有极其明确的回答:不同信号互补,多路融合才是终极答案。
类比一下:你在图书馆找一本书。第一条路(BM25)是按书名/关键词精确检索目录——你知道叫什么名字就能直达。第二条路(向量)是按”这本书大概讲什么”的语义理解来找——你描述主题,图书管理员帮你从相关区域挑。第三条路(知识图谱)是图书管理员知道”这本书的作者还写过另一本关联著作”——即使那本书标题和主题都不直接匹配,也能通过关系链找到。三条路各找各的,最终用 RRF 把各路排名合并成一个统一排序。
这个设计背后的技术直觉非常深刻:没有任何单一检索信号能覆盖所有查询类型。纯向量检索对精确关键词(错误码、API 名、版本号)表现差;纯 BM25 对语义理解(同义词、同义表达、主题推断)表现差;两者合并后还是无法做多跳推理(”A 的发明者写了哪些其他论文”)。只有叠加知识图谱——通过实体关系链找到间接相关的内容——才能覆盖这最后 10-15% 的查询需求。
EventManager + Plugin 管线是另一个精彩设计。所有 RAG 操作(索引、检索、更新)走统一事件总线,每个处理环节注册为独立 Plugin。这像工厂流水线上的独立工位:换一个分块策略只需插拔一个 Plugin,不影响其他环节。你可以随时插入一个新的”质检工位”(Plugin),不影响其他工位。
想象一个具体场景:团队想在 WeKnora 中加入”文档摘要自动生成”功能。传统架构需要在 ingest 管线的多个位置修改代码、处理依赖。WeKnora 的做法是:写一个新的 SummaryPlugin,注册到 EventManager 监听 DocumentIngested 事件,收到事件后自动生成摘要并存储。不需要修改任何已有代码。这种解耦让系统的可扩展性远超”函数调用链”式的传统架构。
复合 Rerank(0.6 模型 + 0.3 基础 + 0.1 来源)也值得细品:即使 Rerank 模型对某些专业术语理解偏差,基础分数和来源可信度还能兜底,鲁棒性远优于单模型 Rerank。
核心架构详解
全局架构图
┌─────────────────────────────────────┐
│ WeKnora 系统全景 │
└──────────────┬──────────────────────┘
│
┌──────────────────────────────┼──────────────────────────────┐
│ │ │
┌────────▼────────┐ ┌──────────▼──────────┐ ┌──────────▼──────────┐
│ 文档摄入管线 │ │ 检索管线 │ │ 知识管理管线 │
│ Ingest Pipeline │ │ Retrieval Pipeline │ │ Knowledge Mgmt │
└────────┬────────┘ └──────────┬──────────┘ └──────────┬──────────┘
│ │ │
┌────────┼────────┐ ┌──────────┼──────────┐ ┌──────────┼──────────┐
│ │ │ │ │ │ │ │ │
解析器 分块器 向量化 BM25 向量检索 图谱 Wiki 巡检 清理
Parser Chunker Embed BM25 Vector Graph Wiki Inspector Cleanup
│ │ │ │ │ │ │ │ │
└────────┼────────┘ └──────────┼──────────┘ └──────────┼──────────┘
│ │ │
└──────────────────────────────┼──────────────────────────────┘
│
┌──────────────▼──────────────────────┐
│ EventManager 事件总线 │
│ (Plugin 注册 / 事件派发 / 生命周期) │
└─────────────────────────────────────┘
EventManager 事件总线深度解析
EventManager 是 WeKnora 整个架构的神经中枢。理解它就理解了 WeKnora 的设计哲学。
核心概念:
EventManager 实现了一个经典的发布-订阅(Pub-Sub)模式。系统中的所有操作——文档上传、解析、分块、向量化、检索、Rerank、生成——都不是直接函数调用,而是通过事件来协调。
事件生命周期:
1. 注册阶段(系统启动时)
└─ 各 Plugin 向 EventManager 声明自己关心哪些事件
├─ ParserPlugin: 监听 "RawDocumentReceived"
├─ ChunkerPlugin: 监听 "DocumentParsed"
├─ EmbedPlugin: 监听 "ChunksCreated"
├─ BM25Plugin: 监听 "QueryReceived"
├─ VectorPlugin: 监听 "QueryReceived"
├─ GraphPlugin: 监听 "QueryReceived"
├─ FusionPlugin: 监听 "AllRetrievalsDone"
├─ RerankPlugin: 监听 "FusionCompleted"
└─ WikiInspectorPlugin: 监听 "ScheduledLint"
2. 运行阶段(用户操作时)
└─ 操作触发事件 → EventManager 派发给订阅者 → 订阅者处理后触发新事件
文档上传 → "RawDocumentReceived"
→ ParserPlugin 处理 → "DocumentParsed"
→ ChunkerPlugin 处理 → "ChunksCreated"
→ EmbedPlugin 处理 → "ChunksIndexed"
3. 错误处理
└─ Plugin 处理失败时触发 "PluginError" 事件
其他 Plugin 可以订阅 "PluginError" 做兜底
为什么选择 Pub-Sub 而不是直接调用链?
日常类比:想象你管理一家餐厅的厨房。直接调用链相当于厨师长亲自把每道菜从洗菜→切菜→炒菜→装盘一个个传递,如果任何一步出问题整个链条就断了,换一个帮厨需要改整条流程。Pub-Sub 相当于厨房有一块写字板:洗菜工完成后在板上写”菜已洗好”,切菜工看到后开始切,切完后写”菜已切好”,炒菜工看到后开始炒。每个工位独立运转,互不知道彼此的存在。要增加一个”质检工位”?只需要让质检员也盯着写字板就行,不需要改任何现有流程。
源码层面的关键设计:
// 伪代码,基于 WeKnora 源码结构推导
type EventManager struct {
subscribers map[EventType][]Plugin
eventChan chan Event
}
func (em *EventManager) Register(eventType EventType, plugin Plugin) {
em.subscribers[eventType] = append(em.subscribers[eventType], plugin)
}
func (em *EventManager) Emit(event Event) {
for _, plugin := range em.subscribers[event.Type] {
go plugin.Handle(event) // goroutine 并发处理
}
}
注意 go plugin.Handle(event) 这行——Go 的 goroutine 让每个 Plugin 可以并发处理事件。这意味着当 QueryReceived 事件触发时,BM25、向量、知识图谱三路检索是并行执行的,总延迟取决于最慢的那一路,而不是三路之和。这是 Go 语言在此场景下的天然优势。
Plugin Pipeline 插件管道
Plugin 是 EventManager 的执行单元。每个 Plugin 必须实现以下接口:
type Plugin interface {
Name() string // 插件名称
SubscribedEvents() []EventType // 关心的事件类型
Handle(event Event) (Event, error) // 处理逻辑,返回新事件
Priority() int // 执行优先级(同事件多个订阅者时)
}
插件分类:
| 类别 | 插件 | 输入事件 | 输出事件 | 说明 |
|---|---|---|---|---|
| 文档处理 | ParserPlugin | RawDocumentReceived | DocumentParsed | 调用 IMA/各格式解析器 |
| 文档处理 | ChunkerPlugin | DocumentParsed | ChunksCreated | 4 级自适应分块 |
| 文档处理 | EmbedPlugin | ChunksCreated | ChunksIndexed | 向量化 + 写入向量库 |
| 检索 | BM25Plugin | QueryReceived | BM25ResultsReady | 关键词检索 |
| 检索 | VectorPlugin | QueryReceived | VectorResultsReady | 语义检索 |
| 检索 | GraphPlugin | QueryReceived | GraphResultsReady | 图谱推理检索 |
| 融合 | FusionPlugin | AllRetrievalsDone | FusionCompleted | RRF 排名融合 |
| 排序 | RerankPlugin | FusionCompleted | RerankCompleted | 三信号精排 |
| 管理 | WikiPlugin | ScheduledLint | LintResultsReady | 知识巡检 |
Plugin 热插拔场景:
假设你想给 WeKnora 加入一个”查询意图识别”环节——在检索前先判断用户是在问事实还是在闲聊。传统架构需要在主流程代码中找到合适的位置插入判断逻辑。WeKnora 的做法是:
type IntentPlugin struct{}
func (p *IntentPlugin) SubscribedEvents() []EventType {
return []EventType{"QueryReceived"} // 监听查询事件
}
func (p *IntentPlugin) Handle(event Event) (Event, error) {
intent := classifyIntent(event.Query)
if intent == "chitchat" {
return Event{Type: "SkipRetrieval", Data: event.Data}, nil
}
return event, nil // 非闲聊,正常继续
}
func (p *IntentPlugin) Priority() int {
return 0 // 最高优先级,在 BM25/Vector/Graph 之前执行
}
注册后,IntentPlugin 会在 BM25Plugin、VectorPlugin、GraphPlugin 之前执行(因为 Priority=0)。如果判断是闲聊,触发 SkipRetrieval 事件绕过检索直接生成。整个过程不需要修改任何现有 Plugin 的代码。
三路混合检索管线深度解析
WeKnora 的核心创新在于三路混合检索。让我逐一拆解每一路的实现细节。
第一路:BM25 关键词检索
BM25(Best Matching 25)是经典的关键词检索算法,诞生于 1994 年。WeKnora 使用它作为精确匹配信号。
为什么需要 BM25?
向量检索擅长语义理解但对精确关键词不敏感。当用户搜索 “ERR_OOM_KILL_001” 这样的错误码时,向量模型会把它理解为”某种错误”的语义,可能召回任何包含错误概念的文档。BM25 则精确匹配字符串——只有包含 “ERR_OOM_KILL_001” 的文档才会被召回。
BM25 公式:
\[BM25(q, d) = \sum_{i=1}^{n} IDF(q_i) \cdot \frac{tf(q_i,d)\,(k_1+1)}{tf(q_i,d)+k_1\left(1-b+b\frac{|d|}{\mathrm{avgdl}}\right)}\]| 其中 $q$ 为查询(词项 $q_1, \ldots, q_n$),$d$ 为文档,$tf(q_i,d)$ 为词频,$IDF(q_i)$ 为逆文档频率,$ | d | $ 为文档长度,$\mathrm{avgdl}$ 为平均文档长度,$k_1 \in [1.2, 2.0]$,$b \approx 0.75$。 |
用日常语言解释 BM25 的每个组件:
IDF(逆文档频率):一个词如果在很多文档中都出现(如”的”“是”“一个”),它对区分文档没什么用,IDF 分数低。如果一个词只在少数文档中出现(如 “RRF” “PMI”),它对找到相关文档非常有用,IDF 分数高。这就像找人——”穿衣服的人”这个描述毫无区分度(几乎所有人都穿衣服),但”穿红色格子衬衫的人”就能快速定位。
tf(词频):一个词在文档中出现的次数。出现越多,文档越可能和这个词相关。但 BM25 有一个精妙设计:词频的增长存在饱和效应——”出现 3 次”比”出现 1 次”的提升很大,但”出现 30 次”比”出现 10 次”的提升几乎可以忽略。k1 参数控制饱和速度。
b(长度归一化):长文档天然包含更多词项,如果不归一化,长文档总是占便宜。b=0.75 意味着文档长度会被适度惩罚——一篇 10,000 字的文档出现 5 次”知识图谱”,和一篇 100 字的文档出现 5 次”知识图谱”相比,后者的 BM25 分数更高。
WeKnora 中的实现特点:
WeKnora 的 BM25 实现支持中文分词(而不是简单按空格分词)。中文的”知识图谱”是一个词还是”知识”+”图谱”两个词?分词器的选择直接影响 BM25 的检索质量。WeKnora 使用经过优化的中文分词器,支持专业术语的自定义词典。
第二路:向量语义检索
核心思想:把文本转换为高维向量(如 768 维或 1536 维),用向量距离衡量语义相似度。
向量检索流程:
1. 离线阶段(文档摄入时)
文档 chunk → embedding 模型 → 768 维向量 → 写入向量数据库
2. 在线阶段(用户查询时)
查询文本 → embedding 模型 → 768 维查询向量
→ 在向量数据库中做 ANN(近似最近邻)搜索
→ 返回 top-K 最相似的 chunks
为什么 WeKnora 支持 10 种向量数据库后端?
不同的向量数据库在以下维度上各有取舍:
| 向量库 | 速度 | 精度 | 可扩展性 | 部署复杂度 | 适合场景 |
|---|---|---|---|---|---|
| Milvus | 快 | 高 | 极高(分布式) | 高 | 大规模生产 |
| Qdrant | 快 | 高 | 高 | 中 | 中大规模 |
| Weaviate | 中 | 高 | 高 | 中 | 混合搜索 |
| Pinecone | 快 | 高 | 极高 | 低(云) | 免运维 |
| Chroma | 中 | 中 | 低 | 低 | 原型/小规模 |
| pgvector | 中 | 中 | 中 | 低 | 已有 PG 的团队 |
| Elasticsearch | 中 | 中 | 高 | 中 | 已有 ES 的团队 |
| FAISS | 极快 | 高 | 低 | 低 | 纯内存/研究 |
| LanceDB | 快 | 高 | 中 | 低 | 嵌入式 |
| 腾讯云 TCVDB | 快 | 高 | 高 | 低(云) | 腾讯云用户 |
WeKnora 通过统一的 VectorStore 接口抽象了这些差异。用户只需在配置中指定使用哪个后端,不需要修改任何业务代码。这种”适配器模式”让用户可以根据自己的基础设施选择最合适的向量库,而不是被 WeKnora 的技术选型绑定。
ANN 搜索的精度-速度权衡:
精确最近邻搜索(暴力搜索)在百万级向量上可能需要数秒。ANN(Approximate Nearest Neighbor)通过牺牲少量精度换取数量级的速度提升。常见算法:
HNSW(Hierarchical Navigable Small World):构建一个多层图结构,搜索时从高层图跳到低层图逐步精化。类比:在城市里找目的地,先从高速公路(高层图)找到大致方向,再转入地面道路(低层图)精确定位。
IVF(Inverted File):先把向量聚类成若干簇,搜索时只扫描查询向量最近的几个簇。类比:图书馆按类别分区,找科技书只需去科技区扫,不需要遍历全馆。
第三路:知识图谱推理检索
知识图谱是 WeKnora 区别于所有竞品的核心差异化。
PMI(点互信息)知识图谱构建:
WeKnora 不是用传统的 NER(命名实体识别)+ 关系抽取来构建知识图谱——那需要训练专门的模型、标注大量数据。它用了一个更简洁的统计方法:PMI(Pointwise Mutual Information,点互信息)。
\[PMI(x, y) = \log_2\left(\frac{P(x,y)}{P(x) \cdot P(y)}\right)\]其中 $P(x)$、$P(y)$ 分别为实体 $x$、$y$ 在语料中出现的概率,$P(x,y)$ 为两者在同一文档/段落中共同出现的概率。
PMI 的直觉理解:
如果两个实体的共现频率远超随机概率(PMI » 0),它们之间很可能存在有意义的关系。如果共现频率接近随机概率(PMI ≈ 0),它们的共现是偶然的。
具体例子:在 WeKnora 的文档库中——
高 PMI 对:”BM25” 和 “关键词检索” → 几乎总是一起出现 → PMI 很高 → 建立强关系边
中 PMI 对:”BM25” 和 “向量检索” → 在讨论混合检索时一起出现 → PMI 中等 → 建立弱关系边
低 PMI 对:”BM25” 和 “用户权限” → 偶尔在同一篇文档中出现但没有语义关联 → PMI 接近 0 → 不建立关系边
PMI 知识图谱的构建步骤:
1. 实体抽取
从文档中提取候选实体(名词短语/专业术语)
→ 使用中文分词 + 词性标注过滤
2. 共现统计
扫描所有文档,统计每对实体的共现频率
→ 窗口大小通常为段落级别(一个 chunk 内)
3. PMI 计算
对每对实体计算 PMI 值
→ 过滤掉低频对(出现 < N 次的不可靠)
4. 图谱构建
PMI > 阈值的实体对 → 在图中建立边
边的权重 = PMI 值
→ 形成加权无向图
5. 图谱索引
对图做预处理(如 BFS/DFS 可达性索引)
→ 支持快速图遍历查询
知识图谱检索的工作方式:
当用户查询到来时,知识图谱检索分三步:
-
实体识别:从查询中提取关键实体。如查询”WeKnora 的知识图谱用了什么算法”,提取出 “WeKnora” 和 “知识图谱” 两个实体。
-
图遍历:从这两个实体出发,在知识图谱中做 1-2 跳遍历,找到关联实体。如 “知识图谱” → “PMI”(1 跳),”PMI” → “共现统计”(2 跳)。
-
文档召回:找到关联实体后,检索包含这些实体的文档 chunk。如包含 “PMI 共现统计” 的那段文档——这段文档可能通过 BM25 搜不到(因为不包含”算法”这个词),通过向量搜索也可能排名不高(因为语义上离”知识图谱算法”有距离),但通过图谱推理找到了。
知识图谱的增量维护:
文档更新时,知识图谱需要同步更新。WeKnora 的做法是增量式的——不是全量重建图谱,而是:
- 新文档加入 → 提取新实体 → 更新相关 PMI 值 → 新增/删除边
- 旧文档删除 → 移除该文档贡献的共现统计 → 重算受影响的 PMI → 删除弱边
- 文档修改 → 先删再加
这种增量维护的效率远高于全量重建,但实现复杂度也更高——需要精确追踪每个 PMI 值中每篇文档的贡献。
RRF 融合器深度解析
三路检索各自返回一个排名列表后,需要把它们合并成一个统一排序。WeKnora 选择了 RRF(Reciprocal Rank Fusion)。
RRF 公式:
\[\text{RRF}_{\text{score}}(d) = \sum_i \frac{w_i}{k + rank_i(d)}\]其中 $rank_i(d)$ 为文档 $d$ 在第 $i$ 路检索结果中的排名(从 1 开始),$k=60$,$w_{\text{向量}}=0.7$,$w_{\text{BM25}}=0.3$,$w_{\text{图谱}}$ 根据图谱密度动态调整。
为什么选择 RRF 而不是分数加权?
核心问题:BM25 分数和向量余弦相似度不在同一个量纲上。BM25 分数范围可能是 [0, 30],余弦相似度范围是 [-1, 1](实际常见 [0.5, 0.99])。直接加权 0.7 × cosine + 0.3 × bm25 没有意义——BM25 的绝对值会主导结果。
传统解决方案是对各路分数做归一化(min-max / z-score / percentile),但这引入新的问题:归一化方式的选择本身就是一个超参数,不同归一化方式在不同数据集上表现差异大。
RRF 的优雅之处在于:完全忽略分数的绝对值,只看排名。排名天然是归一化的——第 1 名就是第 1 名,不管它的分数是 0.99 还是 25.7。这消除了量纲不一致的问题。
k=60 的数学直觉:
k 控制的是排名差异的”敏感度”。举个例子:
当 k=1 时:
- 第 1 名:1/(1+1) = 0.500
- 第 2 名:1/(1+2) = 0.333
- 第 10 名:1/(1+10) = 0.091
- 第 1 名和第 2 名的分数差距:0.167(33%)
当 k=60 时:
- 第 1 名:1/(60+1) = 0.0164
- 第 2 名:1/(60+2) = 0.0161
- 第 10 名:1/(60+10) = 0.0143
- 第 1 名和第 2 名的分数差距:0.0003(1.8%)
k=1 时,排名差一位就有巨大的分数差距,排名第一的结果几乎”一骑绝尘”。k=60 时,排名差距被大幅压缩——前 10 名的分数几乎在同一水平,需要在多路检索中都排名靠前才能脱颖而出。这正是我们需要的效果:鼓励多路检索的”共识”,而不是被单路的极端排名主导。
RRF 融合的具体计算示例:
假设某个文档 D 在三路检索中的排名如下:
BM25: 排名第 3
向量: 排名第 1
图谱: 排名第 8
RRF_score(D) = 0.3/(60+3) + 0.7/(60+1) + w_graph/(60+8)
= 0.3/63 + 0.7/61 + w_graph/68
= 0.00476 + 0.01148 + w_graph × 0.01471
另一个文档 E:
BM25: 排名第 1
向量: 排名第 15
图谱: 未召回(排名 = ∞)
RRF_score(E) = 0.3/(60+1) + 0.7/(60+15) + 0
= 0.3/61 + 0.7/75
= 0.00492 + 0.00933
= 0.01425
文档 D 在向量检索中排名第一(最相关信号),文档 E 在 BM25 中排名第一但向量排名很低。RRF 会倾向于选择 D——因为 D 在多路检索中都有不错的排名,而 E 只在一路中突出。这体现了 RRF “共识优先”的设计哲学。
复合 Rerank 深度解析
RRF 融合后得到的排名列表质量已经不错,但还不够精细。WeKnora 在 RRF 之上叠加了复合 Rerank 做精排。
复合 Rerank 公式:
final_score(d) = 0.6 × rerank_model_score(q, d)
+ 0.3 × base_score(d)
+ 0.1 × source_credibility(d)
其中:
rerank_model_score = Cross-Encoder 模型对 (query, document) 对的相关性评分
base_score = RRF 融合后的基础排名分
source_credibility = 文档来源的可信度评分(官方文档 > 社区帖子 > 个人笔记)
为什么需要三个信号而不是只用 Rerank 模型?
Rerank 模型(通常是 Cross-Encoder)虽然精度高,但有两个弱点:
弱点一:领域适应性差。通用 Rerank 模型在垂直领域的专业术语上可能判断失误。比如在医疗领域,”阳性”是一个关键术语,但通用模型可能不了解”阳性”在不同检查中的不同含义。
弱点二:无法感知来源质量。一段从官方文档摘录的 200 字和一段从论坛帖子复制的 200 字,文本内容可能相似度极高,但可信度天差地别。Rerank 模型只看文本语义,无法区分来源。
WeKnora 用 base_score(RRF 融合分)兜底领域适应性——即使 Rerank 模型判断失误,多路检索的共识排名还在;用 source_credibility 兜底来源质量——文档来源的元数据直接参与评分。
source_credibility 的实现:
来源类型 可信度分数
─────────────────────────
官方文档 1.0
产品文档 0.9
技术博客(官方) 0.8
社区文档 0.6
论坛帖子 0.4
用户笔记 0.3
这个分数不是从文本内容推断的,而是在文档摄入时根据元数据(来源 URL、作者、发布渠道)预先标注的。
4 级自适应分块策略
文档分块(Chunking)是 RAG 系统中看似简单但影响巨大的环节。WeKnora 的 4 级自适应分块是其另一个技术亮点。
为什么分块很重要?
分块太大(如整篇文章):向量表示会”稀释”——一个 768 维向量要承载太多信息,搜索精度下降。而且大 chunk 送入 LLM 时上下文窗口利用率低。
分块太小(如单句):失去上下文——”它使用了 RRF 算法”这句话脱离上下文后,”它”指代谁?LLM 无法根据这个孤立句子给出准确回答。
WeKnora 的 4 级自适应分块:
级别 1:文档级(Document)
└─ 整篇文档作为一个 chunk
└─ 用于:文档级别的粗粒度检索
级别 2:章节级(Section)
└─ 按标题层级(h1/h2/h3)切分
└─ 用于:章节级别的中粒度检索
级别 3:段落级(Paragraph)
└─ 按段落边界切分
└─ 默认最大 512 tokens,重叠 50 tokens
└─ 用于:段落级别的细粒度检索
级别 4:语义级(Semantic)
└─ 基于语义相似度动态切分
└─ 相邻句子语义跳变超过阈值时切分
└─ 用于:跨段落但语义连贯的内容
“自适应”体现在哪里?
WeKnora 不是固定使用某一级分块,而是根据文档类型自动选择策略:
| 文档类型 | 推荐策略 | 原因 |
|---|---|---|
| 技术文档 | 章节级 + 段落级 | 结构清晰,标题层级是天然切分点 |
| FAQ 文档 | 段落级 | 每个 Q&A 对是独立单元 |
| 会议纪要 | 语义级 | 议题之间无明确结构标记,需要语义判断 |
| 代码文件 | 函数/类级 | 以函数或类为自然单元 |
| 长篇报告 | 文档级 + 章节级 | 需要保留全局上下文 |
语义级分块的实现细节:
1. 把文档分成句子序列: [s1, s2, s3, ..., sn]
2. 对每个句子做 embedding: [e1, e2, e3, ..., en]
3. 计算相邻句子的余弦相似度: sim(ei, ei+1)
4. 当 sim(ei, ei+1) < 阈值 → 在 si 和 si+1 之间切分
5. 阈值通常设为 0.5-0.7(可配置)
日常类比:你在读一本书,连续几页都在讲同一个主题,突然下一页话题急转——你自然会在这里”切分”认知,觉得”上面是一段,下面是另一段”。语义级分块就是模拟这个认知过程。
重叠窗口(Overlap):
相邻 chunk 之间有 50 tokens 的重叠区域。为什么?因为切分点两侧的句子可能有语义关联——上一个 chunk 的最后一句和下一个 chunk 的第一句可能构成因果关系。重叠区域确保这种跨 chunk 的语义关联不会丢失。代价是存储空间增加约 10%。
Wiki 知识库自维护机制
这是 WeKnora 最具野心的特性之一:让知识库能”自己维护自己”。
什么是知识库腐化(Knowledge Rot)?
企业知识库最大的问题不是缺少内容,而是内容过时。一篇 2023 年写的 API 文档,到 2026 年可能有一半参数已经变了。如果用户检索到这篇过时文档并照做,后果可能比”找不到文档”更严重——因为用户得到了一个自信但错误的答案。
WeKnora 的 Wiki 自维护机制就是为了解决这个问题。
自维护的工作流程:
定时任务(每天/每周)
│
├─ 1. 过期检测
│ └─ 检查文档的"最后更新时间"
│ └─ 超过阈值(如 90 天)的标记为"疑似过期"
│
├─ 2. 矛盾检测
│ └─ 对同一主题的多篇文档做语义比对
│ └─ 如果两篇文档对同一问题给出矛盾答案 → 标记冲突
│ └─ 通知知识库管理员处理
│
├─ 3. 覆盖缺口检测
│ └─ 分析用户查询日志
│ └─ 找到"高频查询但低检索质量"的主题
│ └─ 提示管理员补充相关文档
│
└─ 4. 质量评分
└─ 对每篇文档打分(引用频率 / 用户反馈 / 新鲜度)
└─ 低分文档标记为"需要审核"
矛盾检测的具体实现:
1. 主题聚类
把所有文档按主题聚类(基于向量相似度)
同一簇内的文档讨论的是同一个主题
2. 断言提取
从每篇文档中提取关键断言(如"最大连接数为 100")
使用 LLM 辅助提取
3. 断言比对
同一簇内的断言两两比对
"最大连接数为 100" vs "最大连接数为 200" → 矛盾
4. 冲突上报
矛盾对连同上下文一起发送给管理员
管理员确认后更新文档
这个机制的精妙之处在于:它把”知识库维护”从人工定期审查变成了系统自动化巡检。管理员不需要遍历所有文档找过期内容——系统会主动告诉他哪些文档有问题。
源码关键实体索引
| Go 包 / 文件 | 核心功能 | 关键结构/函数 |
|---|---|---|
pkg/eventmanager/ |
事件总线 | EventManager, Plugin, Emit(), Register() |
pkg/retriever/bm25/ |
BM25 检索 | BM25Retriever, Search(), BuildIndex() |
pkg/retriever/vector/ |
向量检索 | VectorRetriever, ANN(), EmbedQuery() |
pkg/retriever/graph/ |
图谱检索 | GraphRetriever, Traverse(), PMI() |
pkg/fusion/ |
RRF 融合 | RRFusion, Fuse(), k=60 |
pkg/rerank/ |
复合 Rerank | CompositeReranker, Score() |
pkg/chunker/ |
自适应分块 | AdaptiveChunker, Chunk(), Level |
pkg/wiki/ |
知识自维护 | WikiInspector, DetectStale(), DetectConflict() |
pkg/vectorstore/ |
向量库适配 | VectorStore 接口, 各实现 |
internal/config/ |
配置管理 | 全局配置、模型选择、参数调优 |
与竞品的差异化矩阵
| 能力维度 | WeKnora | Dify | RAGFlow | AnythingLLM | FastGPT |
|---|---|---|---|---|---|
| 检索路数 | 3 路(BM25+向量+图谱) | 2 路(BM25+向量) | 2 路(BM25+向量) | 1 路(向量) | 1 路(向量) |
| RRF 融合 | k=60,支持权重自定义 | 简单线性融合 | 权重融合 | 无 | 无 |
| 知识图谱 | PMI 自动构建 | 无原生支持 | 有(实验性) | 无 | 无 |
| Rerank | 三信号复合(0.6/0.3/0.1) | 单模型 Rerank | 单模型 Rerank | 无 | 单模型 Rerank |
| 分块策略 | 4 级自适应 | 固定大小 | 模板驱动 | 固定大小 | 固定大小 |
| 知识自维护 | 自动巡检(过期/矛盾/缺口) | 无 | 无 | 无 | 无 |
| 插件架构 | EventManager + Plugin | 后端不开放 | 有限可扩展 | 无 | 无 |
| 向量库支持 | 10 种 | 3-4 种 | 3-4 种 | 2-3 种 | 2-3 种 |
| 开发语言 | Go | Python | Python | Node.js | TypeScript |
WeKnora 在检索质量维度的优势是压倒性的。但也要承认它的局限:Go 语言的生态导致 ML/NLP 工具链不如 Python 丰富;部署复杂度相对较高(需要运行知识图谱构建、BM25 索引、向量库等多个组件);社区活跃度和文档完善度不如 Dify(后者有 6 万+ Stars 的社区基础)。
竞赛贡献切入点
WeKnora 的竞赛贡献方向详见 WeKnora 竞赛贡献指南。这里列出与架构理解直接相关的核心方向:
高影响力方向(适合犀牛鸟竞赛):
方向一:Agentic RAG 集成——在 EventManager 中引入 Agent 编排层,让 WeKnora 支持多跳推理(查询分解→子查询→结果合成)。目前 WeKnora 的检索是单轮的,用户问一个复杂问题只能做一次检索。加入 Agent 层后,复杂问题可以被分解成多个子查询,每个子查询独立走三路检索+RRF+Rerank,最后合成答案。参见 Agentic RAG 模式。
方向二:自适应分块增强——在第 4 级(语义级)分块中引入 LLM 辅助判断,让分块边界更智能。目前语义级分块只看相邻句子的向量相似度,可能在同一论点内部因为句式变化而误切。LLM 可以判断”这两段虽然用词不同但在讨论同一个论点,不应该切分”。
方向三:评估框架集成——在 EventManager 中增加一个 EvalPlugin,自动对每次检索做 RAGAS 四维评估(忠实度/答案相关性/上下文精确度/上下文召回率),并将评估结果反馈到知识库自维护流程中。参见 RAG 评估方法论。
交叉引用
| 相关页面 | 关系 |
|---|---|
| RAG 知识库全景 | WeKnora 在 5 项目比较中的定位 |
| 跨项目深度分析 | RRF/Rerank 的跨项目比较 |
| WeKnora 竞赛贡献指南 | 具体贡献路径 |
| Agentic RAG 模式 | 6 种 RAG 模式与 WeKnora 的结合 |
| RAG 评估方法论 | RAGAS 评估如何应用于 WeKnora |
| Memory vs RAG 边界 | D 线(RAG)与 B 线(Memory)的分工 |
| 腾讯记忆生态 | WeKnora 在腾讯 DB-Agent-Memory 生态中的位置 |
| RAG Agent 集成 | Agent 与 RAG 平台的集成模式 |
| 选型决策树 | 什么场景选 WeKnora |
学习建议
如果你是第一次接触 WeKnora 的源码,建议按以下顺序阅读:
- 先读
pkg/eventmanager/——理解整个架构的神经中枢 - 再读
pkg/retriever/vector/——最直观的检索路径 - 然后读
pkg/retriever/bm25/——理解关键词检索的互补作用 - 接着读
pkg/fusion/——理解 RRF 如何合并多路结果 - 再读
pkg/retriever/graph/——最复杂的检索路径,PMI 图谱构建 - 最后读
pkg/wiki/——理解知识自维护的完整闭环
每个包大约需要 1-2 小时精读。建议逐段消化。