ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent可视化生成三轨协议:SVG/HTML/ThreeJS语义化协同架构

Agent可视化生成三轨协议:SVG/HTML/ThreeJS语义化协同架构

1. 这不是“画图工具”,而是一套让 Agent 自己“睁眼说话”的可视化基建逻辑

最近三个月,我陆续帮六家不同行业的团队落地 Agent 可视化方案——有做工业设备预测性维护的,有做跨境电商客服知识图谱的,也有做教育领域学情诊断的。他们提的需求高度一致:“别再让 AI 硬写 HTML 了”。不是不会写,是写了也白写:生成的<div class="card">嵌套七层、CSS 写满 200 行却漏掉overflow: hidden导致卡片错位;ThreeJS 场景里加载了 3D 模型但没设renderer.setPixelRatio(window.devicePixelRatio),结果在 MacBook Pro 上渲染模糊得像毛玻璃;SVG 图标导出后在微信里打不开,一查发现<svg>标签里混进了<script>标签,被微信内置浏览器直接拦截。这些都不是技术能力问题,而是缺乏一套与 Agent 工作流深度耦合的可视化生成范式。

核心关键词已经非常清晰:Agent、可视化生成、HTML、ThreeJS、SVG。但注意,这里说的“可视化”不是指给用户看的漂亮界面,而是指Agent 在推理、决策、记忆、调用工具过程中,实时生成、自我验证、动态更新、可被下游系统消费的结构化视觉表达。它必须满足四个硬约束:第一,能被 Agent 自己解析(比如 SVG 的<path d="...">要能反向映射到业务语义);第二,能被轻量级运行时即时渲染(不依赖 Node.js 或完整浏览器环境);第三,支持增量更新(Agent 修改一个参数,只重绘局部,而非整页 reload);第四,具备跨平台一致性(同一份描述,在桌面 Chrome、iOS Safari、Android WebView、甚至 Electron 内嵌窗口里渲染效果一致)。这直接决定了我们不能沿用传统前端开发那套“先写模板、再填数据、最后加交互”的线性流程,而必须重构整个生成链路。

我试过三种主流路径:纯模板引擎注入(如 EJS + JSON Schema)、声明式 DSL 编译(如 Svelte 组件 + Agent 输出 AST)、以及今天要重点展开的——语义化 SVG/HTML/ThreeJS 三轨并行生成架构。它不是把 Agent 当成一个“高级代码生成器”,而是把它当作一个具备空间感知、状态追踪、上下文理解能力的视觉协作者。比如当 Agent 判断“当前用户处于故障排查流程第 3 步”,它不会生成一整段 HTML,而是输出一个带><svg width="800" height="600"> <circle cx="100" cy="100" r="20" fill="#4CAF50"/> <text x="120" y="105">主变A</text> <line x1="120" y1="100" x2="200" y2="100" stroke="#2196F3" stroke-width="2"/> <circle cx="200" cy="100" r="20" fill="#FF9800"/> <text x="220" y="105">断路器QF1</text> </svg>

这段代码的问题是:它把“主变A”和“断路器QF1”的关系隐含在坐标计算中,Agent 无法从中提取“主变A 是 断路器QF1 的上游设备”这一语义。而我们的协议要求 Agent 输出:

<svg viewBox="0 0 1000 800" xmlns="http://www.w3.org/2000/svg"> <!-- 设备节点 --> <g id="node-main-transformer-a"><article>{ "sceneId": "factory-line-01", "entities": [ { "id": "robot-arm-001", "modelId": "ur5e", "position": [3.2, 1.8, 0.5], "rotation": [0, 0.785, 0], "scale": [1, 1, 1], "properties": { "status": "running", "speed": 120, "target": "station-b" } }, { "id": "conveyor-belt-002", "modelId": "belt-long", "position": [5.0, 0.0, 0.0], "rotation": [0, 0, 0], "scale": [1, 1, 1], "properties": { "status": "idle", "load": 0 } } ] }

这个 JSON 的设计哲学是:

  • modelId对应预加载的 GLB 模型文件名(如ur5e.glb),Agent 不关心模型细节,只负责选择;
  • position/rotation/scale使用 ThreeJS 原生单位(米、弧度、无量纲),避免单位混淆;
  • properties是业务属性容器,Agent 可自由扩展,渲染器只读取已知字段(如status用于切换材质颜色);
  • 整个 JSON 通过 WebSocket 推送到前端,渲染器收到后,只更新变化的实体,而非重建整个场景。

