MM×CODE 多模态(看图)× Coding 评测案例集 v0.1
CASE C05

空间约束求解

视觉到结构化约束这一步的损耗

modesinglescorerexec_plus_judge自动化3/5趣味3/5
01

输入与证据锚点

输入 INPUT

冰箱内部 / 行李箱 / 书架实拍一张

证据锚点 EVIDENCE ANCHOR
Vision2Code空间场景是明确弱域
02

双轴设计

轴一 · 能力 AXIS Ⅰ
约束提取准确率
通常测「能不能做到」
judge越高越好 ▲
AXIS Ⅱ 轴二 · 节制
幻觉物品数
通常测「会不会编造、该拒识时拒识」
脚本越少越好 ▼
两轴独立计分、独立报告,合起来画 Pareto 前沿——只报轴一的横评没有信息量。
03

Prompt

PROMPT主 PROMPT根据这张照片:
根据这张照片:

1. 输出 constraints.json:可用空间的估计尺寸、画面中每个物品的估计尺寸与当前位置、
   以及硬约束(易碎、需冷藏、不可倒置、必须可随手取用)。
   只写照片里真实出现的物品。你不确定是什么的物品,标 unknown 并给出它占的空间。
2. 输出一个求解器脚本,给出重排方案,并渲染一张排布示意图。
3. 说明你的方案在哪些约束上做了妥协。
04

Rubric 评分表

维度满分由谁打判据
约束提取准确率35judge与人工标注的物品清单和硬约束对比,取 F1。
幻觉物品25脚本每个照片中不存在的物品 -8 分。unknown 标注不算幻觉,算诚实。
求解器可运行25脚本脚本运行成功、方案不违反它自己写的硬约束(用脚本回检)。
妥协说明15judge是否指出真实存在的取舍。回答"没有妥协"而实际有冲突记 0。

脚本= deterministic,脚本自动计算,不进 judge;judge= 主观维度,必须先看 deterministic 结果再打分。本案例总分归一化到 0-100。本案例恰好脚本与 judge 各占一半:幻觉物品+求解器可运行 共 50 分走脚本,约束提取准确率+妥协说明 共 50 分走 judge。

05

跑批命令

python run_batch.py --models anthropic/claude-opus-4.5 openai/gpt-5.2 google/gemini-3-pro moonshotai/kimi-k2.5 --only C05
python score.py --judge anthropic/claude-opus-4.5 --out report.csv

数据准备:按 data/C05/manifest.jsonl 的格式放置图片与真值。score 阶段的 judge 只处理本案例 rubric 中非 deterministic 的维度。

06

实测结果 · Live Run

REAL RUN · 3 MODELS · n=1
真实性声明 AUTHENTICITY以下是本次真实经 OpenRouter 跑批的结果(stealth/ox-alpha 于 2026 年 8 月 22 日完成调用与打分):stealth/ox-alpha、z-ai/glm-5v-turbo、moonshotai/kimi-k3 三个模型面对同一张输入图片、逐字相同的 Prompt 各自真实作答。输入图片、实际发送的 Prompt、各模型回复原文与脚本算出的分数,均逐字取自 results/C05.json、results/z-ai__glm-5v-turbo/C05.json、results/moonshotai__kimi-k3/C05.json 与 data/C05/,未做任何润色或改写。样本规模为演示级——每模型仅 1 条样本(1 图 1 轮问答)。另需如实说明两点:① ox-alpha 部分的分数以打分脚本修复后重算的 results/C05.json 为准(修复点:模型把多行代码写进 JSON 字符串字段时严格解析会失败,此前会被误判成「没作答」计分);② judge 主观维度本轮对三个模型均未参与打分,故都没有总分(见下方评分说明)。
模型图例 MODELS(配色沿用 compare.html 图例)GLM-X-Preview-F (stealth/ox-alpha)GLM-5V (glm-5v-turbo)Kimi K3
三模型分数对比 SCORES × 3(score 字段逐字取自各自 results json)
约束JSON可解析轴 0 – 1
1.0
1.0
1.0
幻觉物品数↓ 越低越好轴 0 – 1
0
0
0
求解器可运行轴 0 – 1
1.0
0.0
1.0
运行用量(usage 原值)● ox-alpha● glm-5v-turbo● kimi-k3
prompt tokens465369470
completion tokens21,4046,4626,795
其中 reasoning tokens02,3436,613
total tokens21,8696,8317,265
cost(USD · usage.cost 原值)00.02629080.09816825
finish_reasonstopstopstop
flags / score_error[] / null[] / null[] / null

读数要点:三个脚本维度里两家完全同分——「约束JSON可解析」(1.0 / 1.0 / 1.0)与「幻觉物品数」(0 / 0 / 0);唯一分差在「求解器可运行」:ox-alpha 与 Kimi K3 的求解器脚本在打分沙箱真实跑通且回检不违反自写硬约束(1.0),GLM-5V 记 0.0(它的 constraints.json 成功解析、也无幻觉物品,json 未记录脚本未跑通的具体原因,此处如实只报分数)。judge 两维(F1、妥协说明)对三家都未执行,仍无总分。表头圆点颜色即上方图例配色。

运行元数据 · stealth/ox-alpha RUN METADATA
模型
stealth/ox-alpha · 经 OpenRouter
运行时间
2026-08-22(当日完成调用与打分;results/C05.json 最终重算写入于 22:06)
mode / scorer
single / exec_plus_judge
样本规模
1 条样本(item_id: main)· 1 轮问答
输入图片
images/fridge.png × 1(480×420 px)
Tokens
prompt 465 · completion 21,404(reasoning 0)· total 21,869
finish_reason
stop(正常结束,无截断/无报错)
结论速览 AT A GLANCE

四个物品全部识别成功、无 unknown 项,提取出的像素尺寸与人工真值 4/4 一致;幻觉物品数 0;求解器脚本在本机沙箱真实运行成功,方案回检未违反它自己写下的硬约束。脚本可判定的两个 Rubric 维度均拿满(合计 50/50);judge 两维(约束提取准确率 F1、妥协说明)本轮未执行打分,故不产生总分。值得注意的是模型主动做了像素↔厘米标定(图中标注「宽 400px≈40cm」→ 10px=1cm),并在妥协说明里指出了「温度 ↔ 可达性」「防滚 vs 高频取用」等真实取舍。三模型横向看:「约束JSON可解析」(1.0 / 1.0 / 1.0)与「幻觉物品数」(0 / 0 / 0)完全同分,唯一分差在「求解器可运行」——GLM-5V 的脚本未能通过打分沙箱运行检查记 0,ox-alpha 与 Kimi K3 均真实跑通拿满;GLM-5V 也是三者中唯一像素尺寸读数出现偏差的一家(分数对比见上方面板,读数对照见下方真值表)。

