
1. 从一次线上事故说起Agent 输出为什么总在渲染环节翻车去年年底我接手了一个内部工具项目核心逻辑是用 Agent 自动生成数据分析报告前端负责把报告渲染成可交互的页面。开发阶段一切正常测试环境跑了几十轮也没出问题结果上线第二天就收到反馈报告页面偶尔白屏偶尔图表疯狂闪烁偶尔文字排版错乱到没法看。排查了两天才定位到根因——问题不在 Agent 本身也不在渲染引擎本身而在两者之间的衔接层。Agent 输出的内容结构是动态的、不确定的而渲染层默认拿到的是稳定、可预期的数据结构。这个错位在低频调用时被掩盖了一旦并发上来、输出变长、格式变复杂问题就集中爆发。这件事让我意识到Agent 应用开发和渲染之间的关系不是后端出数据、前端画页面这么简单的一句话能概括的。它涉及输出格式的契约设计、流式渲染的时序控制、富文本与图表的解析策略、以及并发场景下的性能取舍。这篇内容就把我踩过的坑、验证过的方案、以及一些反直觉的结论整理出来适合正在做 Agent 应用、或者准备把 Agent 接入现有前端体系的同学参考。不管你是刚接触 Agent 开发的新手还是已经能跑通 Demo 想往生产环境推进的开发者应该都能从中找到对自己有用的部分。2. Agent 输出与渲染层之间的契约错位2.1 Agent 输出的本质是概率性结构不是确定性数据传统前后端开发里接口返回的数据结构是写死的。你定义了一个ReportData类型字段就是那几个前端照着渲染就行。但 Agent 的输出不一样——它是模型根据上下文生成的即使你给了严格的格式指令它也可能在某个字段上多写一句解释、少写一个括号、或者把数组写成对象。我最初的做法是让 Agent 直接输出 JSON前端JSON.parse之后按字段渲染。听起来很合理但实测下来问题很多模型偶尔会在 JSON 外面包一层 json 代码块标记直接 parse 就报错长文本字段里如果包含引号或换行转义处理不当会导致解析失败并发请求时不同请求的输出格式稳定性不一致有的规范有的不规范后来我改成让 Agent 输出 Markdown前端用 markdown-it 渲染。这个方案的好处是容错性强——即使格式有点偏差Markdown 渲染器也能兜住不会直接白屏。但新的问题又来了Markdown 渲染大量文字时性能会明显下降尤其是报告长度超过几千字、包含大量表格和代码块的时候。2.2 渲染层对稳定输入的假设在 Agent 场景下不成立前端渲染组件——不管是 Markdown 渲染器、图表库还是富文本编辑器——它们的设计前提都是输入是稳定的、可预期的。比如 ECharts 拿到一个 series 数组它假设每个元素的结构是一致的markdown-it 拿到一段文本它假设语法是规范的。但 Agent 的输出天然带有不确定性。同一个提示词两次调用可能生成结构略有差异的内容。这在单次调用时看不出来但在以下场景会暴露场景表现根因流式输出渲染闪烁、内容跳动增量内容触发了全量重渲染长文本页面卡顿、滚动不流畅渲染器每次更新都重新解析全文并发请求部分请求渲染失败输出格式波动导致解析异常图表数据图表闪烁或空白数据更新时机与渲染周期不同步这张表里的每一个问题我都实际遇到过后面会逐个展开讲解决方案。这里先建立一个认知Agent 开发和渲染之间的核心矛盾是概率性输出与确定性渲染之间的错位。解决思路不是让 Agent 变得完全确定这做不到也不是让渲染层变得完全容错这有性能代价而是在两者之间建立一个合理的缓冲层。2.3 缓冲层的设计思路先归一化再渲染我最终的方案是在 Agent 输出和渲染层之间加一个归一化处理层。这个层做三件事格式清洗去掉模型可能添加的额外标记如代码块包裹、多余的解释性前缀把输出统一成标准 Markdown 或结构化数据结构校验检查关键字段是否存在、类型是否正确对缺失字段做默认值填充分块标记把长内容按语义切分成块每块带上类型标记文本块、表格块、图表块方便渲染层按块更新这个归一化层用 Node.js 写一个中间服务就能实现也可以直接在前端做如果 Agent 输出直接到前端的话。关键是要有一个独立的处理环节而不是让渲染组件直接面对原始输出。3. 流式渲染下的闪烁与跳动从 ECharts 到 markdown-it 的实战处理3.1 流式输出为什么会导致渲染闪烁Agent 应用很常见的一个需求是流式输出——模型生成一点前端显示一点用户能实时看到内容在长出来。这个体验很好但实现起来坑很多。最典型的问题是图表闪烁。假设 Agent 生成了一段包含图表数据的报告前端用 ECharts 渲染。流式输出时数据是逐步到达的每到达一个新数据点如果直接调用setOption更新图表ECharts 会重新计算布局、重绘整个画布。数据点少的时候还好数据点多、更新频率高的时候图表就会疯狂闪烁。我试过几种方案方案一等全部数据到齐再渲染。简单粗暴但失去了流式输出的意义用户要等很久才能看到内容。方案二用setOption的notMerge: false模式增量更新。比全量重绘好一些但 ECharts 在数据频繁变化时仍然会有明显的重绘闪烁。方案三节流 批量更新。把短时间内的多次数据更新合并成一次减少重绘次数。这个方案效果最好但需要控制好节流的时间窗口。我最终用的是方案三的变体对图表数据设置一个 200ms 的缓冲窗口窗口内的数据先攒着窗口结束时一次性更新。同时用setOption的lazyUpdate: true选项让 ECharts 在下一帧才真正重绘。实测下来闪烁基本消失了。3.2 markdown-it 渲染大量文字的性能瓶颈Markdown 渲染大量文字时性能问题比图表更隐蔽。markdown-it 本身性能不错但它的工作模式是输入全文、输出 HTML每次内容更新都要重新解析全文。流式输出场景下如果每收到一小段文字就重新渲染全文随着内容越来越长每次渲染的耗时也会线性增长。用户看到的效果就是刚开始很流畅越到后面越卡最后可能直接卡死。我的解决方案是分块渲染 增量更新// 简化的分块渲染逻辑 const blocks []; // 已渲染的块 let pendingText ; // 待渲染的文本缓冲 function onStreamChunk(chunk) { pendingText chunk; // 遇到块级分隔符如空行时把缓冲内容作为一个块渲染 if (pendingText.includes(\n\n)) { const parts pendingText.split(\n\n); // 最后一部分可能不完整留在缓冲里 pendingText parts.pop(); parts.forEach(part { const html md.render(part); blocks.push(html); appendToDOM(html); // 只追加新块不重渲染旧块 }); } }这个方案的核心思路是已经渲染好的块不再重新解析只渲染新增的块。这样每次渲染的耗时是恒定的取决于单个块的大小不会随总内容长度增长。需要注意的是分块渲染对某些跨块的 Markdown 语法不友好比如跨块的列表、引用块。实际使用中我建议在提示词里要求 Agent 输出时保持块级结构的独立性避免跨块语法。3.3 流式渲染的时序控制什么时候该等一等流式渲染还有一个容易被忽略的问题渲染时机与数据到达时机不同步。Agent 的输出是异步到达的而浏览器的渲染是帧同步的。如果数据到达时正好赶上浏览器在忙比如正在处理用户交互、正在执行其他脚本渲染就会被推迟用户看到的就是内容一顿一顿地出现。我的做法是用requestAnimationFrame来对齐渲染时机let renderScheduled false; let pendingContent null; function scheduleRender(content) { pendingContent content; if (!renderScheduled) { renderScheduled true; requestAnimationFrame(() { doRender(pendingContent); renderScheduled false; }); } }这样保证渲染操作在浏览器的下一帧执行与浏览器的渲染周期对齐视觉上会流畅很多。提示流式渲染的节流窗口不要设得太长。我试过 500ms 的窗口虽然性能好了但用户会觉得卡顿感明显。200ms 左右是比较好的平衡点既减少了渲染次数又保持了实时感。4. 富文本、图表、代码块的混合渲染策略4.1 混合内容的解析难点Agent 生成的报告往往不是纯文本而是文本、表格、图表、代码块的混合体。这就带来一个问题不同类型的块需要不同的渲染器。文本块用 markdown-it 渲染图表块用 ECharts 渲染代码块需要语法高亮表格可能需要额外的样式处理。如果所有内容都走同一个渲染管道要么图表渲染不出来要么文本被错误解析。我的做法是在归一化层就给每个块打上类型标记。Agent 输出时要求它用特定的标记来区分块类型比如[CHART:bar] {categories: [A, B, C], values: [10, 20, 30]} [/CHART] [TEXT] 这里是分析文字... [/TEXT]归一化层解析这些标记把内容分发给对应的渲染器。这样每个渲染器只需要处理自己擅长的内容类型互不干扰。4.2 图表渲染的数据契约设计图表是混合渲染里最容易出问题的部分。ECharts 对数据格式有严格要求而 Agent 生成的数据往往需要清洗。我遇到过几种典型情况Agent 生成的数值是字符串类型10而不是10ECharts 渲染时坐标轴刻度会出错分类名称包含特殊字符导致图例显示异常数据数组长度不一致比如 categories 有 5 个但 values 只有 4 个解决方案是在归一化层做数据校验和类型转换function normalizeChartData(raw) { const categories (raw.categories || []).map(String); const values (raw.values || []).map(v { const n Number(v); return isNaN(n) ? 0 : n; }); // 对齐长度 const len Math.max(categories.length, values.length); while (categories.length len) categories.push(项${categories.length 1}); while (values.length len) values.push(0); return { categories, values }; }这段代码看起来简单但它避免了我遇到的大部分图表渲染异常。核心原则是不要相信 Agent 输出的数据格式所有数据在使用前都要校验和转换。4.3 代码块高亮的性能取舍代码块高亮是另一个性能敏感点。常用的高亮库如 highlight.js 或 Prism.js在代码量大、语言种类多的时候高亮耗时会明显增加。我的经验是如果代码块不多少于 10 个直接用高亮库没问题如果代码块很多考虑懒高亮——只高亮视口内的代码块滚动到才高亮如果对性能要求极高可以关闭高亮用等宽字体 简单样式代替实测下来一个包含 20 个代码块、每个代码块 50 行的报告页面全量高亮需要 300-500ms而懒高亮可以把首屏渲染时间控制在 100ms 以内。5. 并发场景下的渲染稳定性Agent 扛并发时前端在经历什么5.1 并发请求对渲染层的冲击Agent 应用扛并发通常讨论的是后端如何调度模型调用、如何管理会话状态。但前端渲染层同样会受到并发冲击这一点容易被忽略。具体表现是当多个 Agent 请求同时返回、同时触发渲染时主线程会被渲染任务占满导致页面卡顿、交互无响应。如果每个请求都触发全量重渲染情况会更糟。我的处理策略是渲染队列 优先级调度所有渲染任务进入一个队列按优先级排序用户当前查看的内容优先级最高每次只执行一个渲染任务执行完通过requestIdleCallback或setTimeout让出主线程低优先级的任务比如后台预加载的内容可以延迟渲染这样即使同时有 10 个请求返回页面也不会卡死用户当前看的内容会优先渲染出来。5.2 渲染失败的降级方案并发场景下部分请求的 Agent 输出可能格式异常导致渲染失败。如果没有降级方案用户看到的就是白屏或错误提示。我的降级策略分三层第一层格式清洗重试。如果解析失败尝试用更宽松的规则重新清洗输出第二层纯文本降级。如果结构化渲染失败把原始内容以纯文本形式展示至少让用户能看到内容第三层错误占位。如果连纯文本都拿不到显示一个友好的错误提示并提供重试按钮这三层降级保证了即使 Agent 输出质量波动用户也不会看到完全空白或崩溃的页面。5.3 渲染性能的监控与预警生产环境里渲染性能问题往往是渐进式的——今天慢一点明天慢一点等到用户大面积反馈时已经很难定位了。我在项目里加了一个简单的渲染性能监控function measureRender(label, fn) { const start performance.now(); fn(); const duration performance.now() - start; if (duration 200) { console.warn([Render] ${label} took ${duration.toFixed(1)}ms); // 上报到监控系统 } }对超过 200ms 的渲染操作打点上报定期 review 这些数据就能在问题恶化之前发现并优化。6. 几个反直觉的结论和踩坑记录6.1 不是所有内容都适合流式渲染我一开始觉得流式渲染是 Agent 应用的标配所有输出都应该流式展示。后来发现对于结构化内容比如图表、表格流式渲染反而会带来更多问题——数据不完整时渲染出来的图表是错的等数据完整了又要重渲染用户体验反而不好。现在的做法是文本内容流式渲染结构化内容等数据完整后再渲染。文本流式输出让用户有实时感结构化内容一次性渲染保证准确性。6.2 渲染优化不是越早越好项目初期我就花了不少时间做渲染性能优化结果后来需求变了很多优化白做了。踩过这个坑之后我的原则是先跑通再优化优化要有数据支撑。具体来说先用最简单的方案把功能跑通加上性能监控等实际数据出来之后再针对瓶颈优化。大部分情况下80% 的性能问题来自 20% 的代码找到那 20% 比全面优化更有效。6.3 Agent 输出的格式稳定性比格式丰富度更重要我试过让 Agent 输出很丰富的格式——多种图表类型、复杂的嵌套结构、自定义的样式标记。结果发现格式越丰富输出稳定性越差渲染层要处理的异常情况越多。后来我收敛了格式种类只保留最常用的几种文本、表格、柱状图、折线图每种格式的模板都经过反复测试。格式种类少了但每种格式的稳定性大幅提升整体开发效率和用户体验反而更好。6.4 前端渲染的瓶颈往往不在渲染本身排查渲染性能问题时我一开始总盯着渲染库的配置和用法。后来用 Chrome Performance 面板分析发现大量时间花在了数据预处理和 DOM 操作上真正的渲染耗时占比并不高。比如 markdown-it 渲染大量文字时瓶颈其实在字符串拼接和 DOM 插入而不是 Markdown 解析本身。优化方向应该是减少 DOM 操作次数用 DocumentFragment 批量插入而不是换更快的 Markdown 解析器。7. 从 Demo 到生产我的 Agent 渲染方案演进路线7.1 第一阶段直接渲染快速验证最开始我的方案很简单Agent 输出 Markdown前端用 markdown-it 渲染图表用 ECharts 单独处理。这个阶段的目标是验证功能可行性不考虑性能和稳定性。这个阶段踩的坑主要是格式问题——Agent 输出不稳定经常需要手动调整提示词。但好处是快速跑通了完整链路对整体流程有了直观认识。7.2 第二阶段加归一化层提升稳定性功能验证通过后我开始处理稳定性问题。核心动作是在 Agent 输出和渲染层之间加了一个归一化处理层做格式清洗、结构校验、分块标记。这个阶段最大的收获是认识到Agent 应用开发中输出格式的契约设计比渲染技术本身更重要。一个好的契约能让渲染层的工作简化很多也能让 Agent 的输出更可控。7.3 第三阶段性能优化支撑并发到了生产环境并发和性能问题开始暴露。这个阶段做了几件事流式渲染改成分块增量更新避免全量重渲染图表更新加节流和批量处理消除闪烁渲染任务加队列和优先级调度保证并发场景下的响应性加渲染性能监控建立预警机制这些优化做完之后页面在 10 个并发请求同时返回的情况下也能保持流畅首屏渲染时间控制在 300ms 以内。7.4 第四阶段降级与容错保证可用性最后一阶段是完善降级和容错机制。Agent 输出质量波动是不可避免的关键是要保证即使输出异常用户也能看到内容而不是白屏或报错。三层降级策略格式清洗重试、纯文本降级、错误占位在这个阶段落地配合监控告警基本做到了输出异常不影响可用性。回头看整个演进过程我觉得最关键的一步是第二阶段加归一化层。这一步做完之后后面的性能优化和降级容错都有了基础整体复杂度也可控了。如果一上来就追求大而全的方案反而容易陷入细节迟迟跑不通完整链路。提示如果你正在做 Agent 应用建议先把归一化层设计好哪怕一开始很简单。这个层的存在会让后续所有工作都轻松很多。8. 一些实用建议和后续可以探索的方向8.1 给正在踩坑的同学的几条建议如果你正在做 Agent 应用开发以下是我觉得最有用的几条经验提示词里明确输出格式并且给出示例。模型对示例的遵循度远高于对规则描述的遵循度。不要相信 Agent 输出的数据格式所有数据在使用前都要校验和转换。这一步多花的时间会在调试阶段省回来。流式渲染要分块不要每次更新都重渲染全文。分块渲染是流式场景下性能优化的关键。图表更新要节流200ms 左右的窗口是比较好的平衡点。加渲染性能监控超过 200ms 的操作打点上报定期 review。设计降级方案保证 Agent 输出异常时用户仍能看到内容。8.2 后续可以探索的方向这个项目做完之后我还有一些想法没有来得及实践列出来供参考渲染层的自适应策略根据设备性能动态调整渲染策略低端设备用更保守的渲染方案Agent 输出的增量校验在流式输出过程中实时校验格式发现异常立即纠正而不是等输出完成后再处理渲染结果的缓存与复用相似结构的报告可以复用渲染结果减少重复渲染开销更细粒度的渲染优先级根据用户视口位置和交互行为动态调整渲染优先级这些方向我后续会继续探索有新的经验再整理分享。Agent 应用开发和渲染之间的关系本质上是一个不确定性管理的问题——如何在接受 Agent 输出不确定性的前提下给用户提供稳定、流畅的渲染体验。这个问题没有一劳永逸的答案但随着实践深入方案会越来越成熟。