news 2026/10/15 4:02:02

Agent应用开发中的渲染层优化:从流式渲染到3D场景的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent应用开发中的渲染层优化:从流式渲染到3D场景的实战指南

聊个容易被忽视的话题。过去两年我一直在做各类 Agent 应用,从企业内部的知识库问答助手,到面向 C 端的电商导购智能体,再到带 3D 场景的数字人项目。中间踩过的坑非常多,但最让我意外的发现是:真正让用户觉得“这个 Agent 好用”或者“这个 Agent 很蠢”的分水岭,往往不在大模型选型、不在 Prompt 设计、也不在工具调用编排,而在于渲染层。

什么叫渲染层?就是你屏幕上看到的那些东西——对话气泡里的文字、正在滚动的 Markdown 文档、实时更新的图表、3D 场景里的模型姿态、流式输出的状态指示。很多做 Agent 的朋友把精力全扑在后端,觉得只要 AI 调用够快、工具编排够稳,前端随便糊一个界面就行。结果一上线就翻车:长文本渲染卡成幻灯片、ECharts 图表疯狂闪烁、并发一高浏览器直接崩溃,用户的第一反应永远是“这 Agent 不行”。

这篇文章不打算讲怎么调 Prompt、怎么选 Agent 框架,这些话题已经说得够多了。我想认真聊聊 Agent 应用开发和渲染之间的关系,结合我实际踩过的坑,把流式渲染、Markdown 长文本、可视化图表、3D 场景、并发压力这些点逐个掰开揉碎。适合正在做 Agent 应用的前后端同学,也适合刚入门 AI 应用开发、想知道“为什么我的界面这么卡”的新手。

1. 为什么 Agent 应用开发总是把渲染当成配角

1.1 默认“Agent = 后端问题”,是最大的误区

你去翻任何 Agent 开发学习路线,大概率看到的是大模型原理、提示词工程、RAG、函数调用、记忆机制、Agent 框架选型,最后才零零散散提一嘴前端。这个顺序本身就把渲染摆在了“收尾工作”的位置上,好像只要后端逻辑足够强大,前端随便摆个聊天框就能交付。

但真实情况是:用户对 Agent“聪不聪明”的判断,几乎全部发生在渲染层。你后端调用再快,如果前端是空白态,用户等 8 秒看到的就是一个“笨”字;你模型推理再强,如果输出是一坨没有任何排版的长文,用户根本读不进去;你工具调用再准,如果中间过程完全不可见,用户只会觉得你在变魔术,无法建立信任。

我做过一个内部知识库 Agent,后端换了好几次模型,用户反馈一直说“反应慢、不智能”。后来我仔细排查,发现真正的问题是前端要等完整响应才渲染,一个检索加生成要跑 15 秒,用户盯着白屏 15 秒,忍不了。改成流式渲染、边生成边显示之后,同样的模型、同样的后端,用户评分直接从 3.2 涨到了 4.6。模型一次都没换,变的只是渲染层。

所以我的第一个观点是:渲染不是 Agent 开发的“最后一步”,而是用户体验的起点。它和后端推理是两条腿,缺一条都走不动。

1.2 渲染不是单向展示,它参与 Agent 的交互闭环

再往深一层想,渲染和 Agent 之间的关系不是单向的“后端产出数据,前端负责展示”,而是一对互相咬合的齿轮。我在实际开发中越来越强烈地感受到,渲染事件的反馈会反过来改变 Agent 的行为轨迹。

举几个例子。第一,流式渲染让用户可以在生成过程中点击“停止”,这个中断信号会通过 AbortController 传回后端,结束这次推理。如果没有流式渲染,用户只能干等完整结果,交互体验完全是另一个物种。第二,Agent 在调用工具时,如果前端把“正在检索文档”“正在查询数据库”这些中间状态实时渲染出来,用户往往会基于这些信息输入更精确的追问,相当于渲染层替 Agent 收集了额外的上下文。第三,在数字人场景里,用户看到虚拟形象的表情和口型变化,会下意识地调整自己说话的节奏,这种感知层面的反馈是纯文本界面完全无法提供的。

