总分只能说明 Agent 在这一批任务上的平均表现,不能说明 Agent 是在单物体建模阶段就无法完成,还是在整场景组装阶段失分。要判断标题里说的能力跨度,可行的做法是把同一批任务按场景复杂度切成几层,每层单独运行、单独记录失败点,再看失败类型在哪一层发生变化。下面的分层标准、记录字段和检查脚本都可以在本地 .blend 场景文件与 Agent 日志上直接执行;阈值属于人为约定,需要结合自己的任务集确认。
总分看不出 Agent 卡在哪一层,要看失败类型随场景复杂度的迁移:单物体层看几何闭合与尺寸,组合场景层看相对位置与父子层级,复杂场景层看灯光与依赖管理。做法是按物体数、材质数、灯光数切三层,逐层运行并记录失败类型,再读取 .blend 中的 dimensions、parent 等字段与任务清单比对。风险边界:分层阈值是人为约定,换任务集需重新校准,单层结论不能外推。
把任务清单按场景复杂度分组
分组依据建议用三个可数的量:物体数、去重后的材质数、灯光数。这三个量都能从任务清单或场景文件里直接读到,不依赖人工主观判断,也不依赖渲染结果。下面是一组保守的初始阈值,可以先用它跑通流程,再按自己的任务集调整:
- L1 单物体:物体数 ≤ 1,材质数 ≤ 1,灯光数 0
- L2 组合场景:物体数 2–8,材质数 2–6,灯光数 1–2
- L3 复杂场景:物体数 > 8,或材质数 > 6,或灯光数 > 2,或存在需要指定父子的挂载关系
分组脚本骨架如下,放在任务清单同目录执行。脚本默认任务清单里每个物体带 type 和 materials 字段;如果你的清单字段名不同,改这两处取值即可,脚本不对物体做几何检查,只做计数分层。
import json
def tier_of(task):
objs = len(task['objects'])
mats = len({m for o in task['objects'] for m in o.get('materials', [])})
lights = sum(1 for o in task['objects'] if o.get('type') == 'LIGHT')
if objs <= 1 and mats <= 1 and lights == 0:
return 'L1_single'
if objs <= 8 and mats <= 6 and lights <= 2:
return 'L2_composite'
return 'L3_complex'
tasks = json.load(open('tasks.json', encoding='utf-8'))
for t in tasks:
t['tier'] = tier_of(t)
json.dump(tasks, open('tasks_tiered.json', 'w', encoding='utf-8'),
ensure_ascii=False, indent=2)
print(sorted({t['tier'] for t in tasks}))
如果任务清单本身没有物体级字段,只有提示词文本,就不要强行用脚本分组;这类任务集应改为在跑完 Agent 后,直接对输出场景文件统计物体、材质和灯光数量,再按统计值归层。两种做法得到的分层结果可能不同,需要在结论里写明是按输入清单分层还是按输出场景分层。
逐层跑分并记录失败类型
分层之后,同一个 Agent 在每层上各跑一遍,失败类型要分得足够细,否则所有问题都会被归成“生成失败”,看不出能力边界。建议先按下面的分类表打标签,一条任务只记主失败类型,并在备注里写次生现象。
| 失败类型 | 典型表现 | 主要出现在 |
|---|---|---|
| 无输出 | 脚本报错、超时、没有产出 .blend | 全层 |
| 物体缺失 | 清单要求的物体在场景中找不到 | L2 / L3 |
| 几何不闭合 | 存在非流形边、面法线方向不一致 | L1 |
| 尺寸越界 | dimensions 与规格偏差超出容差,或缩放未应用 | L1 |
| 材质错误 | 材质槽为空、材质数量不符、贴图路径丢失 | L2 / L3 |
| 位置偏移 | 物体间相对间距明显偏离描述 | L2 |
| 层级错误 | 应挂载的物体挂在场景根节点 | L2 / L3 |
| 穿插重叠 | 包围盒相交且无合理接触语义 | L2 / L3 |
| 灯光缺失或过曝 | 场景无灯、灯数量不符、渲染整体溢出 | L3 |
记录字段建议至少包含这些,用 CSV 或 JSONL 都行,保证每一行能回溯到原始日志:
task_id, tier, object_count, material_count, light_count,
status, failure_type, first_error_step, elapsed_sec,
blend_path, log_path, render_path, note
其中 first_error_step 指 Agent 轨迹里第一次出现异常或开始偏离要求的那一步,取值可以是步骤编号或工具名。这一列是后面判断“卡在哪一步”的主要依据,比总的成败标记更有用。
在单物体任务里检查几何闭合与尺寸
L1 层的目的很窄:确认基础建模能力是否成立。检查项按下面顺序过一遍,任何一项不通过就记为 L1 失败,不要用后面的渲染效果掩盖前面的几何问题。
- 网格非空:
len(obj.data.polygons) > 0 - 无非流形边:逐条检查
edge.is_manifold - 法线方向一致:整体执行一次向外重算后,观察面的朝向是否变化
- 尺寸在容差内:
obj.dimensions三个分量与提示词给出的规格比对,容差建议先设 5%–10% - 变换已应用:
obj.scale三个分量接近 1,否则dimensions的语义容易混乱 - 原点位置合理:
obj.location与预期摆放点一致,避免“尺寸对但整体飘着”
在场景文件中读取这些字段,可以写一个独立脚本,用无界面模式执行:blender `--background` scene.blend `--python` inspect_object.py。
import bpy, bmesh
obj = bpy.data.objects['Cube']
print('dimensions', tuple(round(v, 4) for v in obj.dimensions))
print('scale', tuple(round(v, 4) for v in obj.scale))
print('location', tuple(round(v, 4) for v in obj.matrix_world.translation))
print('polygons', len(obj.data.polygons))
bm = bmesh.new()
bm.from_mesh(obj.data)
non_manifold = [e.index for e in bm.edges if not e.is_manifold]
print('non_manifold_edges', len(non_manifold))
print('volume', round(bm.calc_volume(signed=False), 6))
bm.free()
需要提醒的是,calc_volume 只有在网格闭合时才有物理意义,非流形边不为零时不要拿体积当判断依据。另外单体建模常见的隐性失败是“看起来像但内部有多层壳”,这类问题在非流形边和体积两项上通常会同时暴露,值得交叉看一眼。
在组合场景任务里检查相对位置与层级
L2 层考察的是场景组装:物体各自合格,摆放和挂载关系是否符合描述。matrix_world.translation 给出的是世界坐标,用世界坐标比较相对间距比用局部坐标更稳。层级关系则看 parent 字段,并结合任务清单里期望的挂载语义判断。
import bpy
def dump(o, depth=0):
p = o.parent.name if o.parent else None
loc = tuple(round(v, 3) for v in o.matrix_world.translation)
print(' ' * depth + o.name + ' parent=' + str(p) + ' loc=' + str(loc))
for c in o.children:
dump(c, depth + 1)
for o in bpy.context.scene.objects:
if o.parent is None:
dump(o)
一次典型输出示例如下,可以直接和任务清单对照:清单要求两个杯子放在桌面上,输出里 Cup_01、Cup_02 都带 parent=Table 且 z 值高于桌面平面,说明挂载与接触关系成立;如果两个杯子的 parent 显示为 None,即便坐标看起来合理,也应记为层级错误,因为后续整体移动桌面时杯子不会跟随。
Table parent=None loc=(0.0, 0.0, 0.75)
Cup_01 parent=Table loc=(0.12, -0.05, 0.79)
Cup_02 parent=Table loc=(0.28, -0.05, 0.79)
Lamp parent=None loc=(0.0, 0.6, 1.65)
穿插检查可以先做粗筛:对每两个物体计算世界空间包围盒是否相交,相交的再人工看渲染图或切面图确认是否合理接触。这个粗筛会误报(比如杯子放在桌面上包围盒必然相交),所以只作为线索,不作为最终判定。
按层写出能力边界结论
每层写一句话,句子里必须包含任务集、层阈值和证据来源,否则结论无法被引用。模板如下,把尖括号部分替换成实际内容即可;没有跑到的层就写“未覆盖”,不要用推断补上。
L1 单物体(阈值:物体≤1、材质≤1、无灯光):在 <任务集名称> 上,Agent 的失败集中在 <几何不闭合 / 尺寸越界 / 无输出>,证据为 inspect 脚本输出的 non_manifold_edges 与 dimensions 两列。
L2 组合场景(阈值:物体 2–8、材质 2–6、灯光 ≤2):失败类型从几何问题转向 <位置偏移 / 层级错误>,证据为层级打印结果与任务清单 parent 期望值的逐条比对记录。
L3 复杂场景(阈值:物体>8 或多灯光):主要失败为 <物体缺失 / 灯光缺失 / 无输出>,证据为失败记录表 failure_type 分布与渲染图抽查。
写结论时有两条边界要守住。第一,分层阈值是人为设定,同样一个 Agent 换一套阈值可能落到不同层,结论里必须带上阈值,否则不可比。第二,L3 通常任务数量少、单条耗时长,样本很小,结论宜写成“在该任务集上观察到”而不是普遍判断。如果确实需要更强结论,做法是补充同类任务数量再重跑,而不是把现有结果说得更满。