Files
DevFlow/docs/02-架构设计/aichat流式Markdown渲染调研-2026-06-15.md
绝尘 04032a2a8d 重构: 文档汇总+进度看板+孤儿任务清理脚本+gitignore 噪音排除
- docs/02 架构设计: 新增 aichat审查/异步审批构想/流式渲染调研/generating状态机/密钥迁移健壮性/工作流脚本执行边界/条件表达式引擎/F-07 trait下沉/Agent架构说明/任务推进构想/功能创意池;更新功能决策记录+归档/对抗论证/文档记录规范/经验记录
- docs/03 模块文档: 新增 AI对话引擎/DAG引擎详解;更新 df-knowledge/df-nodes/df-storage/df-workflow/df-ai
- docs/05 代码审查: 新增 全栈审查/全局review/架构审查/近期改动审查/工作区多角度走查/自研memo流式渲染审查
- docs/09 问题排查: 新增 aichat-apikey-401
- docs/INDEX+README 索引同步;docs/todo 待办看板(2026-06-15 汇总)
- PROGRESS.md Sprint 22-25;URGENT.md 加急清单快照(5 项 P0 已全修)
- scripts/cleanup_orphan_tasks.{py,sh} 孤儿任务清理工具
- .gitignore 补 *.broken.bak + tmp/ 噪音排除
2026-06-15 05:14:21 +08:00

