Appearance
完整项目(三):用户端交互与交付验收
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. 生产避坑
- 不要让用户对着空白等待。RAG 链路比普通接口慢得多,必须有中间状态反馈(「检索中」「找到 N 篇」)。这是最便宜的体验优化,成本几乎为零,效果立竿见影。
- 引用必须能定位到具体片段,不能只打开文档。用户点开一份 200 页的手册却找不到答案在哪,这个引用的价值就没了。做法:URL 带
chunkIndex,前端滚动 + 高亮。 - 交付清单要在项目开始时给客户看,不是结束时。很多争议源于双方对「完成」的定义不同。开工前把这张表发出来并确认,能避免大量返工和扯皮。
8. 延伸与锚点
- 思考题:现在系统能「回答问题」了。如果要它「帮我完成一件事」(改订单状态、提交工单),需要什么?(这就是 03 篇 Agent 要解决的事)
- 代码锚点:
git checkout ch02-22-project-frontend - 02 篇完成,进入 03 篇 Agent 与编排
- 对应课件:L02-22 用户端与交付