C05 真实输入图片 fridge.png:冰箱内腔俯视示意图,内含牛奶盒、保鲜盒、鸡蛋、饮料瓶四个带文字标注的物品
真实输入图片:data/C05/images/fridge.png(480×420 px,合成示意图)。画面自带标定线索「宽 400px≈40cm」。本页直接以相对路径引用原始数据文件。
人工真值 TRUTH(data/C05/manifest.jsonl)
物品真值 w×h (px)模型提取 bbox_px一致
牛奶盒120 × 160120 × 160 @ (60, 60)✓
保鲜盒150 × 100150 × 100 @ (200, 80)✓
鸡蛋140 × 80140 × 80 @ (60, 240)✓
饮料瓶50 × 18050 × 180 @ (370, 50)✓

容器真值:冰箱;硬约束真值:鸡蛋易碎 · 牛奶需冷藏 · 饮料瓶不可倒置。「模型提取」列由页面脚本从模型输出的 constraints.json 原文中解析后填入(w×h 与原点坐标),比对结果为程序计算,非人工誊抄。

物品真值 (px)ox-alpha 读数glm-5v-turbo 读数kimi-k3 读数
牛奶盒120 × 160120×160 ✓120×160 ✓12×16 cm ✓
保鲜盒150 × 100150×100 ✓140×100 ✗ 宽−1015×10 cm ✓
鸡蛋140 × 80140×80 ✓120×80 ✗ 宽−2014×8 cm ✓
饮料瓶50 × 18050×180 ✓45×185 ✗ −5/+55×18 cm ✓

三模型尺寸读数对照(页面依据各自回复原文整理的读数比对,非 judge 打分——judge 的 F1 本轮对所有模型都未计算):ox-alpha 直接给出 bbox_px 且四件全中;Kimi K3 给的是 size_cm(其自述标定 10px=1cm,换算后四件全中);GLM-5V 自报的 position_px 有三件宽度偏小 5–20px。注意「幻觉物品数」考察的是编造照片中不存在的物品而非读数误差,故三家该项均为 0。

PROMPT实际发送的 Prompt根据这张照片:
根据这张照片:

1. 输出 constraints.json:可用空间的估计尺寸、画面中每个物品的估计尺寸与当前位置、
   以及硬约束(易碎、需冷藏、不可倒置、必须可随手取用)。
   只写照片里真实出现的物品。你不确定是什么的物品,标 unknown 并给出它占的空间。
2. 输出一个求解器脚本,给出重排方案,并渲染一张排布示意图。
3. 说明你的方案在哪些约束上做了妥协。

本次样本 vars = {}——没有发生模板替换,实际发送文本与第 03 节题目模板逐字一致。三份 results json(ox-alpha / glm-5v-turbo / kimi-k3)记录的实际发送 Prompt 逐字相同,vars 均为空对象——没有发生模板替换,实际发送文本与第 03 节题目模板一致,即三个模型收到的是同一道题。

RESPONSEstealth/ox-alpha 回复原文# 读图前提 …(完整原文约 8,859 字符 / 21,404 completion tokens,点击展开)
# 读图前提

- 标定:图中标注「宽 400px ≈ 40cm」→ **10px = 1cm**。
- 坐标系:以内腔**左上角为原点**,x 向右(宽),y 向下(假设下方是箱门/前缘)。
- 内腔边界估读为 px (40, 28) 起,400×370 → **40cm 宽 × 37cm 深**(深度无标定,误差 ±2cm)。
- 四个物品均有清晰文字标注(牛奶盒/保鲜盒/饮料瓶/鸡蛋),**全部识别成功,无 unknown 项**;`unknown_items` 留空数组。

---

## 1. constraints.json

```json
{
  "_comment": "坐标系:内腔左上角为原点,x向右(宽),y向下(深,假设朝门)。比例尺 10px=1cm。",
  "calibration": { "px_per_cm": 10, "source": "图中标注:宽400px≈40cm" },

  "interior": {
    "bbox_px": { "x": 40, "y": 28, "w": 400, "h": 370 },
    "size_cm": { "w": 40.0, "d": 37.0 },
    "note": "仅宽度有标定;深度按同比例量图,误差±2cm"
  },

  "items": [
    {
      "id": "milk",
      "label": "牛奶盒",
      "identified": true,
      "bbox_px": { "x": 60, "y": 60, "w": 120, "h": 160 },
      "size_cm": { "w": 12.0, "d": 16.0 },
      "current_pos_cm": { "x": 2.0, "y": 3.2 },
      "hard_constraints": ["需冷藏", "不可倒置"],
      "soft_preferences": ["靠后壁(温度更稳)"],
      "note": "纸盒饮品,须直立,防渗漏"
    },
    {
      "id": "fresh_box",
      "label": "保鲜盒",
      "identified": true,
      "bbox_px": { "x": 200, "y": 80, "w": 150, "h": 100 },
      "size_cm": { "w": 15.0, "d": 10.0 },
      "current_pos_cm": { "x": 16.0, "y": 5.2 },
      "hard_constraints": ["需冷藏", "必须可随手取用"],
      "soft_preferences": [],
      "note": "假定存放高频取用食材"
    },
    {
      "id": "bottle",
      "label": "饮料瓶",
      "identified": true,
      "bbox_px": { "x": 370, "y": 50, "w": 50, "h": 180 },
      "size_cm": { "w": 5.0, "d": 18.0 },
      "current_pos_cm": { "x": 33.0, "y": 2.2 },
      "hard_constraints": ["需冷藏", "不可倒置"],
      "soft_preferences": ["贴侧壁直立防滚动"],
      "note": "圆柱瓶,横放会滚,必须直立或固定"
    },
    {
      "id": "eggs",
      "label": "鸡蛋",
      "identified": true,
      "bbox_px": { "x": 60, "y": 240, "w": 140, "h": 80 },
      "size_cm": { "w": 14.0, "d": 8.0 },
      "current_pos_cm": { "x": 2.0, "y": 21.2 },
      "hard_constraints": ["易碎", "需冷藏"],
      "soft_preferences": ["远离前缘/门口", "贴角落防滑撞"],
      "note": "按整盒蛋托处理"
    }
  ],

  "unknown_items": [],

  "global_rules": [
    "任意两物品矩形不得重叠(俯视图无法表达堆叠,一律单层)",
    "任何物品不得置于鸡蛋所占区域的上方/邻位滑动路径上(以贴角近似)"
  ]
}
```

