# 참조 획득 orchestration — 무엇이 이미 있고 무엇이 없나

> 조건 5 설계. **유료 0** — 코드는 한 줄도 안 바꿨다. 실제 배선은 사람 GO 뒤.
> 2026-08-31.

## 0. 결론부터

**조각은 전부 이미 있다.** 없는 것은 **라운드 loop 하나**와, 대상 목록을
`generation_difficulty` 에서 C(c) 두 축으로 옮기는 일이다.

`reference_acquisition_step._execute` 는 지금 대상이 있으면
`NotImplementedError` 로 **선다** — 「키만 2라운드 계약인데 실행은 1라운드」인
상태를 안 만들려고 일부러 그렇게 뒀다.

---

## 1. 사용자가 말한 흐름과, 그것을 이미 하는 함수

| 사용자 설계 | 이미 있는 것 | 어디 |
|---|---|---|
| 판별 | `needs_reference_acquisition` | `grounding_planner.py:75` |
| 웹/이미지 검색 | `search_reference_images` | `search_grounded_ref.py:353` |
| 안전 다운로드 | `download_candidate` | `search_grounded_ref.py:534` |
| 3~5개 후보 동시 제시 | `PER_ROUND_CAP` (라운드당 4장) | `coarse_type_pick.py` |
| VLM 거친 종류/가시성 선택 | `combine_coarse_verdicts` | `coarse_type_pick.py:89` |
| 없으면 최대 1회 재검색 | `load_narrow_hint` + `CENTRAL_ROUNDS = 2` | `coarse_type_pick.py:272` |
| 선택 참조 생성 | `STATUS_SELECTED` / `downstream_blocked` | `reference_acquisition.py` |
| 이중 구매 없애기 | `acquisition_owner` · `assert_not_buying` | `reference_acquisition.py:133` |
| 참조 신원 | `acquisition_contract_sha` | `reference_acquisition.py:28` |

**없는 것: 이 여덟을 잇는 loop.**

## 2. VLM 이 무엇을 판정하고 무엇을 안 하나 — 이미 계약으로 서 있다

`combine_coarse_verdicts` 가 묻는 것은 **둘뿐**이다 —

```
object_type_match  ∈ {yes, no, unsure}   「우리가 찾던 종류가 맞나」
visible            bool                  「또렷이 보이나」
```

시대·국가·제조사·모델·고증 정확성·좋고 나쁨은 **묻지 않는다**. 좁혀 다시
찾을 때도 그 좌표는 **질의에 남기고** VLM 이 검증만 안 한다
(`load_narrow_hint` 주석).

합치는 법도 보수적이다 — 살아남은 **모든** 심판이 `yes` 이고 `visible` 이어야
eligible 이고, 여럿이면 **검색 상위**(가장 낮은 index)를 고른다. 순위는 검색이
정하지 VLM 이 정하지 않는다. eligible 이 없으면 **안 고른다** — 나쁜 것을
고르는 것보다 낫다. 심판이 하나도 유효하지 않으면 **fail-closed**.

★§5·§6 의 고증 정확성·이미지 품질 평가는 **사람만** 한다. 위 두 물음은
「종류가 맞나」이지 품질 판정이 아니다.

## 3. 없는 것 — 라운드 loop

```
대상마다:
  round 1  질의 저작 → 이미지 검색(4장) → 안전 다운로드
           → VLM 거친 판정(심판 여럿) → combine
           → chosen_index > 0 이면 selected. 끝
  round 2  load_narrow_hint 를 붙여 **좁혀** 한 번 더 (같은 4장 상한)
           → 고르면 selected
           → 못 고르면 no_match_after_retry — **terminal**, 하류를 막는다
  그 밖    provider·다운로드·시간·예산으로 다 못 봤으면 retryable
           ★「없다」와 「못 봤다」를 갈라 적는다
```

### 잠글 것

| | |
|---|---|
| 상한 | 대상 수 × 라운드 2 × 라운드당 4장. **손으로 적은 수**로 잠그고 계획이 다르면 장부·provider 앞에서 선다 |
| 장부 | 대상마다 durable — 끊기면 **못 본 대상만** 다시 산다. 답을 못 받은 것은 자동 재구매 안 한다 |
| 신원 | `acquisition_contract_sha(rounds=...)` — `rounds` 를 **호출부가 준다**. 상수를 접었더니 키는 2라운드인데 실제는 1라운드였던 사고가 있다 |
| 소유자 | `assert_not_buying` 로 **한 자리만** 산다 |

## 4. 이중 구매 — 실제로 사는 자리를 다시 셌다

문서에 적혀 있던 「8곳」은 `era_research` 라는 **낱말이 나오는 파일**을 센
수였다. AST 로 되짚은 실제는 —

| 파일 | 줄 | 무엇을 |
|---|---|---|
| `app/services/still_recipe_service.py` | 2189 · 4170 · 4504 | `assess_and_research_cached` |
| `app/modules/reference_image_generator.py` | 255 · 264 | `assess_subjects` · `research_reference` |
| `app/modules/pipeline/grounding_screen.py` | 234 · 304 | `assess_plan_cached` |

