QA G-Cite 移动端功能设计
文档版本:v1.2
更新日期:2026-06-16
文档定位:G-Cite 移动端引用渲染的完整 PRD(含全景数据、SSE 事件接收、流式缓冲、引用编号重映射、摘要栏与半屏渲染、多源差异化、Q&A)
所属体系:QA_Chat Agent 的知识库引用渲染方案,属于 QA_闲聊对话_Agent设计.md 的补充实现文档。
配套文档:
- Dify 平台功能设计(Chatflow 编排 / 合并节点 / LLM 配置 / SSE 协议) → QA_G-Cite_Dify平台功能设计.md
- 交互原型设计(ASCII Wireframes 3 个场景) → 微信小程序_QA_G-Cite_引用归因_交互原型设计.md
阅读建议:看 §2 全景数据先建立直觉,再按 §3 → §4 → §5 顺序理解数据接收 → 处理 → 渲染的完整链路。
1. 背景与目标
1.1 适用范围
| 端 | 状态 |
|---|---|
| 移动端(微信小程序,主端) | ✅ 当前唯一在范围 |
| 大屏 | ❌ 不在范围 |
| Web | ❌ 不在范围 |
本文档所有"前端"均指移动端。
1.2 设计目标
- 不破坏流式 —
[C:N]在流式过程中直接渲染为<sup>1</sup>,不给用户看到裸括号的机会 - 引用编号按 LLM 出现顺序重映射 — 同 chunk 复用同一序号,列表去重(1232 方案)
- 多信息源差异化 — RAG(📄 蓝色)和 Web(🌐 绿色)在摘要栏、半屏做视觉区分
- Dify 平台协议零侵入 — 所有重映射/解析/渲染逻辑都在移动端,不动 Python 合并节点和 LLM
1.3 与 Dify 平台的职责边界
| 职责 | Dify 平台 | 移动端 |
|---|---|---|
| 检索 + Chunk 归一化 + 全局编号 | ✅ | — |
display_subtitle 降级计算 | ✅ | — |
| SSE 事件发送(node_finished / text_chunk) | ✅ | — |
node_finished 事件监听 → chunkMetadataMap | — | ✅ 新增 |
[C:N] 流式解析 + idMap 重映射 | — | ✅ 新增 |
| 摘要栏渲染(idMap 过滤 + 排序 + 去重) | — | ✅ 改造 |
| 半屏 / Webview 渲染 | — | 不变 |
2. 全景数据:一个贯穿全文的例子
以下 10 个 chunk 是合并节点的典型输出(5 RAG + 5 Web,Top-K=5 × 2 源),后续所有章节都基于这个数据集展开。
2.1 10 个 chunk 一览
| id | source_type | document_title | display_subtitle | score | 用途 |
|---|---|---|---|---|---|
| 1 | rag | 巡更操作指南 v2.1 | 测试知识库20260610 | 0.73 | 巡检流程 |
| 2 | rag | 物业管理制度 · 异常上报处理 | 测试知识库20260610 | 0.68 | 异常处理 |
| 3 | rag | 多模态RAG知识库技术方案 | 测试知识库20260610 | 0.55 | 技术背景 |
| 4 | rag | 楼宇设备清单 | 测试知识库20260610 | 0.50 | 设备信息 |
| 5 | rag | 消防应急预案 | 测试知识库20260610 | 0.42 | 应急预案 |
| 6 | web | 物业巡检分类 - 百度百科 | 百度百科 · 2025-09-01 | 0.72 | 公网百科 |
| 7 | web | 物业巡检流程标准 - 知乎 | 知乎 · 2025-08-12 | 0.68 | 公网问答 |
| 8 | web | 巡更系统对比 - CSDN | CSDN · 2025-07-15 | 0.55 | 公网技术博客 |
| 9 | web | 异常处理SOP - 博客园 | 博客园 · 2025-06-20 | 0.50 | 公网实践 |
| 10 | web | 巡检频次规范 - 行业站 | example.com | 0.40 | 公网规范 |
2.2 LLM 如何使用这些 chunk
合并节点并不关心 LLM 会用到哪些——它永远输出 C1~C10 全集。LLM 拿到后按需挑选:
合并节点输出 C1~C10 → LLM 实际用到: [C:7] [C:2] [C:6] [C:2]
↑跳着用 ↑重复用 C2这个场景就是后续 idMap 重映射要解决的问题——LLM 不按顺序用,还可能重复引用同一 chunk(C2 出现两次)。
2.3 chunk_metadata JSON 数组(简化)
{
"chunk_count": 10,
"chunk_metadata": [
{ "id": "1", "source_type": "rag", "document_title": "巡更操作指南 v2.1", "display_subtitle": "测试知识库20260610", "score": 0.73 },
{ "id": "2", "source_type": "rag", "document_title": "物业管理制度 · 异常上报处理", "display_subtitle": "测试知识库20260610", "score": 0.68 },
{ "id": "3", "source_type": "rag", "document_title": "多模态RAG知识库技术方案", "display_subtitle": "测试知识库20260610", "score": 0.55 },
{ "id": "4", "source_type": "rag", "document_title": "楼宇设备清单", "display_subtitle": "测试知识库20260610", "score": 0.50 },
{ "id": "5", "source_type": "rag", "document_title": "消防应急预案", "display_subtitle": "测试知识库20260610", "score": 0.42 },
{ "id": "6", "source_type": "web", "document_title": "物业巡检分类 - 百度百科", "display_subtitle": "百度百科 · 2025-09-01", "score": 0.72 },
{ "id": "7", "source_type": "web", "document_title": "物业巡检流程标准 - 知乎", "display_subtitle": "知乎 · 2025-08-12", "score": 0.68 },
{ "id": "8", "source_type": "web", "document_title": "巡更系统对比 - CSDN", "display_subtitle": "CSDN · 2025-07-15", "score": 0.55 },
{ "id": "9", "source_type": "web", "document_title": "异常处理SOP - 博客园", "display_subtitle": "博客园 · 2025-06-20", "score": 0.50 },
{ "id": "10", "source_type": "web", "document_title": "巡检频次规范 - 行业站", "display_subtitle": "example.com", "score": 0.40 }
]
}3. SSE 事件接收
移动端通过 Dify Chatflow SSE 流接收三类事件。本节覆盖事件类型、接收时序、缓存策略和容错。
3.1 三类事件
| 事件 | Dify 事件名 | 关键字段路径 | 内容 | 接收时机 |
|---|---|---|---|---|
| 元数据 | node_finished | data.outputs.chunk_metadata | 合并节点输出的完整 UnifiedChunk 数组(即 §2.3 的 chunk_metadata) | Code 节点完成后,LLM 文本流之前 |
| 文本流 | text_chunk | data.text | LLM 流式文本 token(含 [C:N] 裸标记) | 每个 token 一次 |
| 结束 | message_end | — | 对话结束信号 | 流结束 |
3.2 事件时序
以 §2 的例子为例,LLM 回答"巡更人员需要先在终端登录[C:7]。登录后..."时的完整时序:
t0 node_finished (Code节点)
→ chunk_metadata = [C1~C10 的 10 个对象] 移动端: 缓存到 chunkMetadataMap
t1 text_chunk: "巡更人员" 移动端: 直接渲染
t2 text_chunk: "需要先在终端登录" 移动端: 直接渲染
t3 text_chunk: "[C:7]。" 移动端: 匹配到 [C:7]
→ idMap 分配序号 1
→ 渲染为 ¹
t4 text_chunk: "登录后系统会根据预设路线" 移动端: 直接渲染
t5 text_chunk: "生成待检点[C:2]。" 移动端: 匹配到 [C:2]
→ idMap 分配序号 2
→ 渲染为 ²
t6 text_chunk: "巡检时如发现异常..." ...
t7 text_chunk: "可在终端直接上报[C:6]。" → 渲染为 ³
t8 text_chunk: "上报后系统会自动通知物业..."
t9 text_chunk: "这是SOP规定的紧急响应流程[C:2]。" → idMap[2] 已存在 → 复用 ²
t10 message_end → flush buffer
→ 按 idMap 顺序 [7→1, 2→2, 6→3]
→ 从 chunkMetadataMap 提取 C7, C2, C6
→ 渲染底部摘要栏(3 条)3.3 chunkMetadataMap 缓存
收到 node_finished 事件后,将 chunk_metadata 数组按 id 索引存入一个 Map 结构:
- Key:chunk 的
id字段(字符串,如"7"、"2") - Value:完整的 Chunk 对象(含
document_title、display_subtitle、source_type、url等) - 生命周期:每轮对话开始时清空,收到
node_finished时重建
摘要栏渲染时(§5.1),通过 chunkMetadataMap[rawId] 即可取到完整元数据。
3.4 node_finished 事件解析要点
- 筛选条件:
event === 'node_finished'且data.node_type === 'code'(对应 Dify 中的"Chunk编号"代码节点) - 容错:若
node_finished解析失败(Dify 版本差异),回退到message_end中的retriever_resources,但后者包含的是全部检索结果而非合并节点格式化数据,摘要栏可能列出 10 条而非 LLM 实际引用的 3 条
💡 Plan B:如
node_finished不可用,在 Code 节点后加 HTTP 请求节点将chunk_metadataPOST 到后端 API(key 为conversation_id),移动端通过conversation_id拉取。
4. 流式缓冲与引用编号重映射
4.1 流式缓冲机制
[C:N] 不应该先在屏幕上闪现为裸括号再变成上标。参考 Markdown 渲染器处理  的方式——从 [ 开始就缓存,不渲染给用户。
三步判断逻辑(针对 buffer 中的内容):
buffer 当前内容 判断 动作
──────────────────────────────────────────────
"[C:7]" 完整匹配 [C:N] → 渲染为 <sup>7</sup>
"[C:" 前缀匹配 → 继续缓存,不输出
"[C:1" 前缀匹配 → 继续缓存,不输出
"登录" 不匹配 → 直接 flush 渲染流式时间线示例(LLM 输出"登录需要[C:1],密码"):
LLM 输出 用户看到
──────────────────────────
登录 登录
需要 登录需要
[ 登录需要 ← 缓存,不显示
C 登录需要 ← 缓存,不显示
: 登录需要 ← 缓存,不显示
1 登录需要 ← 缓存,不显示
] 登录需要¹ ← 匹配成功,渲染上标
, 登录需要¹,
密码 登录需要¹,密码4.2 idMap:引用编号重映射
问题
合并节点按 id=1~10 编号 chunk(§2.1),但 LLM 跳着用且可能重复。如果直接把 chunk ID 当上标序号,用户会看到不连贯的"⁷ ² ⁶ ²"。
方案
移动端维护一个 idMap,保持 raw chunk ID → 连续显示序号 的映射。同 chunk 复用同一序号。
增量建立过程(以 §2.2 的 LLM 输出 [C:7][C:2][C:6][C:2] 为例):
| LLM 输出 | idMap 变化 | 渲染上标 | 说明 |
|---|---|---|---|
[C:7] | {7 → 1} | ¹ | 首次出现,分配 1 |
[C:2] | {7 → 1, 2 → 2} | ² | 首次出现,分配 2 |
[C:6] | {7 → 1, 2 → 2, 6 → 3} | ³ | 首次出现,分配 3 |
[C:2] | 命中已有映射 | ² | 复用,不新建 |
关键不变量:idMap 一旦建立就不变。流式过程中序号是确定性的,不会闪烁。用 Map 保留插入顺序(普通 Object 会对整数键按数字升序迭代,丢失 LLM 出现顺序)。
设计决策速查
| 决策 | 选择 | 理由 |
|---|---|---|
| 文字流编号 | 重映射为连续 1,2,3(同 chunk 复用) | 学术标准,连续可读 |
| 列表条目数 | 去重展示(3 条而非 10 条) | 1232 方案的必然结果 |
| 列表排序 | 按 LLM 出现顺序 | 序号和条目一一对应,跳转路径最短 |
| idMap 生命周期 | 单次回答 | 每轮独立上下文 |
| 执行位置 | 移动端 | 不动后端协议,不破坏流式 |
4.3 最终 DOM 结构
文字流(用户看到 ¹ ² ³ ²)
<p class="answer-text">
巡更人员需要先在终端登录<sup class="g-cite" data-raw-id="7" data-idx="1">1</sup>。
登录后系统会根据预设路线生成待检点<sup class="g-cite" data-raw-id="2" data-idx="2">2</sup>。
巡检时如发现异常,可在终端直接上报<sup class="g-cite" data-raw-id="6" data-idx="3">3</sup>。
上报后系统会自动通知物业,这是 SOP 规定的紧急响应流程
<sup class="g-cite" data-raw-id="2" data-idx="2">2</sup>。
</p>- 用户可见:
¹ ² ³ ²(连续可读,第二个 ² 标识"和上文同源") - 不可见属性:
data-raw-id="7"、data-raw-id="2"、data-raw-id="6"— 永远稳定,用于跳转
摘要栏(去重后 3 条,按 LLM 出现顺序)
<div class="citation-panel">
<div class="g-cite-item" data-raw-id="7"> <!-- C7, 显示序号 1 -->
<sup>1</sup>
<i class="fa-solid fa-globe g-cite-item-icon"></i>
<span class="g-cite-item-title">物业巡检流程标准 - 知乎</span>
<span class="g-cite-item-subtitle">知乎 · 2025-08-12</span>
</div>
<div class="g-cite-item" data-raw-id="2"> <!-- C2, 显示序号 2 -->
<sup>2</sup>
<i class="fa-solid fa-file-lines g-cite-item-icon"></i>
<span class="g-cite-item-title">物业管理制度 · 异常上报处理</span>
<span class="g-cite-item-subtitle">测试知识库20260610</span>
</div>
<div class="g-cite-item" data-raw-id="6"> <!-- C6, 显示序号 3 -->
<sup>3</sup>
<i class="fa-solid fa-globe g-cite-item-icon"></i>
<span class="g-cite-item-title">物业巡检分类 - 百度百科</span>
<span class="g-cite-item-subtitle">百度百科 · 2025-09-01</span>
</div>
</div>列表渲染逻辑:遍历 idMap 的插入顺序 [7, 2, 6] → 从 chunkMetadataMap 取对应 chunk → 去重(C2 出现两次只占一条)→ 排序按 idMap 顺序(不是 chunk ID 升序,不是 score 降序)。
4.4 跳转逻辑
上标和列表条目之间通过 data-raw-id 关联:
- 用户点击上标 ²(data-raw-id="2") → 查找
.g-cite-item[data-raw-id="2"]→ 滚动到该条目 + 高亮闪烁 1s - 用户点击列表条目(data-raw-id="2") →
chunkMetadataMap["2"]取完整元数据 → 打开引用半屏(§5.3)
为什么用 data-raw-id 而不用 data-idx:data-idx(重映射后的 1, 2, 3)在下次回答中可能变成不同的值——因为 LLM 使用顺序会变;data-raw-id(原始 chunk ID 7, 2, 6)永远稳定。
5. 摘要栏与半屏渲染
5.1 摘要栏
摘要栏位于 Bot 回答气泡下方,展示 LLM 实际引用的文档来源。
折叠/展开交互:
- 默认折叠:显示"📄 引用来源(3)" + "展开 v"
- 点击展开:显示引用列表,箭头变为"收起 ^"
- 无引用时不显示摘要栏(寒暄、KB 未命中场景)
渲染数据源:遍历 idMap → 从 chunkMetadataMap 取 chunk → 去重 → 渲染条目。不是从 chunk_metadata 全量遍历。
5.2 多源差异化
摘要栏和半屏中,根据 source_type 做差异化渲染:
| 元素 | RAG ("rag") | Web ("web") |
|---|---|---|
| 前缀图标 | 📄 fa-file-lines(灰色 #8f959e) | 🌐 fa-globe(灰色 #8f959e) |
| 首行标题 | document_title(如"巡更操作指南 v2.1") | document_title(如"物业巡检流程标准 - 知乎") |
| 副标题 | display_subtitle = 知识库名称 | display_subtitle = 降级链结果 |
| 点击 | 打开半屏(蓝色头) | 打开半屏(绿色头) |
视觉示例(来自 §2 的场景:LLM 用了 C7, C2, C6,列表 3 条):
+----------------------------------------+
| [🌐] 1 物业巡检流程标准 - 知乎 |
| 知乎 · 2025-08-12 |
| |
| [📄] 2 物业管理制度 · 异常上报 |
| 测试知识库20260610 |
| |
| [🌐] 3 物业巡检分类 - 百度百科 |
| 百度百科 · 2025-09-01 |
+----------------------------------------+5.3 引用半屏
用户点击摘要栏条目后,从底部弹出半屏展示 chunk 详情。
| 元素 | RAG | Web |
|---|---|---|
| 顶部图标 | 📄 蓝色:背景 #eff6ff / 字色 #2563eb | 🌐 绿色:背景 #f0fdf4 / 字色 #16a34a |
| 标题行 | document_title | document_title |
| 副标题行 | display_subtitle | display_subtitle |
| 正文区 | 段落原文 | 搜索摘要或抓取片段 |
| "完整原文"按钮 | 依据 url 字段(可空则置灰禁用) | 永远启用(Web URL 必填) |
| Office 提示 | 当 url 后缀为 .doc/.pdf 时显示提示条 | 不显示 |
上标本身不做源类型区分:上标保持统一蓝色样式,只承担"出处序号"职责,源类型信息由半屏/摘要栏体现。
5.4 前端实现原则
- 副标题:直接用
display_subtitle,不做任何site_name/published_date拼接 — 降级链在 Dify 平台侧已算好 - 图标:按
source_type分支选择fa-file-lines或fa-globe - "完整原文"按钮:仅判断
url是否非空 - 微信小程序:推荐用 iconfont/SVG 组件实现图标
6. 视觉与交互规范
6.1 上标样式
颜色: #2563eb (蓝色)
字号: 0.75em (相对于正文)
字重: 600
对齐: 上标 (vertical-align: super)
内边距: 0 1px上标为纯视觉标记,不可点击,无 hover 效果。
6.2 交互行为
| 元素 | 行为 |
|---|---|
| 上标 ¹ ² ³ | 纯视觉标记,不可交互 |
| 摘要栏折叠态 | 显示引用数量,点击头部展开列表 |
| 摘要栏展开态 | 条目整行可点击(行高 ≥44px),active 态浅灰背景 |
| 半屏 | 底部弹出,下拉/点击遮罩关闭 |
设计理由:移动端上标太小不适合做 tap target,用户查看来源通过底部摘要栏即可。
7. 局限与边界
| 局限 | 说明 | 缓解措施 |
|---|---|---|
| idMap 依赖流式完整性 | 流被截断(用户关闭、断网)时摘要栏少渲染 | message_end 时强制 flush;半屏仍可正常打开 |
| 跨轮编号不持久 | 每轮 idMap 重置,用户无法跨轮追溯"上次的引用 2" | 暂不持久化;未来可按需加 session_citation_index |
| 缓冲超长兜底 | [C:12345... 超过 20 字符会被 flush 第一个 [ | 实际不触发(chunk ID ≤2 位),防御性设计 |
| node_finished 版本依赖 | 部分 Dify 版本可能不兼容 | Plan B:HTTP 节点 + 后端 API 查询 |
注:通用局限(LLM 自标注准确性、段落级而非句子级、Web 数据质量等)见 Dify 平台功能设计 §6。
