犀牛鸟 2026 研究笔记 estelledc.github.io

WeKnora 贡献指南(犀牛鸟竞赛导向)

调研日期:2025-06-22 WeKnora commit: 5f7ed2f (2026-06-22) 未 Docker 实测,以源码/docs 为准

一句话定位

本篇是犀牛鸟竞赛 可执行贡献路径——不重复 精读: WeKnora 已有的架构分析,聚焦”怎么选切入点、怎么提第一个 PR、怎么从零做到高价值贡献”。


RAG 管线段标注

本篇主要涉及 Ingest - Parse/Chunk - Retrieve - Generate 全段的贡献视角——每段在哪改、改什么、门槛多高、评审怎么看。


1. 犀牛鸟竞赛的评审标准

结论先行:评审不看 PR 数量,看理解深度 + 技术方案可行性 + 独立解决问题的能力

1.1 评审维度拆解

想象你在参加一场厨艺比赛。评委不会因为你切了 100 根葱花就给你高分——他们看的是:你理解这道菜的口味逻辑吗?你的食材搭配方案合理吗?刀工出问题你能自己补救吗?

犀牛鸟评审的逻辑一模一样:

评审维度 权重(推断) 具体表现 常见误区
项目理解深度 最高 跑通 demo、读过源码、能说清架构设计取舍 只读了 README 就开始提 PR
Proposal 质量 技术方案可行、时间规划合理(以周为单位)、体现独立思考 Proposal 写得像读书笔记,没有自己的方案
提前联系导师 展示准备工作和思考,带着具体问题去沟通 从不联系导师,闭门造车
GitHub 活跃记录 有实际 PR 贡献,review 他人 PR 也能加分 只提 trivial PR(修 typo)凑数
解决问题的韧性 遇到卡壳能独立排查,不直接放弃 遇到问题就等导师手把手教
技术含量 贡献涉及核心逻辑而非纯 UI 微调 全部贡献集中在改按钮颜色

往年经验详见 往年经验帖,评审维度的详细案例见该帖”评审看重什么”一节。

1.2 从评审反推贡献策略

评审最看重”理解深度”,所以贡献策略的核心不是”PR 越多越好”,而是”每个 PR 都能证明我理解了什么”。

具体做法:

  1. 第一个 PR 必须跑通全栈——证明你能部署、能定位问题、能验证修复
  2. PR 描述写清楚 why——不只写”修复了 #1633”,而是写”问题根因是搜索回调返回值在 null case 下未处理,导致 undefined 渲染。修复方式是在回调函数中增加 null guard”
  3. 选涉及核心逻辑的 issue——改检索管线的一个参数,比改 10 个前端 typo 更有说服力
  4. 做别人不愿做的苦活——写 eval 脚本、补测试、做 benchmark——这些没人做但维护者极其需要

1.3 与 GSoC 评审的异同

维度 犀牛鸟 GSoC
评审主体 项目导师 + 腾讯开源委员会 项目导师
中期评估 有(8 月初) 有(mid-term)
最终评估 PR 合入 + 书面报告 代码合入
对 Proposal 的重视 极高(直接决定是否入围)
导师沟通 非常重要(导师推荐权重高) 重要

1.4 从评审看”深度贡献”的定义

犀牛鸟评审对”深度贡献”有非常具体的期待——不是”改了很多代码”,而是”对项目有可衡量的正向影响”。以下三个层次,评审会依次考察:

层次一:能跑起来(必要条件) 本地部署成功、能复现 issue、能跑测试。这一步淘汰掉大部分”只看了 README”的参与者。如果你连项目都跑不起来,评审不会相信你理解了项目。

层次二:能定位问题(区分条件) 不只是修 bug,而是能说清楚 bug 的根因。比如修复 #1633,不只是加一个 null check——而是能解释”搜索回调在异步 Promise 链中,当 API 返回空结果时,回调函数的参数结构从 {data: [...]} 变成了 {data: null},下游的 .map() 调用在 null 上抛出 TypeError,导致 undefined 被渲染到页面”。这种根因分析能力,评审极其看重。

层次三:能提出方案(加分条件) 发现了现有架构的设计局限,并提出可行的改进方案。比如发现 WeKnora 的单轮检索无法处理复杂查询,提出多跳查询分解方案——这就是层次三的贡献。大多数参与者停留在层次二,做到层次三的往往是最终获奖者。

评审的详细案例和往年参与者的经验见 往年经验帖,选题策略中的评审维度分析见 选题策略


2. 环境矩阵

