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
}
大师追问(读代码时要能回答):
TopK在 Rerank 前后各是多少?- 空检索时走「纯 LLM」还是返回「知识库无相关内容」?
- 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 分钟)
- 打开浏览器 DevTools → Network,发一条问答,记下 API path 与 response 字段。
- 在仓库
rg "该路径片段" internal/handler定位 handler。 - 从 handler 跳进 service,用 IDE「Find Usages」画 5 层调用深度。
- 写 10 行笔记:我提问时,代码执行顺序是 …
章末 · 大师自检
- 白板画出 Handler → Service → Retrieval → LLM
- 说清 Handler 不应包含的三类逻辑
- 找到 chat 相关 handler 并指出对应 service 方法名
- 解释 citation 为什么必须在 Service 层组装而非 Handler