如何生成AI结果

AI 结果的展示方式直接影响用户体验。一次流畅的流式输出、一个设计合理的加载状态、一套完善的 Markdown 渲染方案,共同决定了用户是「感到惊艳」还是「反复等待」。本文系统拆解 AI 结果从生成到展示的完整链路,覆盖设计原则、流式传输、加载状态、结果格式化和错误处理。


一、结果展示设计原则

AI 产品与传统工具的核心差异在于:结果是「生成」出来的,而非「查询」得到的。生成需要时间,而这段等待时间的体验,完全取决于展示设计。

1.1 即时反馈:消除空白等待

用户对「无反馈等待」的容忍阈值大约是 1 秒。超过这个时间没有视觉变化,用户会怀疑系统卡死。AI 生成通常需要 2-30 秒,因此必须做到:

  • 首 Token 时间(TTFT)可视化:从用户提交到第一个字符出现,控制在 500ms 以内,并通过加载动画填补这段空白。
  • 操作确认:用户点击「生成」后,按钮状态立即变化(loading spinner / 禁用),确认请求已被接收。
  • 进度感知:即使无法精确估算剩余时间,也要通过打字机光标、骨架屏脉冲等方式传递「系统正在工作」的信号。

1.2 渐进展示:让内容自己说话

流式输出的本质是渐进展示(Progressive Disclosure)。不要等全部生成完毕再一次性呈现——这会让用户白白等待 5-30 秒,且失去「观察 AI 思考」的信任感。

渐进展示的三个层次:

层次说明适用场景
逐字展示每个 Token 到达即渲染聊天对话、短文本生成
逐段展示按段落/区块缓冲后渲染长文章、结构化报告
分层展示先出框架(标题、列表骨架),再填充内容复杂文档、代码生成

1.3 可读性优先

AI 输出通常包含混合内容:正文、代码、表格、公式、引用。可读性设计的核心是:

  • 排版呼吸感:段落间距、行高、代码块与正文的视觉区分。
  • 语法高亮:代码块必须有正确的语言和语法着色。
  • 结构清晰:标题层级、列表缩进、引用样式都要一致。

二、流式输出实现

流式输出是 AI 结果展示的底层技术基础。目前主流有三种传输方案。

2.1 Server-Sent Events(SSE)

SSE 是目前最广泛使用的 AI 流式传输协议。它基于标准 HTTP,单向推送,天然适配「服务器 → 客户端」的 Token 流场景。

后端实现要点(Node.js):

// Hono 框架示例
app.post('/api/chat', async (c) => {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder()
      for await (const chunk of llm.stream(prompt)) {
        const data = `data: ${JSON.stringify({ content: chunk.text })}\n\n`
        controller.enqueue(encoder.encode(data))
      }
      controller.enqueue(encoder.encode('data: [DONE]\n\n'))
      controller.close()
    }
  })
 
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      'X-Accel-Buffering': 'no', // 防止 Nginx 缓冲
    }
  })
})

前端消费(React + Fetch API):

async function consumeStream(prompt: string, onToken: (text: string) => void) {
  const response = await fetch('/api/chat', {
    method: 'POST',
    body: JSON.stringify({ prompt }),
  })
 
  const reader = response.body!.getReader()
  const decoder = new TextDecoder()
  let buffer = ''
 
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
 
    buffer += decoder.decode(value, { stream: true })
    const lines = buffer.split('\n')
    buffer = lines.pop() || ''
 
    for (const line of lines) {
      if (line.startsWith('data: ') && line !== 'data: [DONE]') {
        const parsed = JSON.parse(line.slice(6))
        onToken(parsed.content)
      }
    }
  }
}

2.2 WebSocket

WebSocket 支持双向通信,适合需要「边生成边交互」的场景——比如用户可以在 AI 生成过程中发送新消息或修改参数。

