从“思考链展开”看AI时代用户交互基础设施的重要性
有时候我会觉得,做 AI 产品最奇怪的一点是:真正耗人精力的,常常不是那些听上去很大的东西。
不是多智能体,不是长上下文,也不是某个特别唬人的新能力。
反而是一个很小的地方。小到你第一次说出来,别人会觉得这也值得反复折腾?
比如,聊天消息里那个“思考过程”的展开和收起。
就这么一块东西。点开,看看模型在想什么。正文出来了,它再收回去。用户愿意的话,再手动展开。
听起来真的太普通了。
但我们后来为它花掉的时间,比一开始想象的多得多。现在回头看,我反而觉得,这件事很值得记一笔。因为它看上去小,实际上却正好踩在了 AI 时代一个特别关键的交叉点上:用户到底怎么感知一个 Agent 正在工作。
一开始,我们只是想把“思考”单独放出来
最早做这块的时候,出发点其实并不复杂。
模型已经不再只是吐一段最终答案了。它会推理,会调用工具,会在一段时间里没有最终结果,但明显还在工作。如果这些东西都混在正文里,用户很难分清到底哪里是过程,哪里是结果。
所以我们做了一个很自然的决定:把思考过程单独放出来,做成一块可以展开的区域。
这一步其实挺重要的。因为从那一刻开始,我们做的就不只是“把模型输出显示出来”,而是在尝试给 Agent 的工作过程做一个能被人理解的界面。
这件事在今天看几乎是必然的。只要一个产品认真做 Agent,早晚都得面对这个问题。模型不是瞬时给答案的黑盒了,它更像一个会停顿、会试探、会中途换工具的系统。那你就得决定,这些中间状态怎么让人看懂。
思考块,某种意义上就是这个问题的一个最小切口。
后来问题来了:它在移动端太卡了
真正让这件事变难的,不是“怎么做出来”,而是“做出来以后,它居然这么卡”。
尤其是移动端。
思考内容一长,展开的时候就明显不顺。不是那种极端情况下才会出现的卡,而是你自己用两次就会不舒服的那种卡。拖泥带水,像整个页面都在跟着喘气。更麻烦的是,这个区域又偏偏是流式的,会一边生成一边长。于是问题不是一次性的,而是持续性的。
这时候你就会发现,所谓“一个小小的折叠区域”,实际上已经把很多问题绑在一起了:
- 内容不是静态的,它会流式增长
- 内容不是纯文本,它可能是 Markdown
- 内容可能很长,远比普通折叠面板长
- 用户不是展开看一眼就走,而是会盯着它看模型“正在想什么”
- 它还处在聊天列表里,任何重排都会牵连整列消息
到这一步,我们其实已经不在处理一个普通 UI 组件了。我们在处理的是一个会持续变化、同时又要保持可读、流畅、稳定的中间态容器。
我们先做了一件很“工程”的事:先保性能
后来为了移动端的流畅度,我们做过一些现在看起来很“狠”的取舍。
最典型的一次,就是为了让这块先别那么卡,我们一度把里面的 Markdown 表现往回收,尽量往更轻的渲染方向靠。说白了,就是先把最贵的东西拿掉,把主线程压力降下来。
这个决定当时是对的。
因为那时候最重要的问题,不是“它是不是最优雅”,而是“用户点开的时候别难受”。如果一个区域承载的是模型思考过程,但用户每次一展开都觉得卡,那它再高级也没意义。
这类取舍,做前端的人应该都不陌生。很多时候不是不知道理想解长什么样,而是你必须先在真实设备上把最痛的那一下救下来。
只是这种取舍的副作用也很明显:性能保住了,表现力就会被压一截。
而思考内容这玩意儿,偏偏又不只是几行纯文字。它会出现层级、列表、代码块,甚至以后还可能出现更复杂的结构。你把这些全都抹平,虽然快了,但读起来会差很多。
所以这件事从来就不可能停在“先凑合能用”。
等我们把表现力追回来,复杂度也一起回来了
后面我们重新把更完整的 Markdown 体验拿回来,其实也不是为了“好看”。
而是因为这块内容真的值得被更认真地展示。
思考过程不是噪音。至少在很多场景下,它是帮助用户判断模型是否靠谱的重要线索。你能不能看清它的结构,能不能读懂它到底在推什么、分了几层、是不是已经开始收束,这些都会直接影响用户对系统的信任。
但一旦把表现力拿回来,之前被性能问题暂时压住的复杂度也会跟着回来,而且往往是加倍回来。
这个时候事情就变得很典型了:
一边是流式 Markdown;
一边是展开收起动画;
一边又是长列表里的滚动和重排;
再加上移动端这个环境本来就更紧张。
你会感觉自己不像是在做一个“聊天小组件”,更像是在跟浏览器协商:我想要这个东西既自然又稳定,你能不能别在最不该抖的时候抖,别在最不该跳的时候跳。
很多真正磨人的前端问题,就是从这里开始的。
最麻烦的,不是卡,而是“第一次展开居然是空白”
如果说卡顿还算是一个能预期的性能问题,那么后来的“首次展开异常”就更让人头疼了。
因为它看起来特别像那种最难解释的 bug:你明明看得见内容在那里,字符数也对,流也在继续,可用户第一次点开的时候,区域就是不对。要么空一片,要么高度夸张地撑开一大块,然后又弹回去。
这种问题之所以讨厌,是因为它很容易把人带偏。
最直觉的想法当然是:高度算错了。
于是我们就先怀疑高度预测,改测量方式;再怀疑测量时机,改成下一拍再测;接着又怀疑布局还没稳定,于是多测几帧,等它稳定以后再决定最终高度。
这些事情后来证明都不是白做。它们每一步都在逼近真相。
但如果只把它理解成“一个高度计算 bug”,那永远差最后一口气。
真正的问题其实更阴一点:不是我们不会量高度,而是浏览器在某些时刻根本没把那块内容当成“现在应该完整参与真实布局的东西”。
这背后牵扯到的是我们早前为了性能做过的优化。那些优化单看都没错,甚至非常有必要。可一旦它们和展开动画、流式内容、首次展示的时机叠在一起,就会出现一种很麻烦的状态:你以为你量到的是真实内容,其实量到的是一个被浏览器拿来占位的近似状态。
所以后来真正把问题按住,不是因为我们又发明了第 N 版更聪明的高度算法,而是因为我们终于承认了一件事:
这里需要的不只是“算一个高度”,而是一套明确的交互状态协议。
什么时候它可以为了性能跳过真实渲染,什么时候它绝对不能跳,什么时候它正在被用户看、正在动画、正在被重新测量,这些都要说清楚。
一旦这件事说清楚,后面的修复才终于不再像打地鼠。
到这里我才真正意识到:这不是一个“小动效”
回头看,这整个过程最值得记住的,不是某一版代码写得多巧,而是一个认知转变。
就是你得承认,这个东西虽然小,但它不是一个“边角动画”。
在 AI 产品里,这一块区域直接影响的是用户怎么理解“系统现在在干嘛”。
如果它卡,用户会觉得产品不稳。
如果它空白,用户会觉得是不是坏了。
如果它一会儿撑很大一会儿又缩回去,用户会下意识地觉得这套东西不可靠。
也就是说,用户根本不会把它理解成“一个展开组件出现了点样式问题”。用户会把它理解成:这个 Agent 看起来不太对。
这就是为什么这种体验,明明很小,却又跟整个 AI 时代的产品质量息息相关。
我们现在都在谈 Agent,谈可视化,谈中间态,谈工具调用透明化。可这些词最后总要落到界面上。落到界面上以后,就会变成一个很具体的问题:用户看到的那一瞬间,是舒服的、清楚的、值得信任的,还是别扭的、跳的、像没做完的。
而真正拉开产品差距的,往往就是这些地方。
这一路上,我自己记住了几件事
第一,很多 UI 问题,最后都不是单纯的 UI 问题
如果数据语义不清,前端怎么补都很累。
思考块这件事能慢慢稳定下来,一个很重要的前提,其实是前面已经把推理流、正文流、中间态这些语义逐渐理顺了。否则你连“它现在到底该展开还是该收起”都说不准。
第二,通用方案很重要,但有些东西注定会长成特例
我们确实做过统一的折叠动画基建,也确实让很多地方受益了。
但思考块后来还是长成了一个特例。不是因为之前抽象错了,而是因为它同时背了太多职责:流式、长文本、自动展开、自动收起、手动打断、性能优化、真实测量。
这种东西最后有自己的状态机,是正常的,不丢人。
第三,性能优化和正确性优化,真的会互相打架
很多时候不是谁错了,而是两个都正确的优化碰在一起以后,系统复杂度突然上了一个台阶。
这个过程特别像长大以后才会懂的一件事:工程里最难的不是“完全错误的东西”,而是“每一步看起来都挺合理,合在一起却开始不对劲”的那种局面。
第四,别太快看轻这种“小体验”
我们这一代做 AI 产品的人,大概会越来越频繁地碰到这种事:一块很小的界面,背后其实连着用户对模型、对工具、对整个系统的信任。
以前的软件,很多时候只要把结果展示清楚就够了。
现在的软件,越来越需要把“过程”也展示清楚。
而过程,本来就比结果难展示。
现在这件事终于比较像样了
最近这次整理,最让我舒服的一点,不是某个 bug 被压住了,而是结构终于开始对了。
以前这块逻辑散在消息列表里,状态很多,映射很多,越长越像一团必须小心碰的东西。
现在它终于更像一个独立的小系统了:父层只管消息分发,思考块自己管自己的展开、收起、测量、流式同步和性能状态。
这不代表它从此就不会再出问题。
但至少以后再碰到类似问题,我们不需要一头扎进整条聊天列表里翻所有状态。我们知道问题大概率就在这一小块自己的边界里。
这已经是很大的进步了。
最后想留一句很朴素的话
做产品做久了以后,真的会越来越理解一件事:
用户最后记住的,往往不是你做过多少“宏大能力”。
而是那些他每次都会碰到的小地方,到底是不是顺,到底是不是稳,到底有没有被认真对待。
这个思考块就是这样。
它真的只是聊天里的一个小区域。
但它又不只是一个小区域。
因为在 Agent 时代,它其实是在替整个系统说话。
它在告诉用户:这个模型是不是还在认真工作,这个过程是不是可信,这个产品到底有没有把最细的体验也当回事。
所以现在回头看,我反而挺庆幸我们在这件事上花了这么长时间。
不是因为折腾本身值得骄傲。
而是因为这件小事,确实值得被做对。