Files
012-kaopeilian/docs/规划/流式输出视觉呈现规范.md
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

30 KiB
Raw Blame History

流式输出视觉呈现规范

版本v1.3
更新日期2026-01-18
适用范围:联系人侧边栏、智能回复等所有 AI 流式输出场景
关联规范:瑞小美AI接入规范技术栈标准


〇、与现有架构的关系

后端:基于 AIService.chat_stream()

本规范的 SSE 事件协议是对现有 AIService.chat_stream()扩展,需后端配合改造:

# 现有实现(仅输出 AI 文本)
async for chunk in ai.chat_stream(messages, prompt_name="analysis"):
    yield chunk

# 扩展实现(支持本规范的事件协议)
async def enhanced_stream(messages, steps, prompt_name):
    # 1. 发送 start 事件
    yield sse_event("start", {"request_id": req_id})
    
    # 2. 执行准备步骤并发送 step 事件
    for i, step in enumerate(steps):
        yield sse_event("step", {"index": i, "status": "active", **step})
        await step.execute()
        yield sse_event("step", {"index": i, "status": "done", **step})
    
    # 3. 调用 AI 并发送 thinking 事件
    async for chunk in ai.chat_stream(messages, prompt_name=prompt_name):
        if is_thinking_content(chunk):
            yield sse_event("thinking", {"text": chunk})
        else:
            # 4. 解析结构化结果并发送 field 事件
            result = parse_ai_json_response(accumulated_text)
            yield sse_event("result_start", {"fields": FIELD_DEFINITIONS})
            for key, value in result.items():
                yield sse_event("field", {"key": key, "value": value})
    
    # 5. 发送 complete 事件
    yield sse_event("complete", {"tokens_used": response.total_tokens})

前端:扩展现有组件

现有组件 本规范组件 关系
stream.js useAIStream.ts 扩展,增加阶段状态管理
useTypewriter.js TypewriterList.vue 复用,用于列表打字效果
AIStreamDisplay.vue AIStreamContainer.vue 升级替换,支持三阶段

一、设计目标

  1. Agent 步骤感:让用户清晰感知 AI 正在执行的每个步骤
  2. 思考过程透明:展示 AI 的推理过程,增强信任感
  3. 渐进式呈现:结果逐步填充,而非一次性展示
  4. 类 Cursor 体验:思考内容渐隐消失,聚焦最新进展

二、展示模式

根据 AI 功能的使用场景,分为两种展示模式:

2.1 独立页面模式Full Page Mode

适用于功能复杂、结果丰富的 AI 分析,如:消费意向预测、客户画像、项目推荐等。

特点

  • 占据整个页面/Tab 区域
  • 完整展示三阶段(准备→思考→渲染)
  • 支持多模块骨架屏渐进填充

2.2 内嵌卡片模式Inline Card Mode

适用于轻量级 AI 功能嵌入在列表项、会话卡片等位置AI 会话摘要、快速标签等。

状态机

┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│    IDLE     │ ───▶ │  LOADING    │ ───▶ │  COMPLETE   │
│  待触发状态  │      │   加载中     │      │   结果展示   │
└─────────────┘      └─────────────┘      └─────────────┘

必须满足的行为规范

状态 必须呈现的元素 必须支持的交互
IDLE 可点击的触发入口(如按钮/链接) 点击后进入 LOADING
LOADING 加载指示器 + 提示文案 可选:支持取消
COMPLETE AI 生成的结果内容 必须:提供"重新生成"入口
ERROR 错误提示 必须:可点击重试

视觉示意

IDLE待触发

┌────────────────────────────────────────────────────────┐
│  19:12 - 19:25  来自: 瑞小美轻医美—农农    10条对话     │
│                                                        │
│     ✨生成AI摘要   |   查看聊天详情                    │
│                                                        │
└────────────────────────────────────────────────────────┘

LOADING加载中

┌────────────────────────────────────────────────────────┐
│  19:12 - 19:25  来自: 瑞小美轻医美—农农    10条对话     │
│                                                        │
│     ○ 正在生成摘要...                                  │
│                                                        │
└────────────────────────────────────────────────────────┘