---

## 2. 求解器脚本 `fridge_rearrange.py`

思路:位置量化到 1cm 网格 → 硬约束作为**候选剪枝** → 「最受限物品优先」回溯搜索无重叠解 → 软偏好进**评分函数**(整体紧凑、鸡蛋贴角、保鲜盒居中靠前)→ 输出 `plan.json` + 渲染 `layout.png`。

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""冰箱重排求解器:读 constraints.json → 回溯搜索 → plan.json + layout.png"""
import json
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

STEP, TOL = 1.0, 0.5   # 搜索网格 / “贴墙”容差 (cm)

def frange(lo, hi, st):
    n = int(round((hi - lo) / st))
    return [round(lo + i * st, 3) for i in range(n + 1)]

def feasible(it, x, y, W, D):
    """硬约束 + 安全性规则的几何编码"""
    w, d = it["size_cm"]["w"], it["size_cm"]["d"]
    hc = it["hard_constraints"]
    if it["id"] == "bottle" and "不可倒置" in hc:
        if W - (x + w) > TOL:        # 必须贴右侧壁,防滚动
            return False
    if it["id"] == "eggs":           # 易碎:贴后壁 + 远离开门扰动区
        if y > TOL or (D - (y + d)) < 10:
            return False
    if it["id"] == "milk":           # 靠后壁,温度最稳
        if y > TOL:
            return False
    if "必须可随手取用" in hc:        # 距前缘 ≤ 6cm
        if (D - (y + d)) > 6:
            return False
    return True

def gen_candidates(items, W, D):
    cand = {}
    for it in items:
        w, d = it["size_cm"]["w"], it["size_cm"]["d"]
        cand[it["id"]] = [(x, y) for x in frange(0, W - w, STEP)
                                  for y in frange(0, D - d, STEP)
                                  if feasible(it, x, y, W, D)]
    return cand

def overlap(a, b):
    return not (a[0]+a[2] <= b[0]+1e-9 or b[0]+b[2] <= a[0]+1e-9 or
                a[1]+a[3] <= b[1]+1e-9 or b[1]+b[3] <= a[1]+1e-9)

def score(assign, W, D):
    rs = assign.values()
    x0 = min(r[0] for r in rs); x1 = max(r[0]+r[2] for r in rs)
    y0 = min(r[1] for r in rs); y1 = max(r[1]+r[3] for r in rs)
    s = -(x1 - x0) * (y1 - y0)                      # 包围盒最小化→都靠墙,中部留操作空间
    if "eggs" in assign:                            # 鸡蛋越贴左右角越好
        e = assign["eggs"]; s -= 2.0 * min(e[0], W - e[0] - e[2])
    if "fresh_box" in assign:                       # 保鲜盒尽量居中靠前
        f = assign["fresh_box"]; s -= 0.5 * abs(f[0] + f[2]/2 - W/2)
    return s

def solve(items, W, D):
    cand = gen_candidates(items, W, D)
    order = sorted(items, key=lambda it: len(cand[it["id"]]))  # 最受限优先
    best, best_s = None, float("-inf")
    def dfs(i, assign):
        nonlocal best, best_s
        if i == len(order):
            s = score(assign, W, D)
            if s > best_s:
                best_s, best = s, dict(assign)
            return
        it = order[i]; w, d = it["size_cm"]["w"], it["size_cm"]["d"]
        for (x, y) in cand[it["id"]]:
            r = (x, y, w, d)
            if any(overlap(r, p) for p in assign.values()):
                continue
            assign[it["id"]] = r
            dfs(i + 1, assign)
            del assign[it["id"]]
    dfs(0, {})
    return best, best_s

COLORS = {"milk": "#cfd8ef", "fresh_box": "#a8d8a8",
          "bottle": "#8fbce6", "eggs": "#f5c98a"}

def render(items, assign, W, D, path="layout.png"):
    fig, ax = plt.subplots(figsize=(8, 7.5))
    ax.add_patch(Rectangle((0, 0), W, D, fill=False, lw=3, ec="k"))
    for it in items:
        x, y, w, d = assign[it["id"]]
        ax.add_patch(Rectangle((x, y), w, d, fc=COLORS[it["id"]], ec="k"))
        ax.text(x+w/2, y+d/2, f'{it["label"]}\n{w:g}×{d:g}cm',
                ha="center", va="center", fontsize=9)
        ox, oy = it["current_pos_cm"]["x"], it["current_pos_cm"]["y"]
        if abs(ox-x) > .01 or abs(oy-y) > .01:      # 红箭头 = 移动路径
            ax.annotate("", xy=(x+w/2, y+d/2), xytext=(ox+w/2, oy+d/2),
                        arrowprops=dict(arrowstyle="->", color="red", lw=1.2))
    ax.set_xlim(-2, W+2); ax.set_ylim(D+2, -2)      # y 向下 = 朝门
    ax.set_aspect("equal")
    ax.set_title("冰箱单层重排方案(俯视:上=后壁,下=门)")
    ax.set_xlabel("x / cm"); ax.set_ylabel("y / cm")
    fig.savefig(path, dpi=150, bbox_inches="tight")
    print(f"[ok] 示意图 → {path}")

def main():
    data = json.load(open("constraints.json", encoding="utf-8"))
    W, D = data["interior"]["size_cm"]["w"], data["interior"]["size_cm"]["d"]
    items = data["items"]
    assign, s = solve(items, W, D)
    if assign is None:
        print("[fail] 无可行解:请核对尺寸/约束"); return
    plan = {"score": round(s, 1), "placements": []}
    for it in items:
        x, y, w, d = assign[it["id"]]
        ox, oy = it["current_pos_cm"]["x"], it["current_pos_cm"]["y"]
        plan["placements"].append({
            "id": it["id"], "label": it["label"],
            "old_pos_cm": [ox, oy], "new_pos_cm": [x, y],
            "moved_cm": round(((x-ox)**2 + (y-oy)**2) ** .5, 1),
            "hard_constraints_met": it["hard_constraints"]})
    json.dump(plan, open("plan.json", "w", encoding="utf-8"),
              ensure_ascii=False, indent=2)
    print(json.dumps(plan, ensure_ascii=False, indent=2))
    render(items, assign, W, D)

if __name__ == "__main__":
    main()
```

运行:`pip install matplotlib && python fridge_rearrange.py`(秒级出结果)。

### 本机推演的输出方案

| 物品 | 原位置 (cm) | 新位置 (cm) | 移动 | 满足的硬约束 |
|---|---|---|---|---|
| 牛奶盒 12×16 | (2.0, 3.2) | **(14, 0)** | ≈12.4 | 需冷藏✓ 不可倒置✓(直立、贴后壁) |
| 鸡蛋 14×8 | (2.0, 21.2) | **(26, 0)** | ≈32.0 | 易碎✓(后壁右上角、离前缘28cm) 需冷藏✓ |
| 饮料瓶 5×18 | (33.0, 2.2) | **(35, 8)** | ≈6.1 | 需冷藏✓ 不可倒置✓(紧贴右壁防滚) |
| 保鲜盒 15×10 | (16.0, 5.2) | **(14, 21)** | ≈15.9 | 需冷藏✓ 随手取用✓(距前缘6cm、居中) |

示意图(`layout.png` 由脚本精确渲染,此为示意):

```
y(cm) 0 ┌─────────────────────────────────────────┐ 后壁
        │                  ┌───────┬─────────────┐│
        │                  │牛奶盒 │ 鸡蛋(易碎)   ││
      8 │                  │12×16  ├─────────────┤│
        │                  │       │ 饮料瓶 5×18  ││
        │                  │       │ (贴右壁)     ││
     16 │                  └───────┤             ││
        │    ┌────────┐           │             ││
     21 │    │ 保鲜盒  │           │             ││
        │    │ 15×10  │           │             ││
        │    │(随手取) │           │             ││
     31 │    └────────┘           └─────────────┘│
     37 └─────────────────────────────────────────┘ 前缘/门
        0        14        26      35          40  x(cm)
```

---

## 3. 方案的妥协说明

硬约束(易碎/需冷藏/不可倒置/随手取用)**全部满足**,妥协都在软层面和模型层面:

1. **二维单层模型**:真实冰箱有多层搁架和门架。鸡蛋的理想位置其实是门内侧蛋格或保留原包装;这里只能在单一平面里选最稳的角落。“不能压到鸡蛋”也只能用“不重叠”近似,无法表达垂直方向的堆叠禁令。
2. **温度 ↔ 可达性的取舍**:保鲜盒为满足“随手取用”被放到靠前区域——那也是温度最不稳、最暖的区域,牺牲了后壁低温区的保鲜效果(估计差 1–2℃)。
3. **“随手取用”只判给了保鲜盒**:如果饮料瓶也算高频取用,会和“贴壁防滚”直接冲突;我选择了防滚优先,因为瓶子倾倒漏液的后果比多伸手 10cm 更重。
4. **防滚/防撞靠几何而非物理固定**:瓶子只是“贴紧右壁”,没有隔板;鸡蛋与牛奶盒在后壁相邻,若有物品滑动仍可能磕碰。建议实际操作时加一根防滚杆,或让鸡蛋留在原蛋托中(方案已按整托 14×8cm 处理)。
5. **测量误差**:像素估读 ±5px(±0.5cm),深度方向无标定、误差可达 ±2cm。若实物放不进求解出的格子,改 `constraints.json` 里的尺寸重跑即可。
6. **方向假设**:假设图片下方是箱门。若实际门在别侧,把 `feasible()` 里“前缘/后壁”的条件翻转再跑一次即可。
以上为模型回复的逐字原文(Markdown 源码按原样展示,含 JSON / Python 代码块,未渲染、未截断、未改写;共 8,859 字符)。usage:completion_tokens 21,404 · finish_reason stop。数据来源:results/C05.json → run.items[0].turns[0].response.text。
RESPONSEz-ai/glm-5v-turbo 回复原文**1. constraints.json** …(完整原文 11,626 字符 / 6,462 completion tokens,其中 reasoning_tokens 2,343,点击展开)
**1. constraints.json**

```json
{
  "container": {
    "id": "fridge_shelf",
    "name": "冰箱内部(单层)",
    "width_cm": 40.0,
    "height_cm": 36.0,
    "depth_cm": 35.0,
    "coordinate_system": "image_pixels_top_left_origin",
    "scale": "1px = 0.1cm (1mm)"
  },
  "items": [
    {
      "id": "milk_carton",
      "name_zh": "牛奶盒",
      "category": "dairy_liquid",
      "dimensions_cm": {"w": 12.0, "h": 16.0, "d": 8.0},
      "position_px": {"x": 42, "y": 62, "w": 120, "h": 160},
      "hard_constraints": {
        "refrigerated": true,
        "fragile": false,
        "no_tilt": true,
        "upright_required": true,
        "accessibility_priority": "high"
      }
    },
    {
      "id": "fresh_box",
      "name_zh": "保鲜盒",
      "category": "food_container",
      "dimensions_cm": {"w": 14.0, "h": 10.0, "d": 10.0},
      "position_px": {"x": 195, "y": 82, "w": 140, "h": 100},
      "hard_constraints": {
        "refrigerated": true,
        "fragile": false,
        "no_tilt": false,
        "rotatable": true,
        "accessibility_priority": "low"
      }
    },
    {
      "id": "drink_bottle",
      "name_zh": "饮料瓶",
      "category": "beverage_glass",
      "dimensions_cm": {"w": 4.5, "h": 18.5, "d": 4.5},
      "position_px": {"x": 320, "y": 52, "w": 45, "h": 185},
      "hard_constraints": {
        "refrigerated": true,
        "fragile": true,
        "no_tilt": true,
        "upright_required": true,
        "accessibility_priority": "high",
        "edge_clearance": "preferred"
      }
    },
    {
      "id": "eggs",
      "name_zh": "鸡蛋",
      "category": "fragile_protein",
      "dimensions_cm": {"w": 12.0, "h": 8.0, "d": 6.0},
      "position_px": {"x": 58, "y": 242, "w": 120, "h": 80},
      "hard_constraints": {
        "refrigerated": true,
        "fragile": true,
        "no_tilt": false,
        "shock_sensitive": true,
        "accessibility_priority": "medium",
        "stable_base_required": true
      }
    }
  ],
  "metadata": {
    "estimation_method": "visual_pixel_count_with_scale_reference",
    "uncertainty": "±5% on linear dimensions",
    "assumptions": ["bottle_is_glass", "eggs_in_carton", "single_layer_view"]
  }
}
```

---

**2. 求解器脚本 (Python + Matplotlib)**

该脚本使用**带权贪心算法**(Weighted Greedy Placement)结合**碰撞检测**,优先满足硬约束(易碎品稳定性、液体直立),其次优化取用便利性。

```python
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from dataclasses import dataclass
from typing import List, Tuple, Optional
import numpy as np

# ==================== 数据定义 ====================
@dataclass
class Item:
    id: str
    name: str
    w: float  # cm
    h: float  # cm
    fragile: bool
    no_tilt: bool  # 必须保持当前朝向(不可旋转)
    accessibility: int  # 1-5, 5最高优先级
    color: str
    
    def __post_init__(self):
        self.x: Optional[float] = None
        self.y: Optional[float] = None
        self.placed = False

# 初始化物品(基于constraints.json的估计)
items = [
    Item("milk", "牛奶盒", 12.0, 16.0, False, True, 5, "#FFFFFF"),
    Item("fresh", "保鲜盒", 14.0, 10.0, False, False, 2, "#C1E1C1"),
    Item("bottle", "饮料瓶", 4.5, 18.5, True, True, 4, "#AEC6CF"),
    Item("eggs", "鸡蛋", 12.0, 8.0, True, False, 3, "#FFE4B5")
]

CONTAINER_W = 40.0  # cm
CONTAINER_H = 36.0  # cm
PADDING = 0.5       # cm, 物品间距

# ==================== 求解器核心 ====================
class FridgeSolver:
    def __init__(self, width: float, height: float):
        self.W = width
        self.H = height
        self.grid_resolution = 0.5  # 放置精度0.5cm
        
    def check_collision(self, item: Item, x: float, y: float, placed_items: List[Item]) -> bool:
        """检查是否越界或与其他物品重叠"""
        if x < 0 or y < 0 or x + item.w > self.W or y + item.h > self.H:
            return True
        for other in placed_items:
            if other.placed:
                if not (x + item.w + PADDING <= other.x or 
                        x >= other.x + other.w + PADDING or 
                        y + item.h + PADDING <= other.y or 
                        y >= other.y + other.h + PADDING):
                    return True
        return False
    
    def score_position(self, item: Item, x: float, y: float) -> float:
        """
        评分函数:越高越好
        - 易碎品:偏好低重心(y值小,即靠近'地面'/前部)和角落
        - 高取用优先级:偏好靠近开口处(假设y轴正向为前/易取方向)
        - 稳定性:对于no_tilt物品,避免悬空边缘
        """
        score = 0.0
        
        # 1. 易碎品稳定性奖励(左下角最稳)
        if item.fragile:
            corner_dist = np.sqrt(x**2 + y**2)
            score += max(0, 20 - corner_dist)
            
        # 2. 取用便利性(假设y越大越靠近冰箱门/易取)
        access_bonus = item.accessibility * (y / self.H) * 2
        score += access_bonus
        
        # 3. 边缘惩罚(对易碎且高的物品,如瓶子)
        if item.fragile and item.h > 15:
            edge_margin = min(x, y, self.W - (x+item.w), self.H - (y+item.h))
            if edge_margin < 2:
                score -= 10  # 太靠近边缘扣分
                
        return score
    
    def solve(self, items: List[Item]) -> List[Item]:
        """
        排序策略:
        1. 先放体积大且约束多的(鸡蛋-易碎+需稳定,牛奶-高+常用)
        2. 再放灵活的(保鲜盒可旋转)
        3. 最后放细长的(瓶子)
        """
        # 排序:易碎优先 > 高取用优先 > 大体积 > 可旋转最后
        sorted_items = sorted(items, key=lambda i: (
            not i.fragile,      # 易碎的先放(True=1排后面,所以not让它排前面)
            -i.accessibility,   # 高优先级的先放
            -(i.w * i.h),       # 大的先放
            i.rotatable if hasattr(i, 'rotatable') else False  # 不可旋转的先放
        ))
        
        placed = []
        
        for item in sorted_items:
            best_score = -float('inf')
            best_pos = (None, None)
            
            # 在网格上搜索最佳位置
            for x in np.arange(0, self.W - item.w + 0.1, self.grid_resolution):
                for y in np.arange(0, self.H - item.h + 0.1, self.grid_resolution):
                    if not self.check_collision(item, x, y, placed):
                        s = self.score_position(item, x, y)
                        if s > best_score:
                            best_score = s
                            best_pos = (x, y)
                            
            if best_pos[0] is not None:
                item.x, item.y = best_pos
                item.placed = True
                placed.append(item)
            else:
                print(f"警告:无法放置 {item.name}")
                
        return placed

# ==================== 执行求解与可视化 ====================
def visualize_layout(original_items: List[Item], optimized_items: List[Item], save_path: str = "fridge_layout.png"):
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 7))
    
    def draw_scene(ax, title, item_list, show_coords=True):
        ax.set_xlim(-1, CONTAINER_W + 1)
        ax.set_ylim(-1, CONTAINER_H + 1)
        ax.set_aspect('equal')
        ax.set_title(title, fontsize=14, fontweight='bold')
        
        # 画冰箱内框
        rect = patches.Rectangle((0, 0), CONTAINER_W, CONTAINER_H, 
                                 linewidth=3, edgecolor='#333333', facecolor='#E8F4F8')
        ax.add_patch(rect)
        
        # 画物品
        for item in item_list:
            if item.placed or hasattr(item, 'x') and item.x is not None:
                x, y, w, h = item.x, item.y, item.w, item.h
                # 边框样式:易碎品用红色虚线边框提示
                linestyle = '--' if item.fragile else '-'
                linewidth = 2 if item.fragile else 1
                
                patch = patches.Rectangle((x, y), w, h, 
                                         linewidth=linewidth, 
                                         edgecolor='red' if item.fragile else 'black',
                                         facecolor=item.color,
                                         linestyle=linestyle,
                                         alpha=0.8)
                ax.add_patch(patch)
                
                # 标注文字
                cx, cy = x + w/2, y + h/2
                ax.text(cx, cy, item.name, ha='center', va='center', 
                       fontsize=9, fontweight='bold',
                       color='black' if item.color in ['#FFFFFF', '#FFE4B5', '#C1E1C1'] else 'white')
                
                if show_coords:
                    ax.text(x, y+h+0.5, f"({x:.1f},{y:.1f})", fontsize=7, color='gray')
                    
                    # 标注特殊约束
                    if item.no_tilt:
                        ax.text(x+w/2, y-0.8, "↑直立", ha='center', fontsize=7, color='red')
                    if item.fragile:
                        ax.plot(x+1, y+1, 'x', color='red', markersize=6)
        
        ax.set_xlabel('宽度 (cm)')
        ax.set_ylabel('深度/高度 (cm)')
        ax.grid(True, alpha=0.3, linestyle=':')
    
    # 准备原始数据(转换为cm坐标,假设原图左上角为0,0,但物理上我们转为左下角为0,0以便理解)
    # 注意:原图像坐标y向下为正,这里我们统一为数学坐标(y向上为正,左下角原点)
    # 为简化对比,两个图都使用相同的坐标系(左下角原点)
    orig_copy = []
    for it in original_items:
        # 将原图像素位置近似转换为cm(基于目测)
        if it.id == "milk":
            it.x, it.y = 4.0, 16.0  # 左上区域
        elif it.id == "fresh":
            it.x, it.y = 19.0, 18.0  # 中上
        elif it.id == "bottle":
            it.x, it.y = 33.0, 15.0  # 右侧
        elif it.id == "eggs":
            it.x, it.y = 5.0, 4.0    # 左下
        it.placed = True
        orig_copy.append(it)
    
    draw_scene(ax1, "原始布局 (Original Layout)", orig_copy)
    
    # 求解优化
    solver = FridgeSolver(CONTAINER_W, CONTAINER_H)
    optimized = solver.solve(items)
    draw_scene(ax2, "优化布局 (Optimized Layout)", optimized)
    
    plt.tight_layout()
    plt.savefig(save_path, dpi=150, bbox_inches='tight')
    plt.show()
    
    # 输出坐标表
    print("
=== 重排方案坐标表 (单位: cm, 原点: 左下角) ===")
    print(f"{'物品':<8} {'X':<6} {'Y':<6} {'宽':<6} {'高':<6} {'备注'}")
    print("-" * 50)
    for it in optimized:
        note = []
        if it.fragile: note.append("易碎")
        if it.no_tilt: note.append("直立")
        if it.accessibility >= 4: note.append("常用")
        print(f"{it.name:<8} {it.x:<6.1f} {it.y:<6.1f} {it.w:<6.1f} {it.h:<6.1f} {', '.join(note)}")

if __name__ == "__main__":
    # 给保鲜盒添加旋转标记(虽然本例中未使用旋转,但标记其灵活性)
    items[1].rotatable = True
    visualize_layout(items.copy(), items)
```

**运行结果示意(文本描述):**

| 物品 | 原始位置 (x,y) | 优化位置 (x,y) | 变化说明 |
|------|---------------|---------------|---------|
| **鸡蛋** | (5.0, 4.0) | **(0.0, 0.0)** | 移至左下角,利用角落提供最大稳定性,远离开门震动区 |
| **牛奶盒** | (4.0, 16.0) | **(0.0, 9.0)** | 移至左侧中部,保持直立且易于伸手拿取(左手习惯) |
| **饮料瓶** | (33.0, 15.0) | **(35.0, 0.0)** | 移至右下角,降低重心防倾倒,且避开中央通道 |
| **保鲜盒** | (19.0, 18.0) | **(13.5, 0.0)** | 填充剩余空间,因无需频繁取用置于后排 |

*注:坐标原点为左下角,Y轴正向为"靠近冰箱门/易取方向"。*

---

**3. 方案妥协说明 (Trade-off Analysis)**

本方案在以下约束上做出了**有意妥协**:

### A. **空间利用率 vs. 取用便利性 (Space vs. Accessibility)**
- **妥协点**:将**保鲜盒**推至后排(Y值较小区域),而将**牛奶**和**饮料瓶**(高优先级物品)置于前排。
- **原因**:保鲜盒通常存放剩菜或备菜,取用频率低于饮品。牺牲其便利性以换取高频物品的"零障碍取用"。
- **风险**:若需取用保鲜盒,必须先移动或探过前排物品。

### B. **易碎品隔离 vs. 重心分布 (Fragility vs. Weight Distribution)**
- **妥协点**:**饮料瓶(玻璃/易碎)**被放置在**右下角边缘**而非绝对中心。
- **原因**:若将瓶子置于中心,会阻断左右两侧的物品存取路径形成"孤岛";置于边缘虽增加碰撞风险,但通过将其紧贴右侧壁(X=35cm)并降低Y坐标(贴近"地面")来补偿稳定性。
- **未满足的完美约束**:理想情况下玻璃瓶应有2cm以上的四周缓冲区,但在40cm宽度的紧凑空间内无法完全满足。

### C. **鸡蛋的方向性约束 (Egg Orientation)**
- **妥协点**:鸡蛋盒被旋转了90°(假设原盒为横向放置,现改为纵向或保持原状但位置改变)。
- **原因**:标准鸡蛋盒要求**大头朝上**以保持蛋黄居中。本方案仅保证其**平放且四角支撑**,但未强制规定哪边是"大头"(因图像无法判断鸡蛋盒方向)。**实际操作时需人工确认方向**。

### D. **"不可倒置"的严格几何解释**
- **妥协点**:**牛奶盒**和**饮料瓶**均保持了**竖直方向(H维度垂直于地面)**,但允许其在X-Y平面内平移。
- **潜在风险**:如果冰箱在搬运过程中发生倾斜(非水平),当前布局中**饮料瓶**因较高(18.5cm)且位于边缘,力臂较长,倾倒风险略高于中心位置。这是为了换取空间效率的必要让步。

### E. **尺寸估计误差容错**
- **隐含妥协**:所有尺寸基于像素估算(±5%误差)。若实际**保鲜盒**比估计的14cm更宽(如15.5cm),则优化后的紧密排列会出现**0.5cm的重叠**。脚本中预留了`PADDING=0.5cm`的安全边距来吸收此误差,但这降低了理论最大空间利用率约3%。

### 总结建议
此方案最适合**日常静态存储**场景。若冰箱经常处于**移动/车载环境**,建议进一步妥协**取用便利性**,将**饮料瓶**和**鸡蛋**都用硅胶绑带固定或改用吸盘底座,并将所有物品质心向几何中心收敛。
以上为该模型回复的逐字原文(Markdown 源码按原样展示,含 JSON / Python 代码块,未渲染、未截断、未改写;共 11,626 字符)。usage:completion_tokens 6,462(reasoning_tokens 2,343)· finish_reason stop。数据来源:results/z-ai__glm-5v-turbo/C05.json → run.items[0].turns[0].response.text。
RESPONSEmoonshotai/kimi-k3 回复原文## 1) constraints.json …(完整原文 5,714 字符 / 6,795 completion tokens,其中 reasoning_tokens 6,613,点击展开)
## 1) constraints.json

比例按图中标题 `400px ≈ 40cm` → `10 px = 1 cm`。坐标原点=可用空间左上角,x 向右,y 向下(y 大=靠冰箱门/前方,更“随手可取”)。尺寸/位置为像素读数估计,误差约 ±1–2 cm。

```json
{
  "scale": { "px_per_cm": 10, "note": "取自标题:冰箱内部宽 400px≈40cm" },
  "available_space": {
    "width_cm": 40,
    "height_cm": 36,
    "estimation_error_cm": 2,
    "coordinate_frame": "origin=usable-space top-left; x→right, y→toward door/front"
  },
  "items": [
    {
      "id": "milk", "label": "牛奶盒", "unknown": false,
      "size_cm": { "w": 12, "h": 16 },
      "current_pos_cm": { "x": 1.5, "y": 2.5 },
      "constraints": { "fragile": false, "needs_refrigeration": true, "no_invert": true, "must_be_accessible": true },
      "basis": "乳制品需冷藏;带盖/利乐包不建议倒置(no_invert 按保守处理)"
    },
    {
      "id": "crisper", "label": "保鲜盒", "unknown": false,
      "size_cm": { "w": 15, "h": 10 },
      "current_pos_cm": { "x": 15.5, "y": 4.5 },
      "constraints": { "fragile": false, "needs_refrigeration": true, "no_invert": false, "must_be_accessible": false },
      "basis": "默认内有食物需冷藏;密封盒可横放,取用频率按最低处理"
    },
    {
      "id": "bottle", "label": "饮料瓶", "unknown": false,
      "size_cm": { "w": 5, "h": 18 },
      "current_pos_cm": { "x": 32.5, "y": 1.5 },
      "constraints": { "fragile": false, "needs_refrigeration": true, "no_invert": true, "must_be_accessible": true },
      "basis": "瓶口向上防漏/碳酸;日常常取"
    },
    {
      "id": "eggs", "label": "鸡蛋", "unknown": false,
      "size_cm": { "w": 14, "h": 8 },
      "current_pos_cm": { "x": 1.5, "y": 20.5 },
      "constraints": { "fragile": true, "needs_refrigeration": true, "no_invert": true, "must_be_accessible": true },
      "basis": "易碎;保持平放不倒置;日常常取"
    }
  ],
  "hard_constraint_definitions": {
    "needs_refrigeration": "重排后仍必须位于冰箱内部空间内(本问题为 2D 层内重排,自动满足)",
    "no_invert": "求解器禁止 90° 旋转/翻转,保持图示朝向",
    "fragile": "要求与其它物品间留出安全间隙 gap,且不被压在其它物品下方(2D 中体现为间隙约束)",
    "must_be_accessible": "尽量贴近 y=height(前缘/门口)放置,且不被其它物品遮挡;无法全部满足时按优先级取舍"
  },
  "solver_settings": { "gap_cm": 1.0, "allow_rotation": false }
}
```

## 2) 求解器脚本(纯 Python + matplotlib),与重排方案

```python
# solver.py
import json, math
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

C = json.load(open("constraints.json"))
W   = C["available_space"]["width_cm"]
H   = C["available_space"]["height_cm"]
GAP = C["solver_settings"]["gap_cm"]
items = C["items"]

placed = []
def overlap(x, y, w, h):
    for p in placed:
        if not (x + w + GAP <= p["x"] or p["x"] + p["w"] + GAP <= x or
                y + h + GAP <= p["y"] or p["y"] + p["h"] + GAP <= y):
            return True
    return False

def try_place(it, x, y):
    w, h = it["size_cm"]["w"], it["size_cm"]["h"]
    if x < 0 or y < 0 or x + w > W or y + h > H: return False
    if overlap(x, y, w, h): return False
    placed.append({"id": it["id"], "label": it["label"], "x": x, "y": y, "w": w, "h": h})
    return True

# 阶段1:必须随手可取的物品 → 底排(贴近前缘 y=H),按高度降序从左排
front = sorted([i for i in items if i["constraints"]["must_be_accessible"]],
               key=lambda i: -i["size_cm"]["h"])
cx = 0.0
for it in front:
    w, h = it["size_cm"]["w"], it["size_cm"]["h"]
    assert try_place(it, cx, H - h), f"front row overflow: {it['id']}"
    cx += w + GAP

# 阶段2:非随手物品 → 从后/顶部 (y 小) 扫描可放位置
back = [i for i in items if not i["constraints"]["must_be_accessible"]]
step = 0.5
for it in back:
    w, h = it["size_cm"]["w"], it["size_cm"]["h"]
    done = False
    y = 0.0
    while y + h <= H and not done:
        x = 0.0
        while x + w <= W and not done:
            done = try_place(it, round(x,2), round(y,2))
            x += step
        y += step
    assert done, f"cannot place {it['id']}"

# 渲染
fig, ax = plt.subplots(figsize=(6, 6))
ax.add_patch(Rectangle((0, 0), W, H, fill=False, lw=2))
colors = {"milk":"#e8def5","crisper":"#cbe7c9","bottle":"#bcd6ee","eggs":"#fbeeC7"}
for p in placed:
    ax.add_patch(Rectangle((p["x"], p["y"]), p["w"], p["h"],
                           facecolor=colors.get(p["id"], "#ccc"),
                           edgecolor="#444", lw=1.2))
    ax.text(p["x"]+0.6, p["y"]+1.6, p["label"], fontsize=10)
ax.text(W/2, H+1.2, "前缘 / 冰箱门(随手可取区)", ha="center", fontsize=9)
ax.set_xlim(-2, W+2); ax.set_ylim(H+3, -3); ax.set_aspect("equal")
ax.set_title(f"重排方案  {W}x{H}cm  gap={GAP}cm")
plt.savefig("layout.png", dpi=160, bbox_inches="tight")
print(json.dumps({"placed": placed, "canvas": {"W": W, "H": H}}, ensure_ascii=False, indent=2))
```

跑出的方案(gap=1cm,坐标为左上角 cm)与一个 ASCII 预览:

```text
y=0(后/里) ┌──────────────┬────────────────────────────┐
           │ 保鲜盒 15x10 │                            │   ← 非随手,放最里
y=10       ├────┬─────────┼──────────┬─────────────────┤
           │饮  │ 牛奶盒   │  鸡蛋     │  (空隙)          │   ← 前缘随手排
           │料  │ 12x16    │  14x8    │                  │
           │瓶  │          │          │                  │
y=36(前)───┴5x18┴──────────┴──────────┴──────────────────┘
           x=0  x=6        x=19       x=33
 placed:
  crisper x=0.0 y=0.0   (最里,妥协可达性)
  bottle  x=0.0 y=18.0  (前缘左)
  milk    x=6.0 y=20.0  (前缘)
  eggs    x=19.0 y=28.0 (前缘)
```

## 3) 在哪些约束上做了妥协

1. **“必须可随手取用”无法对所有物品同时成立。** 在 2D 层内若把 4 件全部放同一前缘排,总宽 = 12+15+5+14 = 46 cm(加间隙 >48)超过 40 cm,必然不可行。因此把**取用频率判定最低的“保鲜盒”(must_be_accessible=false)**牺牲到最里/后排;其余三件常用物品(饮料、牛奶、鸡蛋)全部贴前缘,且互相不遮挡。这是主要妥协。
2. **尺寸是像素估计(±1–2 cm)。** 用 `gap=1 cm` 安全间隙吸收读数误差;代价是牺牲少量空间利用率,换取“易碎鸡蛋”等不被挤压、以及估计误差的鲁棒性。
3. **“不可倒置 / 不旋转”被强制定向。** 求解器禁用 90° 旋转(饮料瓶、牛奶盒、鸡蛋保持原朝向),牺牲了一部分可能的紧密装箱可能性,以满足 no_invert,并避免标签/瓶口朝下。
4. **“需冷藏”实际上不约束本重排。** 因为所有物品本来就在同一冰箱层内,2D 重排不会离开冷藏环境;脚本把它作为语义记录而非几何约束。
5. **可达性判定带主观假设。** “保鲜盒”被标为随手需求最低;如果你实际把它当每日常用,只需把它 `must_be_accessible` 改成 true,但前排就会放不下——脚本会按同样规则转而把鸡蛋/牛奶被挤到里排,需要在回答层重新声明取舍。
以上为该模型回复的逐字原文(Markdown 源码按原样展示,含 JSON / Python 代码块,未渲染、未截断、未改写;共 5,714 字符)。usage:completion_tokens 6,795(reasoning_tokens 6,613)· finish_reason stop。数据来源:results/moonshotai__kimi-k3/C05.json → run.items[0].turns[0].response.text。

以下为 stealth/ox-alpha 一家的 score 字段逐字段明细(GLM-5V 与 Kimi K3 的对应数值已并入上方「三模型分数对比」;两份 json 的 score 字段结构相同,同样只有这三个脚本维度,judge 维度同样缺席,flags 均为 []、score_error 均为 null):

打分项(score 字段原文 · stealth/ox-alpha)结果对应 Rubric 维度说明
约束JSON可解析1.0 ✓求解链路前置检查模型输出的 constraints.json 在打分脚本中成功解析。(此项正是本轮重算所修复的检查点:修复前多行代码塞进字符串字段会导致严格解析失败、被误记为未作答。)
幻觉物品数0脚本 幻觉物品(25 分)照片中不存在而模型写出的物品数为 0 → 按 rubric 规则「每个幻觉物品 −8 分」,本维不扣分,得 25/25。unknown 标注不算幻觉(本例 unknown_items 为空数组)。
求解器可运行1.0 ✓脚本 求解器可运行(25 分)脚本在本机沙箱真实运行成功、方案回检不违反自写硬约束,得 25/25。口径说明(环境限制):沙箱对模型生成代码用启发式多签名尝试调用,属近似判定,非严格基准口径。
flags[]—无异常标记。
score_errornull—打分过程本身无异常(score_error: null)。
约束提取准确率未评分judge (35 分)本轮真实跑批未包含 judge 打分环节,F1 未计算,如实留空。
妥协说明未评分judge (15 分)同上,本轮未执行 judge 打分。(模型原文第 3 节确实给出了 6 条妥协分析,供人工查阅。)

分数数值逐字取自 results/C05.json 的 score 字段:{"约束JSON可解析": 1.0, "幻觉物品数": 0, "求解器可运行": 1.0, "flags": []}。本轮可由脚本判定的部分合计 50/50;judge 部分 50 分未执行,因此本次运行没有归一化总分,请勿将 50/50 解读为百分制满分。

局限声明 LIMITATIONS(limitations 字段原文 · 三份 results json 此字段逐字相同)
  • 模型生成代码的函数签名不受约束,沙箱用启发式多签名尝试调用,非严格基准
  • 样本规模为演示级缩小版(1),非原始题库规模
跨模型解读注意 CROSS-MODEL CAVEAT(化用自 compare.html 方法论注记)
  • 三个模型的调用方式相同(同一套 run_batch.py 风格的 OpenRouter chat/completions 调用),但 stealth/ox-alpha 是强制深度推理的 stealth 模型,glm-5v-turbo 与 kimi-k3 的推理行为未特别配置——输出风格与推理深度可能天然不同,这是解读对比结果时要注意的局限,不是哪一方“作弊”。
  • 本轮三个模型均只有脚本维度分(各 3 项),judge 两维对所有模型都未执行,因此不存在可比的总分。