优势:低延迟、双向通信、支持多路复用。 劣势:需要维护连接状态、断线重连逻辑更复杂、代理和防火墙兼容性不如 SSE。

2.3 方案对比

维度SSEWebSocket长轮询
协议HTTPWSHTTP
方向单向(服务器→客户端)双向请求-响应
延迟低(200-500ms TTFT)极低高(每轮需新请求)
断线重连浏览器自动重连需手动实现无需(短连接)
实现复杂度
代理兼容好(标准 HTTP)一般(需 WS 支持)
适用场景单轮生成多轮交互、协同编辑兼容性兜底

2.4 打字机效果的细节打磨

真正的「打字机效果」不是简单地把每个 Token 拼接到字符串末尾。需要注意:

  • 光标闪烁:流式输出过程中显示闪烁光标(CSS animation: blink 1s step-end infinite),结束后隐藏。
  • 自动滚动:内容增长时自动滚动到底部,但用户手动滚动后暂停自动滚动,避免干扰阅读。
  • Token 缓冲:高频率 Token 到达时(如每 10ms 一个),用 requestAnimationFrame 批量更新 DOM,避免每 Token 一次渲染导致性能问题。
  • 中断保留:用户点击「停止生成」时,保留已生成的内容,而不是清空。

三、加载状态设计

加载状态是用户在「首 Token 到达前」唯一的视觉反馈,设计质量直接决定感知等待时长。

3.1 加载状态方案对比

方案实现方式感知等待适用场景注意事项
骨架屏占位灰块模拟最终布局结构化内容(卡片、列表)需与实际布局匹配
打字指示器三点跳动动画聊天场景轻量、不打扰
进度条确定/不确定进度可预估时长的任务不确定进度条避免虚假预期
文案提示「正在思考...」「正在分析...」所有场景的兜底文案要匹配任务语义
分步指示步骤 1/2/3 可视化多阶段任务(分析→生成→优化)每步完成时更新状态

3.2 等待时长的心理学

研究表明,「有解释的等待」比「无解释的等待」感知时长短 30-40%。因此:

  • 告诉用户在做什么:「正在分析你的需求...」比「加载中...」更好。
  • 给出大致预期:「通常需要 5-10 秒」比什么都不说要好。
  • 阶段性反馈:如果生成过程有多个步骤,每完成一步都更新状态文案。

3.3 骨架屏设计要点

骨架屏(Skeleton Screen)的关键不是「放几个灰色矩形」,而是:

  1. 布局匹配:骨架屏的尺寸和位置要与最终内容一致,减少内容加载后的视觉跳动(Layout Shift)。
  2. 脉冲动画:柔和的亮度脉冲传递「正在加载」信号,避免用户误以为页面卡死。
  3. 渐进替换:内容到达时逐块替换骨架,而非整页切换。

四、结果格式化

AI 模型的输出通常是 Markdown 格式,但原始 Markdown 不能直接呈现给用户。结果格式化的任务是将 Markdown 转换为可读、美观、安全的富文本。

4.1 Markdown 渲染方案对比

方案特点性能安全性
AST 渲染react-markdown + remark-gfmReact 组件化、插件生态丰富中(需优化)需配合 sanitize
HTML 直出marked + dangerouslySetInnerHTML性能高、实现简单必须 sanitize
流式专用FluidMarkdown专为流式场景设计,支持增量解析内置安全处理
自定义解析器markdown-it + 自定义插件灵活度最高可控

4.2 流式 Markdown 的核心挑战

流式输出中的 Markdown 渲染不是「等内容完整再解析」,而是「边接收边渲染」。这带来三个关键问题:

问题一:未闭合的语法标记

Token 逐个到达时,经常遇到 **加粗文字 没有 ** 闭合的情况。解决方案是在渲染前预处理,自动补全未闭合的标记:

