提示词体系与 3D 双引擎
适用工具:asset-forge(美术资源生成器) 制定日期:2026-09-17 · 状态:已实现并通过测试
本文讲清两件事:提示词是怎么分层的(以及怎么让 AI 帮你写), 3D 生成的两种模式各自适合什么、有什么硬约束。
一、提示词体系:四组十二级 × 七种资产分类
整条产线只有一份提示词库,但它服务两个完全不同的"读者"—— 文生图模型(吃画面描述)与写建模脚本的大模型(吃代码约束)。 两者的语言不同,所以层号分开,避免在同一张表单里混淆。
┌─→ 2D 出图 G0 → L1 → L2 → L3 → L4 → L5 → C1 → C2 (一段 user)
公共风格 G0 ────────────┤
└─→ 3D 写脚本 G0 → M1 → M2 → M3 → M4 → C1 → C2 (system 段)
+ [资产规格](user 段)
3D 那条是两条消息:system = G0+M1~M4+C1+C2,user = 资产规格。 合成一段就没法如实发送 —— LLM 协议里它们本就是两个角色。
| 层 | 名称 | 读者 | 管什么 |
|---|---|---|---|
| G0 | 公共风格 | 两者 | 整体调性(武侠 / 折纸 / 水墨 / 像素…) |
| L1 | 风格前缀 | 文生图 | 画风、着色方式、色板 hex、光照 |
| L2 | 主体描述 | 文生图 | 这个资产长什么样(唯一随资产变化的一层) |
| L3 | 视角与姿态 | 文生图 | 镜头角度、投影、占比(设定图 / 三视图两套) |
| L4 | 技术约束 | 文生图 | 背景色、悬空、边缘锐利、好抠图 |
| L5 | 负向约束 | 文生图 | 明确排除的风格与缺陷 |
| M1 | 脚本风格 | 写码 AI | 造型语言与材质基调(要能在 bpy 里实现) |
| M2 | 技术规则 | 写码 AI | 坐标系、单位、命名、原点等硬性约定 |
| M3 | 输出契约 | 写码 AI | 脚本形式(函数签名、允许的库、返回值) |
| M4 | 负向约束 | 写码 AI | 禁止的几何手法与代码做法 |
| C1 | 分类定位 | 两者 | 这类资产是什么(地块 = 1m 见方的可拼接地面单元;人物 = 直立 Q 版三头身) |
| C2 | 分类禁项 | 两者 | 这类资产绝不能是什么(点名封掉 B.assemble_hall 等建筑构件函数) |
拼接是简单串接:每一层非空就依次接上去,层间空一行。
第二维:category
上面的层号是"管什么"的切片,category 是"给谁用"的切片。两者正交:
解析规则:category 专属优先 → 回落 '*'
- 库里每行模板的坐标是
(layer, name, category),category='*'表示通用兜底 category_of(role, slug)从资产推导分类,与资源总览台的分类导航同一套函数
(见 config.py:building / envtile / character / vegetation / decor / backdrop / test)
- 老资产零迁移:加这一维之前那 10 行自动落到
'*',行为逐字节不变
C 层为什么必须单独存在(而不是改 M2/M3)
因为 M2/M3 是资产无关的通用契约,改它们等于给所有资产改规则。 更关键的是:那批"要台基、要斗拱、要瓦垄"的条款对建筑是对的,不能删。 所以取"末尾追加、反向约束"的做法:
- C 层压在最末 —— 约束越靠后权重越高,它能盖住前面 M 层的要求
- C2 点名封函数 —— 光说"不要屋顶"不够,M3 会继续劝它调
B.assemble_hall - C1/C2 同时进两条链 —— 少进一条,重试时又会退回建筑契约(已有断言钉住)
- C1 要正向交代形态 —— 对人物尤其关键:M2 第 9 条要求"构件一个不省",
只写禁项等于没拦住"要全"那句话,得把形态锚点正面写出来(见 6.9b)
已配 C 层的分类(5 个):envtile character vegetation decor backdrop。 仍走 '*' 的两个各有正当理由:building(通用本为它而写)、test(占位资产)。
为什么要有 G0
在没有 G0 之前,「整体是纸艺低多边形武侠风」这件事被写了两遍: L1:style_prefix(给文生图)里一遍,M1:script_style(给写码 AI)里一遍。 想换成水墨风,得记得改两处,而且 M1 在界面上根本看不到—— 改了一处忘了另一处,就出现"2D 是水墨、3D 还是折纸"的割裂。
抽到 G0 后,改一处,两条产线同时生效。
为什么 G0 默认是空的
这是有意的,不是没填。 L1 与 M1 现有的措辞是反复调优出来的,凭空在最前面 塞一段风格词会稀释它们的效果(模型对靠前的内容权重更高)。所以 G0 出厂留空, 谁想换风格谁自己填,或者点界面上的风格预设按钮。
二、提示词的自动生成
界面上每一层都有「AI 生成」入口。背后是一套每层一份角色说明书的机制 (backend/app/services/prompt_gen.py 的 LAYER_BRIEFS):告诉模型这一层 在产线里承担什么职责、该写什么、不该写什么、大概多长,再把其它层的现行内容 摘一小段给它看(防止写重、写冲突)。
生成时会带上:目标资产信息(若有)、你填的额外要求、以及该层的现行版本(作为参考)。
三条设计原则
- 只生成这一层。不生成整条提示词、不替你改动别的层。拿到的是"能直接填进
这个框的一段文本"。
- 只生成、不保存。结果填进草稿框,你看过、改过、点保存才落库。
自动生成的东西必须过人的眼睛——这是"人工闸门"原则在提示词上的延伸。
- 对硬契约层要泼冷水。M2(技术规则)与 M3(输出契约)是 Blender 执行器
直接依赖的,措辞被调过。自动生成这类层可能产出"看着合理、实则破坏执行器约定" 的东西,所以这两层返回结果时会附一句风险提示,强烈建议不要用生成结果覆盖。
关于温度
写建模脚本用 temperature=0.2(要的是同一份规格每次产出同样的形状); 写提示词用 0.7(这是创作任务,0.2 会反复吐同一套套话,点三次拿到三份 几乎一样的文本,等于没生成)。
三、3D 生成的两种模式
在「生成」按钮旁可以选引擎,界面上会显示每个引擎当前是否可用、缺什么配置。
| AI 脚本 + Blender | 混元 3D(腾讯云) | |
|---|---|---|
| 怎么出形 | 规格 → LLM 写 mf_lib 脚本 → Blender 无头执行 | 提交到腾讯云,轮询取回 GLB |
| 形状来源 | 文字规格(模型规格)+ 可选参考图 | 图片(重建)或文字(生成) |
| 可干预 | ✅ 脚本落盘可看、可改、可只改脚本重跑 | ❌ 黑盒 |
| 可复现 | ✅ 同规格同脚本 → 同结果 | ❌ 每次不一样 |
| 依赖 | Blender 可执行 + ARK_API_KEY | 腾讯云密钥 |
| 计费 | LLM token | 腾讯云按次 |
| 适合 | 作品资产(面数可控、风格统一、可返工) | 快速看形、探索期参考 |
判断依据很简单:要进作品的东西走 Blender 路——面数预算、顶点色、命名规范 都要卡住,只有白盒产线能保证。混元用来快速试形。
混元产线由Admin守护线程推进(不再依赖前端轮询)
混元任务提交到腾讯云后,必须有人不断查询云端才能把 GLB 拉回来。早期这唯一的 推进入口是前端轮询 GET /api/model3d/{id}(读库前顺带推一把)。但前端改版后, 「资源总览」页改轮询只读聚合接口 GET /api/hub/asset/{id},推进混元的那个接口在 活跃前端里 import 了却从未调用——结果混元任务提交后永远停在 RUNNING, 前端 runningModel 计算属性恒为真,「生成 3D」按钮被永久锁死,整条产线等于废掉。
修复方式:让混元产线与 Blender 产线对称——Blender 有进程内Admin线程自治,混元也该 有Admin推进者。新增 services/janitor.py,在 main.py 的 on_startup 里:
reap_orphaned_jobs():启动时回收上一进程遗留的「必死」非终结任务——
blender 未完成(线程随进程Sign out)、hunyuan3d 无 remote_job_id(未成功提交到云端) 一律置 FAILED,避免僵尸 RUNNING 永久锁死按钮;有 remote_job_id 的混元任务不动, 交给轮询线程去取回。
start_poller():启动一个名为hunyuan-poller的守护线程,5 秒一轮扫描所有
未终结的混元任务并调用 poll_model_job 推进,全程不依赖任何前端请求。
踩过的坑:懒轮询 + 前端改版 = 任务黑洞。推进入口只挂在某个前端调用上时, 一旦前端改版不再调它,任务就再也没人推,钱花了却卡在 RUNNING。产线推进这种事 必须放在后端自治的守护线程里,不能绑死在前端行为上。
四、⚠️ 图文同传:两条产线的能力不同
这是最容易踩的坑,界面必须区别对待,不能一套交互套两条路。
Blender 路:✅ 可以「提示词 + 图」一起给
两者分工明确,不冲突:
- 规格(提示词)定结构 —— 有几个开间、屋顶什么形制、面数预算多少;
- 参考图定外形 —— 具体长什么样。
参考图走的是两步视觉通路:多模态模型先看图,产出一份结构化的几何观察, 这份观察再拼进规格交给写代码的模型(见 services/vision_client.py)。 所以还能额外填一句「给视觉模型的叮嘱」。
界面上的表现:图和提示词都可以填,提示词框和参考图槽位同时可用。
混元路:❌ 不能,Prompt 与 Image 互斥
腾讯云官方硬约束:Prompt 与 Image 只能给一个,同时传会直接报错 (唯一例外是 GenerateType=Sketch 草图模式,本项目不用)。
所以混元路下:
- 给了图 → 走图生3D,提示词不发出去;
- 没给图、只给提示词 → 走文生3D。
界面上的表现:同时填了图和提示词时,显示一条黄色告警说明提示词会被忽略, 并提示"想用提示词控制形状请改用 Blender 引擎"。按钮不禁用——这不算错误, 但必须让人知道。
后端也会如实回报:任务的 prompt_sent 字段说明提示词到底有没有上送, 任务历史里会显示。"写了提示词却没生效"不允许变成静默失败。
五、参考图的三条来路
最终都收敛成"一个本地文件路径",下游两条产线无需区分来源。
| 来路 | 参数 | 用在哪 |
|---|---|---|
| 1 设计任务的产出图 | ref_design_job_id + ref_image_index | 界面上在设计图上点「用作参考」(首选,有溯源) |
| 2 用户自己上传 | ref_image_path | 界面上「上传图片」。落盘在 asset_forge_data/work/06_ref_upload/ |
| 3 直接给磁盘路径 | ref_image_path | 脚本 / 高级用法 |
优先级:1 > 23(两者互斥,界面选了一个会清掉另一个)。
上传为什么要过一道 Pillow
services/ref_upload.py 收到图后做三件事,缺了都会在下游炸:
- 按 EXIF 摆正方向 —— 手机竖拍的像素其实是横的,靠 EXIF 标记才显示为竖。
不转的话,视觉模型看到的是躺倒的图,几何观察全歪。
- 统一缩到长边 2000px —— 混元要求单边 128~5000、Base64 ≤6MB。
手机原图动辄 4000×3000、5MB+,直传会吃 InvalidParameter。
- 转成 RGB 存 JPEG —— PNG 透明通道、调色板图、CMYK 在图生3D 环节都可能出问题。
没装 Pillow 时不报错,退回原样落盘并标注 degraded=True—— 少一个可选依赖不该让整条产线不可用。
为什么上传走 base64 而不是 multipart
FastAPI 的 UploadFile 依赖 python-multipart,本项目没装(也不打算为一个上传 功能引入新依赖);而 vision_client 给多模态模型发图本来就是 base64 data URI 的路子, 两者同构。所以上传体是 {"filename": …, "data": "data:image/jpeg;base64,…"}。
六、踩过的坑(别再踩)
1. gen_mode 曾经前后不一致 🔴
混元任务在提交那一刻返回给前端的是 "hunyuan-image", 而写进库里 meta 的是裸的 "image"。结果同一个任务:刚提交时前端看到 hunyuan-image,刷新一次变成 image —— 前端按值做分支就会一半正常一半失效。
教训:一个值的"瞬时返回"与"持久化后再读出"必须是同一套取值, 且要有测试钉住(现在 _smoke_hunyuan_line.py 会比对路由输出与库内值)。
2. 混元的 Prompt/Image 互斥是硬约束,不是"建议"
不要试图"两个都发、让云端自己挑"——会直接吃 InvalidParameter。
3. Model=3.1 不支持 LowPoly
选 3.1 时 GenerateType 不能是 LowPoly,代码里已提前拦截并给出可操作选项, 而不是等腾讯打回一个干巴巴的Error codes。
4. 云端可能只回 OBJ
任务成功但没有 GLB 时,必须失败,不能把 OBJ(实为 zip)存成 .glb —— 文件头不对,后续 Blender 转换必炸。代码里明确报错并列出实际返回的格式。
5. 删除参考图的接口要防路径穿越
接口参数直接来自 URL,../ 能删到别处去。只接受纯文件名, 含分隔符、.、.. 的一律 400。
6. 轮询接口不是只读的
GET /api/model3d/{id} 对混元任务会顺带推一把云端进度(可能在库里写状态、 下载产物)。所以它不是幂等的只读查询。前端轮询间隔别太密。
7. 🔴 整库模板都是按建筑写的 —— 草地、松树、石灯全被盖成房子
症状:地块 / 装饰 / 植被 / 背景 / 人物一出图就是建筑,怎么改描述都没用。
成因(不是模型的锅,是提示词库里没有"分类"这一维):
| 层 | 里面的原文 | 对草地意味着什么 |
|---|---|---|
| M3 | 「建筑类资产的骨架必须用 B.* 搭」 | 明确指示去调建筑构件函数 |
| M2 第 8/9 条 | 「台基三层叠涩、柱础、斗拱、瓦垄、垂脊、鸱吻一个不省」 | 硬性技术规则 |
| L1 / M1 | 举例全是「屋顶坡面、瓦垄、屋檐起翘、栏杆」 | 风格锚点全指向建筑 |
于是一块 1m 见方的草地拿到的是和「大殿」一模一样的 system 消息 —— LLM 照做,把它盖成了房子。M3 那 5112 字的契约里写着"必须用 B.*", 而 B 全是中式建筑构件,这是最直接的一环。
为什么不能直接改 M2/M3:那批条款对建筑是对的,删了建筑就废了。 问题在于它们被当成了通用契约。
修法:加 category 维度 + C1/C2 两层(见第一章)。 三个必须把握的细节:
- C 层压最末。约束放末尾权重最高,否则压不住 M3 那句"必须用 B.*"。
- C2 要点名封函数。「不要屋顶」这种话对 LLM 太软,它还是会去调
B.assemble_hall;直接列出禁调函数名才拦得住。
- 两条链都要进。2D 与 3D 少进一条,那条链路就照旧出建筑 ——
尤其是 3D 的重试路径:只在首次提交时带 category、重试时忘了传, 会静默退回建筑契约(现在 _smoke_prompt_gate.py 有断言钉住)。
判定"缺 C 层算不算隐患"要用 config.parts_building_ok(),别硬编码分类名。 建筑本来就该有建筑构件,对它弹"小心被建成房子"是自相矛盾的。 但反过来说:也别顺手把别的分类标成 True,那等于关掉对它的保护。
8. 🔴 写进图像链的提示词不许有 Markdown
C1/C2 同时进两条链,而 2D 那条的终点是扩散模型: 加粗 不会被渲染,它会变成两个星号 token 混进图像提示词。
既有那批模板(G0/L1/L2/L4/L5)一条 Markdown 都没用,全是「;」分句的纯文本 —— 这不是偶然,是约定。
但 M1/M2/M3 里用了不少 Markdown,那是对的(它们的读者是 LLM,结构标记有益)。 所以守卫断言不能一刀切"全库禁止星号":得先算出哪些层会进图像链再扫。 _smoke_prompt_gate.py 里就是先从一次真实 2D 闸门调用取段名,再拿它过滤种子模板 —— 这样以后谁加了新层,只要进图像链就自动被管住。
9. 「未配」和「即通用」是两回事
分类清单里 configured=false 有两种完全不同的含义:
- 测试这类:真的没配,正在共用按建筑写的通用模板 → 是隐患,该催
- 建筑:通用模板本来就是按它写的 → 不需要专属行,催它配纯属白工
界面上一律显示「未配」会把建筑也标成待办项。所以后端多给一个 generic_is_native 字段(同样由 parts_building_ok() 判定), 前端据此显示「即通用」而不是「未配」。
2026-09-18 更新:人物(character)已补齐 C1/C2,不再是"未配"。 现在 configured=false 只剩两类:建筑(即通用)与测试(占位资产)。 改动前它是唯一"真会被用却缺 C 层"的分类 —— 12 个门人角色全在它下面。
9b. 人物这条为什么补,以及它和前四类的区别
前四类(地块/植被/装饰/背景)要解决的是"别长成建筑",重点是 C2 的禁项。 人物除这层以外还多一个风险:M2 第 9 条把「台基三层叠涩、柱础、斗拱、瓦垄、 垂脊、鸱吻一个不省」写成 所有资产 的面数纪律 —— 人形照做会被盖成一栋小房子。
所以人物 C1 必须是正向的:先把形态锚点(直立 A-Pose、Q 版三头身、脚底 y=0) 和装配方式(一次调用 C.build_humanoid)讲死,再谈禁项。 只写"不要建筑"是挡不住 M2 第 9 条那句"一个不省"的 —— 它没说不要, 它说的是"要全",得有人把话头拽回来。
还有一处实测踩到的坑(已写进 M3 v6):C.build_humanoid 的 body_type 参数 原话是「取 C.BODY_STYLE 的键」,会被读成 C.BODY_STYLE.DISCIPLE → 当场 AttributeError。修法是直接给字符串字面量的例子。 证据留在 asset_forge_data/work/05_script/disciple_21_a1.py(错)与 _a2.py(重试后对)—— 两轮才跑通,白烧一轮 LLM。
七、验证方式
7.1 纯逻辑冒烟(不依赖端口,ASGI 直连)
cd D:/3d-game/asset-forge
PY=backend/.venv/Scripts/python.exe # 生成器自己的 venv(默认 venv 没装 httpx/Pillow)
$PY _tools/_smoke_prompt_gate.py # 🔴 分类隔离 + 闸门视图(113 项)
$PY _tools/_smoke_ref_upload.py # 参考图上传链路 + 双引擎状态 + G0 预览(35 项)
$PY _tools/_smoke_prompt_gen.py # 提示词自动生成:清洗逻辑 + 层清单(22 项)
$PY _tools/_smoke_prompt_gen.py --live # 上面 + 真实调一次 LLM(花钱,约 8s)
$PY _tools/_smoke_hunyuan_line.py # 混元产线端到端,注入假客户端不花钱(51 项)
$PY _tools/_smoke_cleanup.py # 过程清理(37 项)
$PY _tools/_smoke_janitor.py # 孤儿任务回收(14 项)
_smoke_prompt_gate.py 是本体系的核心护栏,必须过。它钉住的不变量:
| 不变量 | 为什么重要 |
|---|---|
| 建筑输出逐字节不变 | 加分类维度是"零回归"改动,改错了建筑会跟着变 |
| 地块/人物输出 == 老配方 + C1 + C2 | 精确证明增量只有分类层,没有别的漂移 |
| C2 在 system 最末 | 顺序错了就压不住 M3,功能悄悄失效 |
| 重试路径也带 category | 只首次带、重试不带 → 静默退回建筑契约 |
| 图像链模板无 Markdown 星号 | 扩散模型会把 ** 当 token |
| 非建筑类缺 C 层仍警告 | 防止 parts_building_ok 被误扩大而关掉保护 |
人物 C1 正向点名 C.build_humanoid | 只写禁项挡不住 M2「构件一个不省」 |
M3 讲死 body_type 传字符串 | 防 C.BODY_STYLE.DISCIPLE 再烧一轮重试 |
零回归的样本只能是**仍走 的分类(建筑 / 测试)。2026-09-18 起 character 已配 C 层,别再拿它当零回归样本*,会假红。
7.2 端到端渲染验证(需服务在线,:8080 单入口)
cd D:/3d-game/asset-forge
node _shot_prompt_gate.js # 提交前闸门(128 项,拦截付费接口不花钱)
node _shot_prompts_view.js # 提示词工坊分类界面(60 项,纯只读)
node _shot_single_entry.js # 单入口:作品 3D + /forge/ 全带前缀(14 项)
⚠️_shot_single_entry.js的 A 段(作品页)假设浏览器已有登录态, 脚本里没有任何登录步骤。库里没有可用账号时页面会停在登录界面, 于是#topbar/#sidebar/ 三角形数三项必然失败 —— 那是前置条件缺失,不是渲染回归。判据:截图里是不是登录卡片。 另:_shot_publish.js已失效(指向 2026-09 架构整合时下线的#/publish), 需要重定目标后才可再用。
前两个脚本都会拦截真花钱的接口(POST /api/design、POST /api/model3d) 并打桩,收尾再反查服务端最新任务 id 确认没造出任务 —— 所以可以随便跑。 判定界面可见性一律用 getBoundingClientRect(),不用 innerText (隐藏元素照样返回文本,会骗人)。
混元那套测试跑在真实库上,结束后会清掉自己造的 3D 任务 / 账本记录 / 假 GLB。
7.3 ⚠️ 已知失效脚本
_shot_publish.js(原「入库作品」面板验证)已失效:它打的是 #/publish 与 .af-kpi,而该页面在 2026-09 架构收敛时并入了资源总览台, /publish 现在是 redirect:/。入库能力仍在,只是变成了资源详情面板里的 「入库」按钮。重跑它之前得先改靶子,否则只会得到一个 ERR_CONNECTION_REFUSED 而误以为部署坏了。
八、相关文档
- 部署与运维(拓扑、双进程、子路径、环境变量全表)→
一体部署与运维.md - 资产台账 →
资产台账.md - 早期选型方案(已过时,仅第〇章的通道对比仍有效)→
素材生成工具-实现方案.md