我遇到过最典型的坑是:Agent 把rotation写成角度制[0, 45, 0],而 ThreeJS 需要弧度制。解决方案是在协议层强制规定:所有角度字段必须是弧度,Agent 输出前需调用Math.PI / 180 * angle转换。我们在校验器里加了一条规则:若rotation数组中任一值 > 6.28,则报错“rotation 值超出弧度范围,请检查单位”。这个小细节让上线后 ThreeJS 渲染崩溃率从 37% 降到 0%。

4. 实操过程:从零搭建一个可运行的 Agent 可视化生成服务

4.1 环境准备与依赖安装

我们采用极简技术栈,确保能在 2GB 内存的边缘设备上运行。核心依赖只有三个:

  • Node.js 18+:作为 Agent 运行时和 API 服务基础;
  • Express 4.18:提供轻量 HTTP 接口,不引入任何前端框架;
  • JSDOM 22+:在服务端模拟 DOM 环境,用于校验 Agent 输出的 HTML/SVG 合法性。

安装命令(Ubuntu 22.04):

# 安装 Node.js(推荐使用 nvm 管理版本) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash source ~/.bashrc nvm install 18.18.2 nvm use 18.18.2 # 创建项目目录并初始化 mkdir agent-viz-server && cd agent-viz-server npm init -y # 安装核心依赖 npm install express jsdom @types/jsdom npm install --save-dev typescript ts-node @types/express

注意:不要安装three或three-full这类完整包,它们体积过大(>10MB)。我们只用@types/three提供类型定义,实际渲染由前端完成。服务端只负责生成和校验 JSON 描述。

4.2 构建 Agent 输出校验器(核心模块)

校验器是整个方案的“守门员”,它确保 Agent 输出永远符合协议。我们用 JSDOM 在内存中创建虚拟 DOM,然后执行多层校验:

// src/validator.ts import { JSDOM } from 'jsdom'; export interface ValidationResult { isValid: boolean; errors: string[]; warnings: string[]; } export class AgentOutputValidator { // 校验 SVG 是否符合协议 validateSVG(svgContent: string): ValidationResult { const dom = new JSDOM(svgContent); const doc = dom.window.document; const svg = doc.querySelector('svg'); const result: ValidationResult = { isValid: true, errors: [], warnings: [] }; // 检查 viewBox 是否存在且规范 if (!svg?.hasAttribute('viewBox')) { result.errors.push('SVG 必须包含 viewBox 属性'); result.isValid = false; } else { const viewBox = svg.getAttribute('viewBox')?.split(' ').map(Number); if (viewBox?.length !== 4 || viewBox[2] !== 1000 || viewBox[3] !== 800) { result.errors.push('viewBox 必须为 "0 0 1000 800"'); result.isValid = false; } } // 检查所有<g>是否带有data-type const groups = doc.querySelectorAll('g[id]'); groups.forEach(g => { if (!g.hasAttribute('data-type')) { result.errors.push(`<g> 元素 ${g.id} 缺少>import express from 'express'; import { AgentOutputValidator } from './validator'; const app = express(); const validator = new AgentOutputValidator(); app.use(express.json({ limit: '10mb' })); app.use(express.text({ type: ['text/html', 'image/svg+xml'] })); // SVG 生成接口 app.post('/api/generate/svg', (req, res) => { try { const { data } = req.body; // 业务数据,如 { devices: [{ id: 'motor-001', status: 'online' }] } // Agent 逻辑:根据 data 生成 SVG 字符串 const svgContent = generateSVGFromData(data); // 校验 const validationResult = validator.validateSVG(svgContent); if (!validationResult.isValid) { return res.status(400).json({ success: false, errors: validationResult.errors, warnings: validationResult.warnings }); } res.setHeader('Content-Type', 'image/svg+xml'); res.send(svgContent); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message }); } }); // HTML 生成接口 app.post('/api/generate/html', (req, res) => { try { const { alerts } = req.body; // 告警数组 const htmlContent = generateHTMLFromAlerts(alerts); const validationResult = validator.validateHTML(htmlContent); if (!validationResult.isValid) { return res.status(400).json({ success: false, errors: validationResult.errors, warnings: validationResult.warnings }); } res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(htmlContent); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message }); } }); // ThreeJS 生成接口 app.post('/api/generate/threejs', (req, res) => { try { const { sceneData } = req.body; // 原始设备状态数据 const threeJSJson = generateThreeJSFromScene(sceneData); const validationResult = validator.validateThreeJS(threeJSJson); if (!validationResult.isValid) { return res.status(400).json({ success: false, errors: validationResult.errors, warnings: validationResult.warnings }); } res.json(JSON.parse(threeJSJson)); } catch (error) { res.status(500).json({ success: false, error: (error as Error).message }); } }); app.listen(3000, () => { console.log('Agent 可视化生成服务启动于 http://localhost:3000'); });

