Skip to content

完整项目(三):用户端交互与交付验收 ​

1. 本节产出 ​

一个可用的用户端:流式打字机、引用可点击跳转原文、多轮对话、反馈入口;外加一份交付验收清单(这是本节最值钱的东西,可直接用于客户验收)。

2. 前置依赖 ​

3. 为什么用户端的细节决定成败 ​

技术链路全部跑通,验收时客户仍可能不满意。原因通常在交互细节:

细节缺失时用户的感觉
流式输出「它卡住了吗?」→ 刷新 → 又烧一次钱
引用可跳转「凭什么信你?」
明确拒答「它骗我」(编造时)
来源展示不知道答案是从哪份文档来的,不敢用
反馈入口错了没地方说,下次就不用了

核心洞察:RAG 产品的信任来自「可验证」,不是来自「答得流畅」。引用、来源、拒答这三件事都是在建立信任。

4. 核心原理 ​

4.1 一次问答的完整交互时序 ​

用户输入
   │
   ▼ 立即展示「检索中」(不要让用户对着空白等)
[0.3s] 检索完成 → 展示「找到 N 篇相关文档」(可展开)
   │
   ▼ 0.8s 首个 Token → 打字机开始
   │
   ▼ 生成中 → 逐字输出
   │
   ▼ 完成 → 展示引用列表 [1][2](可点击)
        → 展示反馈按钮(赞/踩)
        → 展示「相关推荐问题」

「检索中 → 找到 N 篇」这两步很关键。它把等待拆成两段有反馈的状态,感知等待时间显著变短——虽然总时长没变。

4.2 引用怎么展示 ​

答案正文(带角标):
  员工年假按司龄计算:满 1 年不满 5 年的,每年 5 天[1];
  满 5 年不满 10 年的,每年 10 天[1]。病假需提供医院证明[2]。

─────────────────────────────
来源
[1] 《员工手册 2026》 3.2 年假      ← 点击跳转原文位置
[2] 《员工手册 2026》 3.3 病假

点击跳转要能定位到具体位置,不只是打开文档。实现:跳转 URL 带 sourceId + chunkIndex,前端滚动到对应片段并高亮。这个细节的体验差距很大。

4.3 拒答的展示 ​

差的:  「抱歉,我无法回答。」

好的:  「没有找到关于这个问题的规定。
        可能相关:《员工手册》3.2 年假 /《考勤制度》2.1
        你可以:换个说法再问,或联系 HR 咨询。」

三要素:明确说明没找到 + 给出最接近的候选 + 给下一步建议。

4.4 多轮对话与引用 ​

多轮场景下有两个坑:

坑处理
引用标记在多轮后错乱引用映射必须按轮次保存,不能只存最新一次
上下文越来越长记忆窗口限制 + 早期内容摘要(见 01-06)

5. 代码走查 ​

5.1 流式 + 引用的后端协议 ​

java
// 先发元信息(检索结果),再发正文,最后发引用
@GetMapping(value = "/api/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<?>> stream(@RequestParam String q,
                                        @RequestParam String convId) {
    String tenantId = TenantContext.require();

    return Flux.concat(
        // 1. 元信息事件:让前端立刻显示「找到 N 篇」
        Mono.fromSupplier(() -> ServerSentEvent.builder()
                .event("meta")
                .data(new MetaEvent(searchHits(q, tenantId)))
                .build()),

        // 2. 正文流
        chatStream(q, convId, tenantId)
                .map(chunk -> ServerSentEvent.builder()
                        .event("delta").data(chunk).build()),

        // 3. 引用与反馈 ID
        Mono.fromSupplier(() -> ServerSentEvent.builder()
                .event("citations")
                .data(lastCitations(convId))
                .build())
    );
}

5.2 前端消费 ​

javascript
const es = new EventSource(`/api/ask/stream?q=${encodeURIComponent(q)}&convId=${cid}`);

es.addEventListener('meta', (e) => {
  const m = JSON.parse(e.data);
  showStatus(`找到 ${m.count} 篇相关文档`);      // 立刻给反馈
});

es.addEventListener('delta', (e) => {
  out.textContent += e.data;                     // 打字机
});

es.addEventListener('citations', (e) => {
  const c = JSON.parse(e.data);
  renderCitations(c);                            // 渲染引用列表
  renderFeedback(c.answerId);                    // 渲染点赞点踩
  es.close();
});

5.3 引用跳转 ​

javascript
function renderCitations(list) {
  list.forEach(c => {
    const a = document.createElement('a');
    a.href = `/docs/${c.sourceId}#chunk-${c.chunkIndex}`;   // 定位到片段
    a.textContent = `[${c.index}] 《${c.title}》 ${c.titlePath}`;
    citationsBox.appendChild(a);
  });
}

5.4 反馈提交 ​

javascript
function sendFeedback(answerId, positive) {
  fetch('/api/feedback', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({answerId, positive})
  });
}

6. 跑起来与交付验收 ​

这是本节的核心产出,可直接用于项目验收。

功能验收 ​

#项通过标准
1多格式文档灌入PDF/Word/MD/HTML 均可,扫描件明确提示需 OCR
2增量更新单篇更新 < 10s,未变更文档跳过
3删除同步源端删除后不再被检索到
4多租户隔离自动化断言:A 租户检索不到 B 任何内容
5权限过滤无对应角色时检索不到受限文档
6引用溯源答案带 [1][2],可跳转到原文片段
7拒答无答案查询明确拒答,不编造
8诱导测试用户给出错误前提时不顺着答
9流式输出首 Token < 2s,中断时服务端取消
10多轮对话能引用前文,重启不失忆

非功能验收 ​

#项通过标准
11检索 P95< 200ms
12端到端 P95< 3s
13评测集召回率@5≥ 0.80(以基线为准)
14拒答准确率≥ 0.90
15缓存命中率≥ 30%(运行一周后)
16成本单次问答成本可查询,与预算偏差 < 20%
17灌库失败进死信队列并告警,不静默
18配额超 95% 自动降级,超 100% 限流

运维验收 ​

#项通过标准
19一键启动docker compose up -d 起全部依赖
20监控面板分段耗时、缓存命中、拒答率、成本可见
21回归测试CI 中评测集自动跑,退化拦截
22版本矩阵README 有最低可运行版本组合
检查项通过标准
22 项验收逐条打勾,全部通过
演示脚本有一条能 5 分钟演示完的主线
回滚演练至少演练一次版本回滚

7. 生产避坑 ​

  1. 不要让用户对着空白等待。RAG 链路比普通接口慢得多,必须有中间状态反馈(「检索中」「找到 N 篇」)。这是最便宜的体验优化,成本几乎为零,效果立竿见影。
  2. 引用必须能定位到具体片段,不能只打开文档。用户点开一份 200 页的手册却找不到答案在哪,这个引用的价值就没了。做法:URL 带 chunkIndex,前端滚动 + 高亮。
  3. 交付清单要在项目开始时给客户看,不是结束时。很多争议源于双方对「完成」的定义不同。开工前把这张表发出来并确认,能避免大量返工和扯皮。

8. 延伸与锚点 ​

  • 思考题:现在系统能「回答问题」了。如果要它「帮我完成一件事」(改订单状态、提交工单),需要什么?(这就是 03 篇 Agent 要解决的事)
  • 代码锚点:git checkout ch02-22-project-frontend
  • 02 篇完成,进入 03 篇 Agent 与编排
  • 对应课件:L02-22 用户端与交付