147 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI Chat 流式 Markdown 渲染调研2026-06-15
> 范围:`AiChat.vue` 流式渲染优化。当前 AR-1 修复commit f58743e用「流式纯文本短路」防掉帧副作用是流式过程无格式。本调研找不掉帧且有格式的更好方案。
> 方法3 路并行调研(机制方案 / 现成库 / Vue3 落地)+ 主代理整合。
> 性质:调研 + 方案,**未实施**。
> 关联:[aichat审查报告-2026-06-14.md](aichat审查报告-2026-06-14.md) AR-1[近期改动代码审查-2026-06-15.md](../05-代码审查/近期改动代码审查-2026-06-15.md) §亮点「renderMd 流式纯文本短路」。
---
## §1 问题根因
现状(`src/components/AiChat.vue:200,347`
```vue
<div v-html="renderMd(text, isLastAi(msg) && store.state.streaming)"></div>
```
```js
function renderMd(text, isStreaming=false){
if (isStreaming || !mdReady) return escapeHtml(text) // 流式纯文本短路
return _purify.sanitize(_marked.parse(text))
}
```
掉帧两因素叠加:
- **(A) 解析成本**:每个 delta 全量 `marked.parse()` O(N)N 随回复增长,后期单帧解析几十 ms。
- **(B) 重渲染成本**`v-html` 整段替换触发子树重布局。
当前方案牺牲「流式格式」躲掉 (A)(B),但 delta 20-80ms 一个,流式时用户看到裸 markdown 源码(`#``-`、```` ``` ```` 符号),结束才出格式。
---
## §2 五大机制对比
| 机制 | 消除瓶颈 | 流式有格式 | 成本 | 适用 | 独立够吗 |
|---|---|---|---|---|---|
| 1. rAF 批量节流 | 中(砍频率) | 是 | 低 | 全部 | 否(长文本单次仍重) |
| 2. 块级 diff + memo | **高**(砍到 O(末块) | 是 | 低-中 | 全部 | **基本够(主流首选)** |
| 3. Web Worker 解析 | 高(主线程归零) | 是 | 中 | 超长文档/重 sanitize | 通常与 2 叠加 |
| 4a. 代码块延迟高亮 | 高(代码块零成本) | 代码块流式无色 | 低 | 有代码块 | 仅代码块,需叠加 |
| 4b. shiki-stream 增量高亮 | 高 | 代码块流式有色 | 中 | 有代码块 | 仅代码块 |
| 5. 虚拟滚动 | 中(砍 DOM 不砍 parse | 是 | 中 | 长会话消息列表 | 否 |
**机制 2块级 diff + memo是业界主流首选**——Vercel AI SDK 官方 cookbook 做法。原理:`marked.lexer()` 按双换行/代码围栏切块,每块独立 memoize只有「正在生长的末块」重 parse已完成块缓存跳过。解析成本从 O(全文) 降到 O(末块)。
**大厂可见行为**ChatGPT / Claude / Gemini 流式时均有格式,代码块流式时等宽 + 基本着色。Chrome 官方文档明确反对「整篇重 parse + innerHTML 替换」(正是当前实现),推荐 streaming-markdown 增量 append。
**rAF 实测数据**SitepointReact 18.3 生产构建 M2朴素逐 token setState 平均 commit 18ms80 token/s 达 52ms 可见卡顿rAF 批量后 3-5ms。
---
## §3 现成库评估
| 库 | Vue3 兼容 | 流式 | 安全 | 维护 | 结论 |
|---|---|---|---|---|---|
| **markstream-vue** | ✅ 原生组件 | ✅ 双模式(虚拟窗口 + 增量批处理) | ✅ 内置 safe HTML | ✅ 2.2k star / open issue 2 / 近乎日更 / 尤雨溪背书 / 1.0 稳定 | **首选** |
| streamdown (Vercel) | ❌ React 绑定 + 强绑 Tailwind/shadcn | ✅ | ✅ rehype-harden | ✅ | 仅参考issue #19 求 Vue 版未解决) |
| streaming-markdown | ⚠️ callback 驱动需自己接线 | ✅ token 级 | ❌ 需自配 | 功能不全 | 参考 |
| marked当前用 | ✅ | ❌ 无官方流式 API | ❌ 需自配 DOMPurify | ✅ 36.9k star | 不直用做流式层 |
| markdown-it | ✅ | ❌ 无原生流式 | - | ✅ | 同上 |
| antfu/shiki-stream | ✅ 组件 | ✅ 增量高亮 | - | ✅ 511 star | 代码块高亮专库 |
**核心结论**:有 Vue3 原生可直接用的流式 markdown 库 **markstream-vue**尤雨溪推荐命中全部诉求——流式有格式、不掉帧、TS-first、内置 sanitize、`final` prop 解未闭合 token 卡死。内部已含 marked + Shiki + sanitize 组合,等于替你造好轮子。
**降级路径**:若样式侵入太重,只用其解析内核 `stream-markdown-parser`(框架无关纯函数)+ 现有 DOMPurify 自控渲染。
---
## §4 落地方案(递进)
### 方案 ArAF 节流渲染(最小改动)
流式时 `requestAnimationFrame` 每帧最多 parse 一次(封顶 60fps`v-html` 绑响应式 `streamingHtml`。流式结束取消 rAF + 强制最终渲染落 mdCache。
- 性能:频率从「每 delta」降到「每帧」主线程有空隙让浏览器 paint。
- 观感:流式全程有格式;**瑕疵**:未闭合代码块(末尾 ```)会闪烁。
- 成本:低(~60 行)。
- 风险:超长回答(>20k 字)每帧 parse 全文仍偏重。
```js
const streamingHtml = ref('')
let rafId = null, lastStreamText = ''
function scheduleStreamParse(text){
if (rafId !== null) return
rafId = requestAnimationFrame(()=>{
rafId = null
if (text === lastStreamText) return
lastStreamText = text
streamingHtml.value = _purify.sanitize(_marked.parse(text))
})
}
watch(()=>store.state.streaming,(s,old)=>{
if(old && !s){ if(rafId) cancelAnimationFrame(rafId); rafId=null; lastStreamText=''; streamingHtml.value='' }
})
onBeforeUnmount(()=>{ if(rafId) cancelAnimationFrame(rafId) })
```
### 方案 B方案 A + 代码块流式降级(体验最佳,推荐起步)
在 A 基础上,流式期把 fenced code 块正则抠出降级为纯文本(`<pre class="md-code-streaming">`),其余正常 marked。流式结束一次完整 marked 替换。
- 消掉方案 A 的「未闭合代码块闪烁」瑕疵——AI 回答大量含代码块,高频可见。
- 成本:中(~100 行 + CSS
- **推荐起步方案**A→B 平滑叠加B 只替换 `doStreamParse` 一个函数,不返工)。
### 方案 CWeb Worker彻底解耦
marked+sanitize 移 Worker主线程零 parse。Worker 内 DOMPurify 需 jsdom shim或换 `sanitize-html` 免 DOM
- 成本:高(~150 行 + worker 文件 + jsdom
- 适用:极端长回答/多会话。方案 B 不够再上。
### 方案 D换库直上 markstream-vue
用 `<MarkdownRender :content :final />` 替换整个 renderMd 手写层。
- 成本:中(库接入 + 样式对接)。
- 收益:省自造轮子,拿到虚拟窗口 + 增量高亮 + 未闭合处理全套。
- 风险:样式侵入、新依赖、迁移工作量需评估。
---
## §5 推荐结论
**两条路,按风险偏好二选一**
| 路线 | 方案 | 收益 | 风险 | 工作量 |
|---|---|---|---|---|
| **保守(自研改造)** | 方案 BrAF 节流 + 代码块降级) | 流式全程有格式、不掉帧、消代码块闪烁、无新依赖 | 超长回答边界需观察 | 中(~100 行 + CSS |
| **激进(换库)** | 方案 Dmarkstream-vue | 同上 + 虚拟窗口 + 增量高亮 + 长期省维护 | 新依赖、样式侵入、迁移成本 | 中-高 |
**渐进建议**:先上方案 B 跑通(验证 rAF 节流 + 代码块降级在当前 delta 频率下不掉帧)→ 观察长回答边界 → 若需更强能力(虚拟窗口/增量高亮)再评估换 markstream-vue。
**当前 AR-1 临时方案(流式纯文本短路)由方案 D 替换后退役**——纯增量收益,流式全程有格式。
> **决策2026-06-15选方案 D**(换 markstream-vue。理由自研块级 memo 与 D 复杂度接近D 白送 shiki 代码高亮 + 虚拟窗口 + 未闭合 token 修复器,自研只在「坚决不引新依赖」时才划算。落地 todo:ARC-260615-08。
>
> **决策转向2026-06-15同日复盘方案 D 试装后弃用,改自研块级 memo方案 B 增强版)**。复盘markstream-vue@1.0.1 接入 + vue-tsc 通过无技术障碍,但「样式 100% 还原现有 .ai-md」成本高且脆——markstream DOM 异构于 marked 输出(代码块 `.code-block-container` chrome 结构 / 暗色 `--ms-*` 变量体系依赖 `.dark` class 而 DevFlow 用 `[data-theme]` / prose 作用域 `.markstream-vue`4 处对接且随 markstream 升级易漂移。用户优先级是「保留原样式」>「虚拟窗口/shiki 高级能力」,转自研块级 memomarked 输出标准 HTML → .ai-md 样式零对接,借鉴 §2 机制 2块级 memo业界主流实现 splitBlocks代码围栏整体一块/非代码双换行切)+ 前块缓存命中 O(末块) + 末块不缓存处理未闭合 token + rAF 节流。结论:**自研块级 memo = 方案 B 增强rAF + 真块级 memo非仅代码块降级零新依赖、零样式对接、D 级流式性能**。§5 原决策表「自研只在坚决不引新依赖时才划算」判断在「不要高级能力、要原样式」前提下反转——此时自研反而最优。落地ARC-260615-08 自研版已实施vue-tsc exit 0待 dev 运行时验证。
---
## 来源
- [Best practices to render streamed LLM responses — Chrome for Developers](https://developer.chrome.com/docs/ai/render-llm-responses)streaming-markdown 增量 append、Paint flashing 验证)
- [Markdown Chatbot with Memoization — Vercel AI SDK Cookbook](https://ai-sdk.dev/cookbook/next/markdown-chatbot-with-memoization)(块级 diff + memo 官方实现)
- [Streaming Backends & React: Controlling the Re-render Chaos — Sitepoint](https://www.sitepoint.com/streaming-backends-react-controlling-re-render-chaos/)rAF 实测数据)
- [antfu/shiki-stream — GitHub](https://github.com/antfu/shiki-stream)
- [markstream-vue — GitHub](https://github.com/Simon-He95/markstream-vue)
- [vercel/streamdown — GitHub](https://github.com/vercel/streamdown)
- [HuggingFace chat-ui: webworker for markdown parsing #1733](https://huggingface.co/spaces/jdelavande/chat-ui-energy/commit/7d6fc19984864d960f2875391858ea067b030dc6)
- [shikijs/shiki Discussion #891](https://github.com/shikijs/shiki/discussions/891)GrammarState 流式高亮底层)