Index
2026-08-10 — Plan

Finding → VLM → Subgoal 파이프라인 계획

scene_bench 첫 구현 | subgoal 스키마 · 렌더러 · step-wise 루프 · 프롬프트 v1 · 채점
← Task 분류 (Finding / Resuming / Restoring / Routine)

TL;DR

0 확정된 것

출력 방식 = step-wise 1개씩. 매 턴 [현재상태 + 메모리 + 이미 실행한 trace]를 주고 다음 subgoal 1개만 받는다. 문제당 API 콜 4~6회.
메모리 입력 = 세 조건 전부. blind 메모리 없음(하한) / frames 프레임 샘플(실제) / textlog GT 이벤트 로그(oracle 상한).
대상 task = A1 본 이동 (Finding 중 가장 기본). 골든 20개.

1 근거 — MolmoSpaces 실측

~/repos/scenemem/molmospaces 코드에서 직접 추출. 층이 두 개고 서로 다르다는 게 핵심이다.

# (a) 정책(VLA)에 실제로 들어가는 프롬프트 # policy/learned_policy/utils.py:16-30 PromptSampler.DEFAULT_TEMPLATES_BY_TASK "pick" : ["pick up the {}."] "open" : ["open the {}."] "close" : ["close the {}."] "pick_and_place" : ["pick up the {} and place it on the {}."] "packing" : ["pack container."] # (b) get_task_description() — 사람이 읽는 설명 (평가·로깅용). (a)와 다르다! Pick : "Pick up the {}" Pick-and-place : "Pick up the {} and place it in or on the {}" ← (a)는 "on"만 Open : "Open the {}" Navigate : "Navigate to the {}" / "Navigate to any {} ({N} available)" Door : "Push the door open" / "Pull the door open"
발견스키마에 미치는 영향
정책 프롬프트는 5종뿐. next_to·color·navigate·door는 템플릿이 없다subgoal 문법을 이 5개로 제한. 이전 계획의 "7/8 매핑"은 틀렸고 실제로는 5개다
pick_and_place가 한 덩어리 — "pick up the X and place it on the Y."PICK과 PLACE를 따로 내면 정책이 OOD 문구를 먹는다 → place subgoal이 pick을 포함
navigate에 정책 프롬프트가 없다오히려 N0(위치 리스트에서 고르기)를 정당화한다. 학습된 nav 정책이 필요 없으니 없어도 무방
오버라이드 가능한 건 pick/pick_and_place뿐 (utils.py:47)open/close는 문구를 바꿀 수 없다. 그대로 쓴다
객체 이름 = ObjectMeta.short_descriptions(uid)[n-1]우리도 같은 함수를 호출한다. 직접 문자열을 만들면 문구가 어긋난다
⚠️ opening_tasks.py:41-43이 task_type 체크 없이 항상 "Open the {}" 반환close task 설명이 "Open the ..."로 나오는 버그. (a) 쪽을 쓰면 회피

2 핵심 단순화 — 과거는 조건별, 현재는 공통

step-wise + free-run을 하려면 매 턴 "현재 상태"가 갱신돼야 한다. 그런데 물리 롤아웃을 돌리면 첫 파일럿부터 시뮬레이터·정책이 필요해지고, 프레임을 미리 렌더해뒀다면 모델이 다른 경로로 갈 때 맞는 프레임이 없다.

해법: 시간축을 둘로 쪼갠다.
과거(메모리) = 조건별로 다르게 준다 ← 여기가 실험 변수 blind : 아무것도 안 줌 frames : 심기+개입 구간 샘플 프레임 N장 textlog : GT 이벤트 로그 텍스트 현재(관측) = 세 조건 모두 동일한 심볼릭 텍스트 ← 여기는 통제 변수 "당신은 Kitchen에 있다. 손은 비어 있다. 보이는 것: CounterTop_01 위의 mug_A, Sink_01, ... Drawer_02·04는 닫혀 있다."
얻는 것설명
물리 없이 free-run심볼릭 상태 전이기(open→열림, pick→손에 듦)만 있으면 된다. 50줄 남짓
세 조건이 깨끗하게 비교됨현재 관측이 동일하므로 차이가 전부 메모리에서 온다
은폐가 저절로 구현됨서랍이 닫혀 있으면 내용물을 관측 텍스트에 안 쓴다. "현재 관측에 없는 정보" 규칙이 코드 한 줄
첫 파일럿에 MuJoCo 불필요API 콜 + 채점기만 있으면 돈다
대가: 현재 관측이 텍스트라 지각을 안 잰다. 이건 의도된 것이다 — 첫 파일럿의 질문은 "메모리를 쓸 줄 아는가"지 "볼 줄 아는가"가 아니다. 현재 관측을 이미지로 바꾸는 건 Phase 2에서, 같은 골든 세트로 A/B 하면 된다.