现在的 Agent 应用也在从单纯的“聊天框”走向“聊天 + 看板 + 画布 + 3D 场景”的多面渲染。你让 Agent 做一个数据分析,它不光要说话,还要在右侧渲染出折线图;你让 Agent 调一个 3D 模型参数,它不光要回复“改好了”,还要让模型实时变化给你看。这种趋势下,渲染已经从“配角”变成了 Agent 应用的核心架构问题之一。

2. 流式渲染是第一道生死线:markdown-it 为什么会卡

2.1 流式输出的原理与前端落地的三种姿势

大模型生成是逐 token 返回的,这是流式渲染的前提。后端通常通过 SSE 或 WebSocket 把增量文本推给前端,前端再把这些增量拼到界面上。说起来简单,做起来差别很大,我见过三种做法。

第一种是全量替换,收到完整响应后再一次性渲染。这种方式实现最简单,但等待时间等于生成时间加渲染时间,体验极差,基本可以排除。第二种是简单拼接,每收到一个 chunk 就 append 到内容末尾,用 textContent 或 innerHTML 直接更新。这种方式对纯文本还行,但一旦涉及 Markdown、代码块、表格,就会出问题——因为整个消息的 Markdown 结构随时都可能因为新的 chunk 而改变,比如一个代码块的闭合 fence 还没到,你无法知道当前这段到底是代码还是普通文本,只能整段重新解析渲染。

第三种是增量解析加按块渲染,也就是我一直推荐的方式。把 Markdown 流切成逻辑块,已经定稿的块只渲染一次并缓存 HTML,只有正在增长的“尾部块”才持续更新。这样渲染开销不随消息总长度线性增长,而是基本恒定在尾部那一小块上。

2.2 markdown-it 渲染大量文字,到底卡在哪里

很多人在搜“markdown-it 渲染大量文字卡顿”这个问题,我自己也踩过。直接说结论:卡顿的根子不在 markdown-it 本身,而在你的调用方式。

markdown-it 的 render 是一个同步的完整解析过程,输入多长,它就要把整段文字从头到尾 tokenize 一遍。如果你的流式消息已经积累了 5000 行 Markdown,每收到一个 chunk 就对全部 5000 行重新 render 一次,那每一次渲染都是 O(n) 的完整开销。SSE 推送频率一高,主线程就被反复阻塞,每秒可能有好几百毫秒在做无用功,页面不卡才怪。

还有个被忽视的点是代码高亮。很多人用 markdown-it 配 highlight.js,代码块越大、语言越多,高亮成本越高。我在一个 Agent 项目里让模型输出过一个三千行的配置文件,每次增量更新都要重新高亮整个代码块,高亮一次能吃掉两三百毫秒,肉眼可见地掉帧。再加上 DOM 更新之后的浏览器 reflow,整个页面会变得越来越迟钝,直到完全无法操作。

所以“markdown-it 渲染大量文字”的正确解法,不是换一个更快的解析器,而是根本不要“渲染大量文字”这回事。要把渲染单元变小、变可缓存。

2.3 我采用的分块增量渲染方案

这里分享一套我在生产环境验证过的思路,核心就是“定稿块缓存 + 尾部块重渲染”。

先在逻辑层面把流式文本切成块。切分点主要靠 Markdown 的天然边界:空行分隔的段落、表格的整行、代码块的开始和结束 fence。前端维护两个部分:一个 cache 数组存已经定稿的块的渲染结果,一个 tailBuffer 存还在增长的尾部原文。收到新 chunk 时,尝试把 tailBuffer 中超过边界的内容“固化”成定稿块,只对未定稿的尾部重新渲染。

// 伪代码,重点在思路 const md = window.markdownit({ html: false, linkify: true, highlight: (str, lang) => hljs.highlight(str, { language: lang }).value }); let finalizedBlocks = []; // 已定稿块的 HTML let tailBuffer = ''; // 正在增长的尾部 function onChunk(text) { const { closed, open } = splitBlocks(tailBuffer + text); // closed: 已闭合的完整块数组 // open: 仍未闭合的尾部片段 for (const block of closed) { finalizedBlocks.push(md.render(block)); } tailBuffer = open; renderTail(); // 只渲染尾部 syncScroll(); // 保持在可视位置 }

