微交互在 AI 产品里失效了:从一道稳定边界算法看新设计语言

打开 Dan Saffer 那本黄皮《Microinteractions》,第一章给出四件套:触发器、规则、反馈、循环模式。按这个框架,一个按钮按下、一个开关切换、一封邮件归档,全都讲得通。
把它放到 ChatGPT 一次回答上面,立刻塞住。触发器是什么——用户敲了 Enter?规则是什么——一个跑了几秒可能十几秒的随机过程?反馈是什么——一句话从无到有逐字浮现,期间字还可能跳一下、错一下、回退一下?
Saffer 那套写于 2013,针对的是确定性界面:按下去结果可预知,反馈只是把"已发生"画给用户看。AI 产品反过来——结果还在生成中,反馈本身要先把"正在发生什么"建模出来。微交互的主战场,从装饰转成了不确定性的可视化。
一、Saffer 框架的边界:确定性微交互讲不了流
Microinteractions 全书的核心案例都是状态切换:iPod 静音键、Tweetbot 的下拉刷新、Mailbox 的归档手势。这些动作有一个共同点——响应时间小于人感知的阈值(100 毫秒以内),结果可枚举。微交互的全部工程量花在"把结果讲漂亮"上。
AI 产品没有这个奢侈。一次模型调用从首 token 到结尾经常是 3 到 30 秒,中间穿插工具调用、思考链、内容回退。响应不再是一个事件,是一段过程。Saffer 的反馈层概念假设界面在等用户,AI 产品里反过来——界面在等模型,用户在等界面。
这条裂缝被两个二级现象放大:
第一,Markdown 在流式渲染下会撕裂。模型先吐出 ``` 然后才吐 python,这中间界面看到的是一个无效的代码块。直接调用解析器,开始几百毫秒里整段内容会闪、跳、错排。这是 Saffer 那套没碰过的问题——他的所有案例都是离散事件,不是流。
第二,等待时长本身要被建模。按钮反馈做完就完了,AI 一次回答的等待要被分成"思考阶段—输出阶段—卡住阶段"。每段需要不同的微交互语言,否则用户分不清是慢、还是死。
下面三章拆这三条裂缝在代码里怎么补。证据全部来自 Claude Code 源码——这是目前唯一开源到能逐行读的 AI 产品前端。

二、流式 Markdown 的稳定边界:一行注释揭开的算法
打开 src/components/Markdown.tsx,最下面一段函数叫 StreamingMarkdown。注释写得直白:
Renders markdown during streaming by splitting at the last top-level block boundary: everything before is stable (memoized, never re-parsed), only the final block is re-parsed per delta.
流式渲染时按最后一个顶层块的边界把文本切成两半:前面一段稳定(已缓存,再不重新解析),只有最后一块每收到一个 delta 重新解析。
这个算法的核心是一个 stablePrefixRef——一个只往前不往后的指针。每次模型吐出新内容,组件做四件事(Markdown.tsx:202-227):
const stripped = stripPromptXMLTags(children)
const stablePrefixRef = useRef('')
if (!stripped.startsWith(stablePrefixRef.current)) {
stablePrefixRef.current = ''
}
const boundary = stablePrefixRef.current.length
const tokens = marked.lexer(stripped.substring(boundary))
只对边界之后的字符串做 lexer。代码注释把复杂度写明白了——O(unstable length), not O(full text)。一段 5000 字的回答流到 4900 字时,新算的只有最后那 100 字。
边界怎么往前推?看下面这段:
let lastContentIdx = tokens.length - 1
while (lastContentIdx >= 0 && tokens[lastContentIdx]!.type === 'space') {
lastContentIdx--
}
let advance = 0
for (let i = 0; i < lastContentIdx; i++) {
advance += tokens[i]!.raw.length
}
最后一个非空白 token 视为"还在生长的那一块",前面所有 token 都"封冻"。marked.lexer 文档保证未闭合的 ``` 会被当成单个 token 收下——所以边界永远落在合法位置,不会切到代码块中间。
这是一道工程上极克制的设计:不预测模型会吐什么,只承认一件事——已经成段的内容不会再变。代码块还没闭合,没关系,整段都在 unstable 区慢慢长,闭合那一帧整段一起 freeze。
为什么这一段叫"微交互"?因为它解决的是同一个 UX 问题——文字闪烁、布局抖动。只不过解决方法不在 CSS transition 里,而在解析器边界算法里。AI 时代的微交互一半已经下沉到数据流处理层,给上层 React 看到的,已经是稳定的字符串。这一点 Saffer 没办法预见,他那本书里 React 还没诞生。
旁边还有一道相关优化。Markdown.tsx:30-45 给同一个 lexer 套了一层 LRU 缓存:
const TOKEN_CACHE_MAX = 500
const tokenCache = new Map<string, Token[]>()
为什么要缓存?历史消息在终端往上滚再往下滚时会发生 unmount→remount,useMemo 在跨 mount 的场景下不工作,每次重挂都要重新跑一遍 ~3ms 的 lexer。500 条历史滚一圈就是 1.5 秒卡顿。改成模块级 Map 之后,相同 hash 直接命中。注释提到一个具体的回归 issue 编号 #24180——这条规则是被打坏过才补回来的。
更靠前还有一条快速通道:
const MD_SYNTAX_RE = /[#*`|[>\-_~]|\n\n|^\d+\. |\n\d+\. /
function hasMarkdownSyntax(s: string): boolean {
return MD_SYNTAX_RE.test(s.length > 500 ? s.slice(0, 500) : s)
}
只在前 500 字里扫一遍,一个 markdown 字符都没有就跳过整个 lexer,直接当一段 paragraph 输出。AI 产品里的大量短回答和工具输出走这条路——一次大约省下 3 毫秒。20fps 重渲染里,3 毫秒就是一个完整帧的差距。
三层叠起来:稳定边界把每帧的工作量从 O(全文) 降到 O(增量);LRU 缓存把跨 mount 的工作量降到零;快速通道把无意义的 lexer 跳过。整套机制的目的只有一个——让流式文本看上去不像在抖动。这是 AI 时代独有的微交互工程。

三、Agent UI 的信任反馈:Esc 不是关闭,是协作信号
AI 产品多了一类微交互在 Saffer 时代根本不存在——用户对一个还在跑的智能体说"停一下"。
Claude Code 把这件事映射到 Esc 键。hooks/useBackgroundTaskNavigation.ts:149-167 里这段逻辑值得逐行看:
// - If teammate is running: abort current work only (stops current turn,
// teammate stays alive)
// ...
// Abort currentWorkAbortController (stops current turn) NOT abortController
// (kills teammate)
task.currentWorkAbortController?.abort()
注释比代码本身更说明问题。一个 Esc 不是简单的"关掉"——它分两层:当前这一轮工作(current turn) 取消,整个智能体(teammate) 不死。这是一个非常细的判断:用户按 Esc 通常意味着"这次走偏了,重来",不意味着"我后悔创建这个 agent"。微交互在这一秒承担的是协作意图的解读,不是动画。
紧跟着的提示也讲究:
// Escape in selection mode: exit selection without aborting leader
如果用户当时在选项里挑东西,Esc 表示"先不选了",不该顺手把上层 agent 关掉。一个键三种语义,靠上下文区分。这是 UX 设计层面把"撤销"重新建模的过程——不是统一的 undo,是分层的、有边界的、能被 agent 听懂的中断信号。
中断之后,界面给出的回执是另一个微交互。components/FallbackToolUseRejectedMessage.tsx 整段只有一个组件:
return (
<MessageResponse height={1}>
<InterruptedByUser />
</MessageResponse>
)
一个固定高度 1 行的占位,写着"被用户打断"。这一行存在的意义不是装饰——它是把中断这件事写进对话历史。Agent 下一轮回看上下文时,能看到自己上一步是被否决的,而不是被时间扔掉的。微交互在这里同时服务两套读者:人类用户,和模型自己。
Smashing Magazine 2026 年 2 月那篇 "Designing For Agentic AI" 把这套规则总结成一条断言:给智能体可控性的核心,不是确认对话框,是可撤销与可审计的成本。Claude Code 的实现把这条断言切到具体动作——CLAUDE.md 用纯文本而不是结构化配置,因为纯文本可以 git diff、可以 rm、可以人眼一秒看懂。审计和撤销都不需要 UI 元素,只需要承认数据是用户的。
这是 Agent UI 微交互的另一半——信任不是靠动画建立的,靠"东西归你管"建立的。一个 Esc 键、一行 InterruptedByUser、一份 plain text CLAUDE.md,三件事加起来比任何确认弹窗都重。

四、Spinner vs Skeleton:信息论框架下的等待选择
回到等待这件最经典的微交互。前端社区有过一阵子的口水仗——Spinner 落伍了,Skeleton 才是好设计。Linkedin 那篇被引爆的研究指出骨架屏能让等待感降低 30%。
但 Claude Code 整套用的还是 Spinner,没有任何 Skeleton。为什么?
回到 Spinner 那篇拆解里讲过的细节,挑出一段 useStalledAnimation.ts 的代码(早先 《一个 Spinner,12 个文件》 完整拆过这套系统,这里只挑信息论那一面):
const isStalled = timeSinceLastToken > 3000 && !hasActiveTools
const intensity = isStalled
? Math.min((timeSinceLastToken - 3000) / 2000, 1)
: 0
3 秒不吐 token 算卡住,然后 2 秒淡入红色。这条曲线在 Skeleton 屏上没法画——骨架屏的前提是"我知道结果长什么样",所以可以预先描出框架。AI 产品的回答永远不知道结果长什么样,连"会不会有结果"都未知。
Shannon 信息论那条老结论拿到这儿正好用:反馈的设计要匹配可知信息量。
| 等待类型 | 已知信息 | 合适的微交互 |
|---|---|---|
| 列表加载 | 知道有几条、知道每条什么结构 | Skeleton |
| 表单提交 | 知道结果只有成功/失败两种 | 进度条 + 结果状态 |
| AI 回答 | 不知道时长、不知道结构、可能失败 | Spinner + stalled 渐变 + token 计数 |
骨架屏在 AI 产品里反而骗人。用户看到"似乎有结构"的占位,会建立"马上来了"的预期;模型实际响应慢了之后,落差更大。Spinner 加 token 计数这个组合相反——它不预测,只如实播报"还在生成、已经生成 N 个 token、最后一个 token 是 X 秒前"。没有预期就没有落差。
Material 3 Expressive 在 2025 Google I/O 推出的 spring 物理动画系统补了另一个角度。从前的 ease-in/ease-out 是确定时长的曲线,必须先知道动画走完要多久;新的 spring 系统按物理参数走,时长由位移和阻尼自然决定。这个变化在产品文档里写明了原因——"用户对静态时长的容忍度在下降,对动态自然感的偏好在上升"。AI 产品里的 spinner 走的是同一条路:每一帧的颜色和文字由实时状态决定,不是预设时长的动画。
Material 团队的研究披露了一个反直觉数据:M3 Expressive 改造后,参与者识别"邮件发送按钮"等关键元素的速度提升到原来的 4 倍。这个数字不是因为按钮变大变亮,是因为整个界面的微交互和等待节奏更接近物理直觉,注意力不再被消耗在解读不连贯的状态切换上。
回到 Claude Code 的选择就清楚了:spinner 不是落伍,是在不确定性场景下信息密度最高的等待语言。187 条文案、stalled 红色渐变、token 计数器追赶——三件事各自负责一类信息:内容(在干嘛)、健康度(有没有卡)、产出量(已经做了多少)。每一秒钟向用户传达的信息比例都要算过。这是新一代微交互的工程审美。

盲区:还没读到的事
ChatGPT 和 Cursor 的流式 markdown 处理没看到源码。Claude Code 的稳定边界算法是一个具体方案,不知道竞品是不是同一思路。从公开博客看 Vercel AI SDK 推荐"在空白边界或句子终止符 buffer markdown",思路接近但颗粒度更粗。如果 Anthropic 内部有这一段 benchmark,没流出来。
Material 3 Expressive 的"4 倍识别速度"。Google 设计博客披露的数据,研究方法没公开。考虑到样本量是 18000 人、46 个研究、跨多国测试,方法论应该不差,但具体场景细分(哪类按钮、哪类操作)拿不到。
Skeleton 30% 等待感降低那个数据出处不一。社区最常引用的是 LinkedIn 早年内部测试,原始报告找不到了。它在小数据列表场景里成立,在 AI 回答场景下能否复现,没看到验证。
对从业者意味着什么
三条落地判断。
第一,Saffer 那本书可以放下了。它是好书,但 2013 年的范式撑不住 2026 年的 AI 产品。读它能学到状态切换的工程审美,学不到流式渲染怎么做、agent 怎么协作。比它更值得花两天的是读 Claude Code 那 1856 个 .ts 文件——不止 Spinner 系统,整个 src/components 里到处是 AI 时代微交互的工程证据。
第二,微交互正在下沉。CSS transition、Framer Motion 那一层逐渐变成皮肤,真问题搬到了数据流处理:流式 markdown 怎么不抖、token 计数怎么追真实值、stalled 怎么检测、Esc 中断怎么分层。这些不是 UX 设计师一个人能做的事——它需要前端工程师懂 lexer、懂 hash、懂 abort signal、懂 React 的 ref 语义。新一代 UX 工程师的稀缺技能不是 Figma,是 React Compiler 注释的阅读能力。
第三,对不确定性建模而不是隐藏。Skeleton 屏在 AI 场景下会变成一种说谎——它给用户一个不存在的结构预期。Spinner 加实时 token 计数反过来——它把"我也不知道还要多久"诚实播报出来。诚实是新一代微交互的工程美德。能播报多少状态,就播报多少;不能播报的,用 stalled 渐变这种情绪曲线告诉用户"我知道你在等"。
本期关键词
流式微交互(streaming microinteraction) -- 反馈对象从离散事件变成一段持续生成的过程。区别于 Saffer 1.0 的状态切换语言,需要处理 token 速率、布局抖动、阶段切换、可中断。代表实现是 Claude Code 的 StreamingMarkdown 稳定边界算法和 Spinner 系统的多通道时钟。
稳定边界(stable boundary) -- 流式 Markdown 渲染的核心算法。把已经成段的内容封冻,只对最后一个未闭合块每帧重解析。复杂度从 O(全文) 降到 O(增量)。在 Claude Code
Markdown.tsx里通过一个 monotonic 的 ref 实现,注释里专门解释了为什么要 opt-out React Compiler 的 memo。不确定性反馈(uncertainty feedback) -- AI 产品微交互的核心命题。响应时间未知、结构未知、可能失败的场景下,反馈的目标不是"展示结果",是"持续播报状态"。Spinner + token 计数 + stalled 渐变是这条命题在终端的标准答案。Skeleton 屏在这种场景下会变成一种说谎。
协作中断(collaborative abort) -- 用户对运行中智能体发出的"停一下"信号。区别于普通的关闭按钮,需要分层:当前轮次取消,agent 实例不死;选择模式取消,上层任务保留。Claude Code 在 useBackgroundTaskNavigation 用三层 abort controller 编码这套语义。
可审计纯文本(auditable plain text) -- Agent UI 信任反馈的低 fi 形态。CLAUDE.md 用 plain Markdown 写,没走结构化配置那条路,原因是用户可以 git diff、可以一秒看懂、可以删掉。审计和撤销不需要 UI 控件,只需要承认数据归用户。这条规则把"控制感"从交互层迁移到数据层。
spring 物理动画 -- Material 3 Expressive 2025 引入的运动系统。区别于传统 ease 曲线的固定时长,spring 按位移和阻尼参数自然展开。Google 内部研究显示参与者识别关键元素的速度提升到 4 倍。背后逻辑和 AI 产品的实时状态 spinner 是同一条——动画时长由实际状态决定,不是设计师预设。
引用
- Claude Code Markdown.tsx 源码 -- StreamingMarkdown 稳定边界算法的一手代码
- Claude Code Spinner 系统 -- 12 文件多通道时钟与 stalled 检测
- useBackgroundTaskNavigation.ts -- 协作中断的分层 abort 实现
- Microinteractions, Dan Saffer (2013) -- 微交互经典定义,确定性界面的范式
- Designing For Agentic AI: Practical UX Patterns, Smashing Magazine 2026-02 -- agent UI 信任反馈的行业总结
- What Is Streaming UI in AI Applications, thefrontkit -- 流式 UI 的产业实践综述
- Material 3 Expressive Motion System, Google Material Design -- spring 物理动画系统的官方说明
- Material 3 Expressive Deep Dive, Android Authority -- 4 倍识别速度数据的二手报道
- 《一个 Spinner,12 个文件:Claude Code 把等待做成了精密时钟》 -- 本系列前作,Spinner 系统的逐文件拆解