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

#1248 映射:从案例到实战

结论先行

Issue #1248(知识库问答-点赞点踩)可拆为 5 个子任务,每个都有已精读的案例可参考:

没有哪个案例能 100% 覆盖 #1248 的全部需求,但每个子任务都有至少一个案例提供可复用的代码模式或架构思路。下面逐一展开。


#1248 Issue 原文要点摘要

  1. 前端交互:AI 回复气泡底部加点赞/点踩按钮,点踩弹出原因选择弹窗,用户无需感知后端知识库片段关联
  2. 回复-片段关联:AI 生成回答时,自动记录该回复引用的所有知识库片段 ID,持久化为”回复-片段”关联关系
  3. 反馈归因:用户提交点赞/点踩时,系统通过关联关系自动将评价绑定到对应的知识库片段,同步更新片段累计点赞/点踩数
  4. 后台统计:新增片段维度统计——按片段查看累计点赞、点踩、好评率、关联会话数、点踩原因聚合
  5. 权重调整:基于片段好评率动态调整检索召回权重
  6. 数据模型:知识库片段表新增字段 + 新建”回复-片段关联表” + 优化用户评价记录表
  7. 后台管理:支持查看权重变更日志,支持管理员手动重置

子任务-案例映射表

#1248 子任务 最像的案例 可复用什么 新增什么
对话气泡 UI(点赞/点踩按钮 + 点踩原因弹窗) case-1633(null guard)、case-1761(缓存失效) 1633 的防御性字段映射模式——feedback 状态从 API 拿到后用 ?? 兜底渲染;1761 的 TTL 重置——用户提交反馈后立即使消息缓存失效以更新 UI 新建 FeedbackButtons.vue 组件(赞/踩图标 + 已选状态高亮)、FeedbackReasonDialog.vue 弹窗组件(预设原因选项 + 自定义输入);在 botmsg.vue 中集成
POST feedback API case-1774(三层 + 关联校验)、case-1754(POST 契约 + MCP) 1774 的三层分离 + 跨表查询;1754 的 POST body 设计、OpenAPI 与 eval/MCP 对齐——#1248 的 feedback POST 应同样写清 schema 与错误码 新增 feedback_handler.gofeedback_service.gofeedback_repository.go
message→chunk 归因(回复-片段关联持久化) case-1785(管线边界)、case-1700(chunk 上下文) 1785:在正确节点做归因;1700:chunk 的 ContextHeader 决定检索/展示上下文——feedback 绑定的 snippet 必须引用 完整 chunk ID + 展示前缀 生成回答后将 (message_id, chunk_id) 写入关联表;权重调整读 chunk 维度统计
片段统计与权重调整 case-1725(管线思维——中间层输出影响下游假设) 1725 的教训:改一层输出必须验证下游假设。在检索阶段引入 feedback 权重时,必须确认下游的 Rerank 和 LLM 生成不会因为权重变化产生意外行为 在 chunk 检索时读取 feedback_weight 字段作为加权因子;定期任务或写入时增量计算好评率并更新权重
MVP / Phase2 切分 case-index 中 case-1761(3 行修复)和 case-1785(2 文件改动)的范围控制 两个案例的共性:改动范围极小、一个 PR 只解决一个问题。#1248 应拆成多个 PR 逐步合入,而不是一个大 PR 判断标准见下方 MVP/Phase2 章节

Implementation Map(文件级)

以下基于 WeKnora main 分支(commit 122408b2)的目录结构,列出每个子任务可能涉及的文件。标注「NEW」的为新建文件,其余为修改已有文件。

子任务 1:对话气泡 UI