这套方案里有两个细节值得展开。第一是 splitBlocks 的切分逻辑,我会优先处理代码块的 fence,因为代码块内部即使出现空行也不能当作段落边界;其次是表格,Markdown 表格往往一个行一个 chunk 地到达,可以把连续的表头行和分隔行归并成一块。第二是 renderTail 不要每个 chunk 都跑,用 requestAnimationFrame 合并,保证最多一帧一次。

如果聊天历史特别长,比如积累了几百轮对话,我还会配合虚拟滚动。只渲染视口附近的几十条消息,远处的消息全部回收。很多 Agent 应用越用越卡的另一个原因就是 DOM 节点无限膨胀,这件事必须在架构层面解决,而不是指望浏览器替你扛。

3. Agent 输出图表时的 ECharts 闪烁排查实录

3.1 Agent 场景下图表更新为什么这么频繁

Agent 应用和普通报表页面的一个显著区别是:图表配置不是人写的,是模型生成的。模型在一次回答里可能先画一版图,然后根据上下文微调数据,又画一版,甚至同一个会话里反复生成好几种图表。这种高频、全量的配置更新,直接把前端图表组件的弱点全部暴露出来。

我做的第一个带数据分析功能的 Agent,用户问“按月份拆一下销售额趋势”,Agent 调工具拿到数据后,会返回一串 ECharts 配置,前端拿到就 setOption。本来一切正常,直到我发现用户在连续追问时图表开始闪烁。那个闪烁非常典型:每次更新先白一下,再重新绘制,视觉上就像屏幕在眨眼。

3.2 闪烁的根因:setOption 的合并语义没吃透

排查到最后,问题基本都出在 setOption 的使用姿势上。ECharts 的 setOption 有两个关键参数,一个是 notMerge,一个是 lazyUpdate。很多人的习惯是每次更新前先调用 chart.clear(),再 setOption,这样等于完全销毁再重建整个图表实例,所有组件、坐标轴、动画状态全部重来,不闪才怪。

还有一种情况是接收到的配置每次都带了完整的 series 数组,而 setOption 默认的合并模式是智能合并,按理说不会全量重绘。但如果你在配置里手动设置了 notMerge: true,或者通过 clear 重建实例,就会把 ECharts 的增量更新机制废掉,每次都是全量重绘,闪烁和卡顿一起来。

另一个容易被忽略的原因是动画冲突。ECharts 默认开启 animation,频繁 setOption 时每个系列都会执行入场动画,旧的还没放完新的又来了,视觉上就是跳变和闪动。尤其当数据量比较大时,动画的过渡计算还会抢占主线程,进一步放大卡顿。

3.3 我踩过的三个坑和最终方案

先说坑。第一个坑是每次 Agent 返回配置就 clear 再 setOption,这是最常见的错误,解决方法是不要 clear,把实例生命周期拉长,更新时用合并模式。第二个坑是图表更新频率比渲染帧率还高,比如 Agent 在短时间推送了 20 次配置更新,如果每次都同步 setOption,浏览器根本来不及处理。第三个坑是容器尺寸问题,初始化时容器宽度为 0 或者处于隐藏状态,图表按错误尺寸渲染,等容器可见时再 resize,视觉上就会闪一下。

我的最终方案分三层。第一层是取消 clear,保持同一个实例。第二层是把所有更新收进一个队列,用 requestAnimationFrame 合并到每帧最多执行一次。第三层是更新时明确设置 lazyUpdate: true。

let pendingOption = null; let rafId = null; function queueChartUpdate(option) { pendingOption = option; if (rafId === null) { rafId = requestAnimationFrame(applyUpdate); } } function applyUpdate() { chart.setOption(pendingOption, { notMerge: false, lazyUpdate: true }); pendingOption = null; rafId = null; }

这里 lazyUpdate 很关键,它告诉 ECharts 可以先合并变更,等到下一帧统一渲染,避免高频调用时每帧重复绘制。另外我建议关掉或者显著缩短入场动画,尤其是高频流式更新场景。动画的使命是让人理解“变化”,而不是让屏幕闪成迪厅,Agent 场景下数据变化太快,动画基本没有展示机会,还添乱。

