#1248 映射:从案例到实战
结论先行
Issue #1248(知识库问答-点赞点踩)可拆为 5 个子任务,每个都有已精读的案例可参考:
- 对话气泡 UI(点赞/点踩按钮 + 点踩原因弹窗)——参考 case-1633 的 null guard 和 case-1761 的缓存失效
- POST feedback API(handler → service → repository 三层)——参考 case-1774 的完整后端分层
- message→chunk 归因(回复-片段关联持久化)——参考 case-1785 的管线边界过滤思维
- 片段统计与权重调整——参考 case-1725 的管线中间层思维
- MVP / Phase2 切分——综合 case-index 的 scope 控制经验
没有哪个案例能 100% 覆盖 #1248 的全部需求,但每个子任务都有至少一个案例提供可复用的代码模式或架构思路。下面逐一展开。
#1248 Issue 原文要点摘要
- 前端交互:AI 回复气泡底部加点赞/点踩按钮,点踩弹出原因选择弹窗,用户无需感知后端知识库片段关联
- 回复-片段关联:AI 生成回答时,自动记录该回复引用的所有知识库片段 ID,持久化为”回复-片段”关联关系
- 反馈归因:用户提交点赞/点踩时,系统通过关联关系自动将评价绑定到对应的知识库片段,同步更新片段累计点赞/点踩数
- 后台统计:新增片段维度统计——按片段查看累计点赞、点踩、好评率、关联会话数、点踩原因聚合
- 权重调整:基于片段好评率动态调整检索召回权重
- 数据模型:知识库片段表新增字段 + 新建”回复-片段关联表” + 优化用户评价记录表
- 后台管理:支持查看权重变更日志,支持管理员手动重置
子任务-案例映射表
| #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.go、feedback_service.go、feedback_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_count、dislike_count、feedback_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 目标)——让用户能点赞/点踩,数据落库:
- 前端:
FeedbackButtons.vue+FeedbackReasonDialog.vue+botmsg.vue集成 - 后端:
POST /feedbackAPI(handler + service + repository) - 数据模型:
message_feedback表 +message_chunk_ref表 - 归因:在 RAG 管线中记录 message→chunk 关联(如果改动过大,可先硬编码为空列表,Phase2 再接入)
- 预估改动:10–15 文件,600–900 行
Phase2(后续 PR)——让数据产生价值:
- 片段维度的统计聚合(好评率、点踩原因分布)
- 基于好评率的检索权重动态调整
- 后台管理面板(统计表格 + 权重变更日志 + 管理员重置)
- 预估改动:8–12 文件,800–1200 行
判断标准:一个功能放在 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 字符 |
响应:
201 Created:反馈提交成功,返回{ "id": "feedback-uuid", "created_at": "..." }400 Bad Request:rating 值非法、message 不属于该 session404 Not Found:session 或 message 不存在409 Conflict:该用户已对同一条 message 提交过反馈(需先撤销再重新提交,或改为 PUT 语义覆盖)
Service 层校验逻辑(参考 case-1774 的引用检查模式):
- 查询 session 是否存在且属于当前用户
- 查询 message 是否存在且属于该 session
- 查询是否已有 feedback 记录(决定 create 还是 update)
- 写入 feedback 记录
- 通过
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;
设计说明:
message_feedback.reasons用 JSON 数组而非关联表,原因是 reason 种类有限(4–6 种预设值),不需要独立的归一化表。用 JSON 存储可以少建一张表,查询时用JSON_EACH()或应用层解析均可。message_chunk_ref.kb_id是冗余字段。虽然可以通过chunk_id → chunk 表 → kb_id关联查到,但统计查询时直接按kb_id过滤能避免 JOIN,这是读多写少场景下的常见优化。feedback_weight默认值1.0表示”无调整”。好评率高于阈值时上调(如 1.1–1.3),低于阈值时下调(如 0.7–0.9),具体系数需要实验确定。- 所有 ID 字段使用 TEXT(UUID),与 WeKnora 现有表结构保持一致。需本地 docker 确认 WeKnora 的 ID 生成策略(可能是 UUID v4 或 KSUID)。
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 + 重新请求”方案?