3 Subgoal 스키마

// Layer 1 — VLM 출력. 매 턴 정확히 1개. JSON only. {"action":"navigate", "target":"<receptacle_id|room_id>"} {"action":"open", "target":"<articulated_id>"} {"action":"close", "target":"<articulated_id>"} {"action":"pick", "target":"<object_id>"} {"action":"place", "target":"<object_id>", "receptacle":"<receptacle_id>"} // pick 포함 {"action":"done", "status":"success"|"impossible", "reason":"<str>"} {"action":"ask", "question":"<str>"}

Layer 2 — 렌더러 (rule-base, 결정적)

subgoalMolmoSpaces task_type렌더 결과
open(t)open"open the {name(t)}."
close(t)close"close the {name(t)}."
pick(t)pick"pick up the {name(t)}."
place(t, r)pick_and_place"pick up the {name(t)} and place it on the {name(r)}."
navigate(t)— (정책 프롬프트 없음)N0: navmesh standing pose 조회 / N1·N2용: "Navigate to the {name(t)}"
done / ask비물리렌더 없음. 텍스트로 채점
name(id) = ObjectMeta.short_descriptions(uid)[prompt_object_word_num - 1] ← MolmoSpaces와 동일 함수 호출. 직접 문자열 만들지 않는다.

ID 규약

Kitchen/Drawer_04 <room>/<Type>_<kk> kk = 월드 (x,y) 정렬, 매니페스트 동결 Kitchen/Fridge_01/joint_02 관절은 로컬 높이 내림차순 (위가 00) inst:obja:keychain_0031:0 이동 가능 물체. 옮겨져도 ID 불변 Kitchen 방 자체 (navigate 전용)
스키마 위반 처리: JSON 파싱 실패 / 없는 action / 존재하지 않는 ID → 재시도 없이 invalid로 기록하고 그 스텝을 실패 처리. 재시도를 넣으면 모델 간 비교가 오염된다. invalid_rate를 별도 메트릭으로 보고한다.

4 Step-wise 루프

state ← 에피소드 초기 상태 (질의 시점) trace ← [] for step in 1..MAX_STEPS: # A1이면 MAX_STEPS = 8 obs_text ← render_observation(state) # 심볼릭. 닫힌 리셉터클 내용물은 숨김 sg ← VLM(system, inventory, memory, obs_text, trace, instruction) if invalid(sg): log invalid; break trace.append(sg) if sg.action == "done" or "ask": break state ← symbolic_step(state, sg) # open→열림 / pick→손에 듦 / ... score(trace, gt)

두 모드

trace에 무엇이 들어가나재는 것메트릭
FR free-run모델 자신의 출력실제 문제 해결력 (오류 누적 포함)Target Accuracy · redundant_open
TF teacher-forcedGT prefix순수 판단력 (오류 누적 없음)Step Accuracy

FR을 메인으로, TF는 진단용. FR에서 실패했을 때 "어느 스텝에서 틀렸나"를 TF가 알려준다.

심볼릭 상태 전이기