4. 3D 渲染、卡通风格与数字人 Agent 的边界探索

4.1 Agent 应用里为什么会出现 3D 网页渲染

聊完 2D 图表,再往视觉体验更重的地方走。最近 Agent 应用一个明显的趋势是往 3D 场景延伸,搜“3D 网页渲染”这个词的人越来越多,不是搞游戏开发的,而是做 Agent 应用的。

典型场景有三个。第一个是商品导购,用户跟 Agent 说“我想换个颜色、改个材质”,后端 Agent 理解意图后修改 3D 模型的参数,前端通过 WebGL 实时更新材质贴图和光照,用户看到的效果就是模型在眼前直接变化。第二个是数字人,Agent 驱动一个虚拟形象与用户对话,形象的状态、表情、口型全部由渲染引擎实时计算。第三个是空间规划类应用,比如让 Agent 根据用户需求自动布置房间,每一次布局调整都要在 3D 场景里刷新模型位置。

这些场景有一个共同点:Agent 的“输出”不再只是文本和图表,而是直接操作一个三维世界。渲染在这里承担了比展示更重的任务——它要呈现 Agent 的决策结果,并且要足够快、足够流畅,否则用户感觉不到“智能”的存在。

4.2 移动端与 Web 的渲染引擎选型:从 WebGL 到 Impeller

做 3D 渲染,引擎选型是绕不开的问题。Web 端主流是 Three.js 配合原生 WebGL,或者用 React Three Fiber 做声明式开发;移动端如果是 Flutter 技术栈,就会碰到渲染引擎的选择问题。

很多人对 Flutter 的底层不敏感,其实 Flutter 的渲染引擎直接决定了应用的流畅度。旧版本 Flutter 默认用 Skia,它的一个痛点是运行时着色器编译——首次绘制某些复杂图形时,要现场编译着色器,导致掉帧,也就是著名的“first frame jank”。新版本引入的 Impeller 引擎则完全不同,它在构建阶段就把着色器预编译好,运行时不再需要临时编译,画面稳定性大幅提升。

我用一个厨房的类比来解释:Skia 像一个每道菜都从洗菜切菜开始现做的厨子,客人点得越快越容易手忙脚乱;Impeller 像一个提前备好了所有净菜和酱汁的中央厨房,出餐又快又稳。如果你的 Agent 客户端是 Flutter 做的,而且界面里有大量动态图形、数字人动画、图表流转,Impeller 带来的收益会非常明显。这也是为什么做 Agent 应用的人开始关注“渲染引擎原理”这类底层问题——渲染引擎的性能上限,决定了 Agent 的体验天花板。

4.3 数字人 Agent 与卡通渲染的实战经验

数字人 Agent 是个很典型的“渲染参与智能表达”的场景。为了让虚拟形象看起来友好,很多团队选择卡通渲染风格,也就是 NPR(非真实感渲染),搜“liltoon 卡通渲染”“unity shader NPR 卡通渲染”都是在做这类事。卡通渲染的核心是描边、色块、高光控制,让角色有二次元质感,而不是追求物理真实。

这里我要讲一个很容易出问题的点:Agent 的状态和动画之间的映射。我见过一个项目,用 token 到达事件直接驱动数字人的口型,结果就是说话像机器人抽搐,完全没有自然感。正确的做法不是按 token 驱动,而是按语义状态驱动。Agent 的思考过程映射为“思考中”的动画(眼神飘移、轻微点头),输出内容按句子切分映射为“说话”状态,工具调用映射为“操作中”的等待姿态。中间要加一个平滑缓冲,让动画状态切换的过渡时间控制在几百毫秒级别,而不是瞬间跳变。

这类经验的核心是:3D 渲染在 Agent 应用里不只是一个“好看”的问题,它是 Agent 状态机的外部表现层。你设计的每一个内部状态,都要考虑它在渲染层长什么样,否则 Agent 内部逻辑再清晰,用户看到的也只是个僵硬的角色。

5. 并发压力下的渲染治理:Agent 扛并发的前端一战

5.1 “扛并发”从来不只是后端的事