组件 版本要求 说明
Go 1.21+ 核心后端(internal/ 全部);门槛最高
Node.js / React+TS 18+ 前端主力切入点frontend/ 目录)
Python 3.10+ 文档解析微服务(docreader/,约513K)
Docker / Docker Compose 最新 全栈本地部署(docker/deploy/
LLM API Key 必需 检索/Rerank/Agent 对话均依赖
向量数据库 按配置 支持 10 种后端(Milvus/Qdrant/PgVector 等)

注意:仓库今天(2026-06-22)有重大结构变动——原来的 client/ Vue 前端已重构为 frontend/ React+TS 前端,新增 mcp-server/(MCP Server 实现)和 miniprogram/(微信小程序)。

2.1 环境选择策略

面对四种技术栈(Go / React+TS / Python / Docker),新手最常犯的错误是”什么都想学”——既想学 Go 后端又想学前端又想学 Python docreader。结果什么都只学了皮毛,哪个方向都做不出有说服力的贡献。

正确的做法是:选定一个技术栈,做到深度足以提 PR。具体选择逻辑:

2.2 LLM API Key 的选择

WeKnora 的几乎所有功能都依赖 LLM API——检索时的 query embedding、Rerank 时的 Cross-Encoder 打分、Agent 对话的生成。选择 API 时需要考虑:

场景 推荐选择 原因
低成本实验 DeepSeek API 价格极低,中文理解好
稳定生产 OpenAI GPT-4o-mini 稳定性强,文档齐全
腾讯生态 混元 API 与 WeKnora 同属腾讯,兼容性可能更好
本地无网络依赖 Ollama + Qwen2 完全本地,零成本,但质量较低

注意:Rerank 功能对模型质量很敏感。用低质量模型做 Rerank,0.6/0.3/0.1 的权重分配可能不再合理——模型分的权重可能需要从 0.6 降到 0.4,基础分从 0.3 升到 0.5。这是路径 C(评估框架集成)要验证的假设。


3. 当前仓库结构(对照已有精读更新)

cmd/                          -- Go 入口 main
internal/
  |-- eventmanager/           -- 事件总线(Plugin 注册/派发)
  |-- plugin/                 -- 插件管线
  |-- retrieval/              -- bm25 / vector / knowledge_graph / fusion
  |-- rerank/                 -- 复合 Rerank
  |-- chunking/               -- 自适应分块(4 级降级)
  |-- wiki/                   -- 自维护 Wiki(inspector.go 巡检)
frontend/                     -- React+TS 前端(推荐切入点)
mcp-server/                   -- MCP Server 实现(新增)
docreader/                    -- Python 文档解析引擎
client/                       -- Go SDK / 客户端库
cli/                          -- 命令行工具
miniprogram/                  -- 微信小程序(新增)
docker/ / deploy/ / helm/     -- 部署配置
docs/                         -- 文档
skills/ / packages/ / examples/ -- 扩展 / 示例

3.1 目录导航策略

这个仓库很大,直接全读是不现实的。以下是按贡献目标的导航策略:

想改检索管线(路径 A / D 的核心):

  1. internal/retrieval/ 开始,这是检索的入口包
  2. bm25/ 理解关键词检索如何工作
  3. vector/ 理解向量检索如何工作
  4. knowledge_graph/ 理解图谱检索如何工作——这是最复杂的一路
  5. fusion/ 理解 RRF 如何合并三路结果
  6. rerank/ 理解复合 Rerank 的三信号设计

想改分块策略(路径 B / E 的核心):

  1. internal/chunking/ 开始
  2. 找到 adaptive.go——4 级自适应分块的入口
  3. 理解各级分块的触发条件和降级逻辑
  4. 对于路径 E,还要看 docreader/ 的 Python 代码

想加评估(路径 C 的核心):

  1. internal/wiki/ 开始——理解自维护 Wiki 的巡检逻辑
  2. inspector.go——理解巡检如何发现知识库问题
  3. internal/plugin/ 中找到合适的扩展点
  4. 设计 EvalPlugin 与现有 Wiki 巡检的集成方式

想改前端(快速出 PR):

  1. frontend/ 开始
  2. package.json 了解技术栈和脚本
  3. src/components/ 找到对应组件
  4. npm run dev 启动开发服务器

完整的源码精读路径见 精读: WeKnora 的”学习建议”章节。


4. Issue 地图(2026-06-22 gh 核实)

4.1 高新手友好度(推荐前 5)

Issue # 标题 类型 为何适合
#1633 共享空间添加人员弹窗——角色搜索返回 undefined Bug 典型前端 bug,搜索回调返回值处理错误,范围小
#1353 知识库 tag 编辑在列表视图和批量操作栏缺失 Bug UI 组件缺失,纯前端工作,边界清晰
#1565 知识库添加文件报错时不展示错误信息 Enhancement 纯前端错误提示改善,范围极小
#1417 浏览器插件保存时修改标题无效 Bug 浏览器扩展小 bug,字段传递问题
#1444 wiki 模式实体里图片有的无法加载 Bug 图片 URL 处理问题,前端定位

4.2 中等友好度(有一定理解成本)

Issue # 标题 类型 为何有空间
#1606 IM 思考过程输出作为可配置项 Enhancement 加配置开关,前端+配置层
#1716 Cannot Save Agent With Selected Knowledge Bases Bug 前端表单提交 bug,需理解 Agent 配置
#1723 KB 更新 API 缺少 VLMConfig 字段 Bug Go API 层加字段,改动可控
#1725 PaddleOCR-VL 表格 HTML 影响分块效果 Bug 需理解分块逻辑但问题清晰
#1676 对话支持指定具体文档(非只能指定知识库) Enhancement 前端 UI + 后端 API 参数扩展

4.3 竞争情报

4.4 Issue 选择策略

选 issue 不是看哪个简单就选哪个——而是看哪个 issue 能最大化你的”理解深度证明”。以下是选择逻辑:

第一周(破冰期):选一个高新手友好度的前端 bug。目标不是做多有技术含量的贡献,而是证明你能跑通项目、能定位问题、能提 PR。推荐 #1633 或 #1565。

第二周起(深入期):根据你选定的深度路径(A-E),选择与路径直接相关的 issue。这些 issue 可能不在”高新手友好度”列表中,但它们让你在解决实际问题的过程中深入理解相关模块。

Issue 与路径的映射

Issue 关联路径 深入方向
#1725 PaddleOCR-VL 表格 HTML 影响分块 路径 B / E 理解分块逻辑 + docreader
#1723 KB 更新 API 缺少 VLMConfig 字段 路径 E 理解 Go API 层 + 多模态配置
#1676 对话支持指定具体文档 路径 A 理解检索 API + 查询参数扩展
#1606 IM 思考过程输出作为可配置项 路径 C 理解配置层 + 可观测性

4.5 竞争格局分析

了解其他贡献者在做什么,可以帮助你避开”红海”——同一个 issue 被多个人同时修复,维护者只会合入一个。

当前活跃贡献者方向(基于 Open PRs 分析):

贡献者 方向 对你的影响
FFFFFpy 前端功能增强 如果你要做前端贡献,关注他的 PR 避免冲突
coconut-yc Go 后端 bug 修复 检索/分块相关 bug 可能已被他修了
langcaiye 文档 / 部署 文档类贡献可能与他重叠
jack-wang-176 MCP Server 如果你做路径 A,关注他的 MCP 改动

策略建议


5. WeKnora 贡献路径详解

这是本篇的核心。不重复 精读: WeKnora 的架构分析,聚焦”每条路径具体改什么文件、写什么代码、怎么测”。

先给全局地图:

flowchart TD
    START[选定贡献路径] --> A{技术栈偏好?}
    A -->|前端| B[前端 Bug / UI]
    A -->|Go 后端| C{深度目标?}
    A -->|Python| D[docreader 解析]
    A -->|评估/文档| E[Benchmark / Eval]

    C -->|检索编排| F[路径 A: Agentic RAG]
    C -->|分块优化| G[路径 B: 自适应分块]
    C -->|评估闭环| H[路径 C: 评估框架]
    C -->|图谱增强| I[路径 D: 知识图谱]
    C -->|多模态| J[路径 E: 多模态支持]

    B --> K[快速出 PR]
    D --> K
    E --> K
    F --> L[高价值深度贡献]
    G --> L
    H --> L
    I --> L
    J --> L

路径 A:Agentic RAG 集成——多跳查询分解

路径总览与选择建议

五条路径该怎么选?核心原则是”你的技术栈匹配度 x 路径的竞赛价值 x 你能投入的时间”。

路径 技术栈 竞赛价值 时间投入 推荐人群
A: Agentic RAG Go + LLM prompt 极高(最前沿方向) 3-4 周 有 Go 基础 + 对 Agent 感兴趣
B: 自适应分块 Go + LLM prompt 2-3 周 有 Go 基础 + 对 NLP 感兴趣
C: 评估框架 Go + 评估理论 极高(维护者最需要) 3-4 周 有 Go 基础 + 对评估/测试感兴趣
D: 知识图谱 Go + 图论 高(WeKnora 独有特性) 3-4 周 有 Go 基础 + 对图算法感兴趣
E: 多模态 Python + Go 高(实际问题驱动) 2-3 周 会 Python + 对多模态感兴趣

个人推荐排序(假设你有基本的 Go 基础):

  1. 路径 C(评估框架)——维护者最需要、竞争最小、竞赛评估时最容易量化效果
  2. 路径 A(Agentic RAG)——最前沿、最能体现”理解深度”、但风险也最高
  3. 路径 D(知识图谱)——WeKnora 独有特性,做了没人能竞争

如果你只有 Python 基础:路径 E 是唯一选择,但 #1725 issue 保证了这是一个实际问题。

各路径的竞赛得分预期

不同路径在竞赛评审中的预期得分不同。这不是因为某些路径”更好”,而是因为不同路径展示”理解深度”的容易程度不同:

路径 容易量化效果 展示架构理解 维护者需求度 竞争者数量 综合得分预期
A: Agentic RAG 中(需要对比实验) 极高(改核心管线) 中(方向有争议) 中-高
B: 自适应分块 高(分块质量可量化) 高(改分块逻辑) 中-高
C: 评估框架 极高(RAGAS 四指标直接出数) 高(改 Wiki 巡检 + Plugin) 极高(无人做) 极少
D: 知识图谱 高(图谱精度可量化) 极高(改核心差异化特性) 极少
E: 多模态 中(需要图表类 benchmark) 中(改 docreader 较独立)

核心洞察:路径 C 的”容易量化效果”和”维护者需求度”都是极高的——这意味着你做完之后,可以用数字说”我让 WeKnora 的评估能力从零变成了可用”,这个效果比”我加了一个功能”有力得多。

定位与类比

WeKnora 当前是”单轮固定管线”——用户问一个问题,三路全查,RRF 融合,Rerank 精排,生成答案。如果用户问的是复杂问题,比如”WeKnora 的知识图谱算法和 GraphRAG 有什么区别?各自适合什么场景?”——这个问题其实包含两个子问题,但 WeKnora 只做一次检索,可能两边都搜不深。

类比:你现在去图书馆问管理员”中国明朝和英国都铎王朝的税收制度有什么区别?”管理员要么给你找中国的、要么给你找英国的,很难一次把两边都找齐。但如果有一个聪明的研究助理,他会先分解问题——”明朝税收制度是什么”“都铎王朝税收制度是什么”——分别查找,再综合比较。这就是多跳查询分解。

类比边界:不是所有查询都需要分解。”WeKnora 支持哪些向量数据库?”这种简单问题分解反而增加延迟和成本。

设计文档大纲

# Agentic RAG: Multi-hop Query Decomposition for WeKnora

## 1. 背景与动机
- 当前 WeKnora 检索是单轮固定管线
- 复杂查询(对比/因果/多条件)的召回率不足
- 参考: IRCoT 论文、LangGraph Multi-step Retrieval

## 2. 设计方案
### 2.1 查询分解器 (QueryDecomposer)
- 输入: 用户原始查询
- 输出: 1-N 个子查询 + 合成策略
- 实现: LLM-based decomposition (prompt engineering)

### 2.2 并行子查询检索
- 每个子查询独立走三路检索 + RRF + Rerank
- 使用 goroutine 并行执行

### 2.3 结果合成
- 按合成策略合并子查询结果
- 去重(同一 chunk 可能被多个子查询召回)
- 重排序(可选二次 Rerank)

### 3. EventManager 集成
- 新增 QueryDecomposed 事件
- 新增 SubQueryRetrieved 事件
- FusionPlugin 扩展:支持多轮融合

### 4. 配置与降级
- decomposition.enabled: bool(默认关闭)
- decomposition.max_hops: int(默认 3)
- 降级: 分解失败时 fallback 到原始单轮检索

关键代码修改点

1. 新增 internal/retrieval/decomposer/

// decomposer.go - 查询分解器核心逻辑
type Decomposer struct {
    llmClient  LLMClient
    maxHops    int
    enabled    bool
}

// Decompose 将复杂查询分解为子查询列表
// 关键设计: 分解失败时返回原始查询(降级策略)
func (d *Decomposer) Decompose(ctx context.Context, query string) ([]SubQuery, error) {
    if !d.enabled {
        return []SubQuery{{Query: query, Strategy: "direct"}}, nil
    }
    prompt := buildDecompositionPrompt(query)
    resp, err := d.llmClient.Chat(ctx, prompt)
    if err != nil {
        // 降级: LLM 调用失败, 返回原始查询
        return []SubQuery{{Query: query, Strategy: "direct"}}, nil
    }
    subQueries, err := parseDecomposition(resp)
    if err != nil || len(subQueries) == 0 {
        return []SubQuery{{Query: query, Strategy: "direct"}}, nil
    }
    if len(subQueries) > d.maxHops {
        subQueries = subQueries[:d.maxHops]
    }
    return subQueries, nil
}

2. 修改 internal/eventmanager/ 事件定义

events.go 中新增:

QueryDecomposed    EventType = "query_decomposed"    // 新增
SubQueryRetrieved  EventType = "subquery_retrieved"  // 新增
AllSubQueriesDone  EventType = "all_subqueries_done"  // 新增

3. 新增 DecomposerPlugin 注册到 EventManager

type DecomposerPlugin struct {
    decomposer *Decomposer
}

func (p *DecomposerPlugin) SubscribedEvents() []EventType {
    return []EventType{"QueryReceived"}
}

func (p *DecomposerPlugin) Handle(event Event) (Event, error) {
    subQueries, err := p.decomposer.Decompose(event.Ctx, event.Query)
    if err != nil {
        return event, err  // 降级: 返回原始事件
    }
    return Event{
        Type: "QueryDecomposed",
        Data: SubQueryData{Queries: subQueries, Original: event.Query},
    }, nil
}

func (p *DecomposerPlugin) Priority() int {
    return -1  // 在 BM25/Vector/Graph 之前执行
}

4. 修改 FusionPlugin 支持多子查询结果合并

internal/retrieval/fusion/ 中扩展 Fusion 逻辑,新增 MultiQueryFuse() 方法处理来自不同子查询的检索结果,做去重和加权合并。

测试策略

测试层级 内容 工具
单元测试 Decomposer.Decompose() 分解正确性 Go testing + table-driven
单元测试 降级逻辑(LLM 超时/返回格式错误) Mock LLMClient
集成测试 完整 EventManager 流程:Query → Decompose → SubQuery → Retrieve → Fuse Docker compose
端到端测试 对比”有分解”vs”无分解”的检索质量 RAGAS eval script
性能测试 多子查询并行检索的延迟 vs 单查询延迟 pprof + benchmark

关键测试用例设计:

// table-driven 测试
tests := []struct{
    name     string
    query    string
    wantN    int    // 期望分解出几个子查询
    fallback bool   // 是否期望降级
}{
    {"简单查询不分解", "WeKnora支持哪些向量数据库", 1, false},
    {"对比查询分解为2", "BM25和向量检索的区别", 2, false},
    {"三段式查询分解为3", "WeKnora的架构、检索和评估分别是什么", 3, false},
    {"LLM失败降级", "anything", 1, true},
}

预期影响与工作量

维度 估算
代码行数 约 800-1200 行(Go)
涉及文件 新增 decomposer/ 包 + 修改 eventmanager/ + fusion/ + config/
工作量 3-4 周(含测试和文档)
对检索质量的影响 复杂查询 Context Recall 预期提升 15-25%
风险 分解 prompt 质量不稳定;多子查询增加 LLM 调用成本

Agentic RAG 的六种模式详见 Agentic RAG 模式,WeKnora 当前的 EventManager 架构详见 精读: WeKnora 的 EventManager 章节。

Prompt 工程要点

路径 A 的核心是 LLM prompt 的质量——分解查询的 prompt 如果不好,整个功能就是摆设。以下是设计分解 prompt 时需要考虑的关键点:

Prompt 设计三原则

  1. 明确输出格式:不要让 LLM 自由发挥输出格式。要求它输出 JSON 或结构化文本,解析才不会出错。WeKnora 的其他 LLM 调用(如 Rerank)都有严格的输出格式要求,分解 prompt 也不例外。

  2. 提供判断依据:不要只说”把复杂查询分解为简单查询”,而是给出判断标准——什么样的查询需要分解?什么样的不需要?例如:”如果查询包含’对比’‘区别’‘A 和 B’等多主体关键词,则分解为独立子查询;如果查询只涉及单一主题,则不分解。”

  3. 限制分解深度:明确规定最多分解为 N 个子查询(默认 3),防止 LLM 过度分解导致成本失控。

推荐的分解 Prompt 模板

你是一个查询分解器。你的任务是将复杂的用户查询分解为多个简单子查询。

规则:
1. 如果查询只涉及一个主题,直接返回原始查询
2. 如果查询包含多个可独立回答的子问题,将每个子问题提取为独立子查询
3. 最多分解为 3 个子查询
4. 每个子查询必须是可以独立检索的完整问题
5. 如果无法确定是否需要分解,倾向于不分解

输出格式(严格遵守):
{"decompose": true/false, "sub_queries": ["子查询1", "子查询2"], "strategy": "compare/sequence/parallel"}

示例:
输入:"BM25 和向量检索有什么区别?"
输出:{"decompose": true, "sub_queries": ["BM25 检索的原理和特点", "向量检索的原理和特点"], "strategy": "compare"}

输入:"WeKnora 支持哪些向量数据库?"
输出:{"decompose": false, "sub_queries": [], "strategy": "direct"}

Prompt 迭代策略:先用 10-20 个测试查询验证 prompt 的分解质量,对不合理的分解结果迭代修改 prompt。这个迭代过程本身就是一个有价值的贡献——你可以把测试查询集和 prompt 版本记录下来,作为 Proposal 和答辩的素材。


路径 B:自适应分块增强——LLM 辅助语义分块

定位与类比

WeKnora 的 4 级自适应分块中,第 4 级(语义级)使用相邻句子的向量相似度来判断切分点。这个方法有一个已知的缺陷:当同一个论点用不同风格表达时(比如”这个方法的优点是高效”vs”其时间复杂度仅为 O(n log n)”),向量相似度可能低于阈值导致误切。

类比:你听讲座时,讲师先用通俗语言解释一个概念,然后给出数学公式——两者说的是同一件事,但如果只看”用词相似度”,它们看起来完全不同。一个聪明的听众(LLM)能判断这两段其实讲的是同一个论点,不应该在中间断开。

类比边界:LLM 辅助分块的代价是每个 chunk 的分块时间从毫秒级增加到秒级——对大规模文档库可能不可接受。适合作为”高质量模式”在文档首次摄入时使用,不适合实时场景。

设计文档大纲

# LLM-Assisted Semantic Chunking Enhancement

## 1. 背景与动机
- 当前语义级分块仅依赖向量相似度,对"同义不同词"的段落误切率高
- 实测案例: 技术文档中"方法优势"段与"性能数据"段被误切
- 参考: LumberChunker (2024), Dense X Retrieval

## 2. 设计方案
### 2.1 双阶段分块
- Stage 1: 传统向量相似度分块(快速,低精度)
- Stage 2: LLM 判定边界合理性(慢速,高精度)
- 两者结合: Stage 1 粗切 → Stage 2 精调

### 2.2 LLM Boundary Judge
- 输入: 两个相邻 chunk 的文本
- 输出: 是否应该切分 (0-1 分数) + 理由
- Prompt: 结构化判定 prompt

### 2.3 自适应策略选择
- 文档 < 10K tokens: 可选 LLM 辅助
- 文档 > 10K tokens: 仅向量分块(成本考量)
- 用户可配置: chunking.semantic.llm_assisted = true/false

### 3. 实现
- 在 ChunkerPlugin 中增加 post-processing 步骤
- 复用现有 LLM 配置(rerank 模型即可,无需新模型)

关键代码修改点

1. 新增 internal/chunking/llm_judge.go

type LLMJudge struct {
    client    LLMClient
    threshold float64  // 切分判定阈值, 默认 0.5
}

// JudgeBoundary 判定两个相邻 chunk 是否应该切分
// 返回 0-1 分数, > threshold 表示应该切分
func (j *LLMJudge) JudgeBoundary(ctx context.Context, chunkA, chunkB string) (float64, error) {
    prompt := fmt.Sprintf(
        "判断以下两段文本是否属于同一个论述主题。\n"+
        "段落A: %s\n段落B: %s\n"+
        "如果两段讨论同一主题, 回答 MERGE; 否则回答 SPLIT。\n"+
        "给出0到1的分数, 1表示应该切分, 0表示应该合并。",
        chunkA, chunkB,
    )
    resp, err := j.client.Chat(ctx, prompt)
    if err != nil {
        return j.threshold, nil  // 降级: 默认保持 Stage 1 结果
    }
    return parseScore(resp), nil
}

2. 修改 internal/chunking/adaptive.go

AdaptiveChunker.Chunk() 方法中增加 Stage 2 后处理:

func (c *AdaptiveChunker) Chunk(ctx context.Context, doc Document) ([]Chunk, error) {
    // Stage 1: 原有向量相似度分块
    chunks, err := c.semanticChunk(doc)
    if err != nil {
        return nil, err
    }

    // Stage 2: LLM 辅助精调(可配置关闭)
    if c.config.Semantic.LLMAssisted {
        chunks, err = c.llmRefine(ctx, chunks)
        if err != nil {
            // LLM 失败时降级到 Stage 1 结果
            log.Warn("LLM refinement failed, using Stage 1 chunks", "err", err)
        }
    }

    return chunks, nil
}

3. 新增配置项

internal/config/chunking.go 中增加:

type SemanticConfig struct {
    Threshold    float64 `yaml:"threshold"`     // 现有
    Overlap      int     `yaml:"overlap"`       // 现有
    LLMAssisted  bool    `yaml:"llm_assisted"`  // 新增
    JudgeModel   string  `yaml:"judge_model"`   // 新增: 使用的 LLM 模型
    MaxDocTokens int    `yaml:"max_doc_tokens"` // 新增: 超过此大小不启用 LLM
}

测试策略

测试层级 内容
单元测试 LLMJudge.JudgeBoundary() 的 prompt 构造和解析
单元测试 降级逻辑(LLM 超时/返回非数字)
对比测试 同一文档: 纯向量分块 vs LLM 辅助分块的 chunk 边界质量
评估测试 用 RAGAS 评估两种分块策略下的检索质量差异
性能测试 LLM 辅助分块的额外耗时(预期: 单 chunk +1-3s)

关键测试用例:构造一个”同义不同词”的技术文档段落,验证纯向量分块会误切、LLM 辅助分块不误切。

预期影响与工作量

维度 估算
代码行数 约 500-800 行(Go)
涉及文件 新增 llm_judge.go + 修改 adaptive.go + config/
工作量 2-3 周
对检索质量的影响 语义级分块的 Context Precision 预期提升 10-15%
风险 LLM 调用成本增加;大文档场景不可用

WeKnora 当前分块策略的完整分析见 精读: WeKnora 的”4 级自适应分块策略”章节。


路径 C:评估框架集成——RAGAS EvalPlugin

定位与类比

WeKnora 当前没有任何内建评估工具。维护者不知道一次检索变更到底让质量变好了还是变差了——就像医生给病人开了药但没有任何化验指标,只能凭”感觉好像好了”来判断。

RAGAS EvalPlugin 的目标:给 WeKnora 装上”体检仪器”——每次检索都自动跑四维评估(Faithfulness / Answer Relevancy / Context Precision / Context Recall),让质量变化可量化、可归因。

类比边界:评估本身消耗 LLM 调用(RAGAS 用 LLM 判定相关性),所以不能对每次用户查询都跑全量评估。应该做采样评估或离线评估。

WeKnora 架构约束与设计哲学

在开始设计 EvalPlugin 之前,必须理解 WeKnora 的架构约束——否则你的设计可能被维护者以”不符合项目架构”为由拒绝:

约束一:所有功能必须是 Plugin WeKnora 的核心设计哲学是”EventManager + Plugin”。任何新功能都必须以 Plugin 形式注册到 EventManager,监听已有事件或新增事件。直接在主流程中加代码是不被接受的——你必须证明你的功能可以热插拔。

约束二:降级策略是硬要求 WeKnora 是企业级产品,不能因为一个新功能导致整个系统不可用。所有新功能必须有降级策略——当新功能依赖的外部服务(如 LLM)不可用时,系统必须能退回到原始行为继续运行。

约束三:配置驱动,默认关闭 新功能必须有一个配置开关,且默认关闭。这是为了让现有用户升级时不受影响。只有明确配置了 evaluation.enabled = true 的用户才会启用评估功能。

约束四:异步优先 评估功能不应该阻塞检索流程。用户检索一个结果不应该因为”评估还没跑完”而等待。评估应该异步执行,结果异步存储。

理解这些约束后,EvalPlugin 的设计方向就非常清晰了:它必须是一个 Plugin,监听检索完成事件,异步执行评估,降级时跳过评估,通过配置开关控制。

设计文档大纲

# RAGAS EvalPlugin for WeKnora

## 1. 背景与动机
- WeKnora 无内建评估工具
- 竞品对比: Dify 有标注回复功能, LlamaIndex 有 RAGAS 集成
- 当前社区缺少 WeKnora 端到端 benchmark
- 参考: RAGAS 论文, deep-dive-rag-evaluation-methodology

## 2. 设计方案
### 2.1 EvalPlugin 架构
- 注册为 EventManager Plugin
- 监听 RerankCompleted 事件(检索完成时触发评估)
- 采样策略: 可配置采样率 (如 10% 的查询做评估)

### 2.2 评估指标
- Faithfulness: 答案忠实度
- Answer Relevancy: 答案相关性
- Context Precision: 上下文精确度
- Context Recall: 上下文召回率 (需要 ground truth)

### 2.3 评估结果存储
- 写入 WeKnora 的 wiki/ 巡检系统
- 低分查询触发知识库自维护的"覆盖缺口检测"
- 评估结果可导出为 JSON/CSV 供分析

### 2.4 集成 RAGAS
- 方案一: Go 调用 RAGAS Python 包 (CGo / subprocess)
- 方案二: 纯 Go 实现 RAGAS 四指标 (用 Go HTTP 调用 LLM)
- 推荐: 方案二, 避免 Python 依赖

关键代码修改点

1. 新增 internal/plugin/eval/

type EvalPlugin struct {
    sampler    *Sampler       // 采样器
    evaluator  *RAGASEvaluator
    storage    EvalStorage
    sampleRate float64  // 采样率, 默认 0.1 (10%)
}

func (p *EvalPlugin) SubscribedEvents() []EventType {
    return []EventType{"RerankCompleted"}
}

func (p *EvalPlugin) Handle(event Event) (Event, error) {
    // 采样判断: 不是每次都评估
    if !p.sampler.ShouldSample(p.sampleRate) {
        return event, nil  // 不评估, 原样传递
    }

    // 异步评估, 不阻塞检索流程
    go func() {
        result := p.evaluator.Evaluate(event.Ctx, EvalInput{
            Query:    event.Query,
            Contexts: event.Chunks,
            Answer:   event.Answer,
        })
        p.storage.Save(event.Ctx, result)
    }()

    return event, nil
}

2. 新增 internal/eval/ragas.go

type RAGASEvaluator struct {
    llmClient LLMClient
}

type EvalResult struct {
    Query             string
    Faithfulness      float64  // 0-1
    AnswerRelevancy   float64  // 0-1
    ContextPrecision  float64  // 0-1
    ContextRecall     float64  // 0-1 (需要 ground truth, 无 GT 时为 -1)
    Timestamp         time.Time
}

// Evaluate 端到端评估一次检索结果
// 核心设计: ContextRecall 需要 ground truth, 无 GT 时跳过
func (e *RAGASEvaluator) Evaluate(ctx context.Context, input EvalInput) EvalResult {
    result := EvalResult{Query: input.Query, Timestamp: time.Now()}

    // Faithfulness: 检查答案每个 claim 是否被 context 支持
    result.Faithfulness = e.evaluateFaithfulness(ctx, input)

    // Answer Relevancy: 答案是否回答了问题
    result.AnswerRelevancy = e.evaluateAnswerRelevancy(ctx, input)

    // Context Precision: 检索的 chunks 中相关比例
    result.ContextPrecision = e.evaluateContextPrecision(ctx, input)

    // Context Recall: 需要ground truth, 无GT时标记 -1
    if input.GroundTruth != "" {
        result.ContextRecall = e.evaluateContextRecall(ctx, input)
    } else {
        result.ContextRecall = -1
    }

    return result
}

3. 扩展 Wiki 巡检——评估结果反馈

修改 internal/wiki/inspector.go,新增”低分查询触发覆盖缺口检测”:

// 在巡检流程中增加: 查询评估结果中 Faithfulness < 0.6 的
// 说明知识库在该主题上覆盖不足, 触发覆盖缺口检测
func (i *WikiInspector) DetectCoverageGaps(ctx context.Context) []Gap {
    lowScoreQueries := i.evalStorage.QueryByFaithfulness(ctx, 0.6)
    var gaps []Gap
    for _, q := range lowScoreQueries {
        gaps = append(gaps, Gap{
            Topic:     q.Query,
            Reason:    fmt.Sprintf("Faithfulness=%.2f", q.Faithfulness),
            Suggestion: "补充相关文档或更新过时内容",
        })
    }
    return gaps
}

测试策略

测试层级 内容
单元测试 RAGASEvaluator 四个指标的计算逻辑
单元测试 采样器的采样率控制
集成测试 EvalPlugin 注册到 EventManager 后的完整流程
对比测试 与 Python RAGAS 包的评估结果对比(误差 < 5%)
端到端测试 构造中文 QA 数据集,跑完整评估流水线

关键测试用例:构造一个”幻觉答案”的 case(答案包含 context 中不存在的信息),验证 Faithfulness < 0.5。

预期影响与工作量

维度 估算
代码行数 约 1000-1500 行(Go)
涉及文件 新增 eval/ + plugin/eval/ + 修改 wiki/ + config/
工作量 3-4 周
对项目的影响 填补 WeKnora 评估空白,极高维护者需求
风险 RAGAS 指标的 Go 实现与 Python 版本一致性

RAGAS 四指标详解和消融实验设计见 RAG 评估方法论,WeKnora 复合 Rerank 的评估含义见该文第 4 节。


路径 D:知识图谱增强——LLM 增强 PMI 图谱

定位与类比

WeKnora 的知识图谱使用 PMI(点互信息)构建——纯统计方法,只看实体共现频率。PMI 的优势是不需要训练模型、不需要标注数据;劣势是”共现不等于相关”——两个实体在同一篇文档中出现,可能只是偶然。

类比:PMI 就像通过观察”谁和谁经常一起出现在同一张合照里”来推断人际关系。大多数时候这很有效——经常一起拍照的人大概率是朋友。但有时候两个陌生人恰好参加了同一场活动被拍进同一张照片,PMI 就会误判他们有关系。如果有一个”认识所有人”的社交达人(LLM),他能判断”这两个人虽然出现在同一张照片里,但一个是嘉宾一个是工作人员,不是朋友”——这就是 LLM 增强 PMI 的核心思路。

类比边界:LLM 不是万能的。对于专业领域(如特定行业的术语关系),通用 LLM 可能判断错误。PMI 的统计信号在大量数据下仍然可靠。

深度技术分析:PMI 的数学原理与局限

要理解为什么 LLM 增强 PMI 有价值,需要先理解 PMI 的数学原理。PMI(Pointwise Mutual Information,点互信息)衡量的是两个事件一起发生的概率比它们各自独立发生时高多少:

\[PMI(A, B) = \log_2\left(\frac{P(A,B)}{P(A) \cdot P(B)}\right)\]

直觉解释:如果实体 A 和实体 B 完全独立出现,PMI=0;如果它们总是一起出现,PMI 为正值;如果它们倾向于不同时出现,PMI 为负值。WeKnora 使用 PMI 来决定图谱中两个节点之间是否应该建边——PMI 高于阈值的实体对会被连接。

PMI 的三大局限:

局限一:低频共现的噪声。如果实体 A 和实体 B 各自只出现了一次,而且恰好出现在同一篇文档中,PMI 会给它们一个非常高的值——因为 P(A,B) = P(A) = P(B),PMI 达到最大值。但实际上这只是一次偶然共现。这就好比两个人只被拍进过同一张照片,就说他们关系很紧密——显然不合理。

局限二:高频共现的稀释。有些实体非常常见(比如”服务器”、”数据库”),它们几乎会跟所有实体共现。PMI 虽然通过分母 P(A)*P(B) 做了归一化,但在语料不够大的时候,高频实体的 PMI 仍然偏高。这就像一个社交达人跟谁都会被拍进合照——仅凭共现频率无法判断他跟谁是真正的好友。

局限三:缺乏关系类型。PMI 只告诉你”这两个实体有关系”,但不告诉你”是什么关系”。因果关系(A 导致 B)和并列关系(A 和 B 都是 C 的子类)在 PMI 看来没有区别。然而在知识图谱检索中,关系类型非常重要——用户问”为什么 A 会发生”,你需要沿因果关系链(causes)追踪,而不是沿并列关系链追踪。

LLM 增强 PMI 的核心价值就在于弥补这三个局限:LLM 可以判断共现是否有语义基础(解决局限一和二),LLM 可以标注关系类型(解决局限三)。

双信号融合的调参策略

最终关系权重 = alpha * PMI_norm + (1-alpha) * LLM_score,其中 alpha 的选择决定了 PMI 和 LLM 的相对权重。

alpha 的选择不是固定的,需要根据数据特点调整:

实际操作中,建议用网格搜索(grid search)在验证集上搜索最优 alpha——选取一组已知正确关系的实体对作为验证集,调整 alpha 使验证集上的 F1 最高。

LLM 验证的 Prompt 工程关键点

LLM 验证的效果高度依赖 prompt 设计。以下是一些实践中的关键点:

关键点一:提供充分的共现上下文。不要只给 LLM 两个实体名——给它包含两个实体共现的原文片段。否则 LLM 只能凭常识判断,无法区分”数据库”和”备份”在一篇运维文档中是因果关系(数据库故障导致需要备份恢复)还是只是并列提到。

关键点二:要求结构化输出。让 LLM 输出 JSON 格式(关系类型 + 置信度 + 判断理由),而不是自由文本。结构化输出便于程序解析,也能减少 LLM 输出的不确定性。

关键点三:设置”不确定”选项。不要让 LLM 强制选择”有关系”或”没关系”——给它第三个选项”不确定”。当 LLM 输出”不确定”时,保留 PMI 原始值作为权重,不做 LLM 修正。

关键点四:批量验证降低成本。不要一条一条地调用 LLM——把 10-20 个实体对打包成一个 prompt,一次调用验证多条关系。这样可以把 API 调用成本降低到原来的十分之一。

图谱构建性能优化

LLM 验证是异步的,但不意味着不需要考虑性能。假设一个知识库有 5000 个实体,PMI 候选边可能有 10 万条——即使批量验证,也需要 5000-10000 次 LLM 调用。

优化策略:

  1. 按 PMI 值排序,优先验证高分边——PMI 最高的边最有可能是真实关系,先验证这些可以最快提升图谱质量
  2. 设置 PMI 阈值过滤——PMI 低于某个阈值的边直接丢弃,不需要 LLM 验证
  3. 缓存 LLM 结果——相同的实体对不重复验证
  4. 增量验证——新文档加入时只验证新增的候选边,不重新验证所有边

设计文档大纲

# LLM-Enhanced PMI Knowledge Graph

## 1. 背景与动机
- PMI 图谱的"共现≠相关"问题
- 低频共现对(PMI 噪声大)和高频共现但无语义关系的实体对
- 参考: GraphRAG (Microsoft), LLM-based Relation Extraction

## 2. 设计方案
### 2.1 PMI + LLM 双信号
- PMI 计算候选关系(现有逻辑不变)
- LLM 对 PMI 候选关系做二次验证
- 最终关系权重 = alpha * PMI + (1-alpha) * LLM_score

### 2.2 LLM 验证 Prompt
- 输入: 实体A, 实体B, 共现上下文
- 输出: 关系类型 + 置信度 (0-1)
- 关系类型: causes / enables / contradicts / related_to

### 2.3 增量更新
- 新文档加入时, PMI 增量更新(现有逻辑)
- LLM 验证异步执行, 不阻塞索引流程
- 验证结果更新图谱权重

### 2.4 图谱查询增强
- 支持关系类型过滤查询
- 支持推理链解释(为什么从A跳到B)

关键代码修改点

1. 新增 internal/retrieval/knowledge_graph/llm_verifier.go

type LLMVerifier struct {
    client  LLMClient
    alpha   float64  // PMI 权重, 默认 0.6
}

type Relation struct {
    EntityA   string
    EntityB   string
    Type      string   // causes / enables / contradicts / related_to
    Confidence float64 // 0-1
    PMI       float64
    FinalScore float64 // alpha*PMI_norm + (1-alpha)*LLM_score
}

func (v *LLMVerifier) Verify(ctx context.Context, entityA, entityB string, cooccurrenceContext string) (Relation, error) {
    prompt := fmt.Sprintf(
        "判断以下两个实体之间是否存在有意义的语义关系。\n"+
        "实体A: %s\n实体B: %s\n上下文: %s\n"+
        "如果有关系, 输出关系类型(causes/enables/contradicts/related_to)和置信度(0-1)。\n"+
        "如果只是偶然共现, 输出 NONE 和置信度 0。",
        entityA, entityB, cooccurrenceContext,
    )
    resp, err := v.client.Chat(ctx, prompt)
    if err != nil {
        return Relation{Type: "unknown", Confidence: 0.5}, nil // 降级
    }
    return parseRelation(resp), nil
}

2. 修改 internal/retrieval/knowledge_graph/builder.go

在图谱构建流程中增加 LLM 验证步骤:

func (b *GraphBuilder) BuildWithVerification(ctx context.Context, pmiEdges []PMIEdge) error {
    for _, edge := range pmiEdges {
        // 现有: 直接用 PMI 值作为边权重
        // 新增: LLM 验证 + 双信号融合
        relation, err := b.verifier.Verify(ctx, edge.EntityA, edge.EntityB, edge.Context)
        if err != nil {
            continue // 验证失败, 保留 PMI 边
        }

        // 双信号融合
        pmiNorm := normalizePMI(edge.PMI)  // 归一化到 0-1
        finalScore := b.verifier.alpha*pmiNorm + (1-b.verifier.alpha)*relation.Confidence

        if finalScore > b.config.EdgeThreshold {
            b.graph.AddEdge(edge.EntityA, edge.EntityB, Edge{
                Weight:    finalScore,
                Type:      relation.Type,
                Verified:  true,
            })
        }
    }
    return nil
}

3. 增强图谱查询——支持关系类型过滤

修改 internal/retrieval/knowledge_graph/retriever.go

type GraphQuery struct {
    Entities     []string
    MaxHops      int
    RelationType string  // 新增: 可按关系类型过滤
}

func (r *GraphRetriever) Traverse(ctx context.Context, query GraphQuery) ([]GraphNode, error) {
    // ... 现有 BFS 逻辑 ...
    // 新增: 关系类型过滤
    if query.RelationType != "" {
        neighbors = filterByRelationType(neighbors, query.RelationType)
    }
    return results, nil
}

测试策略

测试层级 内容
单元测试 LLMVerifier.Verify() 的 prompt 和解析逻辑
单元测试 双信号融合公式的正确性
对比测试 PMI-only vs PMI+LLM 图谱的查询质量(同一查询集)
评估测试 知识图谱路的 Context Recall 变化
人工评估 抽样验证 LLM 标注的关系类型是否正确

关键测试用例:构造一个”高频共现但无语义关系”的实体对(如”服务器”和”午饭”在同一篇运维日志中共现),验证 PMI-only 会建立边但 PMI+LLM 不会。

预期影响与工作量

维度 估算
代码行数 约 800-1200 行(Go)
涉及文件 新增 llm_verifier.go + 修改 builder.go + retriever.go + config/
工作量 3-4 周
对检索质量的影响 图谱路 Context Precision 预期提升 15-20%
风险 LLM 验证增加图谱构建时间;专业领域 LLM 判断不准

PMI 图谱构建的完整技术分析见 精读: WeKnora 的”第三路:知识图谱推理检索”章节,Graph-augmented RAG 模式见 Agentic RAG 模式 的”模式 3”。


路径组合策略

五条路径不是孤立选择的——它们之间存在协同关系,组合实现能产生更大的竞赛价值。

推荐组合一:路径 C + 路径 A(评估驱动的 Agentic RAG)

先做路径 C(评估框架),为 WeKnora 建立评估基础设施。然后做路径 A(Agentic RAG),用评估框架量化多跳查询分解的效果提升。这样你的贡献不仅有”做了什么”,还有”证明它有效”——这是评审最看重的。

推荐组合二:路径 C + 路径 D(评估驱动的图谱增强)

先做路径 C,再做路径 D。用评估框架量化 LLM 增强 PMI 图谱对检索质量的提升。路径 D 的改动更局部(仅涉及 knowledge_graph/ 包),风险比路径 A 低。

推荐组合三:路径 E + 路径 B(多模态 + 分块优化)

路径 E 改 docreader(Python),路径 B 改 chunking(Go)。两者在分块逻辑上天然衔接——E 产生更好的 chunk 输入,B 优化 chunk 边界。且技术栈互补,展示你的全栈能力。

不推荐的组合

路径间的技术依赖关系详见 Agentic RAG 模式 的”模式演进光谱”。


路径 E:多模态支持——图像/表格理解增强

定位与类比

WeKnora 的文档解析引擎 docreader/ 目前对 PDF 中的图片和表格理解有限。PaddleOCR-VL 可以提取表格为 HTML,但 HTML 表格直接送入分块管线会导致分块效果差(issue #1725 就报了这个问题)。

类比:你读一篇论文,里面有一个实验结果表格。如果你只把表格的每个数字当作独立文本,RAG 检索到”95.3%”时完全不知道这是哪个实验的哪个指标。一个聪明的读者会把表格理解为一个整体:”这是一组对比实验,方法 A 准确率 95.3%,方法 B 准确率 92.1%”——然后基于这个理解来回答问题。

类比边界:多模态理解依赖 VLM(视觉语言模型),模型质量和推理成本是硬约束。不是所有表格都需要 VLM 理解——结构简单的表格用规则提取即可。

深度技术分析:表格理解的技术难点

表格理解看似简单——把 HTML 转成 Markdown 不就行了?但实际操作中有大量技术细节:

难点一:合并单元格。HTML 表格中的 colspan 和 rowspan 在 Markdown 中没有直接对应。一个跨 3 列的表头在 Markdown 中怎么表示?常见做法是把合并单元格拆成多个相同内容的单元格,但这会丢失”这是一个合并表头”的语义信息。

难点二:嵌套表格。有些文档中的表格内部还嵌套了小表格(比如一个单元格里有子弹列表)。Markdown 完全不支持嵌套表格,必须展平或拆分。

难点三:超大表格的检索问题。一个 50 行的表格如果作为一个 chunk,RAG 检索时可能因为太长而被截断;如果按行拆分,每一行缺少列头信息,检索到的”95.3%”完全不知道指的是哪一列。WeKnora 当前就遇到了这个问题(issue #1725)。

解决超大表格的核心思路是”按行组拆分,每组保留列头”——把 50 行的表格按语义分组(比如每 5-10 行一组),每组前面加上列头行。这样每个 chunk 既有足够的信息量,又不会太长。

但”按语义分组”本身也是个难题——如何判断哪些行属于同一组?简单策略是固定行数分组;更好的策略是根据表格内容的语义断点分组(比如当某一列的值发生跳变时分组)。

图片理解的技术难点

图片理解比表格理解更复杂,因为图片的多样性远高于表格:

类型一:流程图。流程图的关键信息是步骤的顺序和分支条件。VLM 需要理解箭头的方向、判断框的条件、流程的起止点。

类型二:架构图。架构图的关键信息是组件之间的关系(调用、依赖、数据流)。VLM 需要识别不同类型的框(服务、数据库、队列)和它们之间的连线。

类型三:截图。截图通常是软件界面的抓取。关键信息是界面上的文字和布局。VLM 需要 OCR 能力来提取文字,但 OCR 已经在 docreader 中实现了。

类型四:照片。照片中可能包含实物场景,这类图片在技术文档中较少见,但在产品文档中可能出现。VLM 需要描述照片中的关键对象和场景。

不同类型的图片需要不同的 VLM prompt——流程图要问”这个流程的步骤顺序是什么”,架构图要问”这个架构有哪些组件、它们怎么连接”。一个通用的”请描述这张图片”prompt 效果不好。

实现时可以先用一个分类器判断图片类型,再根据类型选择专门的 prompt。分类器可以是简单的规则(比如检测图片中是否有箭头形状),也可以用 VLM 做分类。

多模态 Chunk 的检索策略

三种 chunk(text / table / image)统一参与 RRF 融合,但它们的检索特性很不同:

这意味着 image chunk 的检索质量上限取决于 VLM 的描述质量。如果 VLM 把一个流程图描述为”一个包含多个方框和箭头的图片”(太笼统),那任何查询都检索不到这个图片。如果 VLM 描述为”用户注册流程:填写表单 → 邮箱验证 → 创建账户”,那”用户注册”和”邮箱验证”都能检索到。

因此,VLM 的 prompt 设计是路径 E 中最关键的技术决策——它直接决定了图片信息的检索可达性。

设计文档大纲

# Multi-modal Understanding Enhancement for WeKnora

## 1. 背景与动机
- Issue #1725: PaddleOCR-VL 表格 HTML 影响分块效果
- 图片中的信息(流程图、架构图、截图)完全丢失
- 参考: GPT-4V / Qwen-VL / LLaVA 多模态理解

## 2. 设计方案
### 2.1 表格理解
- PaddleOCR-VL 提取表格 HTML → 转换为 Markdown 表格
- 大表格(>10行)按行组拆分, 每组保留列头
- 表格摘要: 用 LLM 生成表格的一句话摘要, 作为 chunk 的上下文

### 2.2 图片理解
- 检测文档中的图片(已有 PaddleOCR 支持)
- 用 VLM 生成图片描述
- 图片描述作为独立 chunk 参与检索
- 保留图片→原文的映射关系

### 2.3 多模态 chunk 结构
- 新增 chunk_type 字段: text / table / image
- table chunk: markdown 表格 + 摘要
- image chunk: VLM 描述 + 原图引用
- 检索时三种 chunk 统一参与 RRF

### 2.4 VLM 集成
- 支持多种 VLM 后端: Qwen-VL / GPT-4V / 本地模型
- 统一接口: VLMClient.Describe(image) → string
- 降级: VLM 不可用时跳过图片理解

关键代码修改点

1. 修改 docreader/ 的表格处理

docreader/processors/table_processor.py 中增加 HTML→Markdown 转换和摘要生成:

class TableProcessor:
    def __init__(self, llm_client=None):
        self.llm_client = llm_client

    def process(self, table_html: str, context: str = "") -> TableChunk:
        # Step 1: HTML → Markdown
        md_table = self.html_to_markdown(table_html)

        # Step 2: 大表格拆分
        if self.row_count(md_table) > 10:
            sub_tables = self.split_by_row_groups(md_table)
        else:
            sub_tables = [md_table]

        # Step 3: LLM 生成摘要(可选)
        summary = ""
        if self.llm_client:
            summary = self.generate_summary(md_table, context)

        return TableChunk(
            markdown=md_table,
            summary=summary,
            sub_tables=sub_tables,
        )

    def generate_summary(self, table_md: str, context: str) -> str:
        prompt = (
            f"用一句话总结以下表格的关键信息。保留具体数字。\n"
            f"上下文: {context}\n表格:\n{table_md}"
        )
        return self.llm_client.chat(prompt)

2. 新增 docreader/processors/image_processor.py

class ImageProcessor:
    def __init__(self, vlm_client=None):
        self.vlm_client = vlm_client

    def process(self, image_bytes: bytes, context: str = "") -> ImageChunk:
        description = ""
        if self.vlm_client:
            description = self.vlm_client.describe(image_bytes, context)
        else:
            description = "[图片](VLM 未启用,无法生成描述)"

        return ImageChunk(
            description=description,
            has_vlm=self.vlm_client is not None,
        )

3. 修改 internal/chunking/ 支持多模态 chunk 类型

在 Go 后端的 chunk 结构中增加类型字段:

type ChunkType string

const (
    ChunkTypeText   ChunkType = "text"
    ChunkTypeTable  ChunkType = "table"
    ChunkTypeImage  ChunkType = "image"
)

type Chunk struct {
    ID       string
    Content  string
    Type     ChunkType  // 新增
    Metadata map[string]interface{}
    // table 特有
    TableSummary string
    // image 特有
    ImageRef     string  // 原图引用
}

测试策略

测试层级 内容
单元测试 HTML→Markdown 转换正确性
单元测试 大表格拆分逻辑
集成测试 图片→VLM 描述→chunk→检索完整流程
对比测试 有/无 VLM 描述的图片检索召回率
评估测试 表格类问题的 Answer Relevancy 提升

关键测试用例:构造一个包含流程图的 PDF,验证 VLM 生成的描述能被检索到并正确回答”这个流程的第一步是什么”。

预期影响与工作量

维度 估算
代码行数 约 600-1000 行(Python + Go)
涉及文件 docreader/processors/ + internal/chunking/ + config/
工作量 2-3 周
对检索质量的影响 含图表文档的检索质量显著提升(量化需 benchmark)
风险 VLM 调用成本高;不同 VLM 质量差异大

Issue #1725 是此路径的直接触发点,多模态 RAG 的行业趋势见 RAG 知识库全景


6. 从零到 PR 的完整流程

这一节覆盖从 fork 仓库到 PR 被合入的全流程。细节决定成败——很多初学者不是技术不行,而是流程不对导致 PR 被忽略。

6.1 Fork 与本地部署

# 1. Fork 仓库(GitHub 页面点 Fork 按钮)
# 2. Clone 你 fork 的仓库
git clone https://github.com/YOUR_USERNAME/WeKnora.git
cd WeKnora

# 3. 添加上游仓库(保持同步)
git remote add upstream https://github.com/Tencent/WeKnora.git

# 4. Docker 全栈部署(首次可能需要 20-40 分钟拉镜像)
cd docker
cp .env.example .env
# 编辑 .env: 填入 LLM API Key, 选择向量数据库后端
docker compose up -d

# 5. 验证部署
docker compose ps           # 所有服务应该 running
curl http://localhost:8080/api/v1/health  # 应返回 200
# 打开 http://localhost:3000 看前端页面

常见部署问题和解决方案:

问题 原因 解决
Milvus 启动失败 内存不足(至少需 4G) 换 pgvector 后端,资源需求低
docreader 报错 Python 依赖缺失 pip install -r docreader/requirements.txt
LLM API 超时 网络问题 检查 API Key 和网络代理配置
前端白屏 Node 版本不对 nvm use 18 切到正确版本

6.2 创建 Feature Branch

# 始终从最新的 main 创建分支
git fetch upstream
git checkout main
git merge upstream/main

# 创建 feature 分支(命名规范很重要!)
# fix: 修 bug
# feat: 新功能
# docs: 文档
# refactor: 重构
git checkout -b feat/multi-hop-query-decomposition

# 或者修 bug
git checkout -b fix/shared-space-role-search-undefined

分支命名规范:观察仓库已有 PR 的命名风格。WeKnora 目前观察到的前缀有 fix: / feat: / docs:。不要用 feature/bugfix/ 这种冗长形式——跟着项目现有风格走。

6.3 开发与本地验证

# Go 后端开发
cd internal/retrieval/decomposer  # 假设你在做路径 A
go mod tidy                       # 拉依赖
go test ./...                     # 跑已有测试, 确保没有破坏
go test -v -run TestDecompose     # 跑你的新测试

# 前端开发
cd frontend
npm install
npm run dev                       # 开发服务器
npm run test                      # 前端测试
npm run lint                      # 代码风格检查(必须过!)

# Python 开发(docreader)
cd docreader
pip install -e .
pytest tests/

6.4 提交代码

# 每个提交做一件事
git add internal/retrieval/decomposer/decomposer.go
git commit -m "feat: add query decomposer for multi-hop retrieval

- Add Decomposer struct with LLM-based query decomposition
- Support fallback to single query when decomposition fails
- Add configuration for max_hops and enabled flag

Refs: #issue-number"

# 推送到你的 fork
git push origin feat/multi-hop-query-decomposition

提交信息规范:

规则 正确 错误
前缀 feat: add... add...
时态 add(祈使句) added / adds
范围 feat(retrieval): add decomposer feat: 修改了很多东西
长度 标题 < 50 字符 标题写 3 行
关联 Refs: #1633 不提 issue

6.5 创建 Pull Request

PR 描述模板(WeKnora 没有 PR 模板,但观察已有 PR 的写法):

## 变更说明

简要描述做了什么、为什么做、怎么做。

## 关联 Issue

Fixes #1633

## 变更类型

- [ ] Bug 修复
- [x] 新功能
- [ ] 重构
- [ ] 文档

## 测试

- [x] 单元测试通过
- [x] 手动验证
- [ ] 截图/GIF 演示(UI 变更必须附)

## 自查清单

- [x] 代码风格符合项目规范(gofmt / eslint)
- [x] 不包含敏感信息(API Key / 密码)
- [x] 不包含无关变更(一个 PR 做一件事)

6.6 Code Review 应对

情况 做法
维护者要求修改 尽快修改并 push,不要关闭 PR 重开
有冲突 git fetch upstream && git rebase upstream/main
长时间无响应 在 PR 下友善地 ping(间隔 > 3 天)
被拒绝 问清楚原因,不要灰心,改了再提
建议”换个方式实现” 先在 PR 里讨论方案,达成一致后再改

6.7 Code Review 中的常见反馈模式

理解维护者通常给什么反馈,可以提前规避,减少 review 轮次:

反馈一:”请增加降级策略” 所有涉及 LLM 调用的功能,维护者都会问”LLM 不可用时怎么办”。提前在代码中写好降级逻辑(如 fallback 到原始行为),并在 PR 描述中说明。

反馈二:”请补充测试” WeKnora 的测试覆盖率可能不完善,但新代码必须有测试。提交 PR 前自查:至少有正常路径测试、边界条件测试、错误路径测试各一个。

反馈三:”请遵循现有命名规范” Go 社区有明确的命名规范:缩写全大写(HTTP 而非 Http)、接口名以 er 结尾(RetrieverEvaluator)、布尔变量以 is/has/can 开头。提交前检查命名是否符合规范。

反馈四:”请拆分为更小的 PR” 如果一个 PR 修改了超过 5 个文件,维护者可能要求拆分。每个 PR 做一件事——一个 bug 修复、一个功能添加、一次重构。

反馈五:”这个功能需要配置开关” 新功能默认不应该改变现有行为。维护者通常要求新功能有一个配置开关,默认关闭。这样即使新功能有 bug,也不会影响现有用户。

6.8 PR 合入后的跟踪

PR 合入不是终点——你需要在合入后持续关注:

  1. 监控是否引入新 bug——合入后几天内关注 issue 列表,看是否有人报告与你相关的问题
  2. 跟进后续优化——维护者可能在你 PR 的基础上做进一步优化,了解他们的改动思路
  3. 在社区中建立声誉——合入的 PR 是你最好的名片,后续提新方案时维护者更容易信任你

往年参与者的具体 PR 经验见 往年经验帖


7. Go 语言新手注意事项

WeKnora 后端约 800+ 个 Go 文件,对 Go 新手来说理解成本不低。这里列出 WeKnora 代码库中常见的 Go 模式——不是 Go 语法教程,而是”读 WeKnora 源码时你会反复遇到什么”。

7.0 读 Go 源码的方法论

在讲具体模式之前,先说方法论。Go 代码的阅读策略和其他语言不同——Go 没有 class 继承,没有泛型(1.18 之前),代码组织主要靠 package 和 interface。

第一步:从 main.go 开始cmd/ 下的 main.go 是程序入口,它会告诉你系统启动时做了哪些初始化——哪些 Plugin 被注册、哪些配置被加载、哪些服务被启动。

第二步:找 interface 定义。Go 的 interface 通常定义在包的根文件中(如 retrieval.go),具体实现放在子目录中(如 bm25/vector/knowledge_graph/)。先读 interface 理解抽象,再读实现理解细节。

第三步:追踪数据流。从 QueryReceived 事件开始,追踪一个查询如何经过各个 Plugin,最终返回结果。在代码中搜索 Emit("QueryReceived") 找到事件触发点,搜索 SubscribedEvents() 找到所有订阅者。

第四步:读测试。Go 的测试文件(*_test.go)是理解代码行为的最好文档。好的测试用例覆盖了典型输入、边界条件、错误处理。WeKnora 的测试可能不完善,但已有的测试仍然很有参考价值。

推荐阅读顺序(按难度递增):

预计阅读时间 核心要理解的东西
cmd/main.go 30 分钟 系统启动流程
internal/config/ 1 小时 配置结构和管理方式
internal/retrieval/bm25/ 2 小时 最直观的检索实现
internal/retrieval/vector/ 2 小时 向量检索的 Go 实现
internal/retrieval/fusion/ 2 小时 RRF 融合逻辑
internal/eventmanager/ 3 小时 事件总线的核心设计
internal/chunking/ 3 小时 4 级自适应分块
internal/rerank/ 2 小时 复合 Rerank
internal/retrieval/knowledge_graph/ 4 小时 PMI 图谱构建和查询
internal/wiki/ 3 小时 自维护 Wiki 巡检

7.1 错误处理:if err != nil 是你的日常

Go 没有 try-catch。错误处理是每个函数调用后的 if err != nil。WeKnora 代码库中这种模式无处不在:

result, err := someFunction()
if err != nil {
    return fmt.Errorf("failed to do X: %w", err)  // 用 %w 包装, 保留错误链
}

新手常见错误:忽略错误返回值。在 WeKnora 中,永远不要_ 忽略 error——即使你觉得不会出错。维护者会在 review 时要求你处理。

7.2 接口隐式实现

WeKnora 的 Plugin 系统基于 Go 的接口隐式实现——不需要 implements 关键字,只要你的 struct 实现了接口的所有方法,它就自动满足接口。

// 接口定义(在 eventmanager 包中)
type Plugin interface {
    Name() string
    SubscribedEvents() []EventType
    Handle(event Event) (Event, error)
    Priority() int
}

// 你的实现(在另一个包中)
type MyPlugin struct{}

func (p *MyPlugin) Name() string { return "my-plugin" }
func (p *MyPlugin) SubscribedEvents() []EventType { return []EventType{"QueryReceived"} }
func (p *MyPlugin) Handle(event Event) (Event, error) { /* ... */ }
func (p *MyPlugin) Priority() int { return 0 }

// MyPlugin 自动满足 Plugin 接口, 无需声明
var _ Plugin = (*MyPlugin)(nil)  // 编译期接口检查(推荐写法)

7.3 goroutine 和 channel

WeKnora 大量使用 goroutine 做并发。三路检索并行就是用 goroutine 实现的。

// 并行执行三路检索
func (r *Retriever) Retrieve(ctx context.Context, query string) ([]Result, error) {
    var wg sync.WaitGroup
    results := make(chan []Result, 3)  // 带缓冲 channel

    // BM25
    wg.Add(1)
    go func() {
        defer wg.Done()
        bm25Result := r.bm25.Search(query)
        results <- bm25Result
    }()

    // Vector
    wg.Add(1)
    go func() {
        defer wg.Done()
        vectorResult := r.vector.Search(query)
        results <- vectorResult
    }()

    // Graph
    wg.Add(1)
    go func() {
        defer wg.Done()
        graphResult := r.graph.Traverse(query)
        results <- graphResult
    }()

    wg.Wait()
    close(results)

    // 收集结果
    var allResults []Result
    for r := range results {
        allResults = append(allResults, r...)
    }
    return allResults, nil
}

新手坑:

错误 正确
忘记 wg.Wait() 导致 goroutine 泄漏 defer wg.Done() 配合 wg.Wait()
不用 channel 直接写共享变量 用 channel 通信,或用 sync.Mutex 保护
goroutine 里 panic 导致整个程序崩溃 recover() 捕获 panic

7.4 context.Context 贯穿始终

Go 的 context.Context 是请求级别的生命周期管理。WeKnora 中几乎所有函数的第一个参数都是 ctx context.Context

// 用 ctx 控制超时和取消
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

result, err := retriever.Search(ctx, query)
if err == context.DeadlineExceeded {
    // 超时处理
}

新手容易忽略 ctx 的传递——在调用链中间”断掉” ctx 会导致超时机制失效。WeKnora 维护者会严格检查 ctx 是否正确传递。

7.5 结构体标签(struct tag)

WeKnora 大量使用 struct tag 做配置映射和 JSON 序列化:

type Config struct {
    Enabled   bool    `yaml:"enabled" json:"enabled"`
    MaxHops   int     `yaml:"max_hops" json:"maxHops"`
    Threshold float64 `yaml:"threshold" json:"threshold"`
}

新增配置项时必须同时写 yamljson tag——前者用于配置文件,后者用于 API 响应。忘写 tag 会导致配置不生效,而且不会报错(Go 的坑)。

7.6 依赖管理:Go Modules

go mod tidy      # 清理未使用的依赖
go mod vendor    # 生成 vendor 目录(部分项目需要)
go sumdb         # 校验依赖完整性

WeKnora 使用 Go Modules。新增外部依赖时:先考虑是否真的需要——WeKnora 维护者对引入新依赖非常谨慎,会要求你说明为什么不能用标准库或项目内已有实现。

7.7 代码风格与 Lint

# 必须通过
gofmt -w .                    # 格式化
go vet ./...                  # 静态分析
golangci-lint run ./...       # 综合lint(WeKnora可能已配置)

# 推荐通过
go test -race ./...           # 竞态检测
go test -cover ./...          # 覆盖率

PR 提交前必须过 gofmtgo vetgolangci-lint 如果项目有配置文件则必须全部通过。

7.8 Go 测试模式

WeKnora 使用 Go 标准库的 testing 包做测试。理解以下测试模式对贡献至关重要:

Table-driven 测试——Go 社区最推荐的测试模式,WeKnora 代码库中大量使用:

func TestDecomposer(t *testing.T) {
    tests := []struct {
        name     string
        query    string
        wantN    int
        wantErr  bool
    }{
        {"简单查询", "WeKnora支持哪些向量数据库", 1, false},
        {"对比查询", "BM25和向量检索的区别", 2, false},
        {"空查询", "", 1, false},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := decomposer.Decompose(ctx, tt.query)
            if (err != nil) != tt.wantErr {
                t.Errorf("Decompose() error = %v, wantErr %v", err, tt.wantErr)
            }
            if len(got) != tt.wantN {
                t.Errorf("Decompose() got %d queries, want %d", len(got), tt.wantN)
            }
        })
    }
}

Mock 接口——Go 的 interface 天然支持 mock。在测试中用 mock 替换真实依赖:

// MockLLMClient 替换真实 LLM 调用
type MockLLMClient struct {
    Response string
    Err      error
}

func (m *MockLLMClient) Chat(ctx context.Context, prompt string) (string, error) {
    return m.Response, m.Err
}

// 在测试中使用 mock
func TestDecomposer_Fallback(t *testing.T) {
    mockClient := &MockLLMClient{Err: errors.New("timeout")}
    d := &Decomposer{llmClient: mockClient, enabled: true}
    
    got, err := d.Decompose(ctx, "test query")
    if err != nil {
        t.Errorf("expected fallback, got error: %v", err)
    }
    if len(got) != 1 || got[0].Query != "test query" {
        t.Errorf("expected fallback to original query, got %v", got)
    }
}

子测试和并行测试——Go 支持 t.Parallel() 标记可并行执行的测试,加速测试套件执行:

func TestRetrieval(t *testing.T) {
    tests := []struct{ name string; query string }{
        {"simple", "test"},
        {"complex", "compare A and B"},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()  // 标记可并行
            // 测试逻辑...
        })
    }
}

