
做了几年Agent应用我越来越觉得“渲染”这个词在Agent项目里的分量被长期低估了。大家聊Agent聊的是大模型选型、Prompt工程、工具调用链路、记忆机制这些当然重要。但真正把一个Agent应用交到用户手里用户看到的是界面、图表、流式文字、动态状态——这些东西背后全是渲染问题。你说Agent应用开发和渲染之间有关系吗关系大得很。先说我自己的经历。前阵子我搭一个行业调研Agent后端把模型输出、工具结果、知识库引用都串好了逻辑层面很顺。结果一打开页面markdown长文渲染卡顿、图表闪烁、流式输出时整个页面跟着抖。用户第一反应是“这工具不行”没人关心我的Agent调度写得多精巧。从那之后我就明白一个道理Agent应用的上限由模型决定但用户感知的下限由渲染决定。这篇文章我想把这些思考完整梳理一遍。会涉及到流式输出场景下markdown-it渲染大量文字的性能问题ECharts在Agent工作台里的闪烁修复也会聊到3D网页渲染、Liltoon和Unity Shader这类卡通渲染/NPR技术为什么开始出现在Agent可视化里最后谈谈Flutter的Impeller渲染引擎原理给我的启发。内容偏向实操反思不是为了讲渲染而讲渲染而是想回答一个问题在一个以“生成”为核心的Agent应用里“渲染”到底应该站在什么位置。1. 被忽略的接口Agent应用为什么绕不开渲染1.1 模型输出得再快最终都要落到一块屏幕上很多做Agent后端的人有个惯性思维觉得前端渲染是“展示层的小事”。我在早期也是这么认为的——Agent的核心是决策和生成前端无非是把结果拼个HTML。但实际做下来这个想法坑了自己。Agent应用和传统CRUD应用有一个本质区别传统应用的数据结构是确定的表格就是表格表单就是表单渲染只是“把数据库里的数据搬出来”。Agent应用的数据结构是不确定的模型输出的是自然语言可能夹杂Markdown、JSON、代码块工具调用的结果可能是图表数据、地图坐标、3D模型文件。你根本没法预知下一个消息是什么形态。这就意味着渲染层必须足够鲁棒。它不是简单的“数据显示”而是一种面向未知结构的适配层。我当时接手那个调研Agent时就吃过亏后端返回的是模型生成的Markdown里面嵌套了表格、代码块、甚至Base64图片前端直接扔给markdown-it渲染结果长文场景下页面直接僵住。这个“页面僵住”不是模型慢而是渲染被堵死了。所以我现在的观点是Agent应用的渲染层应当被当作一个一等公民来设计。它决定了系统能不能把模型的“智能”完整地传达到用户眼里。模型再聪明渲染跟不上用户感受到的就是“卡、慢、乱”。1.2 我们常说的“渲染”其实是三层意思为了避免聊不到一块去我先把“渲染”这个概念拆开。在Agent应用里至少有三层渲染渲染层次技术栈举例典型问题数据渲染JSON序列化、模板引擎、状态管理大对象频繁更新导致的重渲染组件渲染React/Vue虚拟DOM、Flutter Widget树长列表、组件层级过深、状态同步图形渲染Canvas/WebGL、Shader、Skia/Impeller大量图元绘制、GPU瓶颈、帧率不稳很多人一说“渲染”只想到第三层觉得那是游戏引擎、图像处理的事。但在Agent应用里三层是叠加在一起的。拿我做的那个调研Agent举例后端返回一份10万字的行业报告这是数据渲染层要处理的页面需要把这份报告分段展示并支持折叠、目录跳转、代码块高亮这是组件渲染层要处理的如果报告里嵌入了趋势图表、知识图谱那就轮到图形渲染层了Canvas或WebGL开始接管。三层都有各自的坑而且三层之间还会互相影响。比如组件渲染层的一次整页刷新会连带触发图形渲染层的图表重建于是闪了一下。这个问题我后面会专门展开它就是ECharts闪烁的典型场景。所以想理清Agent应用和渲染的关系第一步是把这个“多层渲染”的模型装进脑子里。后面所有的优化手段本质都是在协调这三层之间的关系。2. 流式输出与Token渲染长Markdown文档卡顿的根因2.1 markdown-it渲染大量文字卡点到底在哪流式输出是Agent应用最常见的一种交互形态。模型生成的token像打字机一样往外吐前端实时把增量文字渲染到屏幕上。我刚做的时候以为这很简单——WebSocket收到一段文本拼到当前内容后面重新渲染一遍就行了。等模型真的开始输出长文问题就来了。有一次我测试一个写代码方案的Agent它一口气输出了两千多行Markdown包含多个代码块和表格。前端在token流式到达时每收到一批就调用一次markdown-it.render()结果整个页面明显掉帧滚动都困难。markdown-it本身是性能不错的库单次渲染几千字并不慢。真正的问题在于重复渲染流式输出一秒钟可能到达几十个chunk每个chunk都触发一次全量重新渲染。渲染耗时是指数级叠加的而且虚拟DOM diff的成本也随着文档长度增长。Markdown文档越长每次diff需要对比的节点越多页面就越卡。这跟你手写一个“实时搜索框”却每次全量渲染整个列表是一个道理。我精确测过一次一个约5000字的Markdown文档在Transformer模型架构的页面里初始渲染大约耗时180ms看起来不慢但当它作为流式输出的一部分每200ms被重新渲染一次时UI线程就被占满了滚动、选中文字这些操作全部延迟。体感就是“网页死了”。2.2 增量渲染才是Agent流式界面的解药解决思路其实不复杂不要在token流里做全量渲染。拆成两步走。第一步按段落粒度切分内容。Markdown天然有段落边界按空行切分是合理的。每收到一个新的段落就独立渲染该段落插入到文档末尾。已渲染的段落不再参与后续更新虚拟DOM只需处理新插入的节点。第二步对“半截”的段落做节流。模型可能在一个段落中段停下来思考然后继续输出。对于这个未完成的段落我采用节流策略比如每200ms才执行一次渲染而不是每个token都渲染。这样既保留打字机效果又把渲染频率压到人眼可感知的流畅区间。这里有个细节值得说不管用什么库增量渲染的粒度决定了性能上限。我之前用一个国产的开源Markdown编辑器它内部对整篇文档做富文本建模流式输出时每来一个token整个编辑器就重排一次一万字之后卡到没法用。后来我换成轻量方案每个段落独立渲染页面流畅度肉眼可见地上了一个台阶。2.3 代码高亮和数学公式是隐藏的“渲染大坑”如果说普通Markdown是初级难度那代码高亮就是中级难度。Agent特别喜欢输出代码块而且经常是超长的完整文件。我用的是markdown-it配合highlight.js代码块超过300行时高亮耗时非常夸张。实测数据一个500行的Python代码块highlight.js默认配置高亮需要约80ms但当这个代码块在流式输出中反复重渲染时总耗时很容易突破500ms。后来我做了两件事代码块单独延迟处理先渲染成纯文本等代码块完整收尾后再做语法高亮替换。高亮结果缓存按“语言内容哈希”缓存DOM片段内容没变就直接复用。数学公式同样是个坑。Agent输出的数学推导经常包含LaTeX如果用KaTeX渲染每个公式都要跑一次解析。一篇10个公式的技术文档光公式渲染就要几百毫秒。我的做法是小公式用行内轻量渲染复杂公式懒加载并且用MathJax的延迟类型渲染机制来规避首屏阻塞。这些在上热搜的“markdown-it渲染大量文字”背后其实就是同一类问题。说句实在话很多Agent应用聊界面性能聊到最后都会落到这个点流式生成是高频小步更新渲染必须随之改成增量、批量、可中断的模式否则模型再快用户也感受不到。3. 工作台可视化ECharts闪烁问题排查实录3.1 闪烁的直接原因整个图表在option更新后被强制重建图表是Agent工作台里最常出现的东西。我的调研Agent里给它配了一个“趋势分析”模块让模型基于数据分析结果生成ECharts配置前端直接渲染。一开始效果很理想图表会自动出现。但很快就发现一个问题数据更新时图表会“闪”一下——整个图形先消失再重新出现看起来很难受。排查之后发现根因不只是ECharts。我的代码逻辑是每当Agent返回新的分析结果就把新的option对象整体设置为图表配置。ECharts发现option里某些结构变化后会销毁旧实例创建新实例。这个销毁重建的过程就会白屏也就是闪烁。这是所有可视化方案里最容易踩的坑配置驱动图形时配置一改就全量重绘。在传统报表场景里数据是低频更新的闪烁一下无所谓但在Agent场景里模型可能连续输出多轮分析图表要跟随结果实时刷新闪烁问题就成了体验硬伤。3.2 在Agent数据流场景下正确的更新姿势后来我怎么改的核心原则是让ECharts做增量更新而不是全量替换。第一setOption的第二个参数设为false或者不推荐滥用merge机制时用ECharts提供的增量合并api。对于series中有Id的组件ECharts支持按Id定位然后更新数据不会整体重建。第二将静态配置和动态数据分离。把标题、颜色、坐标轴、图例这些低频配置写成固定option只在初始化时设置一次。每个新数据到来时只更新series.data和xAxis.data。这样ECharts内部会做最小范围的更新动画是流畅过渡不会再出现先消失再出现。第三对于仪表盘类型场景我干脆用setOption带参数的形式配合ECharts的notMerge特性来控制更新粒度。但不要用notMerge: true来全量清空那个反而会加剧重建。这是我在修复“echart闪烁怎么解决前端”这个问题时实际验证过的方案。改完之后Agent连续输出5轮更新图表始终保持稳定过渡没有再闪一下。3.3 dataZoom和异步更新叠加时的性能陷阱还有一个更隐蔽的坑隐藏在dataZoom组件里。我那个调研Agent会生成一个长期趋势图时间跨度有几十万个数据点配了dataZoom支持缩放。在数据进行高频更新时图表出现了严重的卡顿偶发闪烁。问题出在dataZoom的数据窗口计算上。每次更新optionECharts都会重新计算当前缩放窗口对应的数据范围数据量越大计算开销越大。而且Agent在模型输出的不同阶段会多次更新数据dataZoom的窗口状态总被重置回默认范围视觉上就像是图表被“重置”了。我的解决方案是在更新series.data之前先把dataZoom的start和end保存下来更新完成后再通过dispatchAction恢复窗口位置。同时针对几万级的数据点我会预先对原始数据做降采样。ECharts在渲染大量数据点时会自动采样但我们自己降采样之后渲染和缩放都能保持流畅。这个场景给我的感受是Agent工作台跟传统BI报表不一样它不仅要“显示数据”还要“跟随Agent的生成过程动态演进”。动态更新比静态展示对渲染层的耦合性要求高得多。处理不好的话视觉会乱跳用户根本没法把注意力集中在Agent给出的分析结论上。4. 从2D到3D当Agent应用需要“空间感”时4.1 3D网页渲染能解决的Agent场景大多数Agent应用停留在2D页面但有些场景天然需要3D。举几个我见过的实际需求供应链优化Agent需要把仓库、运输路径、库存节点做成3D拓扑图让用户直观看到瓶颈在哪里。机器人控制AgentAgent输出运动控制指令前端需要一个3D场景实时展示机械臂的姿态变化。城市规划Agent结合地理信息数据把建筑体块、交通流线、人口密度渲染成三维场景。知识图谱Agent传统2D图谱节点多了会乱成一团3D布局反而能利用空间维度分散节点。这些场景里3D网页渲染就不再是可选项了而是Agent结果可视化的核心媒介。模型分析完成之后剩下的事情就是把分析结果变成用户可以“走进去看”的空间化表达。我做供应链拓扑时就用过Three.js把Agent输出的物流路径数据渲染成三维弧线。项目里遇到的第一个问题不是画线本身而是数据格式转换。Agent输出的原始数据是JSON里面是节点坐标和连接关系但Three.js需要的是BufferGeometry和纹理贴图。中间要有一层适配器把“语义数据”转成“渲染数据”。这一层适配器本质上就是Agent语义世界和图形世界之间的桥梁。我发现很多团队做3D Agent可视化失败不是3D技术不行而是忘了做这层转换设计。4.2 Liltoon、卡通渲染和NPR在Agent可视化中的价值再往深一层说渲染风格本身也值得琢磨。热搜里那个“liltoon卡通渲染”“unity shader npr卡通渲染”看着像是游戏圈的东西但我在Agent项目里发现它们也有实际价值。所谓NPR就是非真实感渲染卡通渲染是其中一种。跟照片级真实感渲染PBR相比NPR强调信息传达和风格化。这恰恰是Agent可视化需要的。我在机器人控制Agent的可视化界面里就用过类卡通的渲染风格机器人模型不用追求真实材质反而用清晰的描边、明快的色块来指示当前运动状态。比如机械臂的末端执⾏器用亮黄色高亮碰撞区域用红色描边其他部件保持灰白。这种表达方式在信息层级上比真实渲染清晰得多。Unity里的liltoon是专门做日系卡通渲染的Shader插件它的核心能力是控制色阶、描边、高光形状。我在前端项目里虽然没有直接用liltoon但借鉴了它的设计思路区域着色不用渐变而是用断层色阶让状态区分更明显。可控描边描边宽度用来强调选中物体或危险区域。假阴影用贴图或色块模拟阴影避免真实阴影计算消耗GPU。这套思路放在WebGL里同样成立。用Three.js实现简单的Toon Shader难度不高但它能大幅提升Agent可视化界面的清晰度。我做了一次用户测试同样是对机器人状态做可视化卡通风格版本比真实感版本的用户理解速度明显更快。原因很简单真实感渲染带来了太多视觉噪声而NPR风格天然是“为信息传达服务的”。4.3 前端3D还是Unity先想清楚谁来渲染看到3D可视化需求很多团队第一反应是“上Unity”。但我的观点是先想清楚你的Agent应用是Web形态还是要装成客户端。维度Web前端3DThree.js/WebGPUUnity引擎加载成本浏览器原生支持无需安装需要加载引擎运行时包体大交互方式天然匹配Web交互事件适合复杂物理交互和离线场景数据接入直接通过HTTP/WebSocket接Agent数据需要额外的通信桥接层跨平台浏览器即平台需要分别打包各端学习曲线前端开发者容易上手需要掌握Unity编辑器与C#我做供应链Agent时因为必须嵌在Web管理后台里果断选了Three.js。同事另一个做室内设计Agent的项目目标是给设计师提供高性能的实时渲染体验那就用Unity。这里面没有绝对的对错只有数据链路长短的差别Web前端3D跟Agent后端的数据链路最短Unity更适合重量级渲染场景。不过随着WebGPU的普及“浏览器里跑高性能渲染”已经不是痴人说梦。3D网页渲染的体验会越来越接近原生这也让Agent应用的可视化上限高了不少。我觉得这波Agent应用叠加WebGPU的浪潮值得持续关注。5. Impeller引擎给Agent开发者的提醒渲染不能总指望“重排”5.1 Impeller为什么老被提起前面聊的都是Web前端移动端有一条线也不能忽略。Flutter 3.x之后iOS上默认启用了Impeller渲染引擎这个热搜词“impeller 渲染引擎原理”在Flutter社区讨论度相当高。Impeller的核心设计是针对Skia在iOS上的痛点做的重构。Skia作为传统的CPUGPU混合渲染引擎在文本和图形混合场景里会因为着色器编译产生卡顿(jank)。Impeller的做法是在构建期预编译Shader运行时不再做着色器编译而且采用了更贴合移动GPU的后端比如Metal和Vulkan。这条原理拆开看其实很直接Skia是“先画了再说GPU临时编译着色器”Impeller是“提前把着色器烘焙好GPU拿来即用”。这种方式大幅度降低了首帧延迟和长时间运行时的抖动。5.2 对Agent应用开发者的实际启发我为什么要把Impeller放进这篇关于Agent和渲染的思考里因为Impeller背后的思想恰恰是Agent渲染层最缺的——减少运行时的不可控重排把能前置的工作尽量前置。Agent应用里有很多“运行时不可控”的东西模型输出的文本长度不可控Markdown结构不可控。工具返回的数据结构不可控图表维度不可控。用户随时可能会让Agent切换任务界面状态不可控。面对这些不可控如果你的渲染层每次都是“收到新数据就全量重建界面树”那性能和稳定性就无从谈起。Impeller的思路是虽然业务数据不可控但渲染管线的基础设施可以预烘焙。对应到Agent工程就是布局骨架预定义聊天流、图表容器、工具调用卡片的布局结构提前固定数据更新只填充内容区不重建整个视图树。静态资源预解析模板、样式、Shader、组件定义提前加载运行时只做数据绑定。缓存策略前置像Markdown解析后的AST可以缓存重复内容直接复用避免二次解析。这三点我都在项目里落地过效果很明显。最典型的是Flutter版本里的长列表优化模型一次输出几十轮对答每个消息都是一个复杂的混合组件文字代码块引用卡片。如果消息列表用ListView.builder逐条构建滚动非常顺如果我用Column一次性全部构建列表在渲染100条消息后就会出现掉帧。Impeller能解决GPU层的掉帧但它不会帮你解决组件树层的性能问题。两者的关系是下面渲染得再好上面如果动不动全量重建用户体验照样拉胯。所以我觉得聊Impeller不仅仅是在聊Flutter小程序它其实在提醒所有Agent开发者渲染优化要分层底层引擎管好GPU调度应用层管好组件复用和更新粒度。少做重排比什么都强。6. 视图渲染与Agent逻辑的边界架构上如何取舍6.1 视图渲染的职责边界只做“展示相关状态”聊了这么多具体技术最后回到架构层面。我见过不少Agent项目视图层和后端逻辑搅在一起前端组件里塞满了Agent状态判断。比如判断“当前Agent是否在等待工具结果”“当前执行到工作流哪一步”“模型是否在生成结束标签”。这些判断一多前端组件越来越臃肿渲染性能直线下降。我的架构原则很简单视图渲染只负责“把已有状态可视化”不负责推导“Agent下一步该做什么”。Agent的状态机、执行流程、工具调用结果统一在业务层整理好输出为一个“展示快照”。视图层消费这个快照把它渲染出来。举个具体例子。我做Agent执行面板时后端会推送类似这样的展示快照{ 阶段: 工具调用中, 执行步骤: 3, 工具名称: 代码解释器, 输入摘要: 读取data.csv并计算均值, 耗时: 1800, 状态码: running }前端拿到这个快照直接渲染成一个状态页。页面形态很多样有步骤条、有日志流、有卡片但数据来源只有一个标准结构。这样做有三大好处渲染层逻辑简单纯粹不容易出bug。Agent状态变更即使很频繁前端也能用统一的更新通道处理。多端复用非常方便——同一个快照结构Web端、小程序端、桌面端都能用同一套视图层框架去渲染。6.2 Agent应用开发学习路线我给新人的建议最后结合“agent应用开发学习路线”这个热搜词说点我对新人进阶路径的看法。很多人一上来就学LangChain、学扣子Coze然后拼命堆功能。我建议的顺序稍有不同第一阶段把模型调用和流式交互搞扎实。至少要能写一个带流式输出的聊天框理解token增量到达时前端如何正确渲染。这是Agent应用的第一步也是渲染问题最集中的阶段。第二阶段掌握工具调用和工作流编排。这时候开始接触复杂的数据结构要学会如何把工具结果安全地展示到界面上。前端要做防御式渲染后端要输出稳定的展示结构。第三阶段深入渲染和可视化。把markdown-it、ECharts、Three.js、WebGL这些渲染方案逐个吃透明白它们的性能边界在哪里。再学点Shader基础了解NPR渲染的思想这会让你的Agent应用从“能用”变成“好看”。第四阶段回到架构。学习如何设计视图层和Agent逻辑层的边界学习如何做增量渲染优化学习如何利用Impeller这类引擎特性提升移动端体验。这四阶段不是互相割裂的而是会反复交叉。但整体来看我越来越强烈的感受是Agent应用的门槛在模型能力天花板在交互体验而交互体验的地基就是渲染。如果地基不牢模型再强也只是在沙地上盖楼。对我来说做Agent应用最有意思的地方就是你永远需要同时思考“模型接下来会生成什么”和“界面能不能接得住”。这两个问题一辆一尾缺一不可。写这篇东西也是想把自己在这条路上踩过的坑和想明白的事情沉淀下来后面再遇到类似问题至少能少走点弯路。