犀牛鸟 2026 · 开源研究笔记

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 结尾(Retriever、Evaluator)、布尔变量以 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"`
}

新增配置项时必须同时写 yaml 和 json 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 提交前必须过 gofmt 和 go vet。golangci-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 待确认
竞争 18.1K 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 为准