WeKnora 的测试覆盖率目前可能不完善——补测试本身就是高价值贡献。维护者非常欢迎”为现有代码补测试”的 PR。

7.9 WeKnora 代码库中的常见坑

在阅读和修改 WeKnora 源码时,以下是一些容易踩的坑:

坑一:internal/ 包的循环依赖 Go 不允许循环导入——包 A 导入包 B,包 B 就不能再导入包 A。WeKnora 的 internal/ 下有很多包,新增代码时很容易引入循环依赖。解决方法:把共享的类型定义提取到一个独立的 internal/types/ 包中,让其他包都依赖这个类型包。

坑二:goroutine 中的闭包捕获 在 for 循环中启动 goroutine 时,如果闭包捕获了循环变量,所有 goroutine 可能会使用同一个变量值。必须通过参数传递来解决:

// 错误:所有 goroutine 都用同一个 query
for _, query := range queries {
    go func() {
        result := search(query)  // bug: query 可能总是最后一个值
    }()
}

// 正确:通过参数传递
for _, query := range queries {
    go func(q string) {
        result := search(q)
    }(query)
}

坑三:配置热更新 WeKnora 支持部分配置的热更新(不重启服务即可生效)。如果你新增了配置项,需要确认它是否需要支持热更新。如果需要,要在配置变更的监听逻辑中注册你的配置回调。

