七牛云 × XEngineer 实训营第四批的题目二:AI 语音绘图。我交的不是「语音转图工具」,是声绘——一个能听懂复杂需求、还能跟你反复改图的命令式绘图 Agent。
选这个定位本身是个判断。题目的三个评分维度——指令理解的准确与容错、语音到绘图的响应延迟、复杂指令拆解——全指向「听懂 + 逐步执行 + 可纠错」,而不是一张黑盒生成图的美观度。所以核心不是文生图:LLM 推理的是结构化的画布 JSON,不是像素。 这是个结构化推理 + 工具调用任务,我把它当 agent 来做。
诚实说代价:命令式 agent 画不出照片级写实,那是扩散模型的活。换来的是矢量分层、可编辑、可解释、可对话式修改——每条指令都是一个带 id、能回头改的对象。这是有意取舍,不是缺陷。
几个核心机制(技术含量在这)
一、画布即记忆——不建向量库。 画布是一份结构化 JSON(Document{layers:[{elements:[…]}]}),每个元素带 id/label/type/params/style。这份 JSON 同时是渲染源,也是 LLM 的记忆:每轮把当前完整 Document + 用户指令 + 最近对话摘要 + 上一轮操作过的 element_id 喂给模型,老对话只留摘要、Document 永远全量带上。会话历史不需要语义检索,这题上向量库是过度设计——省一套基础设施和它的调用成本。
二、结构化推理而非识图——所以纯文本模型够用。 因为模型推理的是画布 JSON 不是图像,这不是识图任务,不需要多模态视觉模型当大脑;纯文本 function-calling 模型在这套架构里反而更干净、更稳、更便宜。开工前先验证「大脑能不能用」:让 DeepSeek 把「画笑脸」翻译成坐标,8 轮 tool-use 零失败、坐标合理(脸居中、双眼对称、嘴用二次贝塞尔向下凸成笑弧)——先验过大脑再动手。
三、Agent Loop(能白板讲清的核心)。 一轮 utterance 的循环:喂模型 Document + 指令 → 模型吐计划 + 单轮内尽量多个 tool_call(减 round-trip)→ 后端批量执行改 Document、每个成功操作流式推一个 render_patch 给前端增量渲染 → 工具失败(比如引用了不存在的 id)就把 error 当 tool result 回灌给模型自纠 → 模型继续 / 回文字(=完成)/ 调 ask_clarify(=反问)。收敛靠两个正交指标 + 30s 单调用超时。早期用单一 8 轮上限同时管「给复杂图留步数」和「防死循环」,两者打架——客厅要画 810 个元素、模型常一轮只画 12 个,8 轮撞顶就画不全。拆成两个正交指标:轮数上限 18(管复杂度)+ 连续 3 轮零 render_patch 提前停(管死循环,有进展立即归零)。这样轮数能放心调大而不放任烧钱——实测繁忙街道 93 元素 / 7 轮跑完没撞顶(旧的 8 轮一刀切必废)。
四、指代消解——对话式修图的命门。 「把它变大」「刚才那个」靠两个锚点解析:label 语义锚(agent 画元素时自起的语义名,如「棕色沙发」)+ 上轮操作过的 element_id 时序锚。歧义即反问、禁止猜——猜错会静默改错元素,比报错更糟。不确定就调 ask_clarify,置 pending_clarify 缓存待续上下文,下一句默认当澄清回答接着跑,不丢半截画。实测:明确指代「红圆变大」3/3 命中;歧义「那个圆变大」3/3 触发反问、零元素被误改。
管线(语音进 → 出图)
用户语音 → 前端 Web Audio API 取 raw PCM,线性降采样到 16kHz/16bit/mono → WS /ws/stt → Azure 流式识别(仅 final 注入,空/过短去抖丢弃) → WS /ws/agent(首帧 JWT + 项目鉴权) → Agent Loop:DeepSeek tool-use,全量喂 Document JSON + 工具定义 → 后端执行 tool_calls 改画布 → 每步流式推 render_patch → 前端 createElementNS 增量渲染 SVG(每语义图层一个 <g>) → agent 文本 → Azure TTS 口头确认/反问(半双工 gate 防自激) → 导出 SVG(原生分层)/ PNG(cairosvg)/ PSD(pytoshop 分层)为什么 STT 要手动取 PCM:浏览器 MediaRecorder 只能给 webm/opus 压缩流,而 Azure 流式识别要 16kHz/16bit/mono 裸 PCM,直推识别失败——所以用 Web Audio API 手动取 Float32、线性降采样、转 Int16 再推流。
真实踩坑(现象 → 根因 → 解法)
- 畸形工具参数拖崩整个 loop:真实 DeepSeek 场景测试偶发崩溃。根因——模型偶尔把
set_style的 props 传成字符串,dict.update抛 ValueError,而 loop 只接CanvasError,ValueError 直接穿透。解法:工具层假设参数任意错类型、加校验,dispatch 统一兜底把 ValueError/KeyError 转成 CanvasError 回灌自纠;绝不裸 catch Exception(那会吞掉真 bug)。 Bearer undefined401(真人 dogfood 挖出):真人走 UI 报「登录失效」。根因——后端返回access_token,前端读成res.token=undefined,localStorage 存了字符串"undefined",后续请求全是Bearer undefined。之前一直没发现,是因为所有测试都直接打 API、从不走前端解析。解法:改读对字段 + 存前校验非空 + 加 Playwright 真驱动 UI 测试。- 删除指令进 clarify 死循环(最硬的产品 bug):「把台灯删掉」→ 反问是不是落地灯 →「对,整个删掉」→ agent 又一次反问,元素一个没删,会卡死真实用户。根因——用户用词(台灯)和 agent 自起的 label(落地灯柱/灯罩)不一致时,死守精确字符串匹配。解法:提示词改成按语义/类型/颜色/位置近似匹配、某类型唯一即直接执行不反问、反问确认后必须执行绝不重复;并给「找不到元素」的错误附上现有元素候选清单,让模型自纠选对 id。
- 客厅里凭空长出一张笑脸:同句「画一个温馨的客厅」两次会话产出不同,第二次凭空塞进「笑脸/左眼/右眼」三个元素(模型串味)。解法:提示词加「只画明确要求的元素、绝不添加无关物体」,绘图温度降到 0.15(绘图要确定性,可环境覆盖、绝不硬编码)。
- 兑现 PSD 分层导出,踩了两个编译/预览的坑:早期 README 吹了「PSD 1:1
映射、可在 PS 继续编辑」但代码根本没有(三格式按钮全点不到),自查戳破后补齐——每个语义图层栅格化成透明 PNG、用 pytoshop 叠成分层 PSD(语义图层 1:1
,中文图层名走 luni)。坑一:生产镜像
python:3.11-slim没 gcc,编不出 pytoshop 走 packbits 的 Cython 扩展,RLE 压缩会崩——改compression=raw(纯 Python、更可复现)绕过。坑二:nested_layers_to_psd只写图层数据不写合并预览,导致 PS 缩略图全黑——手动 alpha 合成一张扁平预览补进去。线上三格式全 200、psd-tools 读回多图层验证。 - barge-in 结构性失效(独立验证者挖出):用户想打断正在画的长任务、重新说一句,却打不断。根因——后端消息循环收到一条 utterance 后
await它整轮跑完才读下一帧,长任务运行期间根本不读 socket,cancel / 新指令永远读不到,「能打断」只是注释里的空头承诺(实测长任务吐满 86 个 patch 一路画到底、新指令全被无视)。解法:utterance 改 fire-and-forget——同步起后台 task 立即返回、消息循环继续读 socket,新 utterance 先 cancel 在跑的再起新的。修复后打断让长任务从 86 patch 骤降到 2~3 patch。
量化(真实端到端)
公网 HTTPS、真实 DeepSeek:感知首帧 p50=1.48s、完成 p50=5.35s(简单房子 6.28s)。延迟有 stt_final / first_patch / last_patch / agent_say 四时间戳口径,跑 N 次取 p50/p95——诚实评 1.5–3.4s「可接受但偏高,复杂句更慢」。10 个绘图工具、7 种图形;「温馨的客厅」实测拆成 11 元素 4 图层。重复率 3.7%(移植自上一批的 auth 约 180 行 / 产品代码约 4800 行,远低于赛事 50% 红线)。测试:后端 / 前端分套通过(WS 14、agent 10、撤销回归 17 等多套)。约 27 PR、39 commit 跨三天分布。
诚实边界(没做的、不稳的、吹过头后补的)
- 乱码 / 无意义输入 100% 稳定:做不到。同一段乱码有时反问、有时被脑补成图形——这是模型能力天花板,提示词能引导反问但压不到 100%,所以用成功率把它显式化测试,不假装稳定。
- 视觉自我批评循环(agent 看渲染图自己改):评测发现小视觉模型在合成图上不可靠(纯蓝被识成橙色),不敢接进主链路,降级为 stretch,没做。
- 真实麦克风噪声鲁棒性:headless 环境造不出真人对麦克风说话,最终用 headed 浏览器 + 假音频设备(WAV 当麦克风)跑通全链路。但合成语音只证明链路通,不证明噪声鲁棒性,真人真麦还需验证。
- 美术质量:落地灯像「小相框装了个蛋」,沙发是无圆角无靠背的纯色矩形,无透视无阴影。结构可辨认,但远谈不上「温馨」——能力上限,不是 bug。
工程纪律(过程产物占 40% 评分,单独说)
PR-only 工作流:禁直推 main、每 PR 只做一件事、四要素描述、三天 commit 均匀分布、合并后 main 冒烟。append-only 决策台账 32 条,每条「为什么这么做 / 否决了什么 / 何时该改」——直接冲着上一批路演被问倒「讲不清自己旗舰功能的实现机制」那个洞去的,逼自己每条都能一句话复述清楚。TEST-LOG 随做随落盘、每条写清「没覆盖什么」。一份共享契约把 render_patch schema、Document 模型、clarify 状态机、延迟口径全钉死,「改契约先改文档」。验收门:pytest + 渲染 + 卫生检查不过不许 merge;pre-commit 拦内部模型名 / 密钥 / 占位符。每个 builder PR 派一个独立 reviewer——不看报告、自己读 diff、自己跑测试、专挑错。真人 dogfood 明确「人戴耳麦真说话真画、agent 代劳不了」,产出 6 个具体缺陷(缺陷清单为空就退回重做)。
小结
这个项目最说明问题的是那个定位判断:把语音绘图当成结构化推理 + 工具调用的 agent,而不是文生图。 一旦这么看,画布就成了记忆、纯文本模型就够用、矢量就天然分层可编辑、对话式改图就有了支点。技术含量不在调通 STT/TTS,在「画布即记忆」「歧义即反问不准猜」「错误回灌让模型自纠」这些让一个 agent 真能用、真敢给人用的地方——以及那张如实写下来的缺陷清单。