Files
ttipo 439753e1cf
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 12m22s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 10m50s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m4s
CI / Frontend (lint · type-check · test) (push) Successful in 11m11s
CI / Backend (lint · test · e2e · build) (push) Successful in 12m15s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m56s
feat(backend): AI 코스 식사 시간대 맞추기와 프롬프트 v2, 평가로 코스 모델을 Sonnet 5.5 로 정함
기준선(설계 §9.1)에서 자동 채점 실패의 3분의 2가 식당이 식사 시간대 밖에 놓인 것이었다.
식사 시간대는 약한 위반이라 고치기에 걸리지 않아서, 강한 위반을 고친 뒤 식당을 식사 시간대로
옮기는 단계를 더했다(설계 §3.6 2a).

작은 모델이 시간을 셈하다 코스를 짧게 끝내거나 끼니를 빠뜨려서, 장소 수와 끼니를 코드가 계산해
메시지에 적고(course-v2), 조건 해석의 시간 표현 · 동네 이름 규칙을 보탰다(conditions-v2).
프롬프트 예시가 평가셋 문장과 겹치면 그 사례의 점수가 외운 답이 되므로 예시를 바꾸고,
평가 실행마다 겹치는 구절을 찾아 요약에 적는다.

같은 v2 로 두 모델을 재니 자동 채점이 Sonnet 88%, Haiku 69~75%였고 경계 사례는 6/6 과 2/6 이었다.
Haiku 는 반려 동반 · 영업시간 표현 규칙을 실행마다 다르게 지켜, 코스 초안 기본 모델을
Sonnet 5.5(effort low)로 정했다(사용자 결정, 설계 §8 · §9.2). 조건 해석은 Haiku 5.5 그대로다.

코스를 못 만든 사례를 되짚을 수 있게 평가 결과에 AI 호출마다 고른 곳과 위반 목록을 남긴다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 09:12:20 +09:00
..

AI 코스 생성 평가셋 v2 (대구)

WanderGo AI 코스 생성이 조건을 지키는지, 없는 정보를 지어내지 않는지 확인하는 평가용 예시입니다. 모델·프롬프트를 바꿀 때마다 같은 예시로 돌려 점수를 비교합니다. 흐름과 규칙은 AI 코스 생성 설계 §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 로 컨테이너를 다시 만든다).

# 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)를 맞출 수 없다.