坑四:中文分词对 BM25 的影响 修改 BM25 相关代码时,要注意中文分词的特殊性。英文按空格分词很简单,但中文的”知识图谱”到底是一个词还是两个词,取决于分词器的词典。WeKnora 使用了自定义词典,修改 BM25 代码时不要破坏分词器的正常工作。

坑五:向量数据库的兼容性 WeKnora 支持 10 种向量数据库后端。如果你修改了向量检索相关代码,必须确保在所有 10 种后端上都能正常工作——至少在 pgvector 和 Milvus 两个最常用的后端上测试过。


8. 与 mentor 沟通策略

在犀牛鸟竞赛中,与项目导师的沟通质量直接影响你的 Proposal 是否被采纳、是否能入围。但沟通不是”越频繁越好”,而是”越精准越好”。

8.1 沟通节奏

类比:你去医院看病。最好的病人是:清楚描述症状、看过相关资料、准备好回答医生的问题。最差的病人是:什么准备都没有、问”我哪里不舒服你来猜”。

与 mentor 沟通也一样:

阶段 时机 沟通方式 内容
探索期 报名后 1 周 Issue 评论 “我对这个 issue 感兴趣,初步看了一下代码,原因可能是 X,方向对吗?”
准备期 第一周 PR 后 Issue 评论 / 邮件 “我已经提交了 #xxx 修复这个 bug,另外我观察到 Y 这个问题,是否有计划解决?”
Proposal 期 7 月中 邮件 / Issue 完整技术方案,包含设计文档 + 时间线 + 已有贡献证明
实战期 8 月 PR Comment / 邮件 技术问题 + 进展同步(频率 < 每周 2 次)