文件 操作 说明
frontend/src/views/chat/components/FeedbackButtons.vue NEW 点赞/点踩按钮组件,接收 messageId 和当前 feedback 状态作为 props
frontend/src/views/chat/components/FeedbackReasonDialog.vue NEW 点踩原因弹窗,使用 TDesign Dialog + CheckboxGroup
frontend/src/views/chat/botmsg.vue 修改 在 AI 回复气泡底部挂载 FeedbackButtons 组件
frontend/src/stores/chat.ts(或新建 feedback.ts 修改/NEW 管理 feedback 状态、调用 POST API、提交后使缓存失效(参考 case-1761 模式)

子任务 2:POST feedback API

文件 操作 说明
internal/handler/feedback.go NEW 路由注册 + 参数解析 + HTTP 状态码返回
internal/application/service/feedback.go NEW 校验 session/message 归属、写入 feedback 记录、触发归因逻辑
internal/application/repository/feedback.go NEW message_feedback 表和 message_chunk_ref 表的 CRUD
internal/types/interfaces/feedback.go NEW feedback repository 接口定义
internal/handler/router.go(或等效路由注册文件) 修改 注册 feedback 路由组

子任务 3:message→chunk 归因

文件 操作 说明
internal/application/service/chat.go(或 RAG 管线入口) 修改 在 LLM 生成回答后,将引用的 chunk ID 列表写入 message_chunk_ref 表(需本地 docker 确认具体文件路径)
internal/application/repository/feedback.go 修改 新增 BatchCreateChunkRefs 方法

子任务 4:片段统计与权重调整

文件 操作 说明
internal/application/repository/knowledgebase.go 修改 chunk 表新增 like_countdislike_countfeedback_weight 字段的读写方法
internal/application/service/feedback.go 修改 新增 UpdateChunkFeedbackStats 方法——通过归因表聚合计数并更新 chunk
检索相关文件(需本地 docker 确认) 修改 检索时将 feedback_weight 作为向量相似度的加权因子

子任务 5:后台统计面板(Phase2)

文件 操作 说明
internal/handler/feedback.go 修改 新增 GET 统计接口
frontend/src/views/knowledge-base/ 下新增统计页面 NEW 片段维度的 feedback 统计表格 + 图表

MVP / Phase2 切分

参考案例经验:case-1761(3 行)和 case-1785(2 文件 +111 行)之所以能快速合入,关键在于改动范围极度收敛、每个 PR 只解决一个完整但有限的问题。#1248 如果打成一个 PR 提交,改动量预估 20+ 文件、1500+ 行,review 难度极高。建议拆成两阶段。

MVP(第一个 PR 目标)——让用户能点赞/点踩,数据落库:

Phase2(后续 PR)——让数据产生价值:

判断标准:一个功能放在 MVP 还是 Phase2,问两个问题——”没有它用户能不能完成核心操作(点赞/点踩)?”“它的改动是否需要触碰检索管线核心逻辑?”前者为否、后者为是的,放 Phase2。


API 草案

POST /api/v1/sessions/:sessionId/messages/:messageId/feedback

提交用户对某条 AI 回复的点赞/点踩反馈。

请求体

{
  "rating": "like",
  "reasons": [],
  "comment": ""
}
字段 类型 必填 说明
rating string 枚举值:"like" / "dislike"
reasons string[] 点踩原因代码列表,仅 rating=dislike 时有意义。预设值如 "irrelevant""inaccurate""incomplete""outdated"
comment string 用户自由文字补充,最大 500 字符

响应

Service 层校验逻辑(参考 case-1774 的引用检查模式):

  1. 查询 session 是否存在且属于当前用户
  2. 查询 message 是否存在且属于该 session
  3. 查询是否已有 feedback 记录(决定 create 还是 update)
  4. 写入 feedback 记录
  5. 通过 message_chunk_ref 表查出关联的 chunk ID 列表,异步更新各 chunk 的计数

GET /api/v1/knowledge-bases/:kbId/chunks/:chunkId/feedback-stats

查询某个知识库片段的反馈统计(Phase2)。

响应

{
  "chunk_id": "chunk-uuid",
  "like_count": 42,
  "dislike_count": 5,
  "approval_rate": 0.894,
  "feedback_weight": 1.12,
  "associated_sessions": 38,
  "dislike_reasons": {
    "inaccurate": 3,
    "outdated": 1,
    "incomplete": 1
  }
}

表结构草案

表 1:message_feedback(用户评价记录)

CREATE TABLE message_feedback (
    id          TEXT PRIMARY KEY,
    tenant_id   TEXT NOT NULL,
    session_id  TEXT NOT NULL,
    message_id  TEXT NOT NULL,
    user_id     TEXT NOT NULL,
    rating      TEXT NOT NULL CHECK (rating IN ('like', 'dislike')),
    reasons     TEXT,          -- JSON 数组,存点踩原因代码
    comment     TEXT,
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    UNIQUE (message_id, user_id)  -- 每个用户对同一条 message 只能有一条反馈
);
CREATE INDEX idx_feedback_session ON message_feedback(session_id);
CREATE INDEX idx_feedback_message ON message_feedback(message_id);

表 2:message_chunk_ref(回复-片段关联表)

CREATE TABLE message_chunk_ref (
    id          TEXT PRIMARY KEY,
    message_id  TEXT NOT NULL,
    chunk_id    TEXT NOT NULL,
    kb_id       TEXT NOT NULL,    -- 冗余知识库 ID,方便按 KB 维度聚合统计
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    UNIQUE (message_id, chunk_id)
);
CREATE INDEX idx_chunk_ref_message ON message_chunk_ref(message_id);
CREATE INDEX idx_chunk_ref_chunk   ON message_chunk_ref(chunk_id);
CREATE INDEX idx_chunk_ref_kb      ON message_chunk_ref(kb_id);

表 3:chunk 表新增字段(ALTER TABLE)

ALTER TABLE knowledge_base_chunks ADD COLUMN like_count       INTEGER NOT NULL DEFAULT 0;
ALTER TABLE knowledge_base_chunks ADD COLUMN dislike_count    INTEGER NOT NULL DEFAULT 0;
ALTER TABLE knowledge_base_chunks ADD COLUMN feedback_weight  REAL    NOT NULL DEFAULT 1.0;

设计说明:


3 条「若照搬某案例会踩坑」的警告

警告 1:照搬 case-1761 的”重置 TTL”模式处理点赞状态更新,会导致不必要的全量重新请求。

case-1761 中,创建组织后重置 organizationsLoadedAt = 0 再调用 fetchOrganizations() 是合理的——因为组织列表是低频操作,重新拉一次完整列表的代价很小。但在 #1248 的场景中,用户可能在一个长对话中连续对多条 AI 回复点赞/点踩。如果每次点赞都重置缓存 + 重新请求整个消息列表,会产生大量不必要的网络请求。正确做法是乐观更新:点赞后立即在前端 store 中更新对应 message 的 feedback 状态(message.feedback = 'like'),同时异步发送 POST 请求。请求失败时回滚状态并提示用户。

警告 2:照搬 case-1785 的”在最上游过滤”原则来做 message→chunk 归因,可能选错了”上游”的位置。

case-1785 的核心教训是”在 parser 层过滤噪声,而不是在 chunker 或 Rerank 层”。但 #1248 的归因逻辑不能直接套用”越上游越好”的原则。归因(记录哪些 chunk 被引用)必须发生在 RAG 检索完成之后、LLM 生成回答之后——因为只有这时候才能确定最终被引用的 chunk 列表。如果试图在检索阶段就记录关联,会把被 Rerank 淘汰的 chunk 也记录进去,导致归因不准确。正确的插入点是:Rerank 完成 → 选定最终 chunk 列表 → 记录关联 → 将 chunk 内容送入 LLM。需本地 docker 确认 WeKnora RAG 管线中这一步的具体代码位置。

警告 3:照搬 case-1774 的 CountByModelID 聚合查询模式来做 chunk 统计更新,在高并发场景下会有计数不一致风险。

case-1774 的模式是”查询时聚合计数”——每次删除前查一次 COUNT(*)。但 #1248 的点赞/点踩是用户操作,频率远高于删除模型。如果每次收到 feedback 都去 COUNT(*) 重新统计某个 chunk 的赞踩数,在多用户并发场景下可能出现 race condition(两个用户同时点赞同一条引用相同 chunk 的消息,各自读到旧计数后 +1 写入,最终少算一次)。建议改用增量更新:feedback 写入成功后直接 UPDATE chunks SET like_count = like_count + 1 WHERE id = ?,利用数据库的原子操作避免并发问题。全量统计只在后台定期任务中做校准。


思考点

Q1:case-1633 处理的是”API 返回字段名与组件期望不一致”的问题。在 #1248 中,假设 POST feedback API 返回 { "id": "xxx", "created_at": "2026-07-15T10:00:00Z" },但前端 FeedbackButtons.vue 需要的状态是 { status: 'liked' | 'disliked' | 'none' }。请设计一个 normalize 函数,将 API 响应转换为组件所需的格式。对比 case-1633 中 store 层归一化的方案,你的 normalize 函数应该放在 store 层还是组件层?为什么?

Q2:case-1725 和 case-1785 都是管线修复,但修复层级不同——1785 在 Python docreader 层,1725 在 Go docparser 层。#1248 的 message→chunk 归因逻辑应该放在 RAG 管线的哪一层?如果放错了层(比如放在检索层而非生成层),会产生什么具体的数据质量问题?请对比两个案例的”选择修复层级”的决策逻辑来回答。

Q3:case-1774 为删除保护设计了 model_usage.go 聚合层。假设 #1248 的 Phase2 需要一个”feedback 统计聚合层”来汇总多个维度的数据(按 chunk 统计、按 session 统计、按知识库统计),你会参考 case-1774 设计一个 feedback_stats.go 吗?画出它的依赖关系图(它调用哪些 repository、被哪些 service 调用)。

Q4:MVP 阶段只需要前端气泡 + POST API + 两张新表。但假设你在 MVP 的 PR 中没有实现 message→chunk 归因(归因逻辑留到 Phase2),message_chunk_ref 表会是空的。这时候用户的点赞/点踩数据虽然存在于 message_feedback 表中,但无法回溯到具体的 chunk。Phase2 上线时你要怎么处理 MVP 期间积累的”无归因 feedback”数据?有没有办法在 MVP 阶段就做一些准备来降低 Phase2 的迁移成本?

Q5:case-1761 选择了”重置 TTL”而非”乐观更新”,理由是组织创建是低频操作。但本章警告 1 说 #1248 应该用乐观更新。如果 WeKnora 未来增加了”批量点赞”功能(用户一键对整个会话的所有 AI 回复点赞),乐观更新的方案还能撑住吗?在什么阈值下你会考虑切换回”重置 TTL + 重新请求”方案?


延伸阅读