**모듈 3곳 · 호출 자리 7곳.** 나머지 일곱 모듈은 한 푼도 안 산다.

이 셋을 **중앙 결과 조회 전용 또는 명시 adapter** 로 바꾼다. 순서가 중요하다 —
소비자를 **먼저** 새 산출로 돌리고 그 다음에 옛 자리를 no-call 로 내린다.
거꾸로 하면 하류가 끊긴다.

## 5. 대상 목록 — `generation_difficulty` 다리를 언제 걷나

지금 `_targets` 는 두 자리를 **모두** 만족하는 행을 고른다 —
①`grounding_plan` 이 `generation_difficulty` 로 연 것 ②`entity_filter` 를
살아남은 행.

C(c) 가 서면 그 자리는 **두 축**(반복 ≥2 · 같은 행에서 hard AND notice)이
대신한다. ★그 교체는 **새 SOT 와 소비자가 동시에 선 cutover 에서만** 한다 —
한쪽만 바꾸면 대상이 통째로 비거나 두 배가 된다.

## 6. 이 문서가 말하지 **않는** 것

- 지금 배선해도 된다 — **아니다.** 사람 v3 GO 가 먼저다
- 이 흐름이 지금보다 낫다 — **아직 모른다**
- 유료 상한 — 실제 대상 수를 세어 **따로 승인**받는다


---

## 7. 실제로 돌린 결과 (2026-08-31, 유료)

합성 fixture 로 loop 를 끝까지 태웠다. **기구는 돈다.** 다만 재지 **못한** 것이
있고, 그것을 「통과」로 쓰지 않는다.

| | |
|---|---|
| 구간 판독 | 3 — **전부 재사용** (장부가 일했다) |
| 검색 | 16 (라운드 16 · 대상당 1~2) |
| 종류 선택 | 10 (**1심** — 기존 `era_research` 와 같은 모델·팩) |
| 이미지 A/B | **39장 새로 · 1장 재사용 · 실패 0** |
| 결과 | selected **10** · no_match_after_retry **3** |

키 슬롯 전환 1회 — 사유 `credit_balance_exhausted`(primary OpenAI 잔액).

### ★못 고른 3개는 **구조 문제**였다

대상 13개 중 **8개가 다른 대상의 부분**이었고, 못 고른 3개가 **전부 그 8개
안**이다.

```
위에서 아래로 내려가는 글자    ⊂ 정류장 표지     질의가 **아예 안 나감**
글자 왼편에 그어진 짧은 가로줄  ⊂ 정류장 표지     질의가 **아예 안 나감**
대합실 안                  ⊂ 정류장 승강장    0장
```

표지를 찍은 사진에 그 글자와 가로줄이 **이미 들어 있다.** 검색 모델도 「찾을
물건이 아니다」로 보고 질의를 안 냈다.

★기존 검색 팩(`brief_system.md`)이 이미 같은 말을 한다 — 「부수 부분은 버리고
**주 대상 하나만**」. 그것을 **질의 자리에서** 하던 것을 **대상 자리로** 올렸다.

    대상 13 → **5** · 논리 상한 55 → **23**

관계는 **기록된 `part_of`** 로만 본다. 부모가 대상이 **아니면** 그 부분은 그대로
산다. 뺀 것은 `parts_skipped` 로 남긴다.

### ★검색 지시문은 **이름만**

첫 판은 `surface_form` + `visual_brief` 를 통째로 넘겼다 → 검색 모델이 긴
설명을 **따옴표 구절 질의**로 바꿔 0장.

```
'"세로 글자" "왼쪽 가로줄" 정류장 표지'        → 0장
'한쪽 열린 정류장 대기실 벽걸이 긴 벤치'       → 4장
```

production 경로(`era_research.research_reference`)는 `directive_native=name`
으로 **이름만** 넘긴다. 겉모습은 질의가 아니라 **판정 자료**다.

## 8. ★★합성 세계로는 **고증을 못 잰다**

fixture 는 **지어낸 지역**이다. 세계 사실이 정한 「세로쓰기 + 왼쪽 가로줄」은
실제로 없는 형태라 그 사진이 세상에 없다.

그러니 「고름 10개」도 실은 **비슷해 보이는 실제 사물의 사진**이지 그 세계의
정본이 아니다.

    잴 수 있었다   기구가 도나 · 장부·상한·재개 · 「없다」와 「못 봤다」의 구분 ·
                   다섯 갈래 중 여럿이 실제로 지나나
    못 쟀다        **이 참조가 고증상 맞는가** · 참조가 그림을 낫게 하는가

뒤의 것은 **실제 시대·지역이 있는 원고**라야 한다. 이것을 「통과」로 쓰지 않는다.

