ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

一次生成还是多轮修补:两种工作流的真实产出效率对比

一次生成还是多轮修补:两种工作流的真实产出效率对比

一次生成还是多轮修补:两种工作流的真实产出效率对比

用 4B 小模型生成一个能跑的单文件 Three.js 场景,真正的分岔点不在提示词写得多漂亮,而在第一次生成之后你做什么选择:重新掷骰子,还是拿着上一版代码打补丁。前者看起来"干净",后者看起来"脏",但在本地推理的算力账本上,两者的成本结构完全不同。这篇文章不讲玄学,只把两条工作流的步骤、计时段、返工判据摊开,用仓库里能核对的事实(模型卡说明、config.json、generation_config.json、chat_template.jinja、权重索引)和第三方实测结论,回答三个问题:什么东西一次性就能过,什么东西一定要返工,以及返工时该在第几轮止损。

一、先把两条工作流的步骤拆干净

很多"一次生成更快"或"多轮修补更稳"的争论,本质是计时口径不一致:有人只算模型出字的时间,有人把打开浏览器、肉眼验收、Ctrl+F 找变量名全算进去。为了让后面的账能对齐,先定义"轮"(round):

一轮 = 一次完整 decode 结束,到产出可打开的 HTML 文件,并且完成一次验收。验收包含:是否黑屏/报错、相机是否可控、物体是否成组、动画是否在跑。

在此定义下,两条工作流拆成四个计时段:

计时段含义一次生成多轮修补
T_prefill系统提示词 + 用户提示词 +(修补时)上一版代码 + 诊断信息的前向固定,短随代码量增长,但可被前缀缓存命中
D_think<think>段落的解码 token存在(模板默认开启)通常可关闭
D_code/D_patch场景代码或补丁的解码 token全量D_code只出D_patch
T_review人(或脚本)验收的时间全量全量,但可被静态检查压缩

于是:

T_one = T_prefill(P) + D_think + D_code + T_review T_repair = T_prefill(P + C + Δ) + D_think' + D_patch + T_review'

其中C是上一版完整代码,Δ是一行到几行诊断信息("第 214 行const box与第 97 行重名"这种)。

这里有一个容易被忽略的事实:T_repair里的T_prefill几乎不是线性增长的。这颗模型是混合注意力结构,config.json 的layer_types给出 32 层里每 4 层才出现一层full_attention,也就是 24 层linear_attention+ 8 层full_attention。线性注意力层的状态是常数大小,只有 8 层全注意力层随长度累积 KV。按num_key_value_heads: 4、head_dim: 256、fp16 计算:

  • 每 token KV ≈ 8 层 × 2(K/V)× 4 头 × 256 维 × 2 字节 =32 KiB/token
  • 24 层线性注意力状态 = 24 × (16×128 + 32×128) × 4 字节 ≈576 KiB,且与长度无关

也就是说,把一份 8000 token 的场景代码整份留在上下文里做修补,KV 增量约 250 MiB 量级;对 8 GB 显存 + Q4_K_M(实测文件 2,708,804,096 字节,约 2.52 GiB)来说完全放得下。多轮修补在这颗模型上是工程可行的,而不是"上下文撑不住只能重开"。这是后文所有结论的物理前提。

同时,max_position_embeddings: 262144意味着你不需要为爆上下文做代码摘要压缩——摘要本身就会丢结构信息,进而制造新的返工。

二、耗时与返工率:能把话说到什么程度

仓库里能直接核对的事实只有三条,但它们足够改变决策习惯。

第一条:一次成功率不是常数,是随检查点移动的。README.md 的 10 月 27 日检查点说明写明:one-shot success has improved substantially, along with overall scene quality,紧接着一句是 "Some scenes can still require another attempt or a correction"。这句话的价值在于它把"一次生成"从信仰问题降级为概率问题——它承认剩余失败率存在,并且没有说失败就代表前一轮作废。