8.2 提方案的正确姿势

错误示范

“我觉得 WeKnora 应该支持多跳查询分解,因为这样检索质量会更好。”

这个”方案”没有任何信息量——”会更好”好多少?怎么实现?多久能做出来?维护者看到这种模糊提议,第一反应是”又一个空想家”。

正确示范

“我在使用 WeKnora 时发现,复杂查询(如对比类问题)的 Context Recall 只有约 0.45,远低于简单查询的 0.78。原因在于当前单轮检索无法覆盖对比查询的两个方面。

我提议新增查询分解层(QueryDecomposer),具体方案如下:

  1. 新增 internal/retrieval/decomposer/ 包,用 LLM 将复杂查询分解为子查询
  2. 每个子查询独立走三路检索+RRF+Rerank
  3. 结果合并时做去重和可选二次 Rerank
  4. 降级策略:分解失败时 fallback 到原始单轮检索

预估工作量 3-4 周。我已经 fork 了仓库并跑通了本地部署,提了 #1633 的修复 PR。

完整设计文档:[链接]

请问这个方向是否与项目路线图一致?有什么需要调整的吗?”

区别在于:

维度 错误示范 正确示范
问题定义 “会更好” “Context Recall 从 0.45 到目标值”
方案细节 文件级修改点 + 降级策略
可行性证明 已有 PR + 本地部署
工作量估算 3-4 周
请求方式 模糊建议 具体可评审的方案