COMPLETE结果展示

┌────────────────────────────────────────────────────────┐
│  19:12 - 19:25  来自: 瑞小美轻医美—农农    10条对话     │
│                                                        │
│  ┌──────────────────────────────────────────────────┐  │
│  │  📋 AI会话摘要                          [重新生成] │  │
│  │                                                  │  │
│  │  • 员工推送年底消费回馈现金券活动                  │  │
│  │  • 客户对活动内容表示疑惑                         │  │
│  │  • 客户连续追问未获及时回复                       │  │
│  │  • 客户因等待过久表达不满情绪                     │  │
│  │  • 邀约到店领券尚未得到响应                       │  │
│  └──────────────────────────────────────────────────┘  │
│                                                        │
│                          查看聊天详情                  │
│                                                        │
└────────────────────────────────────────────────────────┘

缓存规则

场景 行为
无缓存 显示 IDLE 状态,等待用户触发
有缓存 直接显示 COMPLETE 状态,跳过 IDLE
重新生成 从 COMPLETE 进入 LOADING完成后更新结果

与独立页面模式的区别

特性 独立页面模式 内嵌卡片模式
空间 整个页面/Tab 嵌入在卡片/列表项内
准备阶段 展示步骤列表 省略
思考阶段 展示 <thinking> 内容 省略
渲染阶段 骨架屏渐进填充 直接展示完整结果
初始触发 自动开始 等待用户点击

三、三阶段状态机(独立页面模式)

以下内容适用于独立页面模式

┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  PREPARING  │ ───▶ │  THINKING   │ ───▶ │  RENDERING  │ ───▶ COMPLETE
│   准备阶段   │      │   思考阶段   │      │   渲染阶段   │
└─────────────┘      └─────────────┘      └─────────────┘
      │                    │                    │
      ▼                    ▼                    ▼
  步骤逐个打勾        步骤完成+思考区域      骨架屏+渐进填充

3.1 阶段定义

阶段 常量 触发条件 结束条件
准备阶段 PREPARING 请求开始 所有步骤完成
思考阶段 THINKING 收到第一个 thinking 事件 收到 result_start 事件
渲染阶段 RENDERING 收到 result_start 事件 收到 complete 事件
完成状态 COMPLETE 收到 complete 事件 -
错误状态 ERROR 收到 error 事件 或 请求异常 用户重试

四、视觉规范(独立页面模式)

4.1 准备阶段PREPARING

┌────────────────────────────────────────┐
│  🔄 准备分析数据...                     │
│                                        │
│  ✅ 获取客户信息                        │  ← 已完成
│  ◐ 查询CRM数据                         │  ← 进行中
│  ○ 加载对话记录                        │  ← 待完成
│  ○ 启动AI分析                          │
│                                        │
└────────────────────────────────────────┘

必须实现的行为

步骤状态 必须呈现的元素
已完成 (done) 勾号图标 + 完成态样式
进行中 (active) 加载动画 + 高亮样式
待完成 (pending) 空心/灰色图标 + 弱化样式

步骤数据结构

interface PrepareStep {
  key: string
  text: string
}

4.2 思考阶段THINKING

┌────────────────────────────────────────┐
│  🔄 准备分析数据...                     │
│                                        │
│  ✅ 获取客户信息                        │
│  ✅ 查询CRM数据                         │
│  ✅ 加载对话记录                        │
│  ◐ AI 思考中                           │  ← 保持转圈
│                                        │
│  ─────────────────────────────────────  │
│                                        │
│  ┌──────────────────────────────────┐  │
│  │                     ▲ 渐隐遮罩    │  │
│  │  ...析客户消费偏好发现近3个月  │  │
│  │  主要消费集中在光电类项目...     │  │
│  │  检查到院记录上次到店是12月    │  │
│  │  15日做的是热玛吉面部...       │  │
│  │  结合咨询铺垫记录,客户对抗衰█   │  │  ← 闪烁光标
│  └──────────────────────────────────┘  │
│                                        │
│  💡 AI正在分析CRM数据和对话记录...      │
└────────────────────────────────────────┘