第二条:失效类型是局部的。同一份 README 的 Known issues 明确写:观察到的问题主要集中在 duplicate variable names,而不是结构性问题。这条信息决定了修补路径的成本上限。重名变量意味着:场景图建对了、材质对了、相机对了、动画循环也在跑,只是某个const覆盖了另一个。补丁长度通常只有几行到几十行。

第三条:第三方实测显示,同一个模型在不同宿主环境里结果可以完全不同。在一份覆盖 6 道前端题的横向实测中(纯 CSS 山水画、分形烟花、五子棋、横版跑酷、3D 太阳系、诗词代码雨),同一个 Qwen3.5 在 IDE 环境与 CLI 环境里出现了"一次就崩"和"表现尚可"的互换;3D 太阳系那一题在 CLI 里只渲染出一个球,事后诊断出的失效清单是 6 条可定位的局部问题:Three.js 0.160 用传统 script 方式加载、PointLight强度过低导致行星全黑、太阳光晕用了BackSide、事件绑定发生在init()之前、sunMesh重复存储、以及用 2D 曲线去画 3D 轨道。

这 6 条里有 5 条是单点可改的。如果把这道题交给"重生成",等于用一个随机过程去重新抽这 6 个失效点的组合;交给"修补",等于按清单逐条消项。

由此可以得到一个不该被绕过的结论:返工率必须按失效类型分层统计,而不是按题目统计。把"环境加载失败""MTP 导致的乱码""真正的建模错误"混在一个返工率里算,得到的数字没有指导意义。

顺便把最容易混进返工率的一类噪音排掉。社区一直在传"LM Studio 里必须关闭 MTP",仓库这边能对上:config.json里保留了mtp_num_hidden_layers: 1的字段,但 model.safetensors.index.json 的 723 个权重条目里没有任何mtp.*张量。运行时若按 MTP 路径去找一个不存在的预测头,输出乱码或截断是可以预期的。所以"打开就乱、重试也没用"的轮次,应该记进环境故障,不能记进模型返工——否则你会错误地得出"这模型一次成功率很低"的结论,然后去做大量无意义的重生成。

三、成本模型:什么条件下修补一定赢

把上面的事实写成判据。设首次一次成型概率为p,单次定点修补成功率为q,全量代码解码量为D_code,补丁解码量为D_patch。

  • 只靠重生成的期望解码成本:E_reg ≈ D_code / p
  • 只靠单轮修补的期望解码成本:E_fix ≈ D_patch / q

切换条件化简为:

D_patch / D_code > p / q → 该重生成 D_patch / D_code < p / q → 该修补

代入两类真实失效:

失效类型典型表现D_patch / D_codeq量级推论
局部型(重名变量、光照强度、事件绑定顺序、材质参数)有画面但不对,或控制台单条报错0.02–0.05高(诊断明确了就基本能改对)修补占绝对优势
结构型(黑屏、只有占位球、场景空)整页无内容或只有一个几何体≈ 1(等于重写)低直接重生成

这里的关键不是具体数值,而是分母D_code的量级:一个完整 Three.js 场景单文件通常包含几何构造、材质、光照、相机控制、动画循环、UI 覆盖层,代码体量在数百到两千行、数千到上万 token 之间。任何一次"重掷"都要重新支付这个体量,而修补只支付其中 2%–5%。

再叠加一层人工成本:修补轮的T_review'是定向的("我刚改的那一处好了没"),重生成轮的T_review是全局的(重新检查所有约束)。验收成本的方向和模型成本的方向是一致的,都偏向修补。

四、复杂度区间:一次成型的边界划在哪

仓库的画廊给了四类难度不同的参照物,可以直接当分档样本看。

单主体、单相机机位的场景,一次成型概率最高。比如樱花盆栽这一档:一个花盆、一株主干、若干花瓣实例、一片地面,验收标准是"像不像"而不是"对不对"。