## 9. 사람 화면 둘

    artifact/20260831_ref_canary/ab_review.html   순서 가린 A/B (arm 이름 안 샘)
    artifact/20260831_ref_canary/review.html      원문 근거·질의·후보·판정·참조

정답표는 `ref_ab_review.py <ab.json> --key` 로 **따로** 낸다 — 사람이 고른
**뒤에** 연다.

## 10. 이번 판에 낸 결함

| | |
|---|---|
| 받은 사진 40장이 「없다」로 보였다 | `rel_root` 을 `backend/` 로 줘 경로가 밖으로 샜다. 이제 뿌리 밖이면 **선다** |
| `relative_to` 가 터졌다 | `outdir` 을 절대로 안 풀었다 — 같은 부류를 두 번 |
| **산 것을 덮어써 잃었다** | `--only` 재실행이 기록을 통째로 다시 썼다. 이제 **합친다** |
| fixture 에 없는 축을 지어냈다 | `character` hard&notice — 모델은 근거를 **입은 것**에 놓았고 그쪽이 맞다 |


---

## 11. 시대 원고 판 (2026-08-31, 유료) — ★내 배선 잘못 셋이 드러났다

지어낸 지역으로는 고증을 못 재고 `character` 가 대상이 못 되며, 실제 현대
원고에서는 `outlook` 이 대상이 못 된다. 그래서 **1960년대 한국** 원고를 지었다
(사용자 지시: 「억지로 상황을 만들지 말고 필요하면 시나리오 만들어서 해」).

    참조 구함  10/15 → 12/15 → **14/15**   ← 아래 셋을 고칠 때마다
    갈래       **다섯 갈래 전부** 참조를 구했다

### ①검색 지시문 **저작 단계**를 통째로 건너뛰었다

기존 팩(`brief_system.md`)이 시키는 것 —

- 「여러 부분이 나열돼 있으면 **주 대상 하나만** 골라 쓰고 부수 부분·부착물은
  버려라」
- 「원고 언어를 판정해 **그 언어로** 쓰고 영어 한 단어도 쓰지 마라」

나는 대상 **이름만** 검색에 넘겼다. 그래서 잔부분에 잔부분 질의가 나갔다.

### ②★2라운드가 **죽어 있었다**

    R1  36회 중 질의 나감 23회  (64%)
    R2  10회 중 질의 나감  1회  (**10%**)

원인 — 좁힘 문안이 **전부 영어**(1,099자)인데 그것을 **한국어 지시문 뒤에
붙였다**. 검색 팩의 「영어 한 단어도 쓰지 마라」와 정면으로 부딪힌다.

★그 문안은 **영어 저작기에게** 주는 것이다
(`outdoor_structure_form_reference_step:1012`). 저작 단계를 건너뛰면서 그
입력을 엉뚱한 자리에 붙인 것이다. 대상당 논리 4 → **6**.

### ③심판에게 **조각만** 줬다

    「코」        → 얼굴의 코이기도 하다. 후보 4장 전부 `종류=no`
    「긴 겉옷」    → 무엇의 긴 겉옷인지 없다. 전부 `no`

production 경로(`era_research`)는 판별 단계가 지은 **자립적 대상 이름**을 준다.
C(c) 의 `surface_form` 은 원문에서 떼어 온 조각이라 그 자리를 못 한다.

모델이 **같은 행에 적은 겉모습 한 문장**(`visual_brief`)을 함께 주니 —

    「코」        4장 전부 `no` → **4장 전부 `yes`**
    「긴 겉옷」    전부 `no`    → **2장 `yes`**

★코드가 문장을 안 짓는다. 모델이 쓴 두 값을 잇기만 한다.

### ★셋 다 **신원에 접었다**

저작 계약을 안 접었더니 장부가 옛 답을 **되썼다** — 완전히 다른 방식으로
찾는데 같은 것으로 봤다.

## 12. 유료 실사용 (시대 판)

| | |
|---|---|
| 구간 판독 | 2 — **재사용** |
| 참조 획득 | 15 대상 |
| A/B | 계획 56 · **되쓴 것 40** · 새로 13 · **거절 3** |

★거절 3은 결함이 아니라 **모델 콘텐츠 거절**(`text_refusal`)이다.

★상한 게이트가 **두 번** 섰다 — 대상 13>12 · 이미지 56>48. 둘 다 **실측을
보고 손으로** 올렸다(계획에서 계산해 넓힌 것이 아니다).

## 13. 남은 하나

「챙 없는 둥근 모자」만 사진 11장을 받고도 전부 `종류=no` 다. 검색이 그 물건을
못 찾는 것인지 심판이 틀린 것인지는 **사람이 화면에서** 본다 — 이미지 판정은
사람 몫이다.

## 14. 화면

    artifact/20260831_period_canary/review.html      고증 판정용 (78장)
    artifact/20260831_period_canary/ab_review.html   순서 가린 A/B (67장)

★순서를 먼저 ①로 본다. 참조가 틀렸으면 ②는 「틀린 참조가 그림을 망치는 것」만
보게 된다.
