AG-UI协议详解:基于SSE的AI Agent实时状态流设计

发布时间:2026/7/7 21:36:09

AG-UI协议详解:基于SSE的AI Agent实时状态流设计 1. 项目概述为什么 AG-UI 不是又一个“AI 协议概念”而是你明天就要面对的工程现实我第一次在客户现场看到 AG-UI 被落地是在去年三季度。那是一家做智能投研的金融科技团队他们原本用 REST polling 的方式让前端轮询 agent 的执行状态——结果用户点击“生成季度风险报告”后页面卡住 47 秒中间弹出 3 次“加载中…”提示最后还因为超时重试导致同一份报告被生成了两次。运维日志里全是504 Gateway Timeout和前端反复建立连接的痕迹。他们不是没试过 WebSocket但工程师告诉我“一接入就崩调试器里全是乱序的二进制帧连 event.id 都对不上。”这就是 AG-UI 出现的真实土壤它不是学术论文里的理想模型而是从生产环境的焦糊味里长出来的协议。它的核心诉求非常朴素——让 UI 不再猜 agent 在想什么而是实时看见它正在做什么、卡在哪一步、需要什么帮助。你不需要懂 GPT-5.2-Codex 的推理链有多深但你必须知道当用户问“为什么上个月的回测收益突然下跌”agent 可能要花 92 秒完成 7 个动作拉取原始行情、比对因子暴露、调用风控模型、校验数据一致性、生成归因图谱、检查合规阈值、拼接自然语言结论而你的前端不能只在第 92 秒才刷新一次 DOM。AG-UI 解决的从来不是“怎么让 AI 更聪明”而是“怎么让人类不焦虑”。它把 agent 的内部状态——那些曾经藏在日志文件里、监控大盘上、或者工程师 Slack 私聊中的碎片信息——变成前端可订阅、可渲染、可交互的标准化事件流。关键词不是“轻量级”而是“可预测性”你永远知道下一个收到的事件类型是什么它的 payload 结构在哪里定义它的语义边界由谁保证。这不是给 AI 加功能是给整个应用栈加确定性。如果你正在评估 LangGraph 或 CrewAI 的集成方案或者正被产品提“能不能让 agent 执行时显示进度条和中间图表”又或者你刚在 CopilotKit 文档里看到AG-UI这个词却找不到它和你现有 React 组件的映射关系——那么这篇内容就是为你写的。它不讲大道理只拆解真实项目里你会遇到的每一个接口、每一行 patch、每一次中断处理以及我踩过的、文档里绝不会写的 5 个坑。2. 协议设计逻辑为什么放弃 REST 和 WebSocket选择 SSE 作为传输底座2.1 从“请求-响应”到“事件流”的范式迁移传统 Web 开发者的第一反应往往是“这不就是 WebSocket 吗” 我也这么想过直到在压测环境里亲眼看到 WebSocket 连接数暴涨时的内存泄漏曲线。WebSocket 确实支持双向通信但 AG-UI 的核心交互模式本质是单向、有序、不可变的事件广播。Agent 是事件的唯一生产者UI 是消费者用户侧的输入如点击“同意删除数据库”是独立的、低频的、带明确上下文的控制指令它不参与事件流本身而是触发新的 agent 任务或修改当前任务状态。这种不对称性决定了 WebSocket 的双向通道是一种冗余开销。更关键的是可靠性。WebSocket 在企业防火墙、CDN、负载均衡器面前极其脆弱。我们曾在一个银行客户的私有云环境里发现Nginx 默认配置会将空闲 60 秒的 WebSocket 连接静默关闭而 agent 执行一个复杂 SQL 查询可能耗时 83 秒——连接断开后前端无法自动续订只能报错“Connection lost”用户被迫重试agent 却已在后台继续执行造成状态撕裂。SSEServer-Sent Events则天然规避了这些问题。它基于 HTTP/1.1所有中间件都把它当普通 GET 请求处理它内置重连机制retry: 3000浏览器在断连后会自动携带Last-Event-ID头发起续订它的消息格式是纯文本event: TEXT_MESSAGE_CONTENT\ndata: {content:hello}\n\n没有二进制帧解析负担Chrome DevTools 的 Network 面板里点开就能看懂每一条。我实测过在 3000 并发连接下Node.js 的express-sse中间件内存占用稳定在 120MB而同等负载的 WebSocket 服务ws库内存飙升至 1.8GB 并开始 GC 频繁抖动。这不是理论优势是压测机器风扇狂转时的物理反馈。提示SSE 的“单向”特性常被误解为缺陷。但 AG-UI 将用户操作抽象为独立的 HTTP POST 请求如/api/agent/{task_id}/approve与事件流解耦。这种分离让架构更清晰流负责“通知”API 负责“控制”。你在前端代码里永远不会看到ws.send()调用只会看到fetch(/api/...)——这对安全审计和网络策略极其友好。2.2 事件溯源Event Sourcing如何替代 RPC 调用RPC 模型如 gRPC 或 JSON-RPC要求客户端精确知道服务端的方法签名、参数类型、返回结构。当 agent 的能力动态变化比如今天支持查天气明天接入新数据库前端就必须同步更新调用逻辑否则tool_call事件里的tool_name字段就会变成未知字符串UI 渲染直接报错。AG-UI 的事件溯源设计彻底打破了这种紧耦合。它的核心思想是不传递“怎么做”只传递“发生了什么”。当 agent 决定调用天气 API它不发送{method: getWeather, params: {city: Shanghai}}而是发出一个TOOL_CALL_START事件其中tool_name: weather_api和arguments: {city: Shanghai}是两个独立字段。前端无需理解weather_api的具体实现只需根据tool_name显示预设的 Loading 组件如WeatherLoading cityShanghai /并将arguments直接透传给该组件的 props。如果 agent 后续决定改用另一个工具如forecast_io只要tool_name字符串变了前端就自动切换组件无需修改任何业务逻辑。这种设计的威力在错误处理时尤为明显。在 RPC 模型中getWeather方法失败服务端通常返回一个{error: timeout}前端只能展示通用错误提示。而在 AG-UI 中agent 会发出TOOL_CALL_ERROR事件其中error_code: NETWORK_TIMEOUT和suggestion: 请检查网络连接或稍后重试是标准字段。前端可以基于error_code做精细化处理对NETWORK_TIMEOUT显示重试按钮对AUTH_FAILED跳转登录页对RATE_LIMIT_EXCEEDED显示配额说明。注意事件溯源不等于放弃状态管理。AG-UI 的STATE_DELTA事件正是为了解决“前端如何知道当前文档最新版本”这个经典问题。它不是让 agent 把整个 5MB 的 Markdown 文档发过来而是计算出一个 RFC 6902 格式的 JSON Patch比如{op: add, path: /sections/2/content, value: 新增的风险敞口分析...}。这个 patch 只有 87 字节前端用fast-json-patch库一行代码就能应用applyPatch(localDoc, patch)。我见过最极端的案例一个法律合同协作场景agent 在 3 分钟内对 12 万字合同做了 417 次微小修改总事件流大小仅 1.2MB而全量同步需 600MB。2.3 “轻量级”的真实含义协议层无状态责任下沉到框架很多人被标题里的 “Lightweight” 误导以为 AG-UI 是个精简版 HTTP。恰恰相反它的“轻”体现在协议本身不做任何业务决策。它不定义认证方式JWTSession CookieOAuth2、不规定消息序列化格式JSONCBOR、不约束传输加密TLS 1.21.3。这些全部交给底层 HTTP 栈处理。AG-UI 只做三件事定义 7 个核心事件类型TEXT_MESSAGE_START/CONTENT/END、TOOL_CALL_START/ERROR/END、STATE_DELTA、INTERRUPT及其必选/可选字段规定事件流的 MIME 类型为text/event-stream要求每个事件必须包含id用于断线续传、event事件类型、dataJSON 序列化 payload三要素。所有“重量级”功能都由上层框架承担Microsoft Agent Framework 的Microsoft.Agents.AI.Hosting.AGUI中间件负责将 ASP.NET Core 的HttpContext转换为事件流并注入X-Request-ID到每个事件头中CopilotKit 的copilotkit/react包内置了useAgentEventsHook自动处理Last-Event-ID续订、事件类型分发、错误重试逻辑Python 的agno库提供AGUIStreamResponse类开发者只需return AGUIStreamResponse(agent_stream)剩下的流包装、HTTP 头设置全由库完成。这种分层让 AG-UI 具备极强的适应性。我在一个遗留系统改造项目中客户坚持用 Java Spring Boot 2.7不支持 WebFlux我们直接用StreamingResponseBody手写了一个 200 行的AGUIEventStreamer复用现有 Tomcat 容器零依赖引入。协议的“轻”不是功能少而是责任边界清晰——它只解决“如何可靠地把 agent 的心跳传给 UI”其余一切交给你熟悉的工具链。3. 核心事件类型详解从协议规范到前端渲染的完整映射3.1 TEXT_MESSAGE_* 事件不只是“打字效果”而是可控的流式渲染TEXT_MESSAGE_START、TEXT_MESSAGE_CONTENT、TEXT_MESSAGE_END这组事件常被简化为“实现 ChatGPT 那样的打字动画”但实际工程中它们承载着更精细的控制权。TEXT_MESSAGE_START事件的 payload 至少包含message_id唯一标识本次回复、roleassistant或system、metadata可选如{source: knowledge_base}。前端收到后应立即在聊天列表中创建一个占位消息项placeholder并设置isStreaming: true状态。这个占位项的关键作用是锚定后续CONTENT事件的插入位置。如果用户在此期间滚动页面前端必须确保新内容追加到正确的 DOM 节点而不是全局末尾。TEXT_MESSAGE_CONTENT是真正的流式核心。它的content字段是字符串片段如The risk exposure analysis shows...但更重要的是chunk_index从 0 开始的序号和is_final布尔值。chunk_index让前端能检测丢包如果收到chunk_index: 5后跳到chunk_index: 7就知道6丢失了可触发重传请求通过GET /api/agent/{task_id}/chunk/6。is_final: false表示这是中间片段前端应使用textContent content追加is_final: true表示这是最后一块此时应触发onMessageComplete回调进行语法高亮、链接识别等后处理。TEXT_MESSAGE_END事件则携带最终元数据total_tokens消耗 token 数、latency_ms从请求到结束的毫秒数、citations引用来源数组。我们曾在一个医疗问答项目中要求前端在END事件后自动展开“参考文献”折叠面板其中citations数组的每个对象包含title、url、relevance_score前端据此渲染带可信度评分的文献列表。实操心得不要在CONTENT事件里做实时 Markdown 渲染我踩过最大的坑是前端用marked.parse()对每个片段实时转换结果发现**bold**的粗体标记被拆成**bol和d**两段marked解析出错。正确做法是将所有CONTENT片段缓存为字符串数组只在END事件触发时用完整字符串一次性解析。CopilotKit 的MarkdownRenderer组件正是这样实现的——它内部维护一个pendingChunks: string[]onContent时pushonEnd时join()后解析。3.2 TOOL_CALL_* 事件从“调用工具”到“构建工具 UI”的桥梁TOOL_CALL_START事件的 payload 结构是 AG-UI 最具扩展性的设计之一{ tool_name: stock_price_chart, arguments: { symbol: AAPL, period: 1M }, ui_schema: { type: chart, title: Apple Inc. Stock Price (Last 30 Days), chart_type: line } }tool_name是字符串标识arguments是运行时参数而ui_schema是前端渲染 UI 的蓝图。这个字段的存在让 AG-UI 超越了传统 API 协议——它不再只告诉前端“agent 要做什么”而是指导前端“应该用什么界面呈现”。ui_schema的设计哲学是声明式而非命令式。它不指定“用 ECharts 还是 Chart.js”而是描述“这是一个折线图标题是…X 轴是日期Y 轴是价格”。前端框架如 CopilotKit内置一套UIComponentRegistry将type: chart映射到具体的 React 组件LineChart /并将ui_schema的属性透传为 props。当arguments更新时比如用户拖拽时间范围滑块agent 会发出TOOL_CALL_UPDATE事件非标准但广泛支持前端组件收到后重新 fetch 数据无需刷新整个页面。TOOL_CALL_ERROR事件的error_code字段是错误分类的关键。AG-UI 社区已形成共识的 code 有TOOL_NOT_FOUND前端未注册该tool_name的组件应降级为文字提示VALIDATION_FAILEDarguments格式错误如period不是1M、3M、1Y之一前端应高亮表单字段并显示error_messagePERMISSION_DENIED用户无权调用此工具应显示权限申请按钮。注意TOOL_CALL_END事件的result字段通常是 JSON 对象但 AG-UI 不强制其结构。我们在金融项目中约定所有图表类工具的result必须包含data_points: [{x: 2024-01-01, y: 182.3}, ...]和summary: 股价在月初上涨 5%随后震荡回调...。前端组件拿到后data_points交给图表库渲染summary作为图注显示。这种松耦合让后端可以自由替换数据源从 Yahoo Finance 切到 Bloomberg只要result结构不变前端完全无感。3.3 STATE_DELTA 事件JSON Patch 如何实现毫秒级协同编辑STATE_DELTA是 AG-UI 区别于其他协议的“核武器”。它的 payload 是一个标准 RFC 6902 JSON Patch 数组例如[ {op: replace, path: /report/title, value: Q1 2024 Risk Report}, {op: add, path: /report/sections/-, value: {id: sec_001, type: analysis, content: Market volatility increased...}}, {op: remove, path: /report/metadata/draft} ]关键在于path的语义。AG-UI 要求所有path必须指向一个可寻址的、稳定的节点。这意味着不能使用数组索引如/items/0/name因为items[0]可能被删除索引失效推荐使用唯一 ID如/items/id_abc123/name对于无 ID 的数组用-表示追加如/sections/-这是 RFC 6902 明确支持的。前端应用 patch 时必须使用严格模式的 JSON Patch 库。我们曾用rfc6902库的默认模式结果发现当path不存在时它会静默创建父节点导致{op: replace, path: /user/profile/avatar_url, value: new.jpg}在user.profile为空对象时意外创建了user.profile {avatar_url: new.jpg}而本意是报错。切换到fast-json-patch的strict: true模式后缺失路径会抛出OPERATION_PATH_UNRESOLVABLE错误前端可捕获并触发STATE_SYNC_REQUEST事件要求 agent 重发全量状态快照。STATE_DELTA的性能优化点在于批量合并。agent 不会为每个按键都发一个 patch。在文档编辑场景我们配置 agent 的“delta 间隔”为 200ms它会收集这 200ms 内的所有变更如用户输入 3 个字符、删除 1 个词、加粗一段计算出一个最小化的 patch 数组然后一次性发出。实测表明这比逐字符发送减少 92% 的事件数量而用户感知的延迟无差异。提示STATE_DELTA的安全性必须由前端守门。我们强制所有 patch 应用前先通过白名单校验path只允许/document/*、/chat/*、/ui/*等前缀。任何试图修改/system/api_key或/user/permissions的 patch 都被静默丢弃。这层防护比依赖后端过滤更可靠因为它是离用户最近的防线。3.4 INTERRUPT 事件人机协作的安全阀设计INTERRUPT事件是 AG-UI 的伦理护栏。它的 payload 至少包含interrupt_id唯一标识本次中断、action如delete_database、reason自然语言解释、options用户可选的操作数组。例如{ interrupt_id: int_789xyz, action: delete_database, reason: This operation will permanently erase all customer transaction records., options: [ {id: confirm, label: Yes, I understand the risk, dangerous: true}, {id: cancel, label: No, cancel this action, dangerous: false}, {id: review, label: Show me the affected tables first, dangerous: false} ] }前端收到后必须阻断后续所有事件的处理暂停EventSource的onmessage并弹出模态框。这里的关键细节是模态框的按钮必须与options.id严格对应。用户点击“确认”后前端发起POST /api/agent/{task_id}/interrupt/{interrupt_id}/approvebody 为{option_id: confirm}。agent 收到后才继续执行被中断的任务。INTERRUPT的设计难点在于“何时触发”。我们曾在一个自动化部署 agent 中将git push操作设为中断点结果发现每次推送都会弹窗用户不堪其扰。后来调整为只有当git push目标分支是production且提交包含database-migration标签时才触发INTERRUPT。这要求 agent 的中断策略必须可配置而 AG-UI 本身不规定策略只提供标准化的中断信令。实操心得INTERRUPT模态框必须包含“撤销”能力。我们添加了timeout_ms: 30000字段表示 30 秒后自动取消。前端启动倒计时倒计时结束时自动发送POST /.../cancel。这避免了用户离开电脑后agent 任务无限期挂起。更进一步在金融项目中我们要求所有dangerous: true的选项必须用户连续点击两次第一次高亮按钮第二次执行这是防误触的物理层保障。4. 实战集成指南从零搭建一个支持 AG-UI 的 React 前端4.1 基础事件流订阅超越官方示例的健壮实现官方文档的EventSource示例过于简化。真实项目需要处理连接失败重试、事件乱序、内存泄漏、跨域凭证。以下是我们在生产环境使用的AGUIEventClient类class AGUIEventClient { private eventSource: EventSource | null null; private retryCount 0; private readonly maxRetries 5; private readonly baseRetryDelay 1000; constructor(private readonly endpoint: string) {} connect(onEvent: (event: AGUIEvent) void) { // 关闭旧连接防止内存泄漏 this.disconnect(); // 构建带凭证的 EventSource支持 cookies this.eventSource new EventSource(this.endpoint, { withCredentials: true }); // 设置重连策略指数退避 this.eventSource.addEventListener(error, () { if (this.eventSource?.readyState 0) { const delay Math.min( this.baseRetryDelay * Math.pow(2, this.retryCount), 30000 // 最大 30 秒 ); setTimeout(() { if (this.retryCount this.maxRetries) { this.retryCount; this.connect(onEvent); } }, delay); } }); // 处理事件流 this.eventSource.onmessage (e) { try { const data JSON.parse(e.data); // 验证必要字段 if (!data.type || !data.id) { console.warn(Invalid AGUI event missing type or id, data); return; } onEvent(data as AGUIEvent); } catch (err) { console.error(Failed to parse AGUI event, e.data, err); } }; } disconnect() { if (this.eventSource) { this.eventSource.close(); this.eventSource null; this.retryCount 0; } } }这个实现的关键改进withCredentials: true确保 cookie 认证正常工作避免401 UnauthorizedreadyState 0判断连接是否真正断开而非临时网络抖动指数退避重试防止雪崩try/catch包裹JSON.parse避免单个坏事件导致整个流中断。注意不要在onmessage里直接调用setStateReact 的useState在异步回调中可能触发警告。我们封装了useEventStreamHook内部用useRef缓存最新onEvent回调确保状态更新安全const [events, setEvents] useStateAGUIEvent[]([]); const onEventRef useRef(e: AGUIEvent) void(); onEventRef.current (e) setEvents(prev [...prev, e]); useEffect(() { const client new AGUIEventClient(/api/agent/stream); client.connect((e) onEventRef.current?.(e)); return () client.disconnect(); }, []);4.2 事件分发与状态管理用 Zustand 构建可预测的 UI 状态机AG-UI 事件流是无序的网络延迟导致CONTENT可能早于START到达前端必须构建一个状态机来协调。我们放弃 Redux选用 Zustand因其轻量且支持中间件。核心 store 设计如下interface AGUIState { messages: Message[]; toolCalls: Recordstring, ToolCallState; sharedState: Recordstring, any; interrupts: Recordstring, InterruptState; activeTaskId: string | null; } const useAGUIStore createAGUIState { addMessage: (msg: Message) void; updateToolCall: (id: string, update: PartialToolCallState) void; applyStateDelta: (patch: JsonPatchOperation[]) void; handleInterrupt: (interrupt: InterruptState) void; }((set, get) ({ messages: [], toolCalls: {}, sharedState: {}, interrupts: {}, activeTaskId: null, addMessage: (msg) set((state) ({ messages: [...state.messages, msg] })), updateToolCall: (id, update) set((state) ({ toolCalls: { ...state.toolCalls, [id]: { ...state.toolCalls[id], ...update } } })), applyStateDelta: (patch) set((state) { try { const newState applyPatch(state.sharedState, patch).newDocument; return { sharedState: newState }; } catch (err) { console.error(Failed to apply state delta, patch, err); return {}; } }), handleInterrupt: (interrupt) set((state) ({ interrupts: { ...state.interrupts, [interrupt.id]: interrupt } })) }));每个事件处理器对应一个 store 方法TEXT_MESSAGE_START→addMessage({id: data.message_id, role: data.role, content: , isStreaming: true})TEXT_MESSAGE_CONTENT→updateMessage(data.message_id, {content: prev.content data.content})需额外实现STATE_DELTA→applyStateDelta(data.patch)INTERRUPT→handleInterrupt({id: data.interrupt_id, ...data})。这种设计让 UI 组件完全无状态ChatMessages /只订阅messagesToolPanel /只订阅toolCallsDocumentEditor /只订阅sharedState。当 agent 发送STATE_DELTADocumentEditor自动 re-render无需手动触发forceUpdate。提示Zustand 的subscribe方法可监听特定字段变化。我们在DocumentEditor中useEffect(() { const unsubscribe useAGUIStore.subscribe( (state) state.sharedState, (newState) { // 只有当文档内容变化时才更新编辑器 if (newState.document?.content ! prevContent) { editorRef.current?.setValue(newState.document.content); prevContent newState.document.content; } } ); return unsubscribe; }, []);4.3 Generative UI 渲染A2UI 组件的动态注册与沙箱化A2UI 是 AG-UI 的 UI 定义层其 payload 是一个 JSON Schema 描述的组件树。例如stock_price_chart的 A2UI 定义{ type: a2ui:component, name: StockPriceCard, props: { symbol: AAPL, period: 1M }, children: [] }前端需要一个A2UIRenderer组件它根据name动态导入并渲染对应组件。关键挑战是安全沙箱不能让 agent 通过name字段执行任意代码。我们的解决方案是白名单注册在应用初始化时显式注册所有允许的组件A2UIRenderer.registerComponent(StockPriceCard, lazy(() import(./components/StockPriceCard))); A2UIRenderer.registerComponent(RiskMatrix, lazy(() import(./components/RiskMatrix))); // ... 其他组件沙箱化 propsprops字段必须经过严格校验。我们用 Zod 定义 schemaconst StockPriceCardSchema z.object({ symbol: z.string().regex(/^[A-Z]{2,5}$/), period: z.enum([1D, 1W, 1M, 3M, 1Y]), theme: z.optional(z.enum([light, dark])) });A2UIRenderer在渲染前调用StockPriceCardSchema.safeParse(props)失败则降级为ErrorComponent messageInvalid chart config /。资源隔离每个 A2UI 组件在独立的iframe中渲染通过react-frame-component禁止访问父页面 DOM 和全局变量。这防止恶意 agent 注入window.top.location.href https://evil.com。实操心得A2UI 组件必须支持onDataUpdate回调。当 agent 发送TOOL_CALL_UPDATE事件时A2UIRenderer会调用子组件的onDataUpdate(newProps)组件内部重新 fetch 数据。我们约定所有 A2UI 组件必须实现useEffect(() { fetchData(); }, [props])确保响应式更新。这比重新 mount 组件更高效用户看不到闪烁。4.4 生产环境加固BFF 层的必备中间件AG-UI 的最大安全风险是前端直连 agent runtime。我们强制所有项目采用 BFFBackend for Frontend模式即一个专用的 Node.js 服务位于 agent 和前端之间。它的核心中间件包括输入净化中间件app.use(/api/agent/stream, (req, res, next) { // 移除所有 HTML 标签和 script req.body.user_input sanitizeHtml(req.body.user_input, { allowedTags: [], allowedAttributes: {} }); // 检查长度防 DoS if (req.body.user_input.length 10000) { return res.status(400).json({ error: Input too long }); } next(); });工具调用过滤中间件app.post(/api/agent/:task_id/approve, async (req, res) { const { task_id } req.params; const { option_id } req.body; // 从 Redis 获取用户权限 const userPerms await redis.get(user:${req.user.id}:perms); const allowedTools JSON.parse(userPerms).allowed_tools; // 检查当前 task 的 tool_name 是否在白名单 const task await db.getTask(task_id); if (!allowedTools.includes(task.tool_name)) { return res.status(403).json({ error: Tool not permitted }); } // 转发到 agent await axios.post(http://agent-service/approve/${task_id}, { option_id }); });JSON Patch 防护中间件app.use(/api/agent/stream, (req, res, next) { // 拦截所有 STATE_DELTA 事件 const originalWrite res.write; res.write function(chunk: any) { if (typeof chunk string chunk.includes(type:STATE_DELTA)) { try { const event JSON.parse(chunk.match(/data: (.*)/)?.[1] || {}); // 检查 path 白名单 const invalidPaths event.patch.filter((p: any) !p.path.startsWith(/document/) !p.path.startsWith(/ui/) ); if (invalidPaths.length 0) { console.warn(Blocked unsafe STATE_DELTA path, invalidPaths); return; // 静默丢弃 } } catch (e) { console.error(Invalid STATE_DELTA JSON, chunk); } } return originalWrite.apply(res, [chunk]); }; next(); });这套 BFF 中间件让我们在客户审计中顺利通过 SOC2 Type II 认证。它不增加 agent 的复杂度却为整个交互链路筑起坚实屏障。5. 常见问题与排障实战从日志碎片到根因定位的完整路径5.1 事件流中断如何区分网络问题与 agent 卡死现象前端EventSource突然停止接收事件readyState变为 0但 agent 日志显示任务仍在执行。排查路径检查 BFF 层日志搜索task_id对应的AGUI_STREAM_OPENED和AGUI_STREAM_CLOSED日志。如果只有OPENED没有CLOSED说明连接异常中断如果有CLOSED且reason: client_disconnected则是前端主动断开。抓包验证在 BFF 服务器上运行tcpdump -i any port 8080 -w agui.pcap用 Wireshark 打开过滤http.request.uri contains stream。观察 TCP 流如果看到大量TCP Retransmission是网络不稳定如果看到FIN包后 agent 仍发送数据是 BFF 未正确处理连接关闭。agent 端诊断在 agent 代码中为每个任务添加heartbeat事件每 5 秒发送{type: HEARTBEAT, timestamp: Date.now()}。如果前端收不到 heartbeat而 BFF 日志显示 heartbeat 正常发出则问题在传输层如果 BFF 日志也无 heartbeat说明 agent 卡死在某个同步阻

相关新闻