8.3 常见沟通陷阱

陷阱 表现 替代做法
“小作文” 一封邮件写 2000 字 分段写,每段一个主题,先结论再展开
“伸手党” “这个怎么装?那个怎么跑?” 先查文档和 Issue,提问时附上你试过什么
“幽灵” 提完 PR 后消失 2 周 每周至少同步一次进展
“过度沟通” 每天发 3 条消息问进度 汇总问题一次问,频率 < 每周 2 次
“防御心态” review 意见 = 攻击 review 意见 = 免费的技术指导,认真学习

8.4 何时该问,何时不该问

该问

不该问

8.5 Proposal 写作实战

Proposal 是犀牛鸟竞赛中最重要的文档——它直接决定你是否能入围。一份好的 Proposal 需要包含三个核心部分:

第一部分:技术方案(约 60% 篇幅)

技术方案不是”我想做什么”,而是”我怎么做、为什么这样做、还有什么替代方案”。

标准结构:

  1. 问题定义——用数据说话。不是”检索质量不够好”,而是”在 XX 数据集上,复杂查询的 Context Recall 为 0.45,简单查询为 0.78”
  2. 方案设计——文件级修改点 + 关键数据结构 + 降级策略
  3. 替代方案对比——至少考虑 2 个替代方案,说明为什么选当前方案
  4. 风险与缓解——列出最大的 3 个风险及应对措施