同类还有 LGA 3647 插槽(单一硬件件的结构还原)和书桌、装订工作台(室内静物 + 固定视角)。它们的共同点是:约束条数少、失效后果可辨认。变量重名这类问题在这里表现得很明显——页面直接白屏或某个物件消失,一眼定位。

多区域组合型就进入返工高发区。龙骑士海蚀柱这一档,提示词里同时压进了:平顶海蚀柱、四个外挑石台、中央拱顶孵化洞(含沙巢与排烟口)、木质平台与吊装架、之字形楼梯、水线船屋。这不是"一个物体",是六个功能区的空间关系,任何一处坐标或层级写错,整体轮廓就读不出来。

最上面一档是动态与交互型。README 里的机场塔台例子要求"顶部监视雷达持续旋转 + 旋转信标",属于时间轴正确性问题;而系统提示词又强制"不引入 addons、相机控制必须写在 HTML 里",等于把OrbitControls的实现责任交还给模型。这一档的典型失败不是报错,而是"能渲染但不对"——动画跑得起来但轨道倾角错了,拖拽能转但极角没有夹紧导致穿模。

据此可以把分档和预期轮数对齐:

档位判据典型 prompt 形态预期轮数
A 单主体≤1 个主几何体 + 环境"a sakura bonsai in a ceramic pot"1(大概率)
B 多物件排布3–10 个语义物件,固定机位室内工作台、硬件件1–2
C 多区域组合≥3 个功能区 + 空间连接海蚀柱、运河船闸与中心岛2–3
D 动态交互动画循环 + 相机控制 + UI塔台雷达、可点击标签2–4

注意一个反直觉点:提示词越长,p不一定越高。README 里"运河船闸"只给了一句话,模型自由度极大但验收也跟着变主观;龙骑士那段给到三段细节,模型出错概率上升,但每一处错误都可以对着原文逐条核对。前者的问题是"没有标准",后者的问题是"标准太多"。返工成本高的是后者,但不可判定的是前者——这也是为什么一句话题目经常被误判成"一次成功"。

五、组合切点:在第几轮停止修补

把所有证据收敛成可执行的规则,只需要三个阈值。

切点一:先分清是"模型错"还是"环境错"。在统计任何返工率之前,先确认三件事:MTP 已关闭(权重里没有mtp.*张量,开了就是自找乱码)、量化档位与显存匹配、CDN 可达。Q4_K_M 与 Q8_0 的权重体积差 1.65 倍(2,708,804,096 vs 4,482,402,816 字节),decode 的带宽压力差别明显,但 README.md 对两者给的是同一套生成参数,并没有给出两者一次成功率的对比——这个差异只能自己采样,不能假设。

切点二:按失效清单条数决定动作。

现象动作
页面有内容,报错 ≤1 条,或存在重名变量修补,单轮,只回传诊断行
页面有内容,报错 2–3 条且互相独立修补,一次给全清单,禁止分三轮问
黑屏 / 只有占位体 / 场景为空重生成,先收缩 prompt 范围再跑
连续两轮修补后,同一指标(如"相机能否拖拽")无改善立刻重生成,不要赌第三轮

切点三:需要"换解法"时才动采样参数。仓库给定的区间是Temperature 0.6–0.8、Top P 0.90–0.95、Top K 20–40、Repeat penalty 1.0,而 generation_config.json 的默认值落在保守端(0.6 / 0.95 / 20 / 1.0)。这意味着:第一次生成应当用默认值(贴近训练分布),只有在"重生成时希望得到结构不同的解"才把温度推到 0.8 上界、top_k提到 40。repeat penalty保持 1.0 不要往上加——它压制的正是重复标识符,调高反而会诱导模型换名,制造新的重名冲突。

