Appearance
完整项目(二):管理后台与运营闭环
1. 本节产出
一个可用的管理后台:文档管理(列表/发布/回滚/删除)、运营看板(高频问题、拒答 TOP、知识缺口)、用户反馈(点赞点踩)与评测集自动补充。
2. 前置依赖
3. 为什么管理后台决定项目能否持续
很多 RAG 项目 Demo 做得很好,但上线三个月后死掉。原因不在技术,在运营闭环断了:
| 缺失 | 后果 |
|---|---|
| 不知道用户问什么 | 无法补充知识缺口 |
| 不知道哪些答错了 | 效果一直不改善 |
| 不知道哪些文档没用 | 库越来越臃肿 |
| 文档更新靠运维手动 | 三个月后就没人更新了 |
RAG 不是一次交付,是一个持续运营的知识产品。 管理后台就是把运营动作产品化的地方——让业务方自己能维护,而不是事事找技术。
4. 核心原理
4.1 运营闭环
用户提问
│
├─ 命中 → 展示答案 + 引用 + 点赞/点踩
│ └─ 点踩 → 进入「待改进」列表
│
└─ 拒答 → 记录「知识缺口」
│
▼
运营在后台看到:这个问题被问了 37 次,答不上
│
▼
补充文档 → 灌库 → 自动加入评测集 → 回归验证这个闭环是本节的核心。它把「用户的不满」自动转化成「待办事项」和「测试用例」。
4.2 四个必备看板
| 看板 | 内容 | 谁看 |
|---|---|---|
| 使用概览 | QPS、用户数、满意度 | 管理层 |
| 高频问题 TOP50 | 问题、次数、是否被拒答 | 运营 |
| 知识缺口 | 被拒答次数最多的问题 | 运营/内容负责人 |
| 文档健康度 | 每篇文档的被引用次数、最后更新时间 | 内容负责人 |
「文档健康度」常被忽略但很有用:被引用次数为 0 的文档说明没人关心(可归档),被引用多但用户点踩多的说明质量有问题(要重写)。
4.3 反馈如何变成评测集
点踩的问题
│
▼ 运营补充正确答案(或标记「应拒答」)
新增评测样本
│
▼ 加入 dataset.csv
下次 CI 自动回归这一步让评测集自动增长。上线半年后,你的评测集从 50 条长到 300 条,而且全是真实问题——这比人工构造的评测集有价值得多。
4.4 权限模型
| 角色 | 权限 |
|---|---|
| 普通用户 | 提问、查看有权限的文档、反馈 |
| 内容管理员 | 上传/发布/回滚文档、查看运营看板 |
| 租户管理员 | 管理用户与配额、查看成本 |
| 系统管理员 | 跨租户运维(需严格审计) |
系统管理员的跨租户操作必须留审计日志。这是 SaaS 场景下最容易出事的地方。
5. 代码走查
5.1 反馈采集
java
// ch02-rag/src/main/java/com/aitech/rag/feedback/FeedbackService.java
@Service
public class FeedbackService {
/** 答案返回时带上 answerId,前端用点赞/点踩回调 */
public void record(String answerId, boolean positive, String comment) {
AnswerLog log = answerLogRepo.findById(answerId)
.orElseThrow(() -> new NotFoundException("answerId 不存在"));
feedbackRepo.save(new Feedback(
answerId, log.question(), log.tenantId(),
positive, comment, log.citations()));
meters.counter("rag.feedback", "positive", String.valueOf(positive))
.increment();
if (!positive) {
// 进入待改进队列
improveQueue.add(log.question(), log.citations(), comment);
}
}
}5.2 知识缺口分析
java
// ch02-rag/src/main/java/com/aitech/rag/admin/GapAnalyzer.java
@Service
public class GapAnalyzer {
/** 统计被拒答次数最多的问题(按语义聚合) */
public List<GapItem> topGaps(String tenantId, int limit) {
return jdbc.query("""
SELECT question,
COUNT(*) AS reject_count,
MAX(at) AS last_asked
FROM ai_usage
WHERE tenant_id = ? AND rejected = true
AND at > now() - INTERVAL '30 days'
GROUP BY question
ORDER BY reject_count DESC
LIMIT ?
""", mapper, tenantId, limit);
}
public record GapItem(String question, long rejectCount, Instant lastAsked) {}
}按语义聚合会更好(同一个问题的不同表述应合并),但按字符串聚合实现简单,作为起点够用。语义聚合可以用聚类:对问题向量做简单聚类,取每类的中心问题。
5.3 文档管理接口
java
// ch02-rag/src/main/java/com/aitech/rag/controller/AdminDocumentController.java
@RestController
@RequestMapping("/api/admin/documents")
@PreAuthorize("hasRole('CONTENT_ADMIN')")
public class AdminDocumentController {
@GetMapping
public Page<DocSummary> list(@RequestParam(defaultValue = "0") int page) {
return docService.list(TenantContext.require(), page);
}
@PostMapping("/{sourceId}/publish")
public void publish(@PathVariable String sourceId, @RequestParam String version) {
publishService.publish(sourceId, version);
cacheService.evictBySource(sourceId); // 发布即失效缓存
auditLog.record("PUBLISH", sourceId, version);
}
@PostMapping("/{sourceId}/rollback")
public void rollback(@PathVariable String sourceId, @RequestParam String toVersion) {
publishService.publish(sourceId, toVersion);
cacheService.evictBySource(sourceId);
auditLog.record("ROLLBACK", sourceId, toVersion);
}
@DeleteMapping("/{sourceId}")
public void delete(@PathVariable String sourceId) {
docService.softDelete(sourceId);
cacheService.evictBySource(sourceId);
auditLog.record("DELETE", sourceId, null);
}
}发布/回滚/删除都必须失效缓存,否则用户会继续拿到旧答案(见 02-15)。
5.4 从反馈生成评测样本
java
// ch02-rag/src/main/java/com/aitech/rag/admin/EvalExporter.java
@Service
public class EvalExporter {
/** 把运营标注过的反馈导出为评测集新增行 */
public String exportPending() {
List<Feedback> labeled = feedbackRepo.findLabeledNotExported();
return labeled.stream()
.map(f -> "%s,%s,%s,%s,%s".formatted(
csv(f.question()),
csv(f.expectedDocIds()),
csv(f.expectedAnswer()),
f.shouldReject() ? "false" : "true", // answerable
"FEEDBACK"))
.collect(Collectors.joining("\n"));
}
}6. 跑起来
bash
git checkout ch02-21-project-admin
docker compose up -d
mvn spring-boot:run
# 打开 http://localhost:8080/adminbash
# 1. 提交反馈
curl -X POST http://localhost:8080/api/feedback \
-d '{"answerId":"a-123","positive":false,"comment":"答案过时"}'
# 期望:进入待改进队列
# 2. 看知识缺口
curl http://localhost:8080/api/admin/gaps?tenantId=acme
# 期望:[{"question":"远程办公补贴","rejectCount":37,...}]
# 3. 发布新版本并回滚
curl -X POST "http://localhost:8080/api/admin/documents/handbook/publish?version=v3"
curl -X POST "http://localhost:8080/api/admin/documents/handbook/rollback?toVersion=v2"
# 期望:两次操作后缓存均已失效,检索结果随之变化
# 4. 导出评测样本
curl http://localhost:8080/api/admin/eval/export| 检查项 | 通过标准 |
|---|---|
| 反馈可提交 | 点赞点踩被记录并进队列 |
| 缺口可见 | 拒答 TOP 问题能列出 |
| 发布生效 | 发布后检索结果变化,缓存失效 |
| 回滚可用 | 回滚到旧版本秒级生效 |
| 权限生效 | 非管理员访问 /admin 被拒绝 |
| 审计留痕 | 发布/回滚/删除都有审计记录 |
7. 生产避坑
- 发布/回滚/删除必须联动失效缓存。这是运营后台最容易漏的一步:运营在后台发布了新版本,但用户端因为缓存还在拿旧答案,然后运营会认为「系统有 bug」。把失效逻辑写进发布流程本身,不要靠人工记得点。
- 跨租户的管理操作必须有审计日志。SaaS 场景下系统管理员能看到所有租户数据,这是必要的运维能力,也是最大的风险点。审计日志要记录:谁、什么时候、对哪个租户、做了什么。
- 点踩反馈要能闭环,不能只收集不处理。用户点踩后发现没变化,第二次就不点了,反馈渠道就此失效。做法:把待改进列表做成有 SLA 的工单(比如 7 天内必须处理),并在看板上显示处理率。
8. 延伸与锚点
- 思考题:后台做完了,用户端该长什么样?流式输出、引用展示、多轮对话如何组合?(02 篇最后一节)
- 代码锚点:
git checkout ch02-21-project-admin - 下一课时:02-22 完整项目(三):用户端与交付
- 对应课件:L02-21 管理后台