第二部分:时间规划(约 20% 篇幅)

以周为单位,每周末有可验证的交付物。留 1 周 buffer。不要把计划排得太满——竞赛期间你一定会遇到意想不到的问题。

示例:

第三部分:开源经历(约 20% 篇幅)

列出你已经对 WeKnora 做的贡献——合入的 PR、参与的讨论、写的设计文档。这是证明你”已经准备好了”的最有力证据。没有这部分,Proposal 就是纸上谈兵。

8.6 与维护者沟通的特殊技巧

WeKnora 是腾讯 IMA 团队维护的开源项目,维护者可能有以下特点:

具体技巧

  1. PR 比 issue 更有说服力——不要只在 issue 里讨论,直接提一个 WIP(Work In Progress)PR,让维护者看到代码
  2. 先小后大——先做一个小 PR 建立信任,再提大方案
  3. 参考已有 PR——看看维护者对其他 PR 的 review 意见,了解他们的代码偏好
  4. 中文社区互动——WeKnora 有微信群/企业微信群,在里面积极回答其他用户的问题也能积累信任

8.7 沟通中的文化差异与应对

开源项目的维护者可能来自不同文化背景,沟通风格差异很大:

风格一:直截了当型(常见于欧美维护者)。他们说”这个方案不好”就是真的觉得不好,不是在试探你。应对方式:不要觉得被冒犯,直接问”您觉得哪里有问题?有什么替代建议吗?”

风格二:委婉建议型(常见于亚洲维护者)。他们说”这个方案可以考虑一下其他方向”,实际上是在说”这个方案我不看好”。应对方式:主动追问,确认他们的真实意思,不要以为只是随意建议而忽略。

风格三:技术沉默型。有些维护者很少说话,但每次说话都很有分量。他们不回复不代表没看到——可能只是在思考。应对方式:给足够的时间(5-7 天),然后友善地 ping 一次。

无论哪种风格,有一条通用原则:永远不要在公开场合(GitHub issue/PR)与维护者争论。如果你不同意某个 review 意见,私下沟通(邮件/微信),或者在 PR 中用”请教”的语气表达你的想法——”我理解您的建议是 X,但我在想 Y 这个方向是否也可以考虑,因为 Z”。

8.8 从沟通中提取技术洞察

与维护者的每一次交流都是学习机会。维护者对你的方案提的每一个问题,都暗示了他们关心的技术维度。把这些维度记录下来,它们就是你写 Proposal 时必须覆盖的点。

例如,如果维护者问你”LLM 不可用时怎么办”,说明他们非常关心系统的健壮性——你的 Proposal 中必须有专门的”降级策略”章节。如果维护者问”这个功能会影响现有性能吗”,说明他们关心性能——你的 Proposal 中必须有性能评估。

维护者问的每一个问题,都是 Proposal 大纲的一个条目。这不是巧合——维护者的问题反映了项目的核心价值观。


9. 时间线规划——3 个月竞赛节奏

结合 选题策略 的时间线和 WeKnora 的贡献特性,以下是 3 个月的规划。

9.1 全局时间线

gantt
    title 犀牛鸟竞赛 - WeKnora 贡献时间线
    dateFormat  YYYY-MM-DD
    section Phase 1: 探索
    Fork + 部署              :p1a, 2026-06-23, 2d
    读源码核心包              :p1b, after p1a, 5d
    提第一个前端 PR          :p1c, after p1a, 3d
    section Phase 2: 深入
    选定深度路径              :p2a, 2026-07-01, 2d
    写设计文档                :p2b, after p2a, 5d
    实现核心逻辑              :p2c, after p2b, 10d
    section Phase 3: Proposal
    写 Proposal               :p3a, 2026-07-15, 5d
    联系导师                  :p3b, after p3a, 3d
    section Phase 4: 实战
    完整实现 + 测试           :p4a, 2026-08-01, 15d
    修 review 意见            :p4b, after p4a, 7d
    写评估报告                :p4c, after p4b, 5d

9.2 逐周计划

Phase 1:探索与破冰(6/23 - 6/30)

任务 产出
Day 1 Fork + Clone + Docker 部署 本地跑通全栈
Day 2 internal/eventmanager/ + internal/retrieval/ 笔记:理解事件总线架构
Day 3 frontend/ 结构,定位 #1633 对应组件 本地复现 bug
Day 4 修复 #1633,提 PR 第一个 PR
Day 5 internal/chunking/ + internal/retrieval/knowledge_graph/ 笔记:分块和图谱逻辑
Day 6-7 选定深度路径(A-E 之一),写初步设计文档 设计文档初稿

Phase 2:深入贡献(7/1 - 7/15)

任务 产出
W2 实现深度路径的核心逻辑 + 单元测试 核心代码 + 测试
W3 集成测试 + 端到端测试 + 修 bug 可运行的功能分支
W3 准备 Proposal 材料 Proposal 初稿