搜“ai agent 怎么扛并发”的很多是后端或架构出身的人,第一反应是 GPU 推理集群、负载均衡、消息队列。这些当然重要,但我在实际项目里发现,Agent 应用一旦用户量起来,最先崩的往往是浏览器端和前端渲染。

原因在于 Agent 应用的天然特性:长连接、高频率推送、大文本渲染。每路用户持续维持 SSE 流,每几秒就有新的 chunk 推送过来,每条消息可能是几千字的 Markdown,还要配图表、配工具调用状态。如果有一百个并发用户,后端要处理的是 100 路推理,前端则要在同一台浏览器进程里为这些流做渲染、DOM 更新、页面布局。桌面端浏览器还能扛一扛,移动端的小内存设备很快就顶不住了。

我印象最深的一次线上事故,是因为用户开了多个会话窗口,每个会话都在流式输出长文,结果页面直接白屏崩溃,控制台报的是一堆“JavaScript heap out of memory”。那一刻我才彻底明白,Agent 的并发不是后端单方面的问题,渲染层的资源管理才是真正的短板。

5.2 前端侧的几个治理手段

前端侧的并发治理,我总结下来就四个字:减、分、批、断。

减,是减少不必要的渲染量。聊天记录用虚拟滚动,视口外的 DOM 节点及时回收;Markdown 渲染结果做缓存,同一段内容不重复解析;图片和附件懒加载。分,是把渲染任务拆出去。Markdown 解析这种 CPU 密集型工作可以丢到 Web Worker 里跑,主线程只负责接收解析好的 HTML 字符串然后做 DOM patch,这样即使某条消息特别长,也不会长时间阻塞用户滚动。

批,是对高频更新做合并。不止是 ECharts 需要 requestAnimationFrame 合并,流式文本、滚动位置同步、状态指示器,全部要按帧合并,不允许一个 chunk 触发一次完整渲染。断,是必须有取消机制。用户切换会话、关闭页面、点停止生成时,前端要立刻用 AbortController 中断 SSE 连接,同时取消所有等待中的 requestAnimationFrame 和 Worker 任务,避免废弃数据继续占用渲染资源。