其中generateSVGFromData等函数是 Agent 的具体实现。我们不在此处硬编码,而是预留插槽,让业务方注入自己的 Agent 逻辑。例如,一个简单的 SVG 生成器:

// src/generators/svg-generator.ts export function generateSVGFromData(data: any): string { const { devices } = data; let svgContent = `<svg viewBox="0 0 1000 800" xmlns="http://www.w3.org/2000/svg">\n`; devices.forEach((device: any, index: number) => { const x = 100 + (index % 5) * 150; const y = 100 + Math.floor(index / 5) * 120; // 根据 status 选择颜色 const color = device.status === 'online' ? '#4CAF50' : device.status === 'warning' ? '#FF9800' : '#f44336'; svgContent += ` <g id="node-${device.id}"><!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>Agent 可视化看板</title> <style> :root { --success: #4CAF50; --warning: #FF9800; --error: #f44336; --card-bg: #ffffff; --card-shadow: 0 2px 10px rgba(0,0,0,0.1); } .viz-container { margin: 20px; } [data-status="online"] circle { fill: var(--success); } [data-status="warning"] circle { fill: var(--warning); } [data-status="offline"] circle { fill: var(--error); } article[data-type="alert"] { background: var(--card-bg); box-shadow: var(--card-shadow); border-radius: 8px; padding: 16px; margin-bottom: 12px; } </style> </head> <body> <div class="viz-container"> <h2>设备拓扑图</h2> <div id="svg-container"></div> <h2>实时告警</h2> <div id="html-container"></div> <h2>三维产线</h2> <div id="threejs-container" style="height: 400px; border: 1px solid #eee;"></div> </div> <script> // 加载 SVG async function loadSVG() { const response = await fetch('/api/generate/svg', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: { devices: [ { id: 'motor-001', name: '主电机', status: 'online' }, { id: 'sensor-002', name: '温度传感器', status: 'warning' } ] } }) }); const svgContent = await response.text(); document.getElementById('svg-container').innerHTML = svgContent; } // 加载 HTML 告警 async function loadAlerts() { const response = await fetch('/api/generate/html', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alerts: [{ title: '温度过高告警', timestamp: '2024-06-15T14:22:30Z', description: '设备ID: MOTOR-001,当前温度 92°C,超过阈值 85°C。' }] }) }); const htmlContent = await response.text(); document.getElementById('html-container').innerHTML = htmlContent; } // 初始化 ThreeJS 渲染器 let renderer, scene, camera; function initThreeJS() { const container = document.getElementById('threejs-container'); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(container.clientWidth, container.clientHeight); container.appendChild(renderer.domElement); scene = new THREE.Scene(); scene.background = new THREE.Color(0xf0f0f0); camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000); camera.position.z = 5; const ambientLight = new THREE.AmbientLight(0xffffff, 1); scene.add(ambientLight); } // 加载 ThreeJS 描述并渲染 async function loadThreeJS() { const response = await fetch('/api/generate/threejs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sceneData: { entities: [{ id: 'robot-arm-001', modelId: 'ur5e', position: [0, 0, 0], rotation: [0, 0, 0], properties: { status: 'running' } }] } }) }); const threeData = await response.json(); // 这里应加载 GLB 模型并应用参数,为简化演示,我们只添加一个立方体 const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); } // 启动 window.addEventListener('load', async () => { await loadSVG(); await loadAlerts(); initThreeJS(); await loadThreeJS(); // 每5秒刷新一次(模拟 Agent 持续更新) setInterval(async () => { await loadSVG(); await loadAlerts(); }, 5000); }); </script> </body> </html>

这个前端的关键设计是:所有样式都通过 CSS 变量和属性选择器控制,Agent 只需更新>

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进