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

Ch22: 一次问答的完整旅程——Handler 到 Service 到检索

Part 8: WeKnora 源码走读 前置:Ch21 走读导言 后续:Ch23 EventManager 源码


结论先行:用户点「发送」后,数据沿 Handler(HTTP)→ Service(编排)→ Retriever(三路)→ LLM(生成) 流动。大师级能力 = 能在日志里对齐这条链上的每一步。

一、日常类比:餐厅点菜

餐厅 WeKnora
Handler 服务员接单、验券 解析 JSON、鉴权、返 4xx
Service 厨师长排菜 选知识库、触发检索、拼 prompt
Retriever 各档口找食材 BM25/向量/图谱
LLM 摆盘出菜 生成 + 引用

二、HTTP 层:Handler 做什么、不做什么

:参数绑定、tenant/KB 权限、调用 service.Xxx()、把 error 映射成 HTTP 状态码。

不做:BM25 公式、RRF、分块——这些在更深层。

参考模式(与 case-1774 同构):

func (h *ChatHandler) Query(c *gin.Context) {
    var req QueryRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"code": "invalid_request", "message": err.Error()})
        return
    }
    resp, err := h.chatService.Answer(c.Request.Context(), req)
    if err != nil {
        // AppError → 400/404/500
        writeAppError(c, err)
        return
    }
    c.JSON(200, resp)
}

读源码任务:在 internal/handler/ 找到 chat/knowledge 相关文件,列出 路由 → handler 方法 → 调用的 service 方法 三张表。


三、Service 层:一次 Answer 的编排

伪代码还原(职能等价即可):

func (s *ChatService) Answer(ctx context.Context, req QueryRequest) (*QueryResponse, error) {
    kb, err := s.kbRepo.Get(ctx, req.KnowledgeBaseID)
    if err != nil { return nil, err }

    // 1. 检索(内部触发 EventManager 或直调 retriever)
    chunks, scores, err := s.retrievalService.HybridSearch(ctx, kb, req.Query, req.TopK)
    if err != nil { return nil, err }

    // 2. 拼 context(带 citation 元数据)
    contextBlock := formatChunksForPrompt(chunks)

    // 3. 调 LLM
    answer, err := s.llmClient.Generate(ctx, systemPrompt(kb), req.Query, contextBlock)
    if err != nil { return nil, err }

    // 4. 组装引用
    return &QueryResponse{Answer: answer, Citations: toCitations(chunks, scores)}, nil
}

大师追问(读代码时要能回答):

  1. TopK 在 Rerank 前后各是多少?
  2. 空检索时走「纯 LLM」还是返回「知识库无相关内容」?
  3. citation 的 chunk_id 从哪张表来?

四、数据流图(必会画)

sequenceDiagram
    participant U as 用户/前端
    participant H as Handler
    participant S as ChatService
    participant R as RetrievalService
    participant E as EventManager
    participant L as LLM

    U->>H: POST /chat/query
    H->>S: Answer(req)
    S->>R: HybridSearch(kb, query)
    R->>E: Emit QueryReceived
    E-->>R: BM25 + Vector + Graph 并行
    R->>R: RRF + Rerank
    R-->>S: top chunks
    S->>L: prompt + query
    L-->>S: answer text
    S-->>H: response + citations
    H-->>U: JSON

五、与 #1248 反馈 API 的关系

点赞/点踩 PR 会在链路上 多一个持久化节点

Answer 成功 → 返回 message_id + citation chunk_ids
用户点踩 → Handler(Feedback) → Service → Repository 写入 feedback 表

case-1774 的三层结构,把 DeleteModel 换成 CreateFeedback 即可脑补 #1248。


六、动手练习(30 分钟)

  1. 打开浏览器 DevTools → Network,发一条问答,记下 API path 与 response 字段。
  2. 在仓库 rg "该路径片段" internal/handler 定位 handler。
  3. 从 handler 跳进 service,用 IDE「Find Usages」画 5 层调用深度。
  4. 写 10 行笔记:我提问时,代码执行顺序是 …

章末 · 大师自检


延伸阅读