const controller = new AbortController(); async function connectAgentStream(question) { const res = await fetch('/api/agent/chat', { method: 'POST', body: JSON.stringify({ question }), signal: controller.signal }); // 处理 SSE 流... } // 用户切换会话时 function switchSession() { controller.abort(); cancelPendingRenders(); }

这套组合拳打下来,同一个页面我实测能同时承载 5 到 6 路流式输出而不崩溃,内存占用还降了大约 40%。

5.3 服务端也可以帮渲染减负

还有一点很多人没意识到:渲染压力的根源有一部分在后端。如果后端把 token 一个个推过来,前端就只能跟着 token 走,做什么优化都被动。但如果后端在推送前先做一次“语义分块”,把 token 累积成完整的段落、完整的代码块再推送,前端的工作量会小很多。

我现在的做法是后端在 SSE 事件里定义好事件类型,比如 paragraph_start、paragraph_delta、paragraph_end、tool_call、chart_update。前端收到 paragraph_end 就知道这个块已经定稿,可以直接走缓存逻辑;收到 chart_update 就按图表更新队列处理。相当于前后端约定了一层“渲染协议”,把“什么时候该渲染什么”的决策提前到了服务端。

后端还可以做一些别的减负:文本内容用压缩传输,避免大段 Markdown 在网络层浪费时间;如果前端明确报了性能差,后端可以主动降低推送频率,把多个增量合成一个批次再推。这种双向协商在普通 Web 应用里不多见,但在 Agent 应用里非常必要,因为数据流的节奏是由模型生成决定的,不是用户操作决定的,前端没有天然的节流手段。

6. 常见问题速查与实操心得

6.1 Agent 与渲染问题的速查表

我把这些年反复遇到、也反复被朋友问起的问题整理成一张表,方便你在排查时快速定位。

现象根因处理方向
长回答越到后面越卡每个增量都对全文重新 Markdown 解析分块缓存 + 只渲染尾部
流式文字抖动、跳动每个 token 都触发 DOM 更新requestAnimationFrame 合并
聊天记录很长时内存暴涨DOM 节点无限膨胀虚拟滚动 + 回收视口外节点
图表每次更新先白再画clear 后重建实例保持实例,用 setOption 合并更新
图表高频更新卡顿setOption 调用频率过高lazyUpdate + 队列合并
多会话切换后页面变慢旧 SSE 流未取消,还在持续渲染AbortController 中断
数字人口型像抽搐按 token 直接驱动动画按语义状态 + 平滑缓冲
移动端首帧卡顿运行时着色器编译使用预编译引擎如 Impeller

这张表看起来都是孤立问题,实际上指向同一个答案:Agent 应用里的渲染必须做增量、做合并、做缓存、做取消,这四个词缺一不可。

6.2 几条实操心得

第一,所有高成本渲染都要有降级开关。我在项目里留了一个全局配置,负载高或者设备性能差时,自动关掉图表动画、3D 特效、模糊阴影这些“锦上添花”的东西,只保留文字流式这种核心体验。宁可界面朴素,也不能让用户觉得卡。

第二,优化之前先量化,别靠感觉。用 performance.now 记录每次渲染耗时,用 Long Tasks API 监测主线程阻塞,用 Memory 面板看 DOM 节点数量。我见过太多人凭直觉优化,最后发现瓶颈根本不在他们猜的地方。数据说话,这行永远成立。

第三,渲染协议和前后端架构要一起设计,不要等联调再补。你后端一个事件怎么推、推多细、有没有块边界、支不支持取消,直接决定了前端渲染方案的可行性。把这些在接口设计阶段就定下来,能省掉后面无数个加班的夜晚。

第四,测试用例里必须有大数据量场景。单条 500 字的流式回复,什么方案都能跑通;单条 8000 字的 Markdown 加三个图表,才能看出方案的差距。我在团队里定了个规矩:所有 Agent 前端改动,必须用一组“极端用例”回归,包括超长代码块、巨型表格、连续快速更新的图表,否则不允许上线。

最后说点个人体会。我见过太多团队把 Agent 当成“一个黑盒 API 加一个聊天框”来做,上线后用户反馈差,就怪模型不行。其实很多情况下模型已经尽力了,是渲染层把它的能力打了折扣。我现在做架构评审,一定会问三个问题:流式能不能在 1 秒内出字?长文本会不会拖垮主线程?高频更新能不能保持稳定?这三个问题回答不了,后端再漂亮也白搭。

Agent 应用开发里的渲染,不是把结果摆上去,而是把智能“演”出来。渲染层做得好的 Agent,用户会觉得它聪明;做得差的,再强的模型也会被当成人工智障。这个道理,是我用数不清的白屏和闪烁换来的。希望这篇思考能让你少踩几个我踩过的坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 4:01:37

做了这么久技术博主,我从后台私信里看透了这几种人

做技术专栏这么久,后台私信几千条,我现在是分情况处理的——但一开始不是。 刚开号那会儿我每条都回。有人问问题,我认认真真写解答;有人要加微信,我不好意思拒绝;有人半夜甩过来一整段代码让帮忙看&#x…

作者头像 李华
网站建设 2026/10/15 4:00:07

工业机器人这块可以按 **配线 → 操作编程 → 通信程序** 来理解。下面用 **ABB / FANUC / KUKA / 汇川** 做主线,并补一套 C# 上位机通信示例

工业机器人这块可以按 配线 → 操作编程 → 通信程序 来理解。下面用 ABB / FANUC / KUKA / 汇川 做主线,并补一套 C# 上位机通信示例。 1. 基本配线 机器人控制柜里通常有这几类线:线路类型作用配线要点动力电源给控制柜、伺服供电AC 380V/220V 或 DC 2…

作者头像 李华
网站建设 2026/10/15 4:00:07

51单片机光照强度显示系统:光敏电阻+ADC0804+LCD1602全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 4:00:05

小白程序员轻松入门大模型——多Agent框架实战与避坑指南

本文深入剖析多Agent框架的应用误区,强调其并非简单的角色增加,而是需要精心设计的架构。文章指出,使用多Agent框架前,需评估单Agent结合工具循环是否能完成任务,并明确角色分工、交接协议和终止条件。同时&#xff0c…

作者头像 李华