state = { robot_room, holding, # 로봇 위치 / 손에 든 것 open_joints: set[joint_id], # 열려 있는 관절 obj_loc: {obj_id -> (receptacle_id, "in"|"on")}, opened_by_agent: set[joint_id], # C1(원상복구)용, A1엔 불필요 } navigate(t) → robot_room = room_of(t) open(t) → open_joints += t ; opened_by_agent += t close(t) → open_joints -= t pick(t) → holding = t (선행조건: 같은 방 + 리셉터클이 열려있거나 open 불필요) place(t,r) → holding = None ; obj_loc[t] = (r, "on") 관측 렌더 규칙 ★ : obj_loc[o]의 리셉터클이 닫혀 있으면 o를 관측에 쓰지 않는다 → "현재 관측에 없는 정보" 규칙이 이 한 줄로 구현된다

5 프롬프트 v1

════════ SYSTEM ════════ You are a household robot's high-level planner. Output the NEXT single subgoal as one JSON object. No prose, no markdown fence. Allowed actions (use EXACTLY these forms): {"action":"navigate","target":"<id>"} {"action":"open","target":"<id>"} {"action":"close","target":"<id>"} {"action":"pick","target":"<id>"} {"action":"place","target":"<id>","receptacle":"<id>"} {"action":"done","status":"success"|"impossible","reason":"<short>"} {"action":"ask","question":"<short>"} Rules: - Every <id> MUST be copied verbatim from SCENE INVENTORY. Never invent an id. - "place" already includes picking the object up. Do not emit "pick" before "place". - You must be in the same room as the target before pick/place/open/close. - Objects inside a closed receptacle are NOT listed in CURRENT STATE. If you believe an object is inside one, open it first. - Emit "done" when the instruction is satisfied, or "impossible" if it cannot be. ════════ USER ════════ ## SCENE INVENTORY // 씬 전체. 후보만 주면 힌트가 된다 rooms: Kitchen, Living, Bedroom, Bathroom receptacles: Kitchen/Drawer_01 (drawer) | Kitchen/Drawer_02 (drawer) | Kitchen/Drawer_04 (drawer) Kitchen/Cabinet_01..08 (cabinet) | Kitchen/Fridge_01 (fridge, joints 00-02) Living/CoffeeTable_01 (table, no drawer) | Bedroom/SideTable_01 (side table, drawer) ... (씬의 모든 리셉터클) ## MEMORY // ← 조건별로 여기만 달라진다 [blind] (생략) [textlog] t=0:12 user placed "blue keychain" into Kitchen/Drawer_04 t=0:15 user closed Kitchen/Drawer_04 t=1:20 robot opened Kitchen/GarbageCan_01 ... [frames] <image×N> (심기 구간 조밀 + 개입 경계) ## CURRENT STATE // 세 조건 공통 You are in Living. Your hand is empty. Visible: Living/CoffeeTable_01 (on it: red keychain), Living/Sofa_01 Open: (none) ## EXECUTED SO FAR (none) ## INSTRUCTION "내 열쇠 어디 있지? 가져와"
SCENE INVENTORY를 주는 이유: 안 주면 VLM이 "kitchen drawer" 같은 임의 문자열을 뱉고 접지가 안 돼 fuzzy matching 지옥이 된다. 주되 씬의 모든 리셉터클을 준다 — 후보 3개만 주면 그게 정답 힌트다. blind 조건에도 동일하게 준다(공정성).
프롬프트 고정 원칙: 시스템 프롬프트를 조건별로 바꾸지 않는다. 세 조건에서 다른 건 ## MEMORY 블록 하나뿐이어야 한다. v1에서 "recipe leak"으로 두 번 데었으니, 프롬프트 diff를 자동 검사하는 테스트를 하나 넣는다.

6 채점

메트릭정의모드
M1Target Accuracy ★첫 navigate 목적지 == GT AND pick 대상 == GT 인스턴스FR
M2First-Goal Correctness첫 navigate 목적지만FR
M3redundant_open정답 리셉터클을 열기 전까지 헛되이 연 개수FR
M4Plan F1필수 subgoal 집합 대비 precision/recall (순서 제약 별도)FR
M5Step AccuracyGT prefix 주고 다음 1개 정확도TF
M6invalid_rate파싱 실패 / 없는 ID / 잘못된 action둘 다
M1이 메인인 이유: Finding task의 본질은 어디로 가서 무엇을 집는가다. 서랍을 닫고 나왔는지, 사용자에게 갔는지는 부수적이라 채점하지 않는다. M1은 제어와 완전히 무관하게 메모리만 잰다.

D게이트 — 여기를 통과 못 하면 진행하지 않는다

골든 20개 × 3조건 측정 후 확인: G1 blind M1 ≤ chance + ε chance = 1 / (리셉터클 수) ≈ 5% (20개 기준) → 넘으면 상식 prior로 풀리는 문제 → 배치 재샘플 G2 textlog(oracle) M1 ≥ 0.80 → 낮으면 메모리 문제가 아니라 스키마·프롬프트·채점기 문제다. 여기부터 고친다. G3 blind < frames < textlog 갭이 실제로 존재 → 없으면 그 task는 메모리를 안 재고 있다 ← v1이 두 번 놓친 지점 G4 invalid_rate < 5% → 넘으면 스키마 설명·few-shot 보강

7 골든 20개 스펙 (A1)

수작업으로 만든다. 여기서 형식이 진짜 검증된다 — 20개를 손으로 쓰다 보면 스키마의 빈틈이 다 드러난다.

변주 축수준개수왜
동종 미끼 수0 / 1 / 3—인스턴스 구분 압력
개입 base task 수2 / 5 / 8—망각 압력 (프레임 조건에 특히 영향)
은폐 방식(a) 닫힌 곳 / (b) 다른 방—§규칙 2
반사실 짝정답 인스턴스를 바꾼 쌍8쌍 = 16prior로 찍는 모델을 0점으로
단독—4극단 조건(미끼 3개 / 개입 8개)
에피소드 JSON 1건 스키마 { "id": "a1_007", "scene": "val_640", "inventory": { rooms, receptacles[], objects[] }, # 프롬프트 SCENE INVENTORY 원본 "plant": [ {t, actor:"user", verb:"place", args:[obj, recep]}, {t, verb:"close", args:[recep]} ], "filler": [ {subgoal...} × N ], # 개입 (GT 실행됨) "query": "내 열쇠 어디 있지? 가져와", "init_state": { robot_room, holding, open_joints, obj_loc }, # 질의 시점 상태 "gt": { "target_object": "inst:obja:keychain_0031:0", "target_receptacle": "Kitchen/Drawer_04", "plan": [ {navigate}, {open}, {pick}, {done} ], # 대표 정답 경로 "required": [ "open:Kitchen/Drawer_04", "pick:inst:obja:keychain_0031:0" ], "forbidden": [ "pick:inst:obja:keychain_0117:0" ] # 미끼를 집으면 0점 }, "pair_of": "a1_008" # 반사실 짝 }
프레임 조건은 어떻게 만드나: 골든 20개는 아직 시뮬을 안 돌리므로 프레임이 없다. 두 갈래 — ① 20개만 MolmoSpaces로 실제 렌더(심기+개입 구간). ② Phase 1에서는 blind/textlog 두 조건만 먼저 돌리고 프레임은 Phase 2로. ②를 권한다 — G1·G2·G4는 프레임 없이도 검증되고, G3만 미룬다.

8 파일 구조 · 단계

scene_bench/ spec/ subgoal_schema.json # JSON Schema (검증용) renderer.md # subgoal → MolmoSpaces 프롬프트 매핑 표 data/golden_a1/ a1_001.json … a1_020.json src/ symbolic_sim.py # 상태 전이 + 관측 렌더 (~80줄) prompt.py # 프롬프트 조립. MEMORY 블록만 조건별 분기 runner.py # step-wise 루프 + API 호출 + 캐시 score.py # M1~M6 render_molmo.py # name(id) — ObjectMeta.short_descriptions 호출 tmp/ # 실행 산출물 (프로젝트 규칙)
단계내용산출게이트
A스키마 + 렌더러 확정. molmospaces에서 short_descriptions 호출 경로 검증spec/*, render_molmo.py임의 id 10개에 대해 렌더 문구가 MolmoSpaces와 byte-identical
B골든 20개 수작업 (반사실 8쌍 + 단독 4)data/golden_a1/스키마 검증 통과 + 사람이 읽고 정답 확인
C심볼릭 시뮬 + 프롬프트 + 러너 + 채점기src/*GT plan을 그대로 넣으면 M1 = 1.0 (채점기 자체 검증)
D3조건(→ 우선 2조건) 측정결과 표G1~G4 전부 통과. 실패 시 여기서 멈추고 원인 규명
E프롬프트 반복 → 자동 생성 확장 → A3/A4로——
C단계의 자체 검증이 중요하다. GT plan을 러너에 그대로 흘려넣어 M1=1.0이 안 나오면 채점기가 틀린 것이다. v1에서 순위가 두 번 뒤집힌 원인이 전부 측정 버그였으므로, 모델을 붙이기 전에 채점기를 먼저 증명한다.
비용 추정: step-wise 4~6콜 × 20문제 × 2조건 = 160~240콜. v1 실측 $0.019/콜 → 약 $3~5. 프롬프트를 10번 반복해도 $50 이하. 파일럿 단계에서 비용은 제약이 아니다.

9 열린 항목

항목지금 가정언제 확인
1ObjectMeta.short_descriptions가 objaverse 에셋에 실제로 다 채워져 있나있다고 가정. 없으면 uid.split("_")[0] fallbackA단계
2prompt_object_word_num 기본값을 몇으로 둘까MolmoSpaces 기본값 추종 (확인 필요)A단계
3Letsur 게이트웨이 이미지 입력 배열 상한콜당 8장 (v1 실측)C단계
4질의를 한국어로 낼지 영어로 낼지한국어 발화 + 영어 스키마. 실사용에 가깝고, 영어 지시문이면 모델이 문구에서 힌트를 얻을 여지가 준다D단계에서 A/B
5MAX_STEPS를 넘겼을 때 부분점을 줄지안 준다(M1은 이진). M4로 부분 관찰—