Skip to content

完整项目(二):管理后台与运营闭环 ​

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/admin
bash
# 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. 生产避坑 ​

  1. 发布/回滚/删除必须联动失效缓存。这是运营后台最容易漏的一步:运营在后台发布了新版本,但用户端因为缓存还在拿旧答案,然后运营会认为「系统有 bug」。把失效逻辑写进发布流程本身,不要靠人工记得点。
  2. 跨租户的管理操作必须有审计日志。SaaS 场景下系统管理员能看到所有租户数据,这是必要的运维能力,也是最大的风险点。审计日志要记录:谁、什么时候、对哪个租户、做了什么。
  3. 点踩反馈要能闭环,不能只收集不处理。用户点踩后发现没变化,第二次就不点了,反馈渠道就此失效。做法:把待改进列表做成有 SLA 的工单(比如 7 天内必须处理),并在看板上显示处理率。

8. 延伸与锚点 ​