# AI 코스 생성 평가셋 v2 (대구) WanderGo AI 코스 생성이 **조건을 지키는지**, **없는 정보를 지어내지 않는지** 확인하는 평가용 예시입니다. 모델·프롬프트를 바꿀 때마다 같은 예시로 돌려 점수를 비교합니다. 흐름과 규칙은 [AI 코스 생성 설계](../design/ai-course.md) §2–§3·§9 를 따릅니다. - `course-eval-v2.jsonl` — 대구판 62개. 2026-11-05 사용자 검토를 거쳐 확정했습니다(아래 '검토에서 정한 것'). - `course-eval-v1.jsonl` — 2026-10-29 초안(서울·부산·제주 등). 장소 DB 가 대구만 있어 지금은 쓰지 않고, 지역을 넓힐 때 다시 씁니다. ## 구성 (62개) | 분류 | 개수 | 보는 것 | |---|---|---| | 데이트 | 14 | 분위기 요청·식사 순서·영업시간·월요일 휴관·날짜 범위 끝(30일) | | 축제·행사 | 8 | 그날 열리는 행사인지·행사 시간·마지막 날·없는 축제를 지어내지 않기 | | 반려동물 | 10 | 동반 조건 엄격 적용·미확인 표시·실내 동반 장소가 없을 때 정직하게 | | 가족 | 8 | 걷는 양·쉬는 곳·아이/어르신·접근성 정보를 단정하지 않기 | | 외국인 | 6 | 영어 응답·한국어 장소명·식이 정보를 지어내지 않기 | | 날씨·시간 | 6 | 예보 활용·예보 없는 날짜·이른/늦은 시간·짧은 시간 | | 경계 | 10 | 불가능·범위 밖·위험 요청·다시 만들기·프롬프트 주입 | 조건 해석에서 끝나는 사례가 8개(F04·P05·W04·W06·X02·X06·X07·X10), 생성까지 가는 사례가 54개입니다. ## 필드 - `request` — 사용자가 입력하는 문장 그대로 - `context` — 시스템이 아는 값 - `today`·`now` 를 고정해 결과를 재현한다(2026-11-05 목요일 10:00). 기본값은 이 시각으로 계산한다 - `baseArea` — 앱의 기준 위치(동네 또는 현재 위치 좌표) - `weather` — 그 날짜·지역의 강수확률. 예보가 나오지 않는 날짜(11-09 이후)는 `null` - `previousStops` — 다시 만들기일 때 이전 코스의 장소 이름(X03) - `conditions` — 사용자가 화면에서 확인한 조건 = 생성 API 입력(`CourseConditions`, 설계 §2) - 장소형 지역은 `placeId` 대신 `placeName` 으로 적는다. 실행기가 스냅샷에서 id 로 바꾼다 - 조건 해석에서 끝나는 사례는 문장에서 뽑을 항목만 적는다 - `inferred` — 문장에서 뽑아야 하는 항목. 여기 없는 항목은 기본값이거나 사용자가 화면에서 고른 값이다 - `issues` — 조건 해석이 내야 하는 되묻기(설계 §2: `unsupported_area` · `past_date` · `date_out_of_range` · `multi_day` · `too_short` · `past_midnight` · `unclear_request`) - `expected_behavior` - `course` — 코스를 만든다 - `partial_with_notice` — 조건을 다 채울 수 없으면 적게 만들고 이유를 말한다 - `no_course` — 요청의 핵심을 맞출 후보가 없어 만들지 않는다(`AI_003` + 이유) - `refuse_part` — 위험하거나 부적절한 부분은 거절하고 나머지는 만든다 - `ask_clarification` — 조건 해석에서 `issues` 로 되묻는다(생성하지 않는다) - `decline_out_of_scope` — 지원 범위 밖이라고 안내한다(생성하지 않는다) - `checks` — 코드로 자동 채점하는 조건(조건 해석에서 끝나는 사례는 `null`) - `open_at_arrival` 도착부터 떠날 때까지 영업 중 · `within_window` 시간 안에 끝남 - `stops` 장소 수 범위 · `max_leg_min` 장소 사이 최대 이동 시간(분, 기본 걸어서 15 · 대중교통 30 · 자동차 30) - `pet_policy: strict` 동반이 **확인된** 곳만 동반 가능으로 표시하고 실내는 확인된 곳만 넣는다(미확인 야외는 경고) - `indoor_ratio_min` 실내 비율 하한(비 40% 이상이면 0.5) · `category_min` 서로 다른 분류 수 - `meals` 그 끼니 시간대(점심 11:30–14:00 · 저녁 17:30–20:30)에 도착하는 식당이 있음 - `must_include` / `must_exclude` 꼭 넣을·뺄 것(아래 표) - `judge_focus` — 사람·AI 채점자가 중점으로 볼 것 - `requires` — 이 장소가 AI 후보여야 돌릴 수 있는 사례(큐레이션이 필요하다). 후보에 없으면 '데이터 준비 안 됨'으로 따로 센다 - `note` — 기대 결과가 기대는 데이터 사정 | `must_include` / `must_exclude` | 뜻 | |---|---| | `cafe` · `food` · `culture` · `nature` · `attraction` · `leisure` | 그 분류의 장소. `cafe>=2` 는 2곳 이상. `culture` 는 전시 행사도 센다 | | `event` · `exhibition` | 그날 열리는 행사 · 그중 전시 | | `food_first` · `food_last` · `food_or_cafe_first` | 첫(마지막) 장소가 식당(또는 카페) | | `market` | 전통시장(관광공사 분류 SH060100·SH060200) | | `place:이름` | 그 장소 | | `previous` | 이전 코스의 장소(`context.previousStops`) | ## 채점 1. **조건 해석(코드)** — 모든 사례. `inferred` 항목이 `conditions` 와 같은지, `issues` 가 같은지 본다. 시각은 ±60분까지 맞은 것으로 본다. 시간 표현의 기준값은 아래 표다. 2. **자동 채점(코드)** — 생성 사례. 장소가 모두 후보 목록에 있는지, `checks` 를 모두 지켰는지. 하나라도 어기면 실패. 설계 §3.6 검증과 같은 함수를 쓴다 3. **AI 채점자** — 요청에 맞는지, 한국어(영어)가 자연스러운지, 이유·알림이 데이터와 맞는지 1–5점 4. **사람 검토** — 매 버전 무작위 10개를 직접 본다 | 표현 | 시작 | 끝 | |---|---|---| | 오전 · 오후 · 저녁 · 밤 · 퇴근 후 | 10:00 · 14:00 · 18:00 · 20:00 · 19:00 | 시작 + 6시간(22시까지) | | 하루 종일 | 10:00 | 20:00 | | 반나절 · N시간만 | 기본 시작 | 시작 + 4시간 · 시작 + N시간 | | 저녁까지 · 늦게까지 · 오전에만 | — | 21:00 · 23:00 · 12:00 | | 말이 없을 때 | 오늘이면 다음 정시, 아니면 13:00 | 시작 + 6시간(22시까지) | 목표(초안): 자동 채점 통과율 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 에 옮긴다. - AI 호출마다 고른 장소·머무는 시간과 시간표 결과(실패면 위반 목록)를 `results.jsonl` 의 `course.attempts` 에 남긴다. 코스를 못 만들었거나 다시 부른 사례는 `review.md` 에 'AI 초안 1·2' 로 보인다 — 코드가 고칠 수 없었던 초안을 되짚을 때 본다. - 옵션: `--effort` · `--condition-model` · `--condition-effort` · `--judge-model` · `--judge-effort` · `--part=conditions|course` · `--only=` · `--concurrency=`(기본 3) · `--budget=`(달러, 기본 3 — 닿으면 남은 사례를 돌리지 않는다). ## 만들면서 확인한 데이터 사정 (2026-11-05) 기대 결과는 이날 개발 DB 를 기준으로 정했습니다. 첫 실행 때 후보 스냅샷을 만들면서 다시 확인합니다. - **반려 동반이 확인된 실내 장소가 없다.** AI 후보 721곳 중 동반 정보가 있는 곳은 12곳이고 모두 공원·관광지·케이블카다. 그래서 반려동물 사례 대부분이 `partial_with_notice`·`no_course` 다. 큐레이션으로 동반 카페·식당을 확인하면 기대 결과를 다시 본다. - **전통시장은 AI 후보가 아니다.** 쇼핑 분류라 큐레이션에서 추천(O)·확인해야 후보가 된다(장소 DB §3). 서문시장 야시장 사례(F05·G02)는 `requires` 로 표시했다. - **11월 대구에 축제가 없다.** 행사는 수성국제비엔날레(수성못, 10-27~11-22)와 전시뿐이다. 축제를 찾는 F01 은 '없다고 말하기'를 본다. - **계절별 시간표는 미확인이었다.** 하절기·동절기로 나뉜 곳(수창청춘맨숀·대구예술발전소·향촌문화관 등 23곳)은 영업시간 미확인이라 문화시설이면 후보에서 빠졌다. 2026-11-06 날짜의 달로 고르게 고쳤다(설계 §3.3). 달을 적지 않은 곳(수창청춘맨숀 등 7곳)은 11월에 두 계절 시간이 같은 시각만 판단한다. - 생성 사례는 모두 거른 뒤 후보가 8곳 이상 남는다(설계 §3.2–3.3 규칙으로 근사 계산). 정답 코스는 적지 않았습니다. 평가는 장소 DB 스냅샷 위에서 돌리므로, 정답은 "조건을 지켰는가"로 판단합니다. ## 검토에서 정한 것 (2026-11-05) - **요청의 핵심을 맞출 후보가 없으면 만들지 않는다.** "실내에서만"(P07), "실내 전시만"(X01)인데 그런 곳이 없으면 야외 장소로 채우지 않고 이유를 알린다(`no_course`). 요청과 다른 코스를 대신 내놓지 않는다. - **일부만 못 맞추면 맞출 수 있는 만큼 만들고 알린다.** "산책하고 카페도"(P02), "근대골목 걷다가 카페"(P10)는 산책 코스를 만들고 동반 카페가 없다고 알린다(`partial_with_notice`). - **'돈 안 쓰는 코스'(X09)는 식당·카페를 뺀다.** 입장료가 무료로 확인되지 않은 곳을 무료라고 하지 않는다. ## 첫 실행에서 정한 것 (2026-11-11) - **가족 표현의 인원은 문장에서 뽑는 항목이다.** F02 · K01 · K02 · K04 · K06 · K08 은 기대 인원(3 · 4명)이 이미 문장에서 센 값이었는데 `inferred` 에 `party` 가 빠져 있었다. 넣었다. - **"실내 위주로" · "실내에서" · "실내 놀거리"는 `indoorIfRain` 으로 읽는다.** D13 · P07 · K02 의 `inferred` 에 넣었고, 같은 기준으로 X01("실내 전시만")에도 넣었다. 지금 조건에는 날씨와 상관없는 '실내만' 항목이 없어 가장 가까운 값이다. 날씨와 상관없는 실내 조건은 MVP 뒤 과제다. - **영어 'I'만으로 혼자라고 보지 않는다.** G01 의 `inferred` 에서 `party` 를 뺐다(G02 와 같게). 생성 조건의 1명은 사용자가 화면에서 고른 값으로 둔다. - 프롬프트(조건 해석 · 코스)의 예시는 평가셋 문장과 겹치지 않게 둔다 — 겹치면 그 사례의 점수가 외운 답이 된다. v1 에는 평가셋 문장과 같거나 거의 같은 예시가 여럿 있었다(조건 해석 D01 · K06 · G06 · X02 · F04 · X07, 코스 P02 · X01). v2 첫 판에도 D06 · P07 과 겹치는 예시가 남아 있었다. 실행기가 실행마다 프롬프트와 평가셋 요청이 겹치는 구절(두 낱말 이상, 공백을 빼고 7자 이상)을 찾아 `summary.md` 에 적는다. 장소 이름 한 낱말은 세지 않는다. 짧은 핵심 구절("밤 11시")은 이 검사로 잡히지 않으니 프롬프트를 고칠 때 직접 본다. ## 모델을 정하며 정한 것 (2026-11-12) - **K08 의 기대 인원을 3명에서 2명으로 고쳤다.** "휠체어 쓰시는 아버지와 함께"는 본인과 아버지 두 사람이다. 문장에 나온 사람만 센다. - **실행마다 결과가 흔들린다.** 프롬프트 v2 로 Haiku 를 두 번 돌리니(사이에 3차 다듬기) 자동 채점 52개 중 13개가 통과와 실패를 오갔다. 3~4개 차이는 한 번 실행으로 가리지 않는다. 비교가 빠듯하면 채점자 없이(`--judge` 를 빼고) 몇 번 더 돌려 평균을 본다. - **채점자는 코스 프롬프트의 규칙을 모른다.** 코스 프롬프트가 알려 준 "반려 정보는 개 기준"을 옮겨 쓴 글을 채점자가 지어낸 정보로 셌다(Sonnet P03 2건). 채점자 다음 판(`judge-v2`)에 그 규칙을 알린다. ## 다음 버전에서 볼 것 - 대구에 사는 사람이 실제로 할 법한 요청인지, 빠진 상황이 있는지 - `checks` 기준값(이동 시간, 장소 수, 끼니)이 너무 빡빡하거나 느슨하지 않은지 - 장소 수: 코스 프롬프트 v2 는 90분에 1곳으로 장소 수를 알려 주는데, 평가셋 기대보다 많다 — P08(하루 6곳, 기대 3–5) · W01(6시간 4곳, 기대 2–3). - 이동 시간: P06 · P09 는 앱의 도보 구간 한도(15분)보다 좁은 8~10분을 기대한다. 걷기를 줄여야 하는 동행(노견 · 휠체어 · 어르신)은 조건에 항목이 없어 앱 기본 한도를 쓴다. - 태풍처럼 위험한 날씨는 알려야 한다(W01). 코스 프롬프트 v2 는 날씨 말을 쓰지 않게 해서(날씨는 앱이 보여 준다) W01 이 알림을 남기지 못한다 — 위험 기상 알림은 앱이 붙이는 쪽을 본다. - 큐레이션으로 반려 동반 실내 장소나 전통시장이 후보가 되면 반려동물 사례와 F05·G02 의 기대 결과를 다시 본다. 지금은 반려견과 갈 수 있는 곳이 자연 분류뿐인 사례가 있어 분류 2가지(P04 · P09)를 맞출 수 없다.