必须实现的行为

行为 说明
步骤列表保留 前序步骤显示完成状态,最后一步保持"AI思考中"
思考内容展示 使用淡色字体,区别于正式结果
自动滚动 新内容出现时自动滚动到底部
渐隐效果 顶部内容渐隐消失,仅保留最后几行
闪烁光标 末尾显示闪烁光标,表示内容仍在输出

类 Cursor 效果的核心

  • 固定高度容器,overflow: hidden
  • 顶部渐变遮罩实现渐隐
  • 只显示最后 N 行(建议 5-8 行)
  • 内容自动滚动到底部

4.3 渲染阶段RENDERING

当收到 result_start 事件时,页面平滑切换到结果视图。

骨架屏示意

┌────────────────────────────────────────┐
│  🛒 可能消费项目                        │  ← 标题立即显示
│  ┌───────┐ ┌───────┐ ┌───────┐        │
│  │░░░░░░░│ │░░░░░░░│ │░░░░░░░│        │  ← 标签骨架
│  └───────┘ └───────┘ └───────┘        │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│  ✅ 触发因素                            │
│  ├── ░░░░░░░░░░░░░░░░░░░░░░░░         │  ← 列表骨架
│  ├── ░░░░░░░░░░░░░░░░░░               │
│  └── ░░░░░░░░░░░░░░░░░░░░░            │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│  ⚠️ 阻碍因素                            │
│  ├── ░░░░░░░░░░░░░░░░░░░░░░           │
│  └── ░░░░░░░░░░░░░░░░░░░░░░░░░        │
└────────────────────────────────────────┘

必须实现的行为

行为 说明
阶段切换 从思考阶段平滑过渡到渲染阶段,有淡入淡出动画
骨架屏先行 收到 result_start 时立即显示所有模块的骨架屏
模块标题可见 每个模块的标题和图标立即显示,内容区为骨架
渐进填充 收到 field 事件时,对应模块骨架被实际内容替换
内容动画 内容出现时有渐进动画(打字机效果 或 淡入效果)

字段定义结构

后端在 result_start 事件中声明字段顺序,前端据此渲染骨架:

interface FieldDefinition {
  key: string
  title: string
  type: 'tags' | 'list' | 'text'
  icon?: string
}

字段类型与填充方式

类型 说明 建议填充效果
tags 标签列表 标签逐个淡入
list 文本列表 逐行出现,可选打字机效果
text 普通文本 打字机效果或直接显示

五、SSE 事件协议

5.1 事件类型定义(推荐方案)

事件类型 说明 触发阶段转换
start 流开始 → PREPARING
step 步骤状态更新 -
thinking AI 思考内容(流式) → THINKING
result_start 结果开始,声明字段结构 → RENDERING
field 字段值更新 -
complete 流结束 → COMPLETE
error 错误 → ERROR

5.2 事件数据格式

start

{
  "event": "start",
  "data": {
    "request_id": "req_123456",
    "message": "开始分析"
  }
}

step

{
  "event": "step",
  "data": {
    "index": 0,
    "key": "customer",
    "text": "获取客户信息",
    "status": "done"  // pending | active | done
  }
}

thinking

{
  "event": "thinking",
  "data": {
    "text": "正在分析客户消费偏好发现近3个月..."
  }
}

注意thinking 事件会多次发送,每次携带一小段文本,前端累积拼接。

result_start

{
  "event": "result_start",
  "data": {
    "fields": [
      { "key": "likely_items", "title": "可能消费项目", "type": "tags" },
      { "key": "trigger_factors", "title": "触发因素", "type": "list" },
      { "key": "blocking_factors", "title": "阻碍因素", "type": "list" },
      { "key": "suggestions", "title": "跟进建议", "type": "list" }
    ]
  }
}

field