修补轮还有一个上下文卫生问题,模板层面就能处理。chat_template.jinja 对历史 assistant 消息是这样处理的:如果消息里带reasoning_content,会把它重新包进<think>...</think>再拼进上下文;并且在add_generation_prompt阶段,只有当enable_thinking显式为false时才写入一个空的<think>\n\n</think>块。也就是说:

  • 一次生成默认是带思考的,D_think是T_one里最不可控的一块;
  • 修补轮应该显式关掉思考,否则每一轮都在为"重新想一遍整个场景"付费,而你需要它想的只是那三行补丁;
  • 但要注意,训练数据是带完整 thinking traces 的(README 明确数据集包含 full thinking traces),关思考属于分布外使用,质量变化需要实测,不能当成无脑加速开关。

六、一套能自己跑的采样与验收流程

上面所有判据都依赖两个可测量:一次成功率p和补丁比例D_patch / D_code。它们都能用几十行脚本测出来,不需要主观争论。

先起服务,参数严格照 README 的区间给:

llama-server \ -m ./Qwen3.5-4B-3D-Coder-Q8_0.gguf \ --port 8080 \ --temp 0.6 --top-k 20 --top-p 0.95 --repeat-penalty 1.0

然后用curl的 write-out 直接拿到首 token 时间与总时长,把T_prefill + D_think和总时长分开看:

for i in 1 2 3 4 5; do curl -s -o "out/run-$i.json" \ -w "run $i ttft=%{time_starttransfer}s total=%{time_total}s\n" \ http://127.0.0.1:8080/v1/chat/completions \ -H 'Content-Type: application/json' --data @req.json python3 -c " import json,sys d=json.load(open('out/run-$i.json')) open('out/run-$i.html','w').write(d['choices'][0]['message']['content']) " done

拿到 5 份 HTML 之后,验收不要靠肉眼,先用静态检查把"局部型失效"秒级筛出来。README 的已知问题就是重名变量,这类问题在浏览器里表现为黑屏或物件消失,但在静态检查里是一行报错:

for f in out/run-*.html; do # 1) 是否引用了不允许的 addons / 旧版 examples/js grep -nE 'addons|examples/js|OrbitControls' "$f" && echo "$f: 违反系统提示词约束" # 2) 抽出 module 脚本做语法检查,重名变量会在这里暴露 python3 - "$f" <<'PY' import re,sys,subprocess,tempfile,os s=open(sys.argv[1]).read() m=re.search(r'<script[^>]*type="module"[^>]*>(.*?)</script>', s, re.S) if not m: print(sys.argv[1], 'no module script'); sys.exit() fd,p=tempfile.mkstemp(suffix='.mjs'); os.write(fd, m.group(1).encode()); os.close(fd) r=subprocess.run(['node','--check',p],capture_output=True,text=True) print(sys.argv[1], 'OK' if r.returncode==0 else 'FAIL '+r.stderr.strip().splitlines()[-1]) PY done

这一步把T_review从"打开浏览器看图"压缩到"看一行输出",而且是可复现的。只有通过静态检查的样本,才值得进入人工审美评估——这才是"一次生成"工作流真正省时间的地方,它省的不是 decode,而是验收。

最后是判据落地:把这 5 次的通过数记成p,把每次修补的 diff 行数除以原文件行数记成D_patch / D_code。当D_patch / D_code稳定落在 0.02–0.05、而p在 0.4–0.8 之间时,纯重生成策略的解码成本是修补策略的 8–40 倍,这时候"多轮修补显得慢"只是因为它把时间花在了人看得见的地方。

一个 4B 模型的现实价值不在于它每次都对,而在于它错得足够浅:浅到一行 diff 能修,浅到关闭思考后一轮补丁几十秒就能拿到,浅到整份代码塞进 262144 的位置上下文里也不用做有损压缩。工作流的选择应该围绕这个特性来设计——把重生成留给结构性崩坏,把修补留给局部失配,把参数调整留给真的需要换解法的时候。剩下的,都只是验收流程没做好而已。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

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