function repairIncompleteMarkdown(content: string): string {
  let repaired = content
  // 补全未闭合的代码块
  const fenceCount = (repaired.match(/```/g) || []).length
  if (fenceCount % 2 !== 0) repaired += '\n```'
  // 补全未闭合的粗体
  const boldCount = (repaired.match(/\*\*/g) || []).length
  if (boldCount % 2 !== 0) repaired += '**'
  // 补全未闭合的行内代码
  const inlineCount = (repaired.match(/`/g) || []).length
  if (inlineCount % 2 !== 0) repaired += '`'
  return repaired
}

问题二:频繁重解析的性能开销

每个 Token 到达都触发完整 Markdown 解析,在长回复(数千字)时会造成明显卡顿。优化策略:

  • 分块渲染:将内容按块级边界(段落、标题、代码块)拆分,每个块独立渲染。只有最后一个「正在写入」的块需要重新解析。
  • React.memo + 自定义比较器:已完成的块跳过重新渲染。
  • startTransition:将 Token 到达触发的更新标记为低优先级,避免阻塞用户交互。

问题三:代码块的语法高亮

代码块在流式输出中尤其棘手——在闭合 ``` 到达之前,无法确定代码语言,也无法安全地应用语法高亮。常用策略:

策略说明优点缺点
延迟高亮流式过程中用等宽纯文本显示,闭合后统一高亮简单可靠闭合前后有视觉跳变
渐进高亮每个 Token 到达后重新执行高亮,加 debounce体验连续性能开销大
语言推断检测代码块开头的语言标识,未闭合前按该语言高亮折中方案无语言标识时回退纯文本

推荐使用 Shiki 作为语法高亮引擎——它是 VSCode 的底层高亮器,支持 100+ 语言,主题兼容性好。配合 rehype-highlight 插件可以直接集成到 Markdown 渲染管线中。

4.3 安全性:AI 输出是不可信输入

AI 生成的 Markdown 本质上是不可信内容——用户可以通过 Prompt 注入引导模型输出包含 <script> 标签或 javascript: 链接的恶意内容。渲染器是安全边界,必须做到:

  • 使用 rehype-sanitize 或等效方案,白名单放行安全标签(pacodeimg 等)。
  • 剥离 <script>onerror 事件处理器、javascript: 协议 URL。
  • 对链接添加 rel="noopener noreferrer"target="_blank"

五、错误状态处理

AI 生成过程中可能出现的错误类型和处理策略:

错误类型触发场景处理策略
网络中断用户网络断开保留已生成内容,显示「连接中断」提示,提供「重试」按钮
服务端超时生成时间超过网关限制自动重试 1 次,失败后保留部分内容并提示
模型限流触发 rate limit指数退避重试,显示「当前繁忙,稍后重试」
内容安全触发内容审核策略显示审核提示,不保留被拦截内容
Token 上限达到模型最大输出长度保留已生成内容,提示「已达长度上限」并提供「继续生成」
解析错误SSE 数据格式异常记录日志,尝试恢复流,失败则保留已解析内容

核心原则:永远不要丢弃已生成的内容。 即使生成过程中发生错误,用户已经看到的部分应该保留在界面上,并给出明确的错误说明和可执行的下一步操作。


六、AI 结果生成与展示流程

流程图画布 · 115%
Mermaid 流程图加载中...

七、实战案例

案例一:AI 写作助手的流式输出优化

背景:某 AI 写作工具最初采用「等待完整响应后渲染」的方案,用户平均等待 8-15 秒才能看到结果,跳出率高达 45%。

优化措施

  1. 切换到 SSE 流式输出:首 Token 时间从 8 秒降至 400ms,用户在请求发出后不到半秒即可看到文字开始出现。
  2. 分块 Markdown 渲染:将内容按段落拆分,使用 React.memo 避免重复解析已完成的段落。长文档(3000+ 字)的渲染帧率从 15fps 提升到 55fps。
  3. 骨架屏 → 流式过渡:在等待首 Token 的 400ms 内显示与正文布局一致的骨架屏,首 Token 到达后平滑过渡为流式文本。
  4. 代码块延迟高亮:流式过程中用纯等宽文本显示代码,闭合后调用 Shiki 一次性高亮,消除了代码块的闪烁问题。

效果:用户跳出率从 45% 降至 18%,平均会话时长提升 60%。

案例二:企业级 AI 分析平台的渐进展示

背景:一个面向金融分析师的 AI 平台,生成包含图表、数据表格、文字分析的复杂报告,完整生成需要 20-30 秒。

优化措施

  1. 分层展示策略:先输出报告大纲(标题 + 章节骨架),再逐段填充分析内容。用户在 2 秒内即可看到报告结构。
  2. 分步进度指示:将生成过程拆分为「数据收集 → 趋势分析 → 图表生成 → 报告撰写」四个步骤,每步完成时更新进度条。
  3. 表格渐进渲染:数据表格逐行到达,先显示已有行,新行到达时平滑插入。
  4. 停止与续写:用户可在任何时刻停止生成,已生成的部分保留。停止后提供「从断点继续」选项。

效果:用户满意度评分从 3.2 提升至 4.5(满分 5 分),「等待焦虑」相关反馈减少 70%。


八、无障碍设计

AI 结果展示不能忽略无障碍访问(Accessibility)。流式输出对屏幕阅读器用户尤其具有挑战性——每个 Token 到达都可能触发朗读,导致用户听到的是断断续续的单词流。

关键要求:

  • aria-live="polite":让屏幕阅读器在新内容到达时「等当前朗读完毕再播报」,避免打断。
  • aria-busy="true":流式输出期间标记为忙碌状态,阅读器暂停播报直到生成完成。
  • aria-atomic="false":只播报新增内容,不重复朗读整个回复。
  • 批量播报:将 Token 按 2-3 秒间隔分批推送给阅读器,避免逐字朗读。
  • 不抢占焦点:流式响应开始时不要将焦点从输入框移走。
  • 键盘可达:所有操作按钮(停止、重试、复制)必须可通过键盘访问。

九、实施检查清单

在上线 AI 结果展示功能前,逐项确认:

  • 首 Token 时间(TTFT)控制在 500ms 以内
  • 加载状态已设计并实现(骨架屏 / 打字指示器 / 进度提示)
  • 流式传输使用 SSE 或 WebSocket,且处理了断线重连
  • Markdown 渲染支持流式场景,已处理未闭合语法标记
  • 代码块有语法高亮,且流式过程中不会闪烁
  • 已通过 rehype-sanitize 或等效方案处理 XSS 安全
  • 用户可随时停止生成,已生成内容被保留
  • 错误状态有明确提示和可执行的下一步操作
  • 自动滚动行为合理:默认跟随,用户滚动后暂停
  • 无障碍属性已配置:aria-livearia-busyaria-atomic
  • 所有操作按钮支持键盘访问
  • 长回复(1000+ Token)的渲染性能已验证,无卡顿
  • 移动端适配:触摸滚动流畅、布局不错乱
  • 已测试弱网环境下的降级表现

十、参考资料

  1. The Complete Guide to Streaming LLM Responses in Web Applications — SSE 到实时 UI 的完整实现指南
  2. How To Build a Performant AI Markdown Renderer — 流式 Markdown 渲染的性能优化方案
  3. Streaming — AI UX Pattern — AI 流式输出的 UX 设计模式
  4. antgroup/FluidMarkdown — 面向移动端的流式 Markdown 渲染引擎
  5. Claude API Streaming (SSE) in Practice — Claude API 流式输出的实践指南
  6. The Streamable-UI Pattern: Turn Chat Into a Live React Dashboard — 可流式 UI 模式:将聊天变为实时 React 仪表盘
  7. Ephibbs/flowtoken — 流式 LLM 输出的动画与样式库
  8. 前端流式输出 3 种实现 — Fetch API、SSE、WebSocket 三种前端实现方式对比