feat(backend): AI 코스 식사 시간대 맞추기와 프롬프트 v2, 평가로 코스 모델을 Sonnet 5.5 로 정함 #16

Merged
ttipo merged 1 commits from feat/course-ai-prompt-v2 into main 2026-11-12 09:52:38 +09:00
Owner

무엇을 · 왜

AI 코스 생성 6단계(설계 §14)의 뒷부분이다. 기준선(#15)에서 찾은 실패를 고치고, 같은 조건으로 두 모델을 다시 재서 코스 모델을 정했다.

#15 위에 쌓은 브랜치다. #15 를 먼저 머지하면 이 PR 에는 이번 커밋만 남는다.

플래너 — 식당을 식사 시간대에 (설계 §3.6 2a)

  • 기준선 자동 채점 실패의 3분의 2가 '식사 시간대에 식당 없음'이었다. 식사 시간대는 약한 위반이라 고치기(강한 위반이 있을 때만 돈다)에 걸리지 않았다.
  • 강한 위반을 고친 뒤 식당이 식사 시간대 밖이면 순서 · 머무는 시간을 다시 찾아 식당을 시간대 안으로 옮긴다(fitMeals). 옮길 수 없으면 원래 시간표를 둔다.
  • 비 올 때 실내 비율을 채울 때도 식사를 지키는 순서를 먼저 본다.

프롬프트 v2 (course-v2 · conditions-v2, 설계 §3.5)

  • 코스: 장소 수(1.5시간에 1곳)와 끼니(시간 범위가 그 식사 시간대에 90분 이상 걸칠 때만)를 코드가 계산해 메시지에 적는다.
    쓰는 규칙(실내외 모름 · 가격 · 도착 시각 · 넣지 않은 곳 · 반려 표현)과 비우기 · 일부만 맞추기 기준을 다듬었다.
  • 조건 해석: 시간 표현(말한 시각 우선 · 하루 종일 · 오전에/오전에만 · 저녁까지 · 기념일 · 영어), 반려견 표현, '걷다가'는 이동 수단이 아님, 지시문 주입은 unclear 가 아님.
  • 동네 이름: 표(NEIGHBORHOOD_PLACE_NAMES)를 프롬프트와 코드가 같이 쓴다. AI 가 "수성못"을 장소로 뽑아도 코드가 동네로 바꾼다.
    장소 이름 뒤에 말이 붙으면("팔공산 숲길") 별칭으로 찾는다.
  • 예시: 평가셋에 없는 문장으로 바꿨다. v1 에 평가셋과 같은 예시가 여럿 있어 그 사례의 점수가 외운 답일 수 있었다.

평가 보강

  • 실행마다 프롬프트와 평가셋 요청이 겹치는 구절을 찾아 summary.md 에 적는다(prompt-overlap.ts). 지금은 "없음"이다.
  • AI 호출마다 고른 곳 · 머무는 시간 · 위반 목록을 results.jsonl 의 course.attempts 에 남긴다.
    코스를 못 만들었거나 다시 부른 사례는 review.md 에 'AI 초안 1 · 2'로 보인다. 이를 위해 draftAndPlan 에 onAttempt 를 더했다(파이프라인은 쓰지 않는다).
  • 평가셋 고침(사용자 결정, 평가셋 README):
    • 가족 표현의 인원은 문장에서 뽑는 항목(F02 · K01 · K02 · K04 · K06 · K08)
    • "실내 위주로" · "실내에서" 등은 indoorIfRain(D13 · P07 · K02 · X01)
    • G01 은 영어 'I'만으로 인원을 뽑지 않음, K08 의 기대 인원 3 → 2명

결과와 모델 결정 (설계 §8 · §9.2)

같은 프롬프트 v2 · 같은 후보 스냅샷, effort low, 채점자 Sonnet 5.5 medium.

항목 Haiku 5.5 Sonnet 5.5 목표
자동 채점 통과 36/52 (69%) · 직전 판 75% 46/52 (88%) 95% 이상
경계 사례(주입 · 음주 운전 등) 2/6 6/6 —
반려 조건 위반 0건 0건 0건
지어낸 정보 7건 · 직전 판 1건 3건(채점자 오판 2건, 실제 1건) 0건
AI 채점(요청 · 글 · 근거) 3.35 · 4.52 · 3.69 3.69 · 4.75 · 4.13 —
생성 지연 95% 9.8초 11.6초 20초
생성 1회 비용 $0.0006 $0.0115 —
  • 기준선 대비: 자동 채점 Haiku 42% → 69~75%, Sonnet 38% → 88%. '식사 시간대에 식당 없음'은 Sonnet 1건만 남았다.
  • 코스 초안 기본 모델을 Sonnet 5.5 · effort low 로 바꿨다(사용자 결정). 조건 해석은 Haiku 5.5 · low 그대로다.
    • Haiku 는 반려 동반 · 영업시간 표현 규칙을 실행마다 다르게 지켰다(지어낸 정보 1~7건).
    • 생성 1회 약 16원, 하루 100회면 한 달 약 $35다. 로그인 사용자가 하루 5회를 다 써도 1인 하루 약 80원이다.
  • Sonnet 의 남은 실패 6개는 대부분 모델 밖이다 — 반려 동반 데이터 부족, 평가셋 이동 한도(8~10분)와 앱 도보 한도(15분)의 차이, 장소 수 기준, 위험 날씨 알림.
    무엇을 고칠지는 다음 판에서 정한다(평가셋 README '다음 버전에서 볼 것').
  • 실행마다 흔들림이 크다(같은 v2 로 Haiku 두 번 사이에 13개가 통과와 실패를 오갔다). 같은 평가셋으로 프롬프트를 더 다듬으면 평가셋에 맞춰질 위험이 커서 v2 다듬기는 여기서 멈췄다.

개발 환경

  • 머지하면 개발 서버의 AI 코스 생성도 Sonnet 을 부른다(1회 약 16원). backend/.env.development 에 AI_COURSE_MODEL 을 적어 두었다면 그 값이 이긴다.

확인 방법

# 단위 테스트(실제 호출 없음)
docker exec wandergo-backend npx jest src/commands src/modules/course-generation

# 실행기 확인 — 가짜 AI, 키 · 비용 없음
docker exec wandergo-backend npm run ai:eval -- --fake

# 실제 평가 — 키를 쓰므로 키가 있는 환경에서
docker exec wandergo-backend npm run ai:eval -- --model=claude-sonnet-5-5 --judge
  • 단위 테스트를 20개 더했다(501 → 521): 식사 시간대 옮기기, 장소 수 · 끼니 계산, 동네 이름 바꾸기 · 별칭 앞말 찾기, 겹치는 구절 찾기, AI 초안 기록 · 보이기
  • 빈 임시 DB 로 e2e 전체(6개 스위트, 59개)를 돌려 통과했다. PC 부하로 준비 단계가 기본 5초를 넘어 제한 시간 30초로 돌렸다(설정은 바꾸지 않았다)
  • 가짜 AI 로 실행기를 돌렸고, 실제로는 프롬프트 v2 로 Haiku 3번 · Sonnet 1번을 돌렸다(사용자 실행, 합계 약 $2.25)

영향 범위

  • backend
  • frontend
  • flutter
  • docker / 인프라
  • 규칙 문서(.claude/)

공유 계약: 바뀌지 않는다. API · 에러 코드 · 응답 형식 그대로다. 코스 결과의 장소 순서 · 도착 시각은 식사 시간대에 맞춰 달라질 수 있다.

체크리스트

  • 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과: lint · tsc · 백엔드 단위 테스트 521개 · e2e 59개
  • 주석 한국어, 매직 넘버·하드코딩 문자열 없음: 장소 수 · 끼니 기준은 플래너 상수(MINUTES_PER_STOP · MEAL_OVERLAP_MIN_MINUTES)
  • 사용자 노출 실패는 모두 에러 코드와 함께 처리 — 새 실패 경로 없음
  • 에러 코드를 바꿨다면 npm run sync:error-codes 실행 후 생성 파일까지 커밋 — 바꾸지 않았다
  • 민감정보 미커밋 — 실행 결과(runs/)는 gitignore
  • 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음: 평가 로그는 사례 id · 결과 · 비용만 남긴다

🤖 Generated with Claude Code

## 무엇을 · 왜 AI 코스 생성 6단계(설계 §14)의 뒷부분이다. 기준선(#15)에서 찾은 실패를 고치고, 같은 조건으로 두 모델을 다시 재서 코스 모델을 정했다. > **#15 위에 쌓은 브랜치다.** #15 를 먼저 머지하면 이 PR 에는 이번 커밋만 남는다. ### 플래너 — 식당을 식사 시간대에 (설계 §3.6 2a) - 기준선 자동 채점 실패의 3분의 2가 '식사 시간대에 식당 없음'이었다. 식사 시간대는 약한 위반이라 고치기(강한 위반이 있을 때만 돈다)에 걸리지 않았다. - 강한 위반을 고친 뒤 식당이 식사 시간대 밖이면 순서 · 머무는 시간을 다시 찾아 식당을 시간대 안으로 옮긴다(`fitMeals`). 옮길 수 없으면 원래 시간표를 둔다. - 비 올 때 실내 비율을 채울 때도 식사를 지키는 순서를 먼저 본다. ### 프롬프트 v2 (`course-v2` · `conditions-v2`, 설계 §3.5) - **코스**: 장소 수(1.5시간에 1곳)와 끼니(시간 범위가 그 식사 시간대에 90분 이상 걸칠 때만)를 코드가 계산해 메시지에 적는다. 쓰는 규칙(실내외 모름 · 가격 · 도착 시각 · 넣지 않은 곳 · 반려 표현)과 비우기 · 일부만 맞추기 기준을 다듬었다. - **조건 해석**: 시간 표현(말한 시각 우선 · 하루 종일 · 오전에/오전에만 · 저녁까지 · 기념일 · 영어), 반려견 표현, '걷다가'는 이동 수단이 아님, 지시문 주입은 unclear 가 아님. - **동네 이름**: 표(`NEIGHBORHOOD_PLACE_NAMES`)를 프롬프트와 코드가 같이 쓴다. AI 가 "수성못"을 장소로 뽑아도 코드가 동네로 바꾼다. 장소 이름 뒤에 말이 붙으면("팔공산 숲길") 별칭으로 찾는다. - **예시**: 평가셋에 없는 문장으로 바꿨다. v1 에 평가셋과 같은 예시가 여럿 있어 그 사례의 점수가 외운 답일 수 있었다. ### 평가 보강 - 실행마다 프롬프트와 평가셋 요청이 겹치는 구절을 찾아 `summary.md` 에 적는다(`prompt-overlap.ts`). 지금은 "없음"이다. - AI 호출마다 고른 곳 · 머무는 시간 · 위반 목록을 `results.jsonl` 의 `course.attempts` 에 남긴다. 코스를 못 만들었거나 다시 부른 사례는 `review.md` 에 'AI 초안 1 · 2'로 보인다. 이를 위해 `draftAndPlan` 에 `onAttempt` 를 더했다(파이프라인은 쓰지 않는다). - 평가셋 고침(사용자 결정, 평가셋 README): - 가족 표현의 인원은 문장에서 뽑는 항목(F02 · K01 · K02 · K04 · K06 · K08) - "실내 위주로" · "실내에서" 등은 indoorIfRain(D13 · P07 · K02 · X01) - G01 은 영어 'I'만으로 인원을 뽑지 않음, K08 의 기대 인원 3 → 2명 ### 결과와 모델 결정 (설계 §8 · §9.2) 같은 프롬프트 v2 · 같은 후보 스냅샷, effort low, 채점자 Sonnet 5.5 medium. | 항목 | Haiku 5.5 | Sonnet 5.5 | 목표 | | --- | --- | --- | --- | | 자동 채점 통과 | 36/52 (69%) · 직전 판 75% | 46/52 (88%) | 95% 이상 | | 경계 사례(주입 · 음주 운전 등) | 2/6 | 6/6 | — | | 반려 조건 위반 | 0건 | 0건 | 0건 | | 지어낸 정보 | 7건 · 직전 판 1건 | 3건(채점자 오판 2건, 실제 1건) | 0건 | | AI 채점(요청 · 글 · 근거) | 3.35 · 4.52 · 3.69 | 3.69 · 4.75 · 4.13 | — | | 생성 지연 95% | 9.8초 | 11.6초 | 20초 | | 생성 1회 비용 | $0.0006 | $0.0115 | — | - 기준선 대비: 자동 채점 Haiku 42% → 69~75%, Sonnet 38% → 88%. '식사 시간대에 식당 없음'은 Sonnet 1건만 남았다. - 코스 초안 기본 모델을 **Sonnet 5.5 · effort low** 로 바꿨다(사용자 결정). 조건 해석은 Haiku 5.5 · low 그대로다. - Haiku 는 반려 동반 · 영업시간 표현 규칙을 실행마다 다르게 지켰다(지어낸 정보 1~7건). - 생성 1회 약 16원, 하루 100회면 한 달 약 $35다. 로그인 사용자가 하루 5회를 다 써도 1인 하루 약 80원이다. - Sonnet 의 남은 실패 6개는 대부분 모델 밖이다 — 반려 동반 데이터 부족, 평가셋 이동 한도(8~10분)와 앱 도보 한도(15분)의 차이, 장소 수 기준, 위험 날씨 알림. 무엇을 고칠지는 다음 판에서 정한다(평가셋 README '다음 버전에서 볼 것'). - 실행마다 흔들림이 크다(같은 v2 로 Haiku 두 번 사이에 13개가 통과와 실패를 오갔다). 같은 평가셋으로 프롬프트를 더 다듬으면 평가셋에 맞춰질 위험이 커서 v2 다듬기는 여기서 멈췄다. ### 개발 환경 - 머지하면 개발 서버의 AI 코스 생성도 Sonnet 을 부른다(1회 약 16원). `backend/.env.development` 에 `AI_COURSE_MODEL` 을 적어 두었다면 그 값이 이긴다. ## 확인 방법 ```bash # 단위 테스트(실제 호출 없음) docker exec wandergo-backend npx jest src/commands src/modules/course-generation # 실행기 확인 — 가짜 AI, 키 · 비용 없음 docker exec wandergo-backend npm run ai:eval -- --fake # 실제 평가 — 키를 쓰므로 키가 있는 환경에서 docker exec wandergo-backend npm run ai:eval -- --model=claude-sonnet-5-5 --judge ``` - 단위 테스트를 20개 더했다(501 → 521): 식사 시간대 옮기기, 장소 수 · 끼니 계산, 동네 이름 바꾸기 · 별칭 앞말 찾기, 겹치는 구절 찾기, AI 초안 기록 · 보이기 - 빈 임시 DB 로 e2e 전체(6개 스위트, 59개)를 돌려 통과했다. PC 부하로 준비 단계가 기본 5초를 넘어 제한 시간 30초로 돌렸다(설정은 바꾸지 않았다) - 가짜 AI 로 실행기를 돌렸고, 실제로는 프롬프트 v2 로 Haiku 3번 · Sonnet 1번을 돌렸다(사용자 실행, 합계 약 $2.25) ## 영향 범위 - [x] backend - [ ] frontend - [ ] flutter - [ ] docker / 인프라 - [ ] 규칙 문서(.claude/) > **공유 계약**: 바뀌지 않는다. API · 에러 코드 · 응답 형식 그대로다. 코스 결과의 장소 순서 · 도착 시각은 식사 시간대에 맞춰 달라질 수 있다. ## 체크리스트 - [x] 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과: lint · tsc · 백엔드 단위 테스트 521개 · e2e 59개 - [x] 주석 한국어, 매직 넘버·하드코딩 문자열 없음: 장소 수 · 끼니 기준은 플래너 상수(`MINUTES_PER_STOP` · `MEAL_OVERLAP_MIN_MINUTES`) - [x] 사용자 노출 실패는 모두 에러 코드와 함께 처리 — 새 실패 경로 없음 - [ ] 에러 코드를 바꿨다면 `npm run sync:error-codes` 실행 후 생성 파일까지 커밋 — 바꾸지 않았다 - [x] 민감정보 미커밋 — 실행 결과(`runs/`)는 gitignore - [x] 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음: 평가 로그는 사례 id · 결과 · 비용만 남긴다 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ttipo added 2 commits 2026-11-12 09:13:31 +09:00
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
4c049d2fec
모델과 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>
feat(backend): AI 코스 식사 시간대 맞추기와 프롬프트 v2, 평가로 코스 모델을 Sonnet 5.5 로 정함
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
439753e1cf
기준선(설계 §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>
ttipo merged commit 439753e1cf into main 2026-11-12 09:52:38 +09:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ttipo/wandergo#16