{
  "event": "field",
  "data": {
    "key": "likely_items",
    "value": ["光电类项目(创始人卡续费/使用)", "皮肤管理项目", "抗衰项目"]
  }
}
{
  "event": "field", 
  "data": {
    "key": "trigger_factors",
    "value": [
      "客户已主动预约明天下午2点半到店",
      "客户互动意愿强烈(主动说'你也太容易放弃了吧'",
      "高净值客户累计消费13万+客单价1.8万+",
      "持有创始人卡项,有消费习惯和忠诚度"
    ]
  }
}

complete

{
  "event": "complete",
  "data": {
    "request_id": "req_123456",
    "tokens_used": 1250,
    "duration_ms": 3500
  }
}

error

{
  "event": "error",
  "data": {
    "code": "AI_TIMEOUT",
    "message": "AI 分析超时,请重试"
  }
}

5.3 完整事件流示例

event: start
data: {"request_id": "req_123", "message": "开始分析"}

event: step
data: {"index": 0, "key": "customer", "text": "获取客户信息", "status": "active"}

event: step
data: {"index": 0, "key": "customer", "text": "获取客户信息", "status": "done"}

event: step
data: {"index": 1, "key": "crm", "text": "查询CRM数据", "status": "active"}

event: step
data: {"index": 1, "key": "crm", "text": "查询CRM数据", "status": "done"}

event: step
data: {"index": 2, "key": "chat", "text": "加载对话记录", "status": "active"}

event: step
data: {"index": 2, "key": "chat", "text": "加载对话记录", "status": "done"}

event: step
data: {"index": 3, "key": "ai", "text": "AI 思考中", "status": "active"}

event: thinking
data: {"text": "正在分析客户消费偏好..."}

event: thinking
data: {"text": "发现近3个月主要消费集中在光电类项目..."}

event: thinking
data: {"text": "检查到院记录上次到店是12月15日..."}

event: thinking
data: {"text": "结合咨询铺垫记录,客户对抗衰项目有明确兴趣..."}

event: result_start
data: {"fields": [{"key": "likely_items", "title": "可能消费项目", "type": "tags"}, ...]}

event: field
data: {"key": "likely_items", "value": ["光电类项目", "皮肤管理项目", "抗衰项目"]}

event: field
data: {"key": "trigger_factors", "value": ["客户已主动预约...", "客户互动意愿强烈...", ...]}

event: field
data: {"key": "blocking_factors", "value": ["创始人卡项可能已过期...", "无明确咨询铺垫项目...", ...]}

event: field
data: {"key": "suggestions", "value": ["建议确认创始人卡续期情况", "可推荐当季抗衰活动"]}

event: complete
data: {"request_id": "req_123", "tokens_used": 1250, "duration_ms": 3500}

六、前端组件架构

6.1 组件层次(建议)

<AIStreamContainer>              <!-- 流式输出容器 -->
  ├── <PrepareSteps />           <!-- 准备步骤组件 -->
  ├── <ThinkingArea />           <!-- 思考区域组件 -->
  └── <ResultRenderer />         <!-- 结果骨架/内容组件 -->

6.2 状态管理

推荐封装 useAIStream Composable管理以下状态

// 核心类型定义
type Phase = 'idle' | 'preparing' | 'thinking' | 'rendering' | 'complete' | 'error'
type StepStatus = 'pending' | 'active' | 'done'

interface PrepareStep {
  key: string
  text: string
}

interface FieldDefinition {
  key: string
  title: string
  type: 'tags' | 'list' | 'text'
  icon?: string
}

// Composable 返回值
interface UseAIStreamReturn {
  phase: Ref<Phase>                          // 当前阶段
  isLoading: ComputedRef<boolean>            // 是否加载中
  prepareSteps: ComputedRef<StepWithStatus[]> // 步骤列表(含状态)
  thinkingText: Ref<string>                  // 思考内容
  resultFields: Ref<FieldDefinition[]>       // 结果字段定义
  resultData: Ref<Record<string, unknown>>   // 结果数据
  error: Ref<Error | null>                   // 错误信息
  start: (url: string, params?: Record<string, unknown>) => Promise<void>
}

