让 LLM 写 SVG 之前,先输出一份 JSON 场景图(部件层级树 + 每个叶子的归一化 bbox + 遮挡/包含边),用拓扑排序定出绘制顺序;再采样 3 份 SVG,用 CLIP 挑最好的一份,规则检查 + LLM 打分不过就带着反馈重写,最多 3 次。本质是 plan-then-code + best-of-N + self-refine 套在 SVG 上;真正的新东西是 SVG 元素 id 与规划节点一一绑定,所以产物天然可以按部件编辑和做动画。
截至 2026-11-07,论文 PDF / arXiv / ACM 页面都还没公开(会议 12 月 1–4 日在吉隆坡)。本页内容来自三处:GitHub README、开源代码 structsvg.py(3120 行)、仓库里的 2.5 分钟演示视频(方法图与结果表从视频帧截取,数字逐个核对过)。数据集、指标定义、实验细节等论文正文内容无法确认的地方,都标了“推测”。
另一点要注意:作者的 release PR 写明,开源代码是“code audit 中找到的最完整作者快照”(2026 年 4 月的版本),不一定就是跑出论文数字的那一版。后文 论文图 vs 开源代码 列出了两者的出入。
| 术语 | 在本工作中具体是什么 |
|---|---|
| Semantic Tree | TreeBuilder 写出的 semantic_tree.json:一棵部件层级树。中间节点只做分组(如 bee),叶子节点带 bbox_norm=[x,y,w,h](0–1 坐标)、shape_type(rect/circle/ellipse/path/polygon)、material_ref、重要度。 |
| Topology Graph / Primitive Graph | primitive_graph.json:节点 = 树的全部叶子;边 = OCCLUDES / CONTAINS / INSIDE_BOUNDS_OF / ATTACHES_TO / ALIGNS_WITH;外加由边拓扑排序得到的 draw_order。 |
| material_library | 颜色表:{"mat_wing": {"fill":"#hex","stroke":"#hex"}},叶子按 id 引用;渐变用 gradient_ref 字段描述。 |
| Agent | 同一个 LLM(默认 Gemini 3 Flash)配不同 system prompt 和工具白名单;靠往 inbox/<agent>.md 追加文字来传消息,每回合只能调一个工具。 |
| Candidate | 一次 svg_gen_batch 采样出的 k=3 份完整 SVG(温度 0.2 / 0.35 / 0.5)。 |
| Preflight | 确定性检查:XML 能否解析、viewBox 是否 0 0 256 256、Playwright 渲染后是否全空白、近白像素占比是否 >45%、元素 bbox 是否违反图里的边。 |
| Tier-1 / Tier-2 | 失败分级。Tier-1 = SVG 代码问题,打回 SVG_Engineer 做“最小补丁”重写;Tier-2 = 结构问题(遮挡/包含违规、CLIP≤0.20、id 漂移≥3),打回 TreeBuilder 改树。 |
| Structural drift | 两版 SVG 的 id 集合对称差的大小(增加的 id + 删除的 id 个数)。 |
| TextGrad / TGD | 失败后让 LLM 根据评分和问题清单,改写 TreeBuilder / SVG_Engineer 的 role prompt(“文本梯度下降”);下一轮指标变差就回滚。 |
| ID Cov. | 主表里的指标,定义不公开;从名字和直接 LLM 生成几乎为 0 推测,是“规划节点 id 在 SVG 中出现的比例”。 |
<path>,没法说“把猫的耳朵调大一点”。
flowchart LR
P["Text prompt"] --> TB["TreeBuilder"]
TB -->|"semantic_tree.json"| GB["Graph builder"]
GB -->|"edges + draw_order"| SE["SVG Engineer"]
SE -->|"k=3 candidates"| PF["Preflight"]
PF --> CR["Critic"]
CR -->|"PASS"| OUT["Editable SVG"]
CR -->|"Tier 1"| SE
CR -->|"Tier 2"| TB
CR -.->|"TextGrad"| RP["Role prompts"]
RP -.-> TB
RP -.-> SE
TreeBuilder 只做一件事:读 prompt,写出 semantic_tree.json(schema v2.0)。规则:叶子才有 geometry,对称物体要列出全部实例(左右眼各一个节点),fill 只能写 hex(禁止 CSS gradient 语法),同色相邻部件要加 notes 提醒描边。prompt 里还写死了三条“能力约束”:不准设计“一个形状包住另一个”(C 形身体抱住球)、不准要求两个 path 在精确坐标对接、同一区域遮挡不超过 3 层。也就是说,系统通过在规划阶段回避 LLM 画不好的几何来降低失败率。
Graph builder 是三步的混合流程:(A) 确定性地抽出所有叶子;同一父节点下相邻兄弟,若 bbox 重叠,就加一条“后者 OCCLUDES 前者”的边(兄弟顺序即图层顺序;不重叠的左右眼不加边)。(B) 调一次 LLM(temperature 0.1),只补跨父节点的边,并且只接受 5 种合法类型、只接受已存在的 id。(C) 用 Kahn 算法拓扑排序出 draw_order;如果 LLM 补的边造成环,就把跨父边整体丢掉,只保留兄弟边。
| 边类型 | 对绘制顺序的含义 | 生成后怎么检查 |
|---|---|---|
| A OCCLUDES B | B 先画,A 后画(盖在上面) | SVG 文档顺序中 A 必须在 B 之后 |
| A INSIDE_BOUNDS_OF B | 容器 B 先画 | bbox 包含关系 |
| A CONTAINS B | 容器 A 先画 | bbox 包含关系;提示用 clipPath 实现 |
| A ATTACHES_TO B | 不影响顺序 | 代码里不检查 |
| A ALIGNS_WITH B | 不影响顺序 | 中心点在 x / y 轴上差值 < 画布 5% |
直观理解:树回答“有哪些零件、怎么分组”,图回答“零件之间谁压谁、谁装着谁”。LLM 自己不擅长维护的全局一致性(图层顺序无环)交给了 30 行 Python。
SVG_Engineer 把整张图 JSON + draw_order + prompt + 一份 8 条的 cheat sheet 发给 LLM。cheat sheet 的关键规则:每个图节点对应的 SVG 元素必须使用节点的原 id;严格按 draw_order 写;CONTAINS 用 clipPath;对称部件定义在 <defs> 里再用 <use> 复用;有机形状用“粗描边底层 + 无描边填充上层”的双层技巧消除接缝。一次生成 k=3 个候选,温度依次为 0.2 / 0.35 / 0.5。
另一种 autoregressive 模式:沿 draw_order 每次只生成一个节点的 SVG 片段,把已画部分作为上下文,最后拼起来。代码注释承认这种模式“误差会累积,也没法做整体补丁”,所以一进入重试就强制切回 one-shot。
每个候选生成后要过 preflight:XML 合法、viewBox 固定 256、用 Playwright/Chromium 渲染成 PNG 后不能全透明、可见像素中近白占比不能 >45%(除非有深色描边)、clipPath 里不能放只有描边的几何。重试时还会算 structural drift:新旧 SVG 的 id 增删数 >5 直接判废,强制“最小补丁”。
(round(CLIP×100), HPS) 做字典序取最大:CLIP 差 0.01 以内才看 HPS。FAIL 时由 diagnose_failure_tier 决定打回给谁:出现遮挡 / 包含的结构违规、CLIP ≤ 0.20、或 drift ≥ 3 → Tier-2,回 TreeBuilder 改树(只改出问题的节点);其余 → Tier-1,回 SVG_Engineer,带着 Critic 建议以低温(0.05–0.15)重写。两类重试共享一个计数器,最多 3 次。同时 TextGrad 会根据本轮失败信息改写 TreeBuilder 和 SVG_Engineer 的 role prompt(不改 Critic),下一轮指标下降就回滚到上一版 prompt。任务 PASS 后,LLM 从这次的树和 SVG 中总结“设计经验”,写入跨任务记忆 global_memory.json(只保留最近 8 条),后续任务的所有 agent 都能读到。
4 个角色:Coordinator(看门狗,只在报错、死循环、回合数用到 50% / 75% 时介入)、TreeBuilder、SVG_Engineer、Critic。每个 agent 有工具白名单和文件写权限白名单(例如只有 TreeBuilder 能写 semantic_tree.json),每一步只允许调用一个工具,用 send_message 交出控制权;单任务上限 30 个回合。正常路径是 TreeBuilder → SVG_Engineer → Critic 的固定顺序,“多 agent”实际上就是用对话来驱动一个带分支的状态机。
先让 LLM 用 JSON 写出“零件清单 + 谁压谁”,用 Python 排好图层顺序,再要求 SVG 每个元素沿用零件 id;这样同一份结构既能指导生成、又能自动挑错、还能当编辑句柄。
方法图和 README 描述的系统,比开源代码实际做的事更多。读代码时要知道以下差别:
| 方面 | 论文方法图 / README | 开源 structsvg.py |
|---|---|---|
| Agent 数 | Semantic Decomposer、Topology Planner、Component Drafter、Structure-aware Refiner、Quality GateKeeper | 4 个角色;Topology Planner 退化为工具里的一次 LLM 调用;Refiner 与 GateKeeper 合并为 Critic |
| Refinement | 按部件并行细化 → 全局对齐 → 细节增强,共三步 | 没有逐部件细化;重试 = 整份 SVG 带反馈重写(“最小补丁”只是 prompt 里的要求,drift 检查只看 id 增删) |
| 候选选择 | 选“结构最对齐”的草稿 | 按 CLIP 优先、HPS 次之选择;结构违规只在选完之后才检查 |
| TextGrad | prompt-gradient refinement | 只改写 role prompt。直接优化 SVG 的 textgrad_tier1_patch 和直接优化图的 textgrad_tier2_rebuild 都有定义,但从未被调用;tgd_graph_patched 恒为 False |
| 最终输出 | “best selection” | FAIL 时也会用本轮结果覆盖 current_best_icon.svg,所以重试用尽后输出的是最后一轮,不一定是分数最高的一轮 |
mmsvg_text_sampled.jsonl,应当是从 OmniSVG 的 MMSVG-Bench text-to-SVG 部分采样得到。表中所有成功率都是 1/150 的整数倍(98.7% = 148/150,74.7% = 112/150),推测测试集为 150 条 prompt。| Method | Success | CLIP↑ | HPS↑ | Aesthetic↑ | ImageReward↑ | VQAScore↑ | ID Cov.↑ | Edit↑ |
|---|---|---|---|---|---|---|---|---|
| 专门训练的 SVG 模型 | ||||||||
| OmniSVG | 98.7% | 0.233 | 0.142 | 4.525 | -1.880 | 0.252 | 0.000 | 28.8% |
| InternSVG | 100.0% | 0.242 | 0.145 | 4.605 | -1.866 | 0.251 | 0.000 | 1.7% |
| IntroSVG | 74.7% | 0.245 | 0.150 | 4.542 | -1.799 | 0.327 | 0.000 | 26.9% |
| 通用 LLM 直接生成 vs StructSVG(同一底座) | ||||||||
| Gemini 3 Flash | 99.3% | 0.319 | 0.221 | 4.968 | 0.451 | 0.802 | 0.002 | 80.0% |
| StructSVG†-Gemini 3 Flash | 100.0% | 0.323 | 0.237 | 5.077 | 0.816 | 0.858 | 0.382 | 83.3% |
| GPT-4o | 98.7% | 0.284 | 0.176 | 4.663 | -1.192 | 0.506 | 0.000 | 66.1% |
| StructSVG†-GPT-4o | 100.0% | 0.313 | 0.207 | 4.808 | -0.141 | 0.736 | 0.883 | 73.3% |
| Claude Haiku 4.5 | 99.3% | 0.314 | 0.210 | 4.789 | -0.066 | 0.759 | 0.005 | 70.0% |
| StructSVG†-Claude Haiku 4.5 | 100.0% | 0.316 | 0.213 | 4.873 | 0.027 | 0.769 | 0.554 | 71.7% |
| DeepSeek-V3 | 99.3% | 0.297 | 0.193 | 4.708 | -0.644 | 0.655 | 0.000 | 71.7% |
| StructSVG†-DeepSeek-V3 | 100.0% | 0.310 | 0.204 | 4.832 | -0.241 | 0.721 | 0.733 | 73.3% |
主表(取自演示视频,粗体为全表最佳)。每个底座上 StructSVG 的各项指标都高于直接生成。
| Variant | CLIP↑ | HPS↑ | Aesthetic↑ | ImageReward↑ | VQAScore↑ |
|---|---|---|---|---|---|
| Direct GPT-4o | 0.284 | 0.176 | 4.663 | -1.192 | 0.506 |
| w/o Tree & Graph | 0.282 | 0.185 | 4.735 | -1.151 | 0.470 |
| w/o Tree | 0.293 | 0.188 | 4.705 | -0.923 | 0.579 |
| w/o Graph | 0.312 | 0.208 | 4.795 | -0.203 | 0.742 |
| w/o Refine | 0.309 | 0.201 | 4.754 | -0.475 | 0.693 |
| Full (GPT-4o) | 0.313 | 0.207 | 4.808 | -0.141 | 0.736 |
仓库里的生成结果(原始 SVG,可以直接打开看源码)。例如 bee.svg 的元素 id 有 stem / leaf_left / petals / bee_abdomen / bee_wing_back / bee_wing_front / bee_stinger,就是规划树的叶子。
演示视频中还有风格化(同一 prompt 加 Cartoon / B&W / Watercolor / Geometric / Pixel Art / Minimalist 后缀)和长镜头 SVG 动画(夜空云飘、海底鱼游、第一人称公路)。这些都是 demo 级展示,没有定量评测;编辑器和动画工具也都不在开源仓库中。
CAPABILITY_LIMIT_WRAP_PATH 一类。这类几何不是被解决了,而是在规划阶段被绕开了。python_run 工具直接 exec LLM 写的代码,没有沙箱;FAIL 时会覆盖 current_best,最终交付的是最后一轮而非最佳一轮;跨任务记忆只是“LLM 总结的 8 条经验文本”,没有验证这些经验是否有用。