feat(backend): AI 코스 생성에 Anthropic 클라이언트와 프롬프트 v1, 조건 해석 연결
CI / Backend (lint · test · e2e · build) (pull_request) Successful in 12m37s
CI / Frontend (lint · type-check · test) (pull_request) Successful in 11m24s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m57s
CI / Backend (lint · test · e2e · build) (push) Successful in 12m33s
CI / Frontend (lint · type-check · test) (push) Successful in 11m12s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m18s

4단계까지 서버는 가짜 AI 로 코스를 만들었다. 실제 모델(기본 Haiku 5.5)을 붙여
문장에서 조건을 읽고 후보 중 코스를 고르게 한다(설계 §14 5단계).

- 구조화 출력(JSON 스키마)으로 받고, 같은 시스템 프롬프트는 캐시에 올린다
- 조건 해석은 AI 가 값만 뽑고 기본값과 되물을 것은 코드가 정한다(unknown_place 추가)
- 조건 해석 비용과 응답 뒤 실패한 호출 비용도 하루 비용 상한에 넣는다
- AI_PROVIDER(fake 로 되돌림) · effort 설정, 모델은 가격표에 있어야 기동한다
- 키로 실제 호출을 확인하는 ai:check 명령

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #14.
This commit is contained in:
2026-11-11 17:16:00 +09:00
parent 7ed3411abb
commit 4dc5be72a4
33 changed files with 2862 additions and 89 deletions
+41 -4
View File
@@ -80,7 +80,8 @@
| 값 | 경우 |
| --- | --- |
| `unsupported_area` | 대구 밖 지역("성수동", "경주 황리단길"). 지금은 대구만 된다 |
| `past_date` · `date_out_of_range` | 지난 날짜 · 30일보다 먼 날짜("다음 달 크리스마스") |
| `unknown_place` | 말한 장소를 장소 DB 에서 찾지 못했다. 지역은 기준 위치로 두었다(5단계에서 더함) |
| `past_date` · `date_out_of_range` | 지난 날짜(오늘인데 이미 끝난 시간 포함) · 30일보다 먼 날짜("다음 달 크리스마스") |
| `multi_day` | "3박 4일", "1박" — 하루 코스만 된다 |
| `too_short` · `past_midnight` | 2시간보다 짧다("점심시간 1시간만") · 자정을 넘긴다("밤 11시 이후") |
| `unclear_request` | 코스 요청으로 읽을 수 없다("ㅁㄴㅇㄹ") |
@@ -92,6 +93,17 @@
- `weather` 는 고른 날짜·지역의 예보다(§3.3). 입력 화면의 "토요일 오후 비 60%" 에 쓴다.
- 모델은 작은 모델(Haiku 5.5)에 구조화 출력을 쓴다. AI 호출이 실패하면 기본값만 채운 조건을 돌려준다(입력 화면을 막지 않는다).
- AI 에는 문장, 오늘 날짜·요일, 지원 동네 목록만 보낸다. 좌표·기기 정보는 보내지 않는다.
- **5단계에서 정한 나눔**(2026-11-11, `condition-extraction.ts` · `ai/condition-prompt.ts`):
- AI 는 문장에서 값만 뽑는다. 지역 언급(none · neighborhood · place · near_me · outside), 날짜, 시작·끝, 길이("2시간만"), 자정 넘김, 인원, 반려동물, 이동 수단, 비 오면 실내, 여러 날 일정, 읽을 수 없음이다.
근거가 없는 항목은 null 이다.
- 기본값 채우기와 `issues` 판정은 코드(`applyExtraction`)가 한다. 지난 날짜·짧은 시간 같은 값도 그대로 돌려줘 앱이 그 항목을 짚어 고치게 한다.
자정을 넘기면 끝을 24:00 으로 두고, 12시간보다 길면 12시간으로 줄인다. 시작만 말하면 6시간(22시까지)이되 최소 2시간을 둔다.
- 날짜는 사용자 메시지에 이번 주·다음 주·다다음 주 달력(월~일)을 붙여 모델이 셈하지 않고 고르게 한다.
- 장소는 별칭표(`PLACE_ALIASES`: 팔공산 · 83타워 · 서문시장 · 김광석길) → 이름 검색 순서로 찾는다.
이름 검색은 공백을 무시하고, 이름이 같은 곳 → AI 후보 순위가 있는 곳 → 명소 분류 → 짧은 이름 순으로 고른다.
- 조건 해석 비용도 하루 비용 상한에 들어간다. 생성 기록이 없어 서버 메모리에 더한다(서버 한 대, 다시 시작하면 0부터).
상한에 닿으면 AI 를 부르지 않고 기본값만 채운다.
- "비 오는 토요일"처럼 날씨를 묘사만 한 문장으로는 `indoorIfRain` 을 채우지 않는다. 비 여부는 예보로 판단한다(첫 실제 호출에서 잘못 채워 규칙을 더했다).
## 3. 생성
@@ -204,6 +216,26 @@ E01 | ○○ 빛 축제 | 축제 | 실외 | 남 300m | 18:00–22:00 | 반려:
- 시스템 프롬프트는 프롬프트 캐시에 올린다. 트래픽이 적으면 캐시가 5분 안에 다시 쓰이지 않아 이득이 작다.
- 제한 시간 30초, SDK 재시도 1회. 프롬프트는 코드에 두고 버전(`PROMPT_VERSION`)을 생성 기록에 남긴다.
**프롬프트 v1 (5단계, 2026-11-11 · `ai/course-prompt.ts` · `ai/anthropic-course-ai.client.ts`)**
- SDK 는 `@anthropic-ai/sdk` 1.5.0 이다. 구조화 출력은 `output_config.format`(JSON 스키마)으로 받고 코드가 형식을 다시 본다.
번호가 후보에 있는지는 `planCourse` 가 본다(`unknown_ref`). 추론은 adaptive, 깊이는 `AI_COURSE_EFFORT`(기본 low)다.
- 시스템 프롬프트에는 날짜·후보 같은 바뀌는 값을 넣지 않는다(캐시가 맞게). 분류별 머무는 시간과 식사 시간대는 코드 상수에서 만든다.
- 후보 한 줄: `번호 | 이름 | 분류 | 실내외 | 지역 중심에서 방향·거리 | 그날 영업 | (반려 조건) | 출처`.
반려 조건은 요청에 반려동물이 있을 때만 넣는다. 출처는 확인한 곳 · 관광공사 · 행사 기간이다.
- 사용자 메시지에 '답할 언어'(요청의 lang)를 적는다. 다시 부를 때는 '직전 초안의 문제'로 위반 목록을 붙인다.
- 응답을 받은 뒤 실패하면(거절 · 잘림 · JSON 이 아님 · 형식이 다름) `AI_002` 이고, 그 호출의 사용량을 생성 기록에 남긴다. 비용이 이미 나갔기 때문이다.
- 화면을 지키려고 글자 수를 자른다. 제목은 40자, 요약은 200자, 이유는 120자, 알림은 3개·200자까지다. 프롬프트로는 20 · 120 · 60자 이내로 쓰게 한다.
- 아직 넣지 않은 것:
- 확인한 곳의 추천 이유(`place_curation.reason_ko`) · 반려 조건 문구 · 소개 앞 80자 · 영어 장소 이름
- 확인한 곳이 0곳이라 뺐다. 큐레이션이 생기거나 평가(6단계)에서 필요하다고 나오면 넣는다.
- 거절 대비(server-side fallback)는 넣지 않았다. Haiku 5.5 는 지원하지 않고, Sonnet 5.5 로 올리면 다시 본다.
- 날씨·예보는 앱이 보여 주므로 summary·notices 에 쓰지 않게 했다. 예보는 4일 뒤까지만 있어 없는 경우가 많은데, 첫 실제 호출에서 "예보가 없어…" 알림을 썼다.
- 첫 실제 호출(2026-11-11, Haiku 5.5 · effort low, `ai:check`)의 결과다.
- 조건 해석: 3.2초 · $0.0005
- 코스 초안: 후보 12곳, 13.3초, 출력 2,154토큰(대부분 추론), $0.0015. 결과는 장소 3곳(일부만 맞춤)이었다.
- 첫 호출이라 캐시 쓰기만 있었다. 지연·비용 분포는 6단계 평가로 본다.
### 3.6 시간표·검증·고치기 (코드)
**이동 시간 추정** — MVP 는 경로 API 없이 직선거리로 계산한다.
@@ -430,8 +462,10 @@ E01 | ○○ 빛 축제 | 축제 | 실외 | 남 300m | 18:00–22:00 | 반려:
| --- | --- | --- | --- |
| `ANTHROPIC_API_KEY` | `backend/.env.{env}` | — | 운영에서는 필수(없으면 기동 실패). 개발에서 없으면 AI API 가 `AI_002` 를 준다 |
| `PUBLIC_DATA_SERVICE_KEY` | `backend/.env.{env}` | — | 이미 있는 공공데이터포털 키(장소 동기화). 날씨도 쓴다. 없으면 날씨만 '예보 없음'이다 |
| `AI_COURSE_MODEL` | 루트 `.env` → compose | 평가 뒤 결정(Haiku 5.5 부터 시험) | 코스 생성 모델 |
| `AI_CONDITION_MODEL` | 루트 `.env` → compose | `claude-haiku-5-5` | 조건 해석 모델 |
| `AI_PROVIDER` | `backend/.env.{env}` | `anthropic` | `fake` 면 키 없이 흐름만 돌리는 가짜 AI(개발용). 운영에서는 `fake` 면 기동 실패 |
| `AI_COURSE_MODEL` | 루트 `.env` → compose | `claude-haiku-5-5`(평가 뒤 결정) | 코스 생성 모델. 가격표(`shared/utils/ai-pricing.ts`)에 있는 모델만 받는다 |
| `AI_CONDITION_MODEL` | 루트 `.env` → compose | `claude-haiku-5-5` | 조건 해석 모델(같은 가격표) |
| `AI_COURSE_EFFORT` · `AI_CONDITION_EFFORT` | `backend/.env.{env}` | `low` · `low` | 추론 깊이 low · medium · high. 평가(6단계)로 정한다 |
| `AI_DAILY_LIMIT_GUEST` · `AI_DAILY_LIMIT_MEMBER` | 루트 `.env` → compose | 2 · 5 | §5 |
| `AI_DAILY_BUDGET_USD` | 루트 `.env` → compose | 개발 3 | 하루 비용 상한(정수 달러) |
| `AI_TIMEOUT_MS` | 루트 `.env` → compose | 30000 | AI 호출 제한 시간 |
@@ -440,6 +474,9 @@ E01 | ○○ 빛 축제 | 축제 | 실외 | 남 300m | 18:00–22:00 | 반려:
- 4단계(2026-11-08)는 `AI_DAILY_LIMIT_*` · `AI_DAILY_BUDGET_USD` · `AI_TIMEOUT_MS` 를 `ConfigService` 로 읽고 없으면 위 기본값을 쓴다(형식은 `env.validation.ts` 가 본다).
compose 로 넘기는 설정은 값을 바꿀 일이 생길 때 더한다 — 그 전에는 `backend/.env.{env}` 에 적으면 된다.
- SDK(`@anthropic-ai/sdk`)는 컨테이너 안에서 설치하고, 잠금 파일을 npm 10 으로 확인한다(nestjs 스킬의 의존성 주의 항목).
5단계(2026-11-11)에 1.5.0 을 넣었다. 컨테이너 npm 11 이 지운 선택 peer 항목(chokidar 3개)은 이전 잠금 파일에서 되살렸다.
- 모델 값은 가격표에 있어야 기동한다. 하루 비용 상한이 그 가격으로 계산되기 때문이다. 모델을 더하면 가격표와 테스트를 함께 고친다.
- 실제 호출 확인은 `npm run ai:check -- --text="..." [--course]` 다. 키를 쓰므로 사용자가 `!` 로 실행한다. 결과는 저장하지 않고 날씨는 부르지 않는다.
## 12. MVP 에서 하지 않는 것
@@ -473,7 +510,7 @@ v0.1 의 추천안을 사용자가 모두 그대로 골랐다.
2. 순수 로직 — 계절별 시간표를 날짜로 고르기(§3.3) → 후보 거르기 · 줄이기 · 이동 시간 · 시간표 · 검증 · 고치기 + 단위 테스트 (키 불필요). 2026-11-06 반영(§3.6 끝)
3. 날씨(기상청 단기예보 · 격자 변환 · 캐시) + 공휴일 표 (공공데이터 키 — 실제 호출 확인은 사용자가 `!` 로). 2026-11-08 반영(§3.3)
4. 테이블 · 에러 코드 · API 골격(가짜 AI 클라이언트) + e2e (키 불필요, CI 에서도 돈다). 2026-11-08 반영 — 5단계 전까지 서버는 가짜 AI(`FakeCourseAiClient`)로 코스를 만든다
5. Anthropic 연동 — SDK 설치(컨테이너), 프롬프트 v1, 조건 해석
5. Anthropic 연동 — SDK 설치(컨테이너), 프롬프트 v1, 조건 해석. 2026-11-11 반영(§2 끝 · §3.5 끝 · §11) — 서버 기본 AI 가 Anthropic 이 됐다(`AI_PROVIDER=fake` 로 되돌린다)
6. 평가 실행(사용자가 `!`) → 모델 · effort 결정 → 이 문서 갱신
7. Flutter 화면 8–10 연결
8. 출시 준비 — 처리방침(국외 이전) · AI 표시 문구 · Console 한도 · 보관 정리 작업