Skip to content

QA G-Cite 移动端功能设计

文档版本:v1.2

更新日期:2026-06-16

文档定位:G-Cite 移动端引用渲染的完整 PRD(含全景数据、SSE 事件接收、流式缓冲、引用编号重映射、摘要栏与半屏渲染、多源差异化、Q&A)

所属体系:QA_Chat Agent 的知识库引用渲染方案,属于 QA_闲聊对话_Agent设计.md 的补充实现文档。

配套文档

阅读建议:看 §2 全景数据先建立直觉,再按 §3 → §4 → §5 顺序理解数据接收 → 处理 → 渲染的完整链路。


1. 背景与目标

1.1 适用范围

状态
移动端(微信小程序,主端)✅ 当前唯一在范围
大屏❌ 不在范围
Web❌ 不在范围

本文档所有"前端"均指移动端。

1.2 设计目标

  1. 不破坏流式[C:N] 在流式过程中直接渲染为 <sup>1</sup>,不给用户看到裸括号的机会
  2. 引用编号按 LLM 出现顺序重映射 — 同 chunk 复用同一序号,列表去重(1232 方案)
  3. 多信息源差异化 — RAG(📄 蓝色)和 Web(🌐 绿色)在摘要栏、半屏做视觉区分
  4. 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 一览

idsource_typedocument_titledisplay_subtitlescore用途
1rag巡更操作指南 v2.1测试知识库202606100.73巡检流程
2rag物业管理制度 · 异常上报处理测试知识库202606100.68异常处理
3rag多模态RAG知识库技术方案测试知识库202606100.55技术背景
4rag楼宇设备清单测试知识库202606100.50设备信息
5rag消防应急预案测试知识库202606100.42应急预案
6web物业巡检分类 - 百度百科百度百科 · 2025-09-010.72公网百科
7web物业巡检流程标准 - 知乎知乎 · 2025-08-120.68公网问答
8web巡更系统对比 - CSDNCSDN · 2025-07-150.55公网技术博客
9web异常处理SOP - 博客园博客园 · 2025-06-200.50公网实践
10web巡检频次规范 - 行业站example.com0.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 数组(简化)

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_finisheddata.outputs.chunk_metadata合并节点输出的完整 UnifiedChunk 数组(即 §2.3 的 chunk_metadataCode 节点完成后,LLM 文本流之前
文本流text_chunkdata.textLLM 流式文本 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_titledisplay_subtitlesource_typeurl 等)
  • 生命周期:每轮对话开始时清空,收到 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_metadata POST 到后端 API(key 为 conversation_id),移动端通过 conversation_id 拉取。


4. 流式缓冲与引用编号重映射

4.1 流式缓冲机制

[C:N] 不应该先在屏幕上闪现为裸括号再变成上标。参考 Markdown 渲染器处理 ![alt](url) 的方式——从 [ 开始就缓存,不渲染给用户。

三步判断逻辑(针对 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 结构

文字流(用户看到 ¹ ² ³ ²)

html
<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 出现顺序)

html
<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-idxdata-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 详情。

元素RAGWeb
顶部图标📄 蓝色:背景 #eff6ff / 字色 #2563eb🌐 绿色:背景 #f0fdf4 / 字色 #16a34a
标题行document_titledocument_title
副标题行display_subtitledisplay_subtitle
正文区段落原文搜索摘要或抓取片段
"完整原文"按钮依据 url 字段(可空则置灰禁用)永远启用(Web URL 必填)
Office 提示url 后缀为 .doc/.pdf 时显示提示条不显示

上标本身不做源类型区分:上标保持统一蓝色样式,只承担"出处序号"职责,源类型信息由半屏/摘要栏体现。

5.4 前端实现原则

  • 副标题:直接用 display_subtitle,不做任何 site_name/published_date 拼接 — 降级链在 Dify 平台侧已算好
  • 图标:按 source_type 分支选择 fa-file-linesfa-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

Released under the Private License.