关键检查点:W2 结束时,核心逻辑应该能通过编译和单元测试。如果还做不到,说明方案太复杂或理解不够——需要调整范围。

Phase 3:Proposal 提交(7/15 - 7/31)

任务 产出
W4 完善 Proposal + 联系导师 提交 Proposal
W5 继续 PR 贡献 + 社区活跃 2-3 个 PR 合入

Proposal 三要素(参考 往年经验帖):

  1. 技术方案:设计文档 + 代码修改范围 + 测试策略
  2. 时间规划:以周为单位的 8 周计划(留 1 周 buffer)
  3. 开源经历:已有的 PR 合入记录 + 设计文档

Phase 4:课题实战(8/1 - 9/10)

任务 产出
W6-W7 完整实现 + 性能优化 Feature branch 完成
W8 修 code review 意见 + 文档 PR 合入
W9 评估报告 + 答辩准备 最终报告

9.3 风险预案

风险 概率 预案
Docker 部署失败 先用 dev 模式跑单个服务,不全栈部署
第一个 PR 被拒 同时准备 2-3 个备选 issue
深度路径实现遇到技术困难 降级到简单版本,先交核心功能再迭代
维护者响应慢 同时做 2 个方向,哪个先得到响应就推哪个
Proposal 未入围 继续贡献,争取优秀贡献者证书

9.4 每阶段里程碑与验收标准

每个阶段结束时,你应该能回答”我做了什么”——这不仅是自我检查,也是 Proposal 和答辩时的核心素材。

Phase 1 验收标准(6/30 前):

Phase 2 验收标准(7/15 前):

Phase 3 验收标准(7/31 前):

Phase 4 验收标准(9/10 前):

9.5 时间管理的常见陷阱

陷阱一:”先学完再动手” 这是最常见的错误。Go 不需要”学完”才能开始——边做边学,遇到不懂的语法再查。你不需要理解 Go 的所有特性才能修一个前端 bug。

陷阱二:”一个方向钻到底” 如果你选的深度路径在第二周发现行不通(比如 LLM prompt 质量不稳定),不要继续死磕。及时切换到路径 C 或 B,风险更低。

陷阱三:”完美主义” PR 不需要完美才能提。先提一个 WIP PR,让维护者看到方向是否正确,再逐步完善。等”完美”了再提,可能已经错过了竞赛窗口。

陷阱四:”只做代码不做文档” 竞赛评估不只看代码——还看你的理解深度和表达能力。每完成一个功能,花 1-2 小时写技术文档。这 1-2 小时的投入在答辩时回报巨大。


10. 检索管线”贡献地图”

想改哪一段,就去读哪个目录:

flowchart LR
    A[想改 RRF 融合] --> B["internal/retrieval/fusion/"]
    C[想改 Rerank] --> D["internal/rerank/"]
    E[想改 Wiki 自维护] --> F["internal/wiki/"]
    G[想改分块策略] --> H["internal/chunking/"]
    I[想改知识图谱] --> J["internal/retrieval/knowledge_graph/"]
    K[想改前端 UI] --> L["frontend/"]
    M[想改文档解析] --> N["docreader/"]
    O[想加 MCP 能力] --> P["mcp-server/"]

11. 与 DB-Agent-Memory 的关系

WeKnora 是 RAG 引擎(”Agent 查什么”),DB-Agent-Memory 是记忆引擎(”Agent 记什么”)。两者同属腾讯犀牛鸟开源项目,但定位完全不同:

两者重叠区域在于”知识图谱”——WeKnora 从文档构建图谱,DB-Agent-Memory 在 L1 层提炼原子事实。潜在冲突:Agent 从对话学到的事实 vs 文档记录不一致时谁优先?

完整决策树见 B 线 - Memory 与 RAG 边界 腾讯生态分工见 B 线 - 腾讯记忆生态


12. 双赛道策略建议(WeKnora vs DB-Agent-Memory)

维度 WeKnora DB-Agent-Memory
语言 Go(高门槛)+ React+TS(中低门槛) TypeScript(中门槛)
新手引导 无 CONTRIBUTING / 无 good-first-issue 待确认
竞争 14.3K star,竞争大 相对较新,竞争可能小
推荐切入 前端 bug/UI,逐步深入 核心 TypeScript 模块
策略 快速贡献,积累信任,争取深度 issue 直接参与核心设计

结论:若竞赛允许双项目,可以 WeKnora 前端做”快速出成果”,DB-Agent-Memory 做”深度贡献”并行推进。

12.1 双赛道并行的时间分配

如果选择双赛道并行,时间分配是关键。以下是一个可行的分配方案:

时间段 WeKnora 投入 DB-Agent-Memory 投入 说明
W1-W2 70% 30% 先把 WeKnora 前端 PR 快速做出来
W3-W4 40% 60% 转向 DB-Agent-Memory 核心贡献
W5-W8 30% 70% 侧重 DB-Agent-Memory,WeKnora 维持小 PR

这个分配的逻辑是:WeKnora 前端是”快钱”——快速出 PR 建立存在感;DB-Agent-Memory 是”长线投资”——核心 TypeScript 模块的深度贡献在竞赛评估时更有分量。

12.2 两项目的协同效应

WeKnora 和 DB-Agent-Memory 的贡献经验可以互相转化:

Memory 与 RAG 的边界详见 B 线 - Memory 与 RAG 边界,腾讯内部记忆生态的分工见 B 线 - 腾讯记忆生态


13. 风险与诚实评估

13.1 风险的优先级排序与应对

不是所有风险都一样重要。按”发生概率 x 影响程度”排序:

优先级 风险 概率 影响 应对
最高 Go 后端门槛高,读不懂代码 致命 先从前端 PR 入手,Go 代码一天读一个包
最高 维护者不响应 PR 致命 同时准备 2-3 个 PR,不把鸡蛋放一个篮子
深度路径技术方案不可行 每周评估进展,不行就切换路径
LLM API 成本超预算 用 DeepSeek / Ollama 降低成本
前端重构导致 issue 失效 提 PR 前先确认 issue 在新代码中仍存在
竞争者抢了你想做的 issue 做深度路径不会被抢——那是你提出的新方案

13.2 技术债务与隐性成本

除了上述显性风险外,WeKnora 作为大型开源项目还有一些隐性成本需要考虑:

隐性成本一:代码库的文档缺口。WeKnora 的代码注释和文档可能不完善——很多设计决策只在代码中体现,没有文档说明。这意味着你需要花大量时间阅读源码来理解”为什么要这样设计”,而不能只看文档。

隐性成本二:配置项的复杂性。WeKnora 的配置文件选项非常多(向量数据库后端、LLM 提供商、分块策略、Rerank 模型等),每个选项组合可能产生不同的行为。调试问题时,首先要排除配置问题——”你的配置对吗”是最常见的排查起点。

隐性成本三:测试数据的准备。做检索质量评估需要测试数据集——标准的问答对+上下文。WeKnora 可能没有提供标准的评估数据集,你需要自己构造。构造一个高质量的评估数据集本身就是一项不小的工程——至少需要 50-100 个问答对,覆盖简单查询、复杂查询、对比查询等不同类型。

隐性成本四:PR 排队时间。WeKnora 的维护者可能同时在处理多个 PR。你的 PR 从提交到被 review 可能需要 3-7 天,从 review 到合入可能又需要 3-7 天。这个等待时间在做时间规划时必须考虑进去。

13.3 诚实评估:我是否应该选 WeKnora

选择 WeKnora 作为竞赛项目之前,诚实地问自己:

你满足以下至少两个条件吗?

  1. 有基本的 Go 语言经验(至少写过 HTTP server 或 CLI 工具)
  2. 有基本的 React+TypeScript 经验(至少用过 hooks 和状态管理)
  3. 理解 RAG 的基本概念(向量检索、embedding、chunking)
  4. 对信息检索或 NLP 有兴趣,愿意深入学
  5. 每周能投入至少 15 小时

如果只满足 0-1 个条件:WeKnora 可能不是最佳选择,考虑 Dify(Python)或 TDesign(前端) 如果满足 2-3 个条件:WeKnora 是合理选择,从前端 PR 入手逐步深入 如果满足 4-5 个条件:WeKnora 是好选择,可以直接挑战路径 A 或 D


14. 总结:从第一天到第一个深度 PR 的行动清单

以下是从零开始的完整行动路径,按天排列。严格执行这个清单,3 个月内你一定能做出有说服力的贡献。

Day 1-2:环境搭建

Day 3-4:源码精读

Day 5-7:第一个 PR

Day 8-14:深度路径选择 + 设计文档

Day 15-28:核心实现

Day 29-35:Proposal 提交

Day 36-80:课题实战

关键心态

14.1 答辩准备的关键要点

竞赛最终的答辩环节是你展示成果的窗口。以下是答辩准备中容易忽略但非常重要的几个点:

要点一:量化你的贡献。不要说”我优化了检索质量”——说”我将复杂查询的 Context Recall 从 0.45 提升到 0.72,提升幅度 60%”。数字是最有说服力的证据。因此在实现过程中就要注意收集数据——每次改进前后都要跑评估,记录指标变化。

要点二:展示你的思考过程。评审不只看结果,还看你怎么得到结果的。遇到问题时你怎么分析的?考虑了哪些替代方案?为什么选择了当前方案?这些思考过程体现了你的技术深度。在设计文档和日常笔记中记录这些思考,答辩时才能讲清楚。

要点三:诚实面对不足。没有任何方案是完美的。如果你主动说出”这个方案在 XX 场景下效果不好,原因是 YY,未来可以通过 ZZ 改进”,评审会认为你对问题有深入理解——这比回避问题要好得多。

要点四:用演示代替纯文字。一个 3 分钟的现场演示比 10 页幻灯片更有冲击力。提前准备好演示环境,确保网络稳定、数据就绪、功能可操作。同时准备一个演示视频作为备选——万一现场出了问题,可以播放视频。

要点五:与开源社区的协作故事。竞赛不只是技术比拼,也是协作能力的展示。讲一讲你怎么与维护者沟通、怎么根据 review 意见修改、怎么帮助其他贡献者——这些故事让评审看到你不只是会写代码,还会合作。


读完你应能


15. 交叉引用

相关页面 关系
精读: WeKnora 本篇的架构基础——EventManager/RRF/Rerank/分块的完整技术分析
Agentic RAG 模式 路径 A 的理论基础——6 种 RAG 编排模式
RAG 评估方法论 路径 C 的理论基础——RAGAS 四指标 + 消融实验设计
RAG 知识库全景 WeKnora 在 5 项目比较中的定位
Agent 与 RAG 平台集成 MCP 集成模式——WeKnora 如何被外部 Agent 调用
RAG 平台选型决策树 什么场景选 WeKnora
Memory vs RAG 边界 D 线(RAG)与 B 线(Memory)的分工与重叠
腾讯记忆生态 WeKnora 在腾讯 DB-Agent-Memory 生态中的位置
选题策略 犀牛鸟竞赛全局策略 + 时间线
往年经验帖 评审标准 + Proposal 攻略 + 往届参与者经验

证据等级

来源 标签
gh issue list 2026-06-22 [Issue]
WeKnora 源码目录结构 [源码]
deep-dive-weknora.md 架构分析 [已有]
agentic-rag-patterns.md 六种模式 [已有]
rag-evaluation-methodology.md RAGAS 指标 [已有]
rag-knowledge-landscape.md 5 项目横评 [已有]
rag-agent-integration.md MCP 集成 [已有]
B 线 memory-vs-rag-boundary [已有]
strategy.md 时间线 [已有]
past-experience.md 评审标准 [已有]
未 Docker 实测 以源码/docs 为准