最近在从0到1搭建Agent AI应用的前端架构,跑通之后回头梳理了一份设计文档。今天把这份文档的核心思路和落地细节整理出来,重点讲讲Agent AI前端和传统后台管理系统前端在架构层面到底差在哪,你会遇到哪些绕不开的坑,以及一套经过实际项目验证的分层方案。
Agent AI前端本质上不是"页面套接口"那么简单。它的核心职责是呈现一个AI代理从理解问题、制定计划、调用工具到输出结果的完整过程,并且让用户在这一过程中可以随时介入、纠偏、继续追问。这带来了一系列传统前端没有碰过的架构问题:流式数据处理、任务状态机、并发会话管理、工具调用的可视化编排。所以这不是一篇泛泛讲前端概念的文章,而是面向已经在做或准备做Agent AI产品的团队,聊一聊实际架构设计时要做的取舍和判断。
1. Agent AI前端的本质挑战:从"页面渲染"到"过程编排"
1.1 传统前端与Agent前端的根本差异
先理解一个关键区别。我们做传统后台管理前端时,面对的交互模型是"用户主动发起请求,后端返回结果,前端把结果渲染到页面上"。比如一个订单列表,点查询、拿数据、渲染表格,整个过程是线性、瞬时、确定的。前端不需要关心后端"怎么查的",只需要关心"查到了什么"。
Agent AI前端完全不同。用户提交一个问题后,AI要经过规划、拆解、调用搜索、读取知识库、生成中间结果、最终汇总等多个步骤。这些步骤是异步的、流式的、不确定的,而且可能持续几十秒甚至几分钟。前端要做的不是等结果回来再渲染,而是要像一个直播导演一样,把AI执行的每一个关键动作实时呈现出来。
我做了个类比,传统前端是"点播",Agent前端是"直播"。点播模式下怎么设计都行,加载慢一点也能忍;直播模式下每一帧都有意义,丢帧、卡顿、起播慢都是事故。Agent AI前端必须保证AI每一步执行过程都不落地无声,用户能随时看到"AI正在干什么"。这就是"过程编排"的核心。
1.2 Agent前端的核心功能域拆解
把Agent AI前端的功能拆开看,主要落在三个领域。
第一个是会话域。用户和AI的每一轮对话都要被管理,不仅仅是聊天记录,还包括每一轮对话对应的上下文、附件、引用来源。会话不是简单数组,而是一个有状态的实体。
第二个是任务域。AI在执行一个复杂任务时,会经历多个阶段。前端需要把这个任务的生命周期完整呈现出来,从待执行、执行中、等待用户确认、执行成功到执行失败。我曾经在处理一个Agent任务时候,AI中途调用了一个需要用户授权的工具,这时候前端必须弹出一个授权卡片,暂停任务流,等用户点击后再继续。这就是任务状态的介入点,传统前端根本不会遇到。
第三个是工具域。Agent会调用搜索、代码执行、数据库查询等工具,每个工具都有入参、出参、执行耗时。前端要把这些"偷偷做的事"可视化,让用户看到AI到底用了哪些工具、传了什么参数、拿到了什么结果。
这三个域交织在一起,构成了Agent前端的复杂度。所以架构设计的起点,不是选什么UI组件库,而是先把这三类数据模型定义清楚。
2. 架构分层的思路与核心技术选型
2.1 分层设计:状态层、引擎层、渲染层
纯前端项目最容易踩的坑,就是所有逻辑都堆在组件里。组件既要管状态、又要管网络请求、又要管渲染,一开始看着挺快,等会话一多、消息一长,代码根本没法维护。我在这个项目里反复调整之后,采用了三层结构。
状态层负责全局会话状态、任务状态、上下文数据的存储与变更。它不关心数据从哪里来,也不关心界面长什么样,是纯粹的数据中心。引擎层负责和后端通信,包括建立流式连接、解析协议、调度事件分发、管理并发请求。它是整个前端的大脑。渲染层只做一件事:把状态转换成界面。所有组件都消费状态层的数据,通过引擎层派发的事件驱动UI更新。
这三层之间通过明确的事件机制通信。引擎层收到后端数据后,不直接操作UI组件,而是把数据清洗成规范化的消息事件,丢给状态层;状态层更新后,渲染层通过订阅机制自然响应。这样做的好处是每层都能独立测试,也能独立替换。比如后端从WebSocket换成SSE,只需要改引擎层,渲染层完全不动。
2.2 协议设计:从"接口"到"事件流"
Agent前端和后端的通信,不能再用传统的REST请求-响应模式硬套。REST适合获取资源快照,但Agent执行过程是持续产生数据的,后端必须要主动推送。我在项目里最终确认了以SSE为主、WebSocket为辅的通信方案。
SSE的优势是轻量、基于HTTP、自动重连机制成熟,特别适合AI生成内容的单向推送场景。WebSocket的优势是双向通信,适合需要前端频繁向后端发送控制指令的场景。实际项目里,如果只是对话加工具调用展示,SSE完全够用;如果要做复杂的协同编辑、多人会议、大量控制指令下发,WebSocket更稳妥。
无论是SSE还是WebSocket,前端都要定义一套统一的事件协议。我定义的消息结构包含四个核心字段:消息ID、父消息ID(用于线程追踪)、消息类型、消息内容。消息类型精确分为文本增量、文本结束、状态切换、工具调用、工具结果、错误信息、会话结束。这套协议是整个前端架构的基石,协议设计不好,后面的渲染、状态管理、断线重连都会出问题。
2.3 并发模型:多个Agent同时运行时前端怎么扛
"AI Agent怎么扛并发"这个话题在相关从业者中间热度一直很高。很多人以为并发只是后端的事,前端只要老老实实发请求就行。实际操作下来根本不是这样,Agent前端面临的并发压力同样棘手。
一个用户可能同时开了三个任务:一个在写长文,一个在搜索资料,一个在整理数据。三个任务同时跑,每个都往页面推数据。这就像同时看三场直播,还要随时切换关注重点。前端要做的事比后端还复杂。
我采用的方案是会话级隔离的状态切片。每个会话在状态层里有独立的分片,互不干扰。事件分发时,引擎层根据消息ID找到对应的会话分片,进行精准更新。渲染层用虚拟列表优化长会话,每个会话组件只渲染可视区域内的消息。这样即使并发跑十个任务,页面也不会僵住。
连接管理上也要做约束。一个页面同时建立几十条SSE连接不现实,浏览器对HTTP/1.1的连接数限制是硬伤。我把连接抽象成连接池,空闲自动释放,同域只保留有限活跃连接。新任务启动时,如果连接池已满,就排队等待,而不是无脑建立新连接。这是很多人忽略但至关重要的优化。
3. 核心模块设计与实现要点
3.1 前端Agent Runtime运行时设计
Runtime是引擎层的核心模块,可以理解成前端世界里的"Agent运行时容器"。它负责一个会话从开始到结束的全部生命周期管理,包括连接的建立、事件的监听、状态的推进以及异常恢复。
Runtime对外暴露的接口设计成命令式与事件式混合。命令式接口用于主动操作,比如启动会话、停止生成、发送用户指令;事件式接口用于被动接收,比如onToken、onToolCall、onStatusChange。这种设计思路和架构设计工具里描述的内部元素关系很像——每个模块职责清晰,模块间通过定义好的事件总线进行通讯,避免网状依赖。
我写的Runtime大致是这样工作的:收到startCommand后,根据会话ID新建一个执行上下文,发起SSE连接;连接建立后,监听消息流,每条消息经过协议解析器清洗成标准结构;根据消息类型分发给对应的处理器,比如文本增量走tokenBuffer聚合,工具调用走工具状态机更新;最后所有状态变更统一提交到状态层。一个会话结束后,Runtime负责清理连接、释放内存、标记会话状态。
Runtime还有一个重要职责是取消与中断。用户点击停止生成时,前端不能假装停了下拉流,而是要真正断开连接、清除缓冲区间、把会话状态切换到已中止。这个逻辑如果没有收敛在Runtime里,散落在各组件中,很容易出现页面显示还在生成但后端早已停止的诡异现象。
3.2 渲染引擎:消息类型、任务流与工具调用可视化
Agent渲染这块,普通的聊天界面远远不够。我按照消息协议将渲染组件拆分成消息渲染器和任务流渲染器两种形态。
消息渲染器处理用户消息、AI文本、错误提示、参考引用。AI文本使用流式渲染,每收到一个增量就追加到对应的消息块里。需要考虑的是Markdown渲染的性能,Agent输出量大,每次都重新编译整段Markdown代价太高。我的做法是分片渲染,先按增量内容直接插入文本,在流结束后再对完整消息做最后的格式编译。实测下来,体感流畅很多。
任务流渲染器处理的是AI的计划、工具调用、执行结果。这块是Agent前端独有的设计。我把AI执行的整个思考过程渲染成一张阶段卡片流,每张卡片包含:AI的思考意图、调用的工具名称、传入的关键参数、执行状态和返回摘要。卡片默认折叠,只露出AI"正在做什么",点击展开才能看到细节。这样做的原因很实际:大多数用户不关心技术细节,但遇到问题时,展开卡片可以快速定位是哪个环节出了问题。工具调用的可视化,本质上就是给用户一把"可回溯的放大镜"。
渲染层这块还有一个容易忽略的点,就是稳定标识。每个渲染节点必须和消息ID绑定。React的key绝对不能直接用数组索引,因为流式场景下中间插入、乱序到达都有可能,用索引当key会引发严重渲染错乱。我踩过这个坑,后来统一用消息ID作为所有渲染节点的稳定key。
3.3 状态管理:从Redux到可恢复会话状态机
状态管理是Agent前端最容易被低估的部分。传统前端的状态顶多分个全局态、局部态,Agent前端至少要管理三类状态,它们的生命周期各不相同。
第一类是UI状态,比如侧边栏开关、弹窗显隐,这类状态死了就死了,刷新拉倒。第二类是会话快照,包括历史消息、上下文ID、工具调用记录,这类状态必须持久化,用户刷新页面后要能恢复现场。第三类是执行状态机,描述当前Agent处于什么阶段。这个状态机比前两类复杂得多,它的值包括空闲中、连接中、收取中、等待工具确认、已暂停、已完成、已失败。
我尝试过用Redux直接管理所有状态,结果很痛苦。Action和Reducer数量爆炸,同事们看Action日志都看不懂在干什么。后面换成Zustand配合状态机的方式,核心状态用轻量store,执行状态用状态机模型管理。状态机的价值是强制约束状态的合法迁移路径,不允许从"收取中"直接跳到"已完成",必须经过"连接关闭"等中间态。这就避免了消息还没收完就显示完成这种逻辑漏洞。
4. 实操过程:从0到1搭建一个最小可用架构
4.1 项目骨架与依赖选型
这里分享一套经过实践验证的最小可复现方案,适合作为Agent AI前端项目的起点。技术栈选型遵循克制原则:能用React和TypeScript就不再加更多重量级框架,极简方案用Vite构建,核心依赖只有三个。
状态管理用Zustand,比Redux代码量少得多,适合管理频繁更新的会话状态;通信方案用原生fetch + SSE,没有额外封装,避免抽象过度;渲染方案用React组件自绘,暂时不上重型可视化库。为了让代码结构清晰,我按分层思路建立了目录骨架,每个模块各管一块,边界明确。
目录里engine放的是Agent运行时、协议解析、连接池管理;store放的是会话状态切片和任务状态机;components按渲染器和任务块拆分;types是全局类型定义,尤其是消息协议类型,这是团队协作的契约文件。类型定义一定要先写,前后端联调全靠它对齐字段,字段不确定就先Demo验证再固定,否则后期改类型的成本是几何级数增加的。
4.2 核心代码实现:Agent运行时与流式渲染闭环
我写了一个极简的Agent Runtime类,描述这个运行时如何工作。它接收一个会话ID和任务参数,发起请求,然后通过内部的消息处理器循环处理收到的事件。事件分两类:文本增量和工具调用。文本增量会更新消息缓冲区和已完成的文本内容;工具调用会新增一条工具执行记录,并立刻更新UI。
这个循环逻辑指向整个架构的要点:不要面向"完整的最终消息"编码,要面向"持续到来的增量片段"编码。updater依赖了React的setState函数,但在实际复杂项目里,这种直接依赖UI更新函数的方式会被事件总线取代,由事件分发中心统一通知多个store和组件更新,这样更利于隔离。
流式渲染的部分,我用一个Recorder组件说明设计:新建会话时,引擎层创建Runtime实例,通过onUpdate回调把最新状态提交到状态层;状态层的shallow比较保证不产生冗余渲染;最终render函数消费状态,进行界面更新。手动模式下,开始按钮由用户触发,实际上运行时可以在收到后台推送的状态events时自动开启下一个任务。
4.3 并发控制与连接池的实际落地
并发这块我实现了最简连接池模型。池子维护一个活跃连接列表,提供获取连接和执行任务的方法。获取不到空闲连接时,任务进入等待队列,避免无限制地创建资源。
实际落地时,我发现连接池的等待策略需要配合用户预期管理。如果一个任务等太久,用户会以为系统卡死了,所以等待队列里超过一定时间的任务会触发UI提示,让用户知道"前面还有两个任务在跑,你的任务排队中"。这个体验细节是从实际项目中长出来的,不做的话,用户分不清是排队还是故障。
4.4 接入AI组件库的替代路线
考虑到并非所有团队都有精力从零搭建Agent前端,这里补充一条"站在巨人肩膀上"的路线。目前市面已有不少开源的AI对话前端组件库,直接复用可以省掉大量基础工作。这类组件库通常已经内置了流式聊天、消息气泡、Markdown渲染、停靠式工具展示等能力,适合MVP快速验证、内部工具、活动页面等场景。
但沉浸式Agent编排、深度自定义工具卡片、复杂状态恢复这些场景,通用组件库往往不够用。组件库给的是零件,你要做的是整车,零件通用,但底盘、引擎、电路设计还是得自己来。我实际项目里的选择是,先用通用组件库快速搭出第一版跑通流程,验证Agent交互逻辑可行后,再针对核心体验自研组件替换掉通用实现。花在调研组件库上的时间不算浪费,它能让你快速摸到这个领域有哪些标准交互范式。
5. 常见问题与排查技巧实录
5.1 SSE连接数受限与HTTP/2改造
浏览器对同域HTTP/1.1连接数限制通常是6个。如果有三个会话同时跑,每个会话一条SSE连接,再加上正常API请求,很容易撞上限。现象是页面请求全部pending,新任务一直不返回。
排查思路第一步看Network面板,确认是否为连接数限制;第二步确认当前协议是HTTP/1.1还是HTTP/2。HTTP/2多路复用能极大缓解连接数问题,但需要网关支持。如果只能在HTTP/1.1下工作,就需要收紧连接池活跃数,同时把不用的连接及时关闭。另外,大型场景下可以考虑将流式服务的域名独立出来,避免和业务API争抢连接。
5.2 流式渲染卡顿与React更新优化
我实际测试中遇到过一个典型的性能问题:一段长文本流式返回,页面每100毫秒收到一个片段,React每次setState整棵树都在更新,CPU占用直接拉满。现象是页面滚动不跟手、输入框打字滞后、动画掉帧严重。
原因很简单,每次增量都触发了整个消息列表组件的全量diff。解决思路是收敛更新范围和频率:用分片聚合缓冲增量,达到一定量再统一提交更新;重渲染体量大时,虚拟列表只渲染可视区;最后对高频更新组件包memo,用shallow compare拦截无关更新。实测之后CPU占用从高到明显下降,页面保持顺畅。这里顺便提一句,如果你看React DevTools里高亮更新列表一大片,说明更新范围没收敛,代码层面基本就是父子组件没切断不必要的响应。
5.3 消息重复与状态不一致问题
流式连接中断后自动重连,是SSE自带的能力,但重连后经常遇到消息重复推送的问题。表现为同一条文本出现两次、工具卡片重复展示、消息计数对不上。
根因在后端没有维护消息消费游标,重连时从起点重发。前端的规避方案是在协议解析层做幂等处理,每条消息携带唯一ID,解析器维护已处理ID集合,重复消息直接丢弃。更彻底的做法是让后端支持按游标续推。排查的时候先看Network里有没有重连请求,再看重连后的首条消息ID是否从0开始,基本上能快速定位责任方。
5.4 状态恢复:刷新页面后现场还原
用户刷新页面后,正在执行的任务断了,重新打开只看到历史消息,Agent已经执行到一半的工具链全部丢失。这是状态快照设计不完善导致的。
我最终采用的做法是给任务状态加上持久化快照。引擎层每收到一次关键状态变更,就同步写入本地存储;会话关键节点也落盘,包括当前上下文、已执行工具列表、等待用户确认的挂起状态。刷新后恢复流程是:从本地存储读取快照,重建Runtime上下文,尝试恢复连接;连接成功则询问后端当前任务是否还在执行,在的话继续订阅;不在的话把状态标记为中断,并补一条系统提示说明情况。这套机制上线后,用户再也没有反馈过"刷新一下任务就消失了"。
个人实操中的一些体会
架构文档写起来条理清楚,实际落地全是一地鸡毛。这个项目做到现在,我最深的体会是:Agent AI前端架构的成败不在某个技术选型多高明,而在你对"AI执行过程"的理解深度。你把这个过程想清楚了,协议、状态机、渲染分层都是水到渠成的事;想不清楚,堆再多技术也没用。我建议所有准备做Agent前端的团队,先别急着写代码,花两周时间把你们的AI代理在后台会发生哪些关键节点完整列出来,定义成消息类型,前端的骨架自然就出来了。这是最省时间的路径。