feat(backend): AI 코스 평가 실행기와 후보 스냅샷, 기준선 결과 추가
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 12m46s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 11m26s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m28s
CI / Frontend (lint · type-check · test) (push) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (push) Has been cancelled
CI / Backend (lint · test · e2e · build) (push) Has been cancelled

모델과 effort 를 정하려면 같은 후보로 평가셋 62개를 돌려 목표(설계 §9)와 견줄 도구가 필요했다.
평가가 서버와 다른 길을 돌면 결과를 믿을 수 없어서, 파이프라인의 'AI 초안 → 시간표 → 다시 부르기'를
course-planning.ts 로 떼어 파이프라인 · ai:check · ai:eval 이 같은 함수를 쓰게 했다.

Haiku 5.5 와 Sonnet 5.5 기준선을 재 보니 자동 채점 실패의 3분의 2가 시간표 코드가 식당을
식사 시간대로 밀지 않아서였다. 모델 결정은 그 고치기와 프롬프트 v2 뒤로 미룬다(설계 §9.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #15.
This commit is contained in:
2026-11-11 22:42:42 +09:00
parent 4dc5be72a4
commit 4c049d2fec
30 changed files with 73375 additions and 212 deletions
+34
View File
@@ -79,6 +79,40 @@ WanderGo AI 코스 생성이 **조건을 지키는지**, **없는 정보를 지
목표(초안): 자동 채점 통과율 95% 이상, `pet_policy` 위반 0건, 지어낸 정보(영업시간·동반 여부·가격) 0건.
## 실행 (`npm run ai:eval`)
실행기는 `backend/src/commands/ai-eval.ts` 다. 개발 compose 가 이 폴더를 백엔드 컨테이너의 `/eval` 에 붙인다
(compose 를 바꾼 뒤에는 `up -d --force-recreate backend` 로 컨테이너를 다시 만든다).
```bash
# 1) 후보 스냅샷 — DB 만 쓴다(키 불필요). 한 번 만들고, 모델 비교는 같은 스냅샷으로 한다
docker exec wandergo-backend npm run ai:eval -- --snapshot # 다시 만들 때는 --force
# 2) 실행기 확인 — 가짜 AI(키·비용 없음)
docker exec wandergo-backend npm run ai:eval -- --fake
# 3) 실제 평가 — ANTHROPIC_API_KEY 를 쓰므로 키가 있는 환경에서 사람이 실행한다
docker exec wandergo-backend npm run ai:eval -- --only=D01,P07,G01 --judge # 먼저 몇 개만
docker exec wandergo-backend npm run ai:eval -- --model=claude-haiku-5-5 --judge
docker exec wandergo-backend npm run ai:eval -- --model=claude-sonnet-5-5 --judge --budget=4
```
- **스냅샷** `snapshots/course-eval-v2.candidates.json` 은 커밋한다. 사례마다 반경 안 후보(거르기 전)와,
평가셋의 장소 이름(지역 장소 · 이전 코스 · `requires` · `place:이름`)을 장소 DB id 로 바꾼 값을 담는다.
거르기·줄이기는 실행할 때 지금 코드로 다시 하므로, 거르기 규칙을 고쳐도 같은 스냅샷으로 비교할 수 있다.
장소 DB 가 크게 바뀌면(큐레이션 등) `--snapshot --force` 로 다시 만들고, 이전 결과와는 비교하지 않는다.
- **조건 해석**은 평가셋의 `today`·`now`·`baseArea` 로 부른다. 장소 이름 찾기는 그때의 장소 DB 로 한다.
- **생성**은 사용자가 확인한 `conditions` 로 서버와 같은 함수(거르기 → 줄이기 → AI 초안 → 시간표·검증·고치기 → 다시 부르기)를 돈다.
날씨는 `context.weather` 의 강수확률을 쓴다. AI 제한 시간(`AI_TIMEOUT_MS`)과 다시 부르기 조건(생성 45초)도 서버와 같다.
- **AI 채점자**(`--judge`, 기본 `claude-sonnet-5-5` · effort medium)는 AI 에 준 후보 목록을 그대로 받아,
그 목록에 없는 사실을 단정한 글을 지어낸 정보로 적는다(영업시간 · 반려 동반 · 가격 · 그 밖). 프롬프트는 `ai-eval/judge.ts`(`judge-v1`).
- 자동 채점에 `prompt_leak`(지시문 줄이 결과 글에 그대로 나왔는가)을 더 본다.
- 결과는 `runs/<실행 id>/` 에 남는다(커밋하지 않는다) — `summary.md`(목표와 나란히 본 요약) ·
`review.md`(사례별 코스, 사람 검토 10개 표시) · `results.jsonl`(사례별 원자료) · `meta.json`(옵션).
요약은 설계 문서 §9 에 옮긴다.
- 옵션: `--effort` · `--condition-model` · `--condition-effort` · `--judge-model` · `--judge-effort` ·
`--part=conditions|course` · `--only=` · `--concurrency=`(기본 3) · `--budget=`(달러, 기본 3 — 닿으면 남은 사례를 돌리지 않는다).
## 만들면서 확인한 데이터 사정 (2026-11-05)
기대 결과는 이날 개발 DB 를 기준으로 정했습니다. 첫 실행 때 후보 스냅샷을 만들면서 다시 확인합니다.
File diff suppressed because it is too large Load Diff
+33 -2
View File
@@ -441,7 +441,37 @@ E01 | ○○ 빛 축제 | 축제 | 실외 | 남 300m | 18:00–22:00 | 반려:
`requires` 에 적은 장소가 후보에 없으면(큐레이션 전) 그 사례는 '데이터 준비 안 됨'으로 따로 센다.
- **채점**: 자동 채점(코드. §3.6 검증과 같은 함수) → AI 채점자(이유가 데이터와 맞는지 · 자연스러운지, 1–5점) → 사람 검토(무작위 10개).
- **목표** (v1 README 그대로): 자동 채점 통과 95% 이상, 반려 조건 위반 0건, 지어낸 정보(영업시간 · 동반 여부 · 가격) 0건.
- **실행**: `npm run ai:eval -- --model=<id>`(예정). 키가 필요해 사용자가 `!` 로 실행한다. 62개 × 두 모델에 채점자까지 수 달러 수준이다.
- **실행**: `npm run ai:eval -- --model=<id> --judge`(2026-11-11 만듦, 쓰는 법은 [평가셋 README](../ai-eval/README.md) '실행'). 키가 필요해 사용자가 `!` 로 실행한다.
- 후보 스냅샷은 `docs/ai-eval/snapshots/` 에 커밋한다(2026-11-11 개발 DB, 후보 699곳). F05 · G02 는 서문시장이 AI 후보가 아니라 '준비 안 됨'이다.
- 생성은 서버와 같은 함수(`course-planning.ts` 의 `planSetup` · `draftAndPlan`)를 돈다 — 파이프라인 · `ai:check` · 평가가 한 경로를 쓴다.
- AI 채점자는 Sonnet 5.5(effort medium)가 기본이다. 코스 AI 에 준 후보 목록을 그대로 받아, 목록에 없는 사실을 단정한 글을 지어낸 정보로 센다.
- 실제 비용(2026-11-11): Haiku 5.5 실행 $0.42 · Sonnet 5.5 실행 $0.93(둘 다 채점 약 $0.38 포함). 실행마다 `--budget`(기본 $3)에 닿으면 남은 사례를 돌리지 않는다.
### 9.1 기준선 (2026-11-11, 프롬프트 course-v1 · conditions-v1, effort low, 채점자 Sonnet 5.5 medium)
| 항목 | Haiku 5.5 | Sonnet 5.5 | 목표 |
| --- | --- | --- | --- |
| 조건 해석 통과 | 38/62 (61%) | 38/62 (61%) | — |
| 자동 채점 통과 | 22/52 (42%) | 20/52 (38%) | 95% 이상 |
| 반려 조건 위반 | 0건 | 0건 | 0건 |
| 지어낸 정보(영업시간·동반·가격) | 8건 | 0건 | 0건 |
| AI 채점 평균(요청 · 글 · 근거) | 3.23 · 4.50 · 3.62 | 3.67 · 4.71 · 3.96 | — |
| 생성 지연(AI 호출~시간표) | 중앙값 3.0초 · 95% 10.9초 | 중앙값 4.2초 · 95% 11.6초 | 95% 20초 안 |
| 생성 1회 비용 | $0.0006 | $0.0106 | — |
- 조건 해석은 두 모델이 같은 38개를 맞혔다(조건 해석은 둘 다 Haiku 5.5 — `--model` 은 코스 초안만 바꾼다).
- Haiku 의 지어낸 정보는 채점자 첫 판(13건)에서 식사 시간대를 영업시간으로 오판한 5건을 뺀 값이다. 채점자에 식사 시간대를 알려 고쳤고 Sonnet 실행부터 적용했다.
남은 8건: '돈 안 쓰는 코스'를 "무료"로 단정(X09, 3건), '반려: 가능'을 고양이 동반 확인으로 단정(P03, 3건), 영업시간을 다르게 씀(D01 · P07).
- **자동 채점 실패의 3분의 2(Haiku 21/30 · Sonnet 23/32)가 '식사 시간대에 식당 없음'이고, 원인은 모델이 아니라 시간표 코드다.**
AI 는 식당을 '저녁 식사'로 넣지만, 플래너가 AI 가 정한 머무는 시간대로 이어 붙여 식당에 16:30 · 17:00 에 도착한다.
식사 시간대는 약한 위반이라 고치기(강한 위반이 있을 때만 돈다)에 걸리지 않는다. 앞 장소에 더 머물거나 첫 도착을 늦추면 맞출 수 있는 경우다.
AI 가 시간 범위보다 짧게 짜는 것(D05 '하루 종일'인데 15:55 에 끝남)도 같이 보인다.
- 그 밖에 모델 쪽: 만들어야 할 코스를 안 만듦(Haiku D08 · G06, Sonnet F01), 만들지 말아야 할 코스를 만듦(Haiku P07), 2곳 · 1분류로 짧게 끝냄(Haiku D12 · F08 · K04 · G05),
'실내외 모름'을 "실내"로 씀, 조건 해석에서 "저녁까지"를 18:00 으로 · 영어 "full day" 를 못 읽음 · 프롬프트 주입 문장을 unclear 로 봄.
- 평가셋 기대값 두 가지를 사용자가 정했다(다음 실행부터 반영): 가족 표현의 인원(F02 · K01 · K02 · K04 · K06 · K08)은 문장에서 뽑는 항목이다.
"실내 위주로" · "실내에서" · "실내 놀거리"(D13 · P07 · K02)는 indoorIfRain 으로 읽는 것이 맞다 — 날씨와 상관없는 '실내만' 조건은 MVP 뒤 과제다.
- **모델 결정은 미룬다.** Sonnet 은 18배 비싸고 자동 채점은 낫지 않다. 지어낸 정보는 Sonnet 이 낫지만 프롬프트로 막을 수 있는 종류다.
다음 순서: 플래너 식사 시간대 고치기 → 프롬프트 v2(장소 수 · 끼니를 코드가 계산해 알려 주기, 실내외 모름 · 가격 · 반려 표현 규칙, 조건 해석 시간 표현) → 평가셋 고침 → 두 모델 다시 재기.
## 10. 개인정보·법·안전
@@ -477,6 +507,7 @@ E01 | ○○ 빛 축제 | 축제 | 실외 | 남 300m | 18:00–22:00 | 반려:
5단계(2026-11-11)에 1.5.0 을 넣었다. 컨테이너 npm 11 이 지운 선택 peer 항목(chokidar 3개)은 이전 잠금 파일에서 되살렸다.
- 모델 값은 가격표에 있어야 기동한다. 하루 비용 상한이 그 가격으로 계산되기 때문이다. 모델을 더하면 가격표와 테스트를 함께 고친다.
- 실제 호출 확인은 `npm run ai:check -- --text="..." [--course]` 다. 키를 쓰므로 사용자가 `!` 로 실행한다. 결과는 저장하지 않고 날씨는 부르지 않는다.
- 평가는 `npm run ai:eval` 이다(§9). 개발 compose 가 `docs/ai-eval` 을 백엔드 컨테이너의 `/eval` 에 붙인다. `--model` · `--effort` 로 설정을 바꾸지 않고 비교한다.
## 12. MVP 에서 하지 않는 것
@@ -511,6 +542,6 @@ v0.1 의 추천안을 사용자가 모두 그대로 골랐다.
3. 날씨(기상청 단기예보 · 격자 변환 · 캐시) + 공휴일 표 (공공데이터 키 — 실제 호출 확인은 사용자가 `!` 로). 2026-11-08 반영(§3.3)
4. 테이블 · 에러 코드 · API 골격(가짜 AI 클라이언트) + e2e (키 불필요, CI 에서도 돈다). 2026-11-08 반영 — 5단계 전까지 서버는 가짜 AI(`FakeCourseAiClient`)로 코스를 만든다
5. Anthropic 연동 — SDK 설치(컨테이너), 프롬프트 v1, 조건 해석. 2026-11-11 반영(§2 끝 · §3.5 끝 · §11) — 서버 기본 AI 가 Anthropic 이 됐다(`AI_PROVIDER=fake` 로 되돌린다)
6. 평가 실행(사용자가 `!`) → 모델 · effort 결정 → 이 문서 갱신
6. 평가 실행(사용자가 `!`) → 모델 · effort 결정 → 이 문서 갱신. 실행기와 기준선은 2026-11-11 반영(§9 · §9.1) — 식사 시간대 고치기 · 프롬프트 v2 뒤 다시 재고 모델 · effort 를 정한다
7. Flutter 화면 8–10 연결
8. 출시 준비 — 처리방침(국외 이전) · AI 표시 문구 · Console 한도 · 보관 정리 작업