6.3 SSE 事件与状态映射

SSE 事件 状态变更
start phasepreparing
step 更新对应步骤状态
thinking phasethinking,追加 thinkingText
result_start phaserendering,设置 resultFields
field 更新 resultData[key]
complete phasecomplete
error phaseerror,设置 error

七、动画效果要求

具体动画参数由开发自行决定,以下为必须实现的效果:

独立页面模式

效果 要求
步骤完成 有明显的"打勾"反馈(如弹出、闪烁)
思考滚动 新内容出现时自动滚动到底部,旧内容渐隐上移
阶段切换 有过渡动画,避免突兀跳转
骨架屏 有脉冲/闪烁效果表示加载中
内容填充 渐进式出现(打字机或淡入均可)

内嵌卡片模式

效果 要求
加载状态 有旋转/跳动等动态指示器
结果展开 有展开动画,避免突然出现
列表项 建议逐条淡入,增强流式感

八、错误处理

8.1 错误类型

错误码 说明 用户提示
NETWORK_ERROR 网络异常 "网络连接失败,请检查网络后重试"
AI_TIMEOUT AI 响应超时 "AI 分析超时,请稍后重试"
AI_QUOTA_EXCEEDED API 配额不足 "AI 服务暂时不可用,请联系管理员"
DATA_NOT_FOUND 数据不存在 "未找到客户数据,请确认客户信息"
PARSE_ERROR 解析失败 "数据解析失败,请重试"

8.2 错误界面

┌────────────────────────────────────────┐
│                                        │
│            ⚠️                          │
│                                        │
│      AI 分析超时,请稍后重试            │
│                                        │
│         [ 🔄 重试 ]                     │
│                                        │
└────────────────────────────────────────┘

九、缓存与重新分析规范(强制)

⚠️ 强制要求:所有 AI 分析功能必须支持缓存机制和重新分析能力

9.1 核心原则

原则 要求
优先缓存 如有有效缓存,必须首先加载缓存数据,跳过流式请求
重新分析 页面必须提供"重新分析"按钮,允许用户强制刷新
缓存标识 必须展示数据来源(缓存/实时),让用户知晓数据时效

9.2 缓存状态定义

在三阶段状态机基础上,新增 CACHED 状态:

┌─────────────┐
│   CACHED    │  ◀──── 有有效缓存时直接进入
│  缓存加载    │
└──────┬──────┘
       │ 用户点击"重新分析"
       ▼
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  PREPARING  │ ───▶ │  THINKING   │ ───▶ │  RENDERING  │ ───▶ COMPLETE
└─────────────┘      └─────────────┘      └─────────────┘

9.3 界面规范

缓存数据展示

┌────────────────────────────────────────┐
│  🛒 消费意向分析                         │
│                                        │
│  ┌──────────────────────────────────┐  │
│  │  💡 数据来源缓存2小时前分析   │  │  ← 缓存标识
│  └──────────────────────────────────┘  │
│                                        │
│  📊 消费概率75%(高意向)              │
│  🛍️ 可能项目:光电类、皮肤管理           │
│  ...(完整结果内容)...                  │
│                                        │
│  ─────────────────────────────────────  │
│                                        │
│         [ 🔄 重新分析 ]                  │  ← 必须有此按钮
│                                        │
└────────────────────────────────────────┘

必须呈现的元素

元素 要求
缓存标识 弱化样式,显示缓存时间(如"2小时前分析"
重新分析按钮 明确可点击,点击后进入流式请求流程

9.4 前端实现要求

缓存相关数据结构:

interface CachedResult {
  data: Record<string, unknown>
  cachedAt: string              // ISO 8601 时间戳
  validUntil: string            // 过期时间
}

必须实现的逻辑

  1. 页面加载时优先检查缓存
  2. 有效缓存直接展示,跳过流式请求
  3. 提供"重新分析"功能,触发 force_refresh

9.5 后端 API 要求

所有 AI 分析接口必须支持 force_refresh 参数:

GET /api/sidebar/purchase-intent?external_userid=xxx&force_refresh=true
参数 类型 说明
force_refresh boolean true 时跳过缓存,强制重新分析

响应中包含缓存元信息

{
  "code": 0,
  "data": {
    "probability": 0.75,
    "likely_items": ["光电类", "皮肤管理"],
    "...": "..."
  },
  "meta": {
    "from_cache": true,
    "cached_at": "2026-01-18T10:30:00+08:00",
    "valid_until": "2026-01-19T10:30:00+08:00"
  }
}

9.6 缓存有效期建议

分析类型 建议有效期 说明
消费意向预测 24 小时 数据变化不频繁
客户画像 12 小时 对话可能更新
项目推荐 24 小时 依赖消费历史
接待策略 2 小时 到店场景需实时

十、AI 响应解析与容错

参考:瑞小美AI接入规范 - AI 响应解析规范章节

10.1 后端解析要求

AI 返回的 JSON 可能存在格式问题,后端必须使用公共解析函数:

from shared_backend.services.ai_service import parse_ai_json_response, safe_parse_ai_json

# 在发送 field 事件前解析 AI 输出
try:
    result, thinking = parse_ai_json_response(ai_response.content)
    
    # 发送 thinking 事件(如有)
    if thinking:
        yield sse_event("thinking", {"text": thinking})
    
    # 发送 result_start 和 field 事件
    yield sse_event("result_start", {"fields": FIELD_DEFINITIONS})
    for key, value in result.items():
        yield sse_event("field", {"key": key, "value": value})
        
except json.JSONDecodeError as e:
    # 解析失败,发送错误事件
    yield sse_event("error", {
        "code": "PARSE_ERROR",
        "message": f"AI 输出解析失败: {str(e)}"
    })

10.2 推荐:使用 json-repair 库

对于 JSON 格式问题较多的场景,推荐使用 json-repairGitHub 4000+ 星):

from json_repair import loads as json_repair_loads

# 自动修复常见 JSON 格式问题
result = json_repair_loads(ai_raw_output)

能修复的问题

  • 单引号 → 双引号
  • 未闭合括号补齐
  • True/False/Nonetrue/false/null
  • 尾部逗号移除
  • Markdown 代码块提取

10.3 前端容错处理

前端收到 field 事件时,应做类型校验:

function onField(data: { key: string; value: unknown }): void {
  const { key, value } = data
  
  // 类型校验
  const fieldDef = resultFields.value.find(f => f.key === key)
  if (!fieldDef) return
  
  // 根据类型校验 value
  if (fieldDef.type === 'tags' && !Array.isArray(value)) {
    console.warn(`字段 ${key} 期望数组,实际: ${typeof value}`)
    resultData.value[key] = []
    return
  }
  
  if (fieldDef.type === 'list' && !Array.isArray(value)) {
    console.warn(`字段 ${key} 期望数组,实际: ${typeof value}`)
    resultData.value[key] = []
    return
  }
  
  resultData.value[key] = value
}

十一、性能优化建议

  1. 防抖思考文本更新:每 50ms 批量更新一次 DOM避免频繁重绘
  2. 虚拟滚动:思考区域内容过长时使用虚拟滚动
  3. 骨架屏预渲染:结果骨架使用 v-show 而非 v-if
  4. 动画硬件加速:使用 transformopacity,避免触发重排

十二、兼容性说明

  • SSE 支持:所有现代浏览器均支持 fetch + ReadableStream
  • CSS 动画:使用标准 CSS3 动画,兼容 Chrome 60+, Safari 12+, Firefox 55+
  • 字体回退:等宽字体使用系统回退链
  • 字符编码UTF-8符合技术栈标准
font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Code", "Consolas", monospace;

十三、待确认事项

  • 后端 SSE 事件协议改造排期
  • 是否需要支持中断/取消流式请求
  • 移动端适配细节(触摸滚动、字体大小)
  • 是否需要保存分析历史 → 已确认:必须支持缓存